diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2025-12-03 17:06:59 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2025-12-03 17:06:59 +0000 |
| commit | de683147779eaf57376a90e73bbdd123846a01e3 (patch) | |
| tree | 3f4ecaf6e49bdb3d7b338df669ecd8cead98e2e6 /src/nostr/builder.rs | |
| parent | dd1b44132199aa72c2b699e1160fbe6b885f0ef6 (diff) | |
feat: accept maintainer announcements without service listing
Diffstat (limited to 'src/nostr/builder.rs')
| -rw-r--r-- | src/nostr/builder.rs | 168 |
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 |