upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src/nostr
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2025-12-01 11:56:49 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2025-12-01 11:58:34 +0000
commit7a78815e29b01c83f3d0ec195ba717a2eba8cd37 (patch)
tree4c5ccd9b812f1d1d75ed218501192ddc5459fd12 /src/nostr
parente6ceab90de1acad154624022a6036efac18abab6 (diff)
reject push when refs/nostr/<event-id> doesnt match known event and delete incorrect ref on event receive
Diffstat (limited to 'src/nostr')
-rw-r--r--src/nostr/builder.rs389
-rw-r--r--src/nostr/events.rs6
2 files changed, 339 insertions, 56 deletions
diff --git a/src/nostr/builder.rs b/src/nostr/builder.rs
index 7aa2b97..8e9926a 100644
--- a/src/nostr/builder.rs
+++ b/src/nostr/builder.rs
@@ -14,8 +14,8 @@ use nostr_relay_builder::prelude::*;
14use crate::config::{Config, DatabaseBackend}; 14use crate::config::{Config, DatabaseBackend};
15use crate::git; 15use crate::git;
16use crate::nostr::events::{ 16use crate::nostr::events::{
17 validate_announcement, validate_state, RepositoryAnnouncement, RepositoryState, 17 validate_announcement, validate_state, RepositoryAnnouncement, RepositoryState, KIND_PR,
18 KIND_REPOSITORY_ANNOUNCEMENT, KIND_REPOSITORY_STATE, 18 KIND_PR_UPDATE, KIND_REPOSITORY_ANNOUNCEMENT, KIND_REPOSITORY_STATE,
19}; 19};
20 20
21/// NIP-34 Write Policy with Full GRASP-01 Event Validation 21/// NIP-34 Write Policy with Full GRASP-01 Event Validation
@@ -36,7 +36,11 @@ pub struct Nip34WritePolicy {
36} 36}
37 37
38impl Nip34WritePolicy { 38impl Nip34WritePolicy {
39 pub fn new(domain: impl Into<String>, database: Arc<MemoryDatabase>, git_data_path: impl Into<PathBuf>) -> Self { 39 pub fn new(
40 domain: impl Into<String>,
41 database: Arc<MemoryDatabase>,
42 git_data_path: impl Into<PathBuf>,
43 ) -> Self {
40 Self { 44 Self {
41 domain: domain.into(), 45 domain: domain.into(),
42 database, 46 database,
@@ -48,7 +52,7 @@ impl Nip34WritePolicy {
48 /// Path format: <git_data_path>/<npub>/<identifier>.git 52 /// Path format: <git_data_path>/<npub>/<identifier>.git
49 fn ensure_bare_repository(&self, announcement: &RepositoryAnnouncement) -> Result<(), String> { 53 fn ensure_bare_repository(&self, announcement: &RepositoryAnnouncement) -> Result<(), String> {
50 let repo_path = self.git_data_path.join(&announcement.repo_path()); 54 let repo_path = self.git_data_path.join(&announcement.repo_path());
51 55
52 // Check if repository already exists 56 // Check if repository already exists
53 if repo_path.exists() { 57 if repo_path.exists() {
54 tracing::debug!("Repository already exists at {}", repo_path.display()); 58 tracing::debug!("Repository already exists at {}", repo_path.display());
@@ -56,13 +60,12 @@ impl Nip34WritePolicy {
56 } 60 }
57 61
58 // Create parent directory (npub directory) 62 // Create parent directory (npub directory)
59 let parent = repo_path.parent().ok_or_else(|| { 63 let parent = repo_path
60 format!("Invalid repository path: {}", repo_path.display()) 64 .parent()
61 })?; 65 .ok_or_else(|| format!("Invalid repository path: {}", repo_path.display()))?;
62 66
63 std::fs::create_dir_all(parent).map_err(|e| { 67 std::fs::create_dir_all(parent)
64 format!("Failed to create directory {}: {}", parent.display(), e) 68 .map_err(|e| format!("Failed to create directory {}: {}", parent.display(), e))?;
65 })?;
66 69
67 // Initialize bare repository using git command 70 // Initialize bare repository using git command
68 let output = std::process::Command::new("git") 71 let output = std::process::Command::new("git")
@@ -165,7 +168,11 @@ impl Nip34WritePolicy {
165 tracing::debug!( 168 tracing::debug!(
166 "Found authorized announcement for {}: owner={}, maintainer={}", 169 "Found authorized announcement for {}: owner={}, maintainer={}",
167 identifier, 170 identifier,
168 if is_owner { event.pubkey.to_hex() } else { "n/a".to_string() }, 171 if is_owner {
172 event.pubkey.to_hex()
173 } else {
174 "n/a".to_string()
175 },
169 is_maintainer 176 is_maintainer
170 ); 177 );
171 authorized.push(announcement); 178 authorized.push(announcement);
@@ -198,10 +205,7 @@ impl Nip34WritePolicy {
198 let head_ref = match &state.head { 205 let head_ref = match &state.head {
199 Some(h) => h, 206 Some(h) => h,
200 None => { 207 None => {
201 tracing::debug!( 208 tracing::debug!("State event for {} has no HEAD reference", state.identifier);
202 "State event for {} has no HEAD reference",
203 state.identifier
204 );
205 return Ok(0); 209 return Ok(0);
206 } 210 }
207 }; 211 };
@@ -232,11 +236,9 @@ impl Nip34WritePolicy {
232 }; 236 };
233 237
234 // Find all announcements where state author is authorized 238 // Find all announcements where state author is authorized
235 let announcements = Self::find_authorized_announcements( 239 let announcements =
236 database, 240 Self::find_authorized_announcements(database, &state.identifier, &state.event.pubkey)
237 &state.identifier, 241 .await?;
238 &state.event.pubkey,
239 ).await?;
240 242
241 if announcements.is_empty() { 243 if announcements.is_empty() {
242 tracing::debug!( 244 tracing::debug!(
@@ -271,7 +273,7 @@ impl Nip34WritePolicy {
271 } 273 }
272 274
273 // Build repository path: <git_data_path>/<owner_npub>/<identifier>.git 275 // Build repository path: <git_data_path>/<owner_npub>/<identifier>.git
274 let repo_path = self.git_data_path.join(&announcement.repo_path()); 276 let repo_path = self.git_data_path.join(announcement.repo_path().clone());
275 277
276 match git::try_set_head_if_available(&repo_path, head_ref, head_commit) { 278 match git::try_set_head_if_available(&repo_path, head_ref, head_commit) {
277 Ok(true) => { 279 Ok(true) => {
@@ -291,11 +293,7 @@ impl Nip34WritePolicy {
291 ); 293 );
292 } 294 }
293 Err(e) => { 295 Err(e) => {
294 tracing::warn!( 296 tracing::warn!("Failed to set HEAD in {}: {}", repo_path.display(), e);
295 "Failed to set HEAD in {}: {}",
296 repo_path.display(),
297 e
298 );
299 } 297 }
300 } 298 }
301 } 299 }
@@ -338,6 +336,191 @@ impl Nip34WritePolicy {
338 (addressable_refs, event_refs) 336 (addressable_refs, event_refs)
339 } 337 }
340 338
339 /// Validate refs/nostr/<event-id> ref against a PR or PR Update event's `c` tag
340 ///
341 /// When a PR event (kind 1618) or PR Update event (kind 1619) is received,
342 /// this checks if a corresponding refs/nostr/<event-id> ref exists in the
343 /// repository and validates that it points to the correct commit (from the
344 /// `c` tag). If the ref exists but points to a different commit, the ref is
345 /// deleted.
346 ///
347 /// PR and PR Update events can have multiple `a` tags to update multiple
348 /// repositories simultaneously.
349 ///
350 /// This is part of GRASP-01 compliance: ensuring refs/nostr refs are consistent
351 /// with their corresponding events.
352 ///
353 /// # Arguments
354 /// * `database` - Database for looking up repository announcements
355 /// * `event` - The PR event (kind 1618) or PR Update event (kind 1619)
356 ///
357 /// # Returns
358 /// Ok(Some(n)) if n refs were deleted, Ok(None) if no action taken, Err on failure
359 async fn validate_pr_nostr_ref(
360 &self,
361 database: &Arc<MemoryDatabase>,
362 event: &Event,
363 ) -> Result<Option<usize>, String> {
364 let event_id = event.id.to_hex();
365
366 // Extract the `c` tag (commit hash) from the PR event
367 let expected_commit = event.tags.iter().find_map(|tag| {
368 let tag_vec = tag.clone().to_vec();
369 if tag_vec.len() >= 2 && tag_vec[0] == "c" {
370 Some(tag_vec[1].clone())
371 } else {
372 None
373 }
374 });
375
376 let expected_commit = match expected_commit {
377 Some(c) => c,
378 None => {
379 tracing::debug!(
380 "PR event {} has no 'c' tag, skipping ref validation",
381 event_id
382 );
383 return Ok(None);
384 }
385 };
386
387 // Extract ALL `a` tags (repository references) from the PR event
388 // PR events can reference multiple repositories
389 // Format: 30617:<pubkey>:<identifier>
390 let repo_refs: Vec<String> = event
391 .tags
392 .iter()
393 .filter_map(|tag| {
394 let tag_vec = tag.clone().to_vec();
395 if tag_vec.len() >= 2 && tag_vec[0] == "a" && tag_vec[1].starts_with("30617:") {
396 Some(tag_vec[1].clone())
397 } else {
398 None
399 }
400 })
401 .collect();
402
403 if repo_refs.is_empty() {
404 tracing::debug!(
405 "PR event {} has no repo 'a' tags, skipping ref validation",
406 event_id
407 );
408 return Ok(None);
409 }
410
411 let mut deleted_count = 0;
412
413 // Process each repository reference
414 for repo_ref in repo_refs {
415 // Parse the repo reference: 30617:<pubkey>:<identifier>
416 let parts: Vec<&str> = repo_ref.split(':').collect();
417 if parts.len() < 3 {
418 tracing::debug!(
419 "PR event {} has invalid 'a' tag format: {}",
420 event_id,
421 repo_ref
422 );
423 continue;
424 }
425
426 let repo_pubkey = match PublicKey::from_hex(parts[1]) {
427 Ok(pk) => pk,
428 Err(_) => {
429 tracing::debug!(
430 "PR event {} has invalid pubkey in 'a' tag: {}",
431 event_id,
432 parts[1]
433 );
434 continue;
435 }
436 };
437 let identifier = parts[2];
438
439 // Look up repository announcement to get the npub for path
440 let filter = Filter::new()
441 .kind(Kind::from(KIND_REPOSITORY_ANNOUNCEMENT))
442 .author(repo_pubkey)
443 .custom_tag(
444 SingleLetterTag::lowercase(Alphabet::D),
445 identifier.to_string(),
446 );
447
448 let announcements: Vec<Event> = match database.query(filter).await {
449 Ok(events) => events.into_iter().collect(),
450 Err(e) => {
451 tracing::warn!(
452 "Failed to query for repository announcement for PR {}: {}",
453 event_id,
454 e
455 );
456 continue;
457 }
458 };
459
460 if announcements.is_empty() {
461 tracing::debug!(
462 "No repository announcement found for PR event {} (repo {}:{})",
463 event_id,
464 repo_pubkey.to_hex(),
465 identifier
466 );
467 continue;
468 }
469
470 // Process each matching announcement (there could be multiple)
471 for announcement_event in announcements {
472 let announcement = match RepositoryAnnouncement::from_event(announcement_event) {
473 Ok(a) => a,
474 Err(e) => {
475 tracing::warn!(
476 "Failed to parse announcement for PR {} validation: {}",
477 event_id,
478 e
479 );
480 continue;
481 }
482 };
483
484 // Build repository path
485 let repo_path = self.git_data_path.join(&announcement.repo_path());
486
487 // Validate the ref
488 match git::validate_nostr_ref(&repo_path, &event_id, &expected_commit) {
489 Ok(true) => {
490 tracing::info!(
491 "Deleted mismatched refs/nostr/{} in {} (expected commit {})",
492 event_id,
493 repo_path.display(),
494 expected_commit
495 );
496 deleted_count += 1;
497 }
498 Ok(false) => {
499 tracing::debug!(
500 "refs/nostr/{} in {} is valid or doesn't exist",
501 event_id,
502 repo_path.display()
503 );
504 }
505 Err(e) => {
506 tracing::warn!(
507 "Failed to validate refs/nostr/{} in {}: {}",
508 event_id,
509 repo_path.display(),
510 e
511 );
512 }
513 }
514 }
515 }
516
517 if deleted_count > 0 {
518 Ok(Some(deleted_count))
519 } else {
520 Ok(None)
521 }
522 }
523
341 /// Check if any addressable events (repositories) exist in database 524 /// Check if any addressable events (repositories) exist in database
342 /// Returns the first matching addressable reference found, or None if none match 525 /// Returns the first matching addressable reference found, or None if none match
343 async fn find_accepted_repository( 526 async fn find_accepted_repository(
@@ -377,16 +560,17 @@ impl Nip34WritePolicy {
377 use std::collections::HashMap; 560 use std::collections::HashMap;
378 let mut by_kind: HashMap<u16, Vec<_>> = HashMap::new(); 561 let mut by_kind: HashMap<u16, Vec<_>> = HashMap::new();
379 for (addr, kind, pubkey, identifier) in parsed_refs { 562 for (addr, kind, pubkey, identifier) in parsed_refs {
380 by_kind.entry(kind).or_default().push((addr, pubkey, identifier)); 563 by_kind
564 .entry(kind)
565 .or_default()
566 .push((addr, pubkey, identifier));
381 } 567 }
382 568
383 // Query each kind group 569 // Query each kind group
384 for (kind, refs) in by_kind { 570 for (kind, refs) in by_kind {
385 let authors: Vec<PublicKey> = refs.iter().map(|(_, pk, _)| *pk).collect(); 571 let authors: Vec<PublicKey> = refs.iter().map(|(_, pk, _)| *pk).collect();
386 572
387 let filter = Filter::new() 573 let filter = Filter::new().kind(Kind::from(kind)).authors(authors);
388 .kind(Kind::from(kind))
389 .authors(authors);
390 574
391 match database.query(filter).await { 575 match database.query(filter).await {
392 Ok(events) => { 576 Ok(events) => {
@@ -445,7 +629,7 @@ impl Nip34WritePolicy {
445 event: &Event, 629 event: &Event,
446 ) -> Result<bool, String> { 630 ) -> Result<bool, String> {
447 let kind_u16 = event.kind.as_u16(); 631 let kind_u16 = event.kind.as_u16();
448 632
449 // Check if this is any kind of replaceable event 633 // Check if this is any kind of replaceable event
450 let is_regular_replaceable = kind_u16 >= 10000 && kind_u16 < 20000; 634 let is_regular_replaceable = kind_u16 >= 10000 && kind_u16 < 20000;
451 let is_parameterized_replaceable = kind_u16 >= 30000 && kind_u16 < 40000; 635 let is_parameterized_replaceable = kind_u16 >= 30000 && kind_u16 < 40000;
@@ -454,7 +638,9 @@ impl Nip34WritePolicy {
454 // Build the appropriate address format based on event type 638 // Build the appropriate address format based on event type
455 let address = if is_parameterized_replaceable { 639 let address = if is_parameterized_replaceable {
456 // For parameterized replaceable: kind:pubkey:d-identifier format (2 colons) 640 // For parameterized replaceable: kind:pubkey:d-identifier format (2 colons)
457 let identifier = event.tags.iter() 641 let identifier = event
642 .tags
643 .iter()
458 .find_map(|tag| { 644 .find_map(|tag| {
459 let tag_vec = tag.clone().to_vec(); 645 let tag_vec = tag.clone().to_vec();
460 if tag_vec.len() >= 2 && tag_vec[0] == "d" { 646 if tag_vec.len() >= 2 && tag_vec[0] == "d" {
@@ -464,12 +650,17 @@ impl Nip34WritePolicy {
464 } 650 }
465 }) 651 })
466 .unwrap_or_default(); // Empty string if no 'd' tag 652 .unwrap_or_default(); // Empty string if no 'd' tag
467 format!("{}:{}:{}", event.kind.as_u16(), event.pubkey.to_hex(), identifier) 653 format!(
654 "{}:{}:{}",
655 event.kind.as_u16(),
656 event.pubkey.to_hex(),
657 identifier
658 )
468 } else { 659 } else {
469 // For regular replaceable: kind:pubkey format (1 colon) 660 // For regular replaceable: kind:pubkey format (1 colon)
470 format!("{}:{}", event.kind.as_u16(), event.pubkey.to_hex()) 661 format!("{}:{}", event.kind.as_u16(), event.pubkey.to_hex())
471 }; 662 };
472 663
473 // Check addressable reference tags: a, A, q (with address format) 664 // Check addressable reference tags: a, A, q (with address format)
474 let addressable_tags = [ 665 let addressable_tags = [
475 SingleLetterTag::lowercase(Alphabet::A), // 'a' - addressable event reference 666 SingleLetterTag::lowercase(Alphabet::A), // 'a' - addressable event reference
@@ -479,7 +670,7 @@ impl Nip34WritePolicy {
479 670
480 for tag_type in &addressable_tags { 671 for tag_type in &addressable_tags {
481 let filter = Filter::new().custom_tag(tag_type.clone(), address.clone()); 672 let filter = Filter::new().custom_tag(tag_type.clone(), address.clone());
482 673
483 match database.query(filter).await { 674 match database.query(filter).await {
484 Ok(events) => { 675 Ok(events) => {
485 if !events.is_empty() { 676 if !events.is_empty() {
@@ -492,7 +683,7 @@ impl Nip34WritePolicy {
492 } else { 683 } else {
493 // For regular events, check event ID reference tags: e, E, q (with hex ID) 684 // For regular events, check event ID reference tags: e, E, q (with hex ID)
494 let event_id_hex = event.id.to_hex(); 685 let event_id_hex = event.id.to_hex();
495 686
496 let event_id_tags = [ 687 let event_id_tags = [
497 SingleLetterTag::lowercase(Alphabet::E), // 'e' - standard event reference 688 SingleLetterTag::lowercase(Alphabet::E), // 'e' - standard event reference
498 SingleLetterTag::uppercase(Alphabet::E), // 'E' - NIP-22 root event reference 689 SingleLetterTag::uppercase(Alphabet::E), // 'E' - NIP-22 root event reference
@@ -501,7 +692,7 @@ impl Nip34WritePolicy {
501 692
502 for tag_type in &event_id_tags { 693 for tag_type in &event_id_tags {
503 let filter = Filter::new().custom_tag(tag_type.clone(), event_id_hex.clone()); 694 let filter = Filter::new().custom_tag(tag_type.clone(), event_id_hex.clone());
504 695
505 match database.query(filter).await { 696 match database.query(filter).await {
506 Ok(events) => { 697 Ok(events) => {
507 if !events.is_empty() { 698 if !events.is_empty() {
@@ -545,7 +736,7 @@ impl WritePolicy for Nip34WritePolicy {
545 // Note: We still accept the event even if repo creation fails 736 // Note: We still accept the event even if repo creation fails
546 // The git operation failure shouldn't prevent event acceptance 737 // The git operation failure shouldn't prevent event acceptance
547 } 738 }
548 739
549 tracing::debug!( 740 tracing::debug!(
550 "Accepted repository announcement: {}", 741 "Accepted repository announcement: {}",
551 event_id_str 742 event_id_str
@@ -563,11 +754,7 @@ impl WritePolicy for Nip34WritePolicy {
563 } 754 }
564 } 755 }
565 Err(e) => { 756 Err(e) => {
566 tracing::warn!( 757 tracing::warn!("Rejected repository announcement {}: {}", event_id_str, e);
567 "Rejected repository announcement {}: {}",
568 event_id_str,
569 e
570 );
571 PolicyResult::Reject(e.to_string()) 758 PolicyResult::Reject(e.to_string())
572 } 759 }
573 }, 760 },
@@ -577,7 +764,10 @@ impl WritePolicy for Nip34WritePolicy {
577 match RepositoryState::from_event(event.clone()) { 764 match RepositoryState::from_event(event.clone()) {
578 Ok(state) => { 765 Ok(state) => {
579 // Try to set HEAD for all authorized repos if this is the latest state 766 // Try to set HEAD for all authorized repos if this is the latest state
580 match self.try_set_head_for_authorized_repos(&database, &state).await { 767 match self
768 .try_set_head_for_authorized_repos(&database, &state)
769 .await
770 {
581 Ok(count) if count > 0 => { 771 Ok(count) if count > 0 => {
582 tracing::info!( 772 tracing::info!(
583 "Set HEAD from state event {} for {} repo(s) with identifier {}", 773 "Set HEAD from state event {} for {} repo(s) with identifier {}",
@@ -600,11 +790,8 @@ impl WritePolicy for Nip34WritePolicy {
600 ); 790 );
601 } 791 }
602 } 792 }
603 793
604 tracing::debug!( 794 tracing::debug!("Accepted repository state: {}", event_id_str);
605 "Accepted repository state: {}",
606 event_id_str
607 );
608 PolicyResult::Accept 795 PolicyResult::Accept
609 } 796 }
610 Err(e) => { 797 Err(e) => {
@@ -620,14 +807,104 @@ impl WritePolicy for Nip34WritePolicy {
620 } 807 }
621 } 808 }
622 Err(e) => { 809 Err(e) => {
810 tracing::warn!("Rejected repository state {}: {}", event_id_str, e);
811 PolicyResult::Reject(e.to_string())
812 }
813 },
814 // KIND_PR (1618) and KIND_PR_UPDATE (1619): Validate refs/nostr/<event-id> refs before acceptance
815 KIND_PR | KIND_PR_UPDATE => {
816 // Validate refs/nostr refs for this PR event
817 // This deletes any refs/nostr/<event-id> that points to wrong commit
818 if let Err(e) = self.validate_pr_nostr_ref(&database, event).await {
623 tracing::warn!( 819 tracing::warn!(
624 "Rejected repository state {}: {}", 820 "Failed to validate refs/nostr for PR event {}: {}",
625 event_id_str, 821 event_id_str,
626 e 822 e
627 ); 823 );
628 PolicyResult::Reject(e.to_string()) 824 // Don't reject - just log the error and proceed with normal validation
629 } 825 }
630 }, 826
827 // Continue with standard reference checking (same as default case)
828 let (addressable_refs, event_refs) = Self::extract_reference_tags(event);
829
830 // Check 1: Does this event reference an accepted repository?
831 match Self::find_accepted_repository(&database, &addressable_refs).await {
832 Ok(Some(addr_ref)) => {
833 tracing::debug!(
834 "Accepted PR event {}: references accepted repository {}",
835 event_id_str,
836 addr_ref
837 );
838 return PolicyResult::Accept;
839 }
840 Ok(None) => {
841 // No matching repositories, continue to next check
842 }
843 Err(e) => {
844 tracing::warn!(
845 "Database query failed for PR {}, rejecting (fail-secure): {}",
846 event_id_str,
847 e
848 );
849 return PolicyResult::Reject(format!("Database query failed: {}", e));
850 }
851 }
852
853 // Check 2: Does this event reference an accepted event?
854 match Self::find_accepted_event(&database, &event_refs).await {
855 Ok(Some(event_ref)) => {
856 tracing::debug!(
857 "Accepted PR event {}: references accepted event {}",
858 event_id_str,
859 event_ref
860 );
861 return PolicyResult::Accept;
862 }
863 Ok(None) => {
864 // No matching events, continue to next check
865 }
866 Err(e) => {
867 tracing::warn!(
868 "Database query failed for PR {}, rejecting (fail-secure): {}",
869 event_id_str,
870 e
871 );
872 return PolicyResult::Reject(format!("Database query failed: {}", e));
873 }
874 }
875
876 // Check 3: Is this event referenced by an accepted event?
877 match Self::is_referenced_by_accepted(&database, event).await {
878 Ok(true) => {
879 tracing::debug!(
880 "Accepted PR event {}: referenced by accepted event",
881 event_id_str
882 );
883 return PolicyResult::Accept;
884 }
885 Ok(false) => {
886 // No forward references found, continue to rejection
887 }
888 Err(e) => {
889 tracing::warn!(
890 "Database query failed for PR {}, rejecting (fail-secure): {}",
891 event_id_str,
892 e
893 );
894 return PolicyResult::Reject(format!("Database query failed: {}", e));
895 }
896 }
897
898 // No valid references found - reject as orphan event
899 tracing::info!(
900 "Rejected orphan PR event {}: no references to accepted repos or events",
901 event_id_str
902 );
903 PolicyResult::Reject(
904 "PR event must reference an accepted repository or accepted event"
905 .to_string(),
906 )
907 }
631 // GRASP-01: Check if event references accepted repositories or events 908 // GRASP-01: Check if event references accepted repositories or events
632 _ => { 909 _ => {
633 // Extract all reference tags from event 910 // Extract all reference tags from event
@@ -709,7 +986,7 @@ impl WritePolicy for Nip34WritePolicy {
709 event_refs.len() 986 event_refs.len()
710 ); 987 );
711 PolicyResult::Reject( 988 PolicyResult::Reject(
712 "Event must reference an accepted repository or accepted event".to_string() 989 "Event must reference an accepted repository or accepted event".to_string(),
713 ) 990 )
714 } 991 }
715 } 992 }
@@ -786,4 +1063,4 @@ pub fn create_relay(config: &Config) -> Result<RelayWithDatabase> {
786 relay: LocalRelay::new(builder), 1063 relay: LocalRelay::new(builder),
787 database, 1064 database,
788 }) 1065 })
789} \ No newline at end of file 1066}
diff --git a/src/nostr/events.rs b/src/nostr/events.rs
index 97688b1..6a62ccd 100644
--- a/src/nostr/events.rs
+++ b/src/nostr/events.rs
@@ -15,6 +15,12 @@ pub const KIND_REPOSITORY_ANNOUNCEMENT: u16 = 30617;
15/// NIP-34 Repository State Announcement (kind 30618) 15/// NIP-34 Repository State Announcement (kind 30618)
16pub const KIND_REPOSITORY_STATE: u16 = 30618; 16pub const KIND_REPOSITORY_STATE: u16 = 30618;
17 17
18/// NIP-34 Pull Request (kind 1618) - has `c` tag for commit
19pub const KIND_PR: u16 = 1618;
20
21/// NIP-34 Pull Request Update (kind 1619) - has `c` tag for commit
22pub const KIND_PR_UPDATE: u16 = 1619;
23
18/// Repository announcement details extracted from NIP-34 event 24/// Repository announcement details extracted from NIP-34 event
19#[derive(Debug, Clone)] 25#[derive(Debug, Clone)]
20pub struct RepositoryAnnouncement { 26pub struct RepositoryAnnouncement {