upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/nostr/builder.rs
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2025-12-03 17:06:59 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2025-12-03 17:06:59 +0000
commitde683147779eaf57376a90e73bbdd123846a01e3 (patch)
tree3f4ecaf6e49bdb3d7b338df669ecd8cead98e2e6 /src/nostr/builder.rs
parentdd1b44132199aa72c2b699e1160fbe6b885f0ef6 (diff)
feat: accept maintainer announcements without service listing
Diffstat (limited to 'src/nostr/builder.rs')
-rw-r--r--src/nostr/builder.rs168
1 files changed, 140 insertions, 28 deletions
diff --git a/src/nostr/builder.rs b/src/nostr/builder.rs
index 904cba4..00e5969 100644
--- a/src/nostr/builder.rs
+++ b/src/nostr/builder.rs
@@ -39,6 +39,8 @@ struct AlignmentResult {
39/// 39///
40/// Validates all events according to GRASP-01 specification: 40/// Validates all events according to GRASP-01 specification:
41/// - Repository announcements must list service in clone and relays tags 41/// - Repository announcements must list service in clone and relays tags
42/// EXCEPTION: Recursive maintainer announcements are accepted even without
43/// listing the service, to enable maintainer chain discovery and GRASP-02 sync
42/// - Repository state announcements must have valid structure 44/// - Repository state announcements must have valid structure
43/// - Other events must reference accepted repositories or events 45/// - Other events must reference accepted repositories or events
44/// - Forward references are supported (events referenced by accepted events) 46/// - Forward references are supported (events referenced by accepted events)
@@ -442,6 +444,57 @@ impl Nip34WritePolicy {
442 result 444 result
443 } 445 }
444 446
447 /// Check if a pubkey is listed as a maintainer in any announcement for this identifier
448 ///
449 /// A pubkey is considered a maintainer if:
450 /// 1. They are the owner (pubkey) of an accepted announcement with this identifier, OR
451 /// 2. They are listed in the maintainers tag of ANY announcement with this identifier
452 ///
453 /// This enables accepting announcements from maintainers even when they don't list
454 /// this GRASP server, for maintainer chain discovery and GRASP-02 sync.
455 async fn is_maintainer_in_any_announcement(
456 database: &SharedDatabase,
457 identifier: &str,
458 author: &PublicKey,
459 ) -> Result<bool, String> {
460 // Query all announcements with this identifier that are already in the database
461 let filter = Filter::new()
462 .kind(Kind::from(KIND_REPOSITORY_ANNOUNCEMENT))
463 .custom_tag(
464 SingleLetterTag::lowercase(Alphabet::D),
465 identifier.to_string(),
466 );
467
468 let announcements: Vec<Event> = match database.query(filter).await {
469 Ok(events) => events.into_iter().collect(),
470 Err(e) => return Err(format!("Database query failed: {}", e)),
471 };
472
473 if announcements.is_empty() {
474 // No existing announcements for this identifier - author cannot be a maintainer
475 return Ok(false);
476 }
477
478 let author_hex = author.to_hex();
479
480 // Check each announcement to see if author is listed as a maintainer
481 for event in &announcements {
482 // Check if author is the owner of this announcement
483 if event.pubkey == *author {
484 return Ok(true);
485 }
486
487 // Check if author is listed in the maintainers tag
488 if let Ok(announcement) = RepositoryAnnouncement::from_event(event.clone()) {
489 if announcement.maintainers.contains(&author_hex) {
490 return Ok(true);
491 }
492 }
493 }
494
495 Ok(false)
496 }
497
445 /// Extract all reference tags from an event (a, A, q, e, E) 498 /// Extract all reference tags from an event (a, A, q, e, E)
446 /// Returns (addressable_refs, event_refs) 499 /// Returns (addressable_refs, event_refs)
447 fn extract_reference_tags(event: &Event) -> (Vec<String>, Vec<EventId>) { 500 fn extract_reference_tags(event: &Event) -> (Vec<String>, Vec<EventId>) {
@@ -862,43 +915,102 @@ impl WritePolicy for Nip34WritePolicy {
862 let event_id_str = event.id.to_bech32().unwrap_or_else(|_| event.id.to_hex()); 915 let event_id_str = event.id.to_bech32().unwrap_or_else(|_| event.id.to_hex());
863 916
864 match event.kind.as_u16() { 917 match event.kind.as_u16() {
865 KIND_REPOSITORY_ANNOUNCEMENT => match validate_announcement(event, &domain) { 918 KIND_REPOSITORY_ANNOUNCEMENT => {
866 Ok(_) => { 919 // First, try normal validation (announcement lists service)
867 // Parse announcement to get repository details 920 match validate_announcement(event, &domain) {
868 match RepositoryAnnouncement::from_event(event.clone()) { 921 Ok(_) => {
869 Ok(announcement) => { 922 // Parse announcement to get repository details
870 // Try to create bare repository if it doesn't exist 923 match RepositoryAnnouncement::from_event(event.clone()) {
871 if let Err(e) = self.ensure_bare_repository(&announcement) { 924 Ok(announcement) => {
925 // Try to create bare repository if it doesn't exist
926 if let Err(e) = self.ensure_bare_repository(&announcement) {
927 tracing::warn!(
928 "Failed to create bare repository for {}: {}",
929 event_id_str,
930 e
931 );
932 // Note: We still accept the event even if repo creation fails
933 // The git operation failure shouldn't prevent event acceptance
934 }
935
936 tracing::debug!(
937 "Accepted repository announcement: {}",
938 event_id_str
939 );
940 PolicyResult::Accept
941 }
942 Err(e) => {
872 tracing::warn!( 943 tracing::warn!(
873 "Failed to create bare repository for {}: {}", 944 "Failed to parse repository announcement {}: {}",
874 event_id_str, 945 event_id_str,
875 e 946 e
876 ); 947 );
877 // Note: We still accept the event even if repo creation fails 948 PolicyResult::Reject(format!(
878 // The git operation failure shouldn't prevent event acceptance 949 "Failed to parse announcement: {}",
950 e
951 ))
879 } 952 }
880
881 tracing::debug!(
882 "Accepted repository announcement: {}",
883 event_id_str
884 );
885 PolicyResult::Accept
886 } 953 }
887 Err(e) => { 954 }
888 tracing::warn!( 955 Err(validation_err) => {
889 "Failed to parse repository announcement {}: {}", 956 // Validation failed - check if this is a recursive maintainer announcement
890 event_id_str, 957 // GRASP-01 Exception: Accept announcements from recursive maintainers
891 e 958 // even without listing the service, for chain discovery and GRASP-02 sync
892 ); 959
893 PolicyResult::Reject(format!("Failed to parse announcement: {}", e)) 960 // Try to parse the announcement to get identifier
961 match RepositoryAnnouncement::from_event(event.clone()) {
962 Ok(announcement) => {
963 // Check if author is listed as maintainer in any existing announcement
964 match Self::is_maintainer_in_any_announcement(
965 &database,
966 &announcement.identifier,
967 &event.pubkey,
968 )
969 .await
970 {
971 Ok(true) => {
972 tracing::info!(
973 "Accepted maintainer announcement {} (author {} is listed as maintainer for {})",
974 event_id_str,
975 event.pubkey.to_hex(),
976 announcement.identifier
977 );
978 // Don't create bare repository for external announcements
979 // (they point to other servers)
980 PolicyResult::Accept
981 }
982 Ok(false) => {
983 tracing::warn!(
984 "Rejected repository announcement {}: {} (not a maintainer)",
985 event_id_str,
986 validation_err
987 );
988 PolicyResult::Reject(validation_err.to_string())
989 }
990 Err(e) => {
991 tracing::warn!(
992 "Failed to check maintainer status for {}: {}",
993 event_id_str,
994 e
995 );
996 // Fail-secure: reject on database errors
997 PolicyResult::Reject(validation_err.to_string())
998 }
999 }
1000 }
1001 Err(parse_err) => {
1002 tracing::warn!(
1003 "Rejected repository announcement {}: {} (parse error: {})",
1004 event_id_str,
1005 validation_err,
1006 parse_err
1007 );
1008 PolicyResult::Reject(validation_err.to_string())
1009 }
894 } 1010 }
895 } 1011 }
896 } 1012 }
897 Err(e) => { 1013 }
898 tracing::warn!("Rejected repository announcement {}: {}", event_id_str, e);
899 PolicyResult::Reject(e.to_string())
900 }
901 },
902 KIND_REPOSITORY_STATE => match validate_state(event) { 1014 KIND_REPOSITORY_STATE => match validate_state(event) {
903 Ok(_) => { 1015 Ok(_) => {
904 // Parse state to get HEAD and branch info 1016 // Parse state to get HEAD and branch info