diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2026-01-12 17:40:25 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2026-01-12 17:40:25 +0000 |
| commit | c29191b1e1239e931c575a926ec9480e594476d6 (patch) | |
| tree | 6fcb776ba34b6fab766ceb613997b07b18e780df /src/nostr/policy | |
| parent | 2b8992631b9dedcfd4ea44e8565b14ac8a5ed8ea (diff) | |
feat(grasp-05): implement archive mode for backup/mirror operation
Implements GRASP-05 specification for accepting repository announcements
that don't list this relay, enabling archive, mirror, and backup use cases.
Core Features:
- Three whitelist formats: <npub>, <npub>/<identifier>, <identifier>
- Archive-all mode for complete ecosystem mirrors
- Fail-fast npub validation at startup
- Read-only enforcement (archived repos reject pushes)
- Full GRASP-02 sync (git data + Nostr events)
- Dynamic archive status (no flags/metadata)
Implementation:
- Add ArchiveWhitelistEntry enum with Pubkey/Repository/Identifier variants
- Add ArchiveConfig with validation and matching logic
- Update AnnouncementResult to include AcceptArchive variant
- Refactor validate_announcement() to return AnnouncementResult with archive check
- Update AnnouncementPolicy with catch-all pattern for cleaner code
- Wire archive config through builder and policy layers
Configuration:
- NGIT_ARCHIVE_ALL: Accept all announcements (⚠️ storage risk)
- NGIT_ARCHIVE_WHITELIST: Comma-separated whitelist entries
- Updated docs, .env.example, and nix/module.nix
Testing:
- 28 unit tests for config parsing and whitelist matching
- 7 integration tests for archive mode validation
- All 296 tests passing
Validation Priority:
1. Lists our service → Accept (GRASP-01, read/write)
2. Is maintainer → AcceptMaintainer (multi-maintainer, read/write)
3. Matches archive config → AcceptArchive (GRASP-05, read-only)
4. None of above → Reject
Security Considerations:
- Archive-all mode has storage/bandwidth DoS risk
- Identifier-only format matches any pubkey (use npub/identifier for high-value)
- Invalid npubs cause startup failure (fail-fast)
Documentation:
- Concise explanation focused on rationale
- Reference docs updated with all config options
- README updated to reflect completed feature
- Removed from roadmap, added to compliance section
See docs/explanation/grasp-05-archive.md for details.
Diffstat (limited to 'src/nostr/policy')
| -rw-r--r-- | src/nostr/policy/announcement.rs | 42 |
1 files changed, 25 insertions, 17 deletions
diff --git a/src/nostr/policy/announcement.rs b/src/nostr/policy/announcement.rs index 61840fb..db87976 100644 --- a/src/nostr/policy/announcement.rs +++ b/src/nostr/policy/announcement.rs | |||
| @@ -5,15 +5,18 @@ | |||
| 5 | use nostr_relay_builder::prelude::{Alphabet, Event, Filter, Kind, PublicKey, SingleLetterTag}; | 5 | use nostr_relay_builder::prelude::{Alphabet, Event, Filter, Kind, PublicKey, SingleLetterTag}; |
| 6 | 6 | ||
| 7 | use super::PolicyContext; | 7 | use super::PolicyContext; |
| 8 | use crate::config::ArchiveConfig; | ||
| 8 | use crate::nostr::events::{validate_announcement, RepositoryAnnouncement}; | 9 | use crate::nostr::events::{validate_announcement, RepositoryAnnouncement}; |
| 9 | 10 | ||
| 10 | /// Result of announcement policy evaluation | 11 | /// Result of announcement policy evaluation |
| 11 | #[derive(Debug)] | 12 | #[derive(Debug, Clone, PartialEq)] |
| 12 | pub enum AnnouncementResult { | 13 | pub enum AnnouncementResult { |
| 13 | /// Accept: Event passes validation | 14 | /// Accept: Event lists our service (GRASP-01 compliant) |
| 14 | Accept, | 15 | Accept, |
| 15 | /// Accept as maintainer: Event accepted via maintainer exception | 16 | /// Accept as maintainer: Event accepted via maintainer exception (multi-maintainer) |
| 16 | AcceptMaintainer, | 17 | AcceptMaintainer, |
| 18 | /// Accept as archive: Event accepted via GRASP-05 archive whitelist (read-only) | ||
| 19 | AcceptArchive, | ||
| 17 | /// Reject: Event fails validation with reason | 20 | /// Reject: Event fails validation with reason |
| 18 | Reject(String), | 21 | Reject(String), |
| 19 | } | 22 | } |
| @@ -22,31 +25,34 @@ pub enum AnnouncementResult { | |||
| 22 | #[derive(Clone)] | 25 | #[derive(Clone)] |
| 23 | pub struct AnnouncementPolicy { | 26 | pub struct AnnouncementPolicy { |
| 24 | ctx: PolicyContext, | 27 | ctx: PolicyContext, |
| 28 | archive_config: ArchiveConfig, | ||
| 25 | } | 29 | } |
| 26 | 30 | ||
| 27 | impl AnnouncementPolicy { | 31 | impl AnnouncementPolicy { |
| 28 | pub fn new(ctx: PolicyContext) -> Self { | 32 | pub fn new(ctx: PolicyContext, archive_config: ArchiveConfig) -> Self { |
| 29 | Self { ctx } | 33 | Self { |
| 34 | ctx, | ||
| 35 | archive_config, | ||
| 36 | } | ||
| 30 | } | 37 | } |
| 31 | 38 | ||
| 32 | /// Validate a repository announcement event | 39 | /// Validate a repository announcement event |
| 33 | /// | 40 | /// |
| 34 | /// Returns `Accept` if the announcement lists the service properly, | 41 | /// Returns `Accept` if the announcement lists the service properly, |
| 35 | /// `AcceptMaintainer` if accepted via maintainer exception, | 42 | /// `AcceptMaintainer` if accepted via maintainer exception, |
| 43 | /// `AcceptArchive` if accepted via GRASP-05 archive config, | ||
| 36 | /// or `Reject` with reason. | 44 | /// or `Reject` with reason. |
| 37 | pub async fn validate(&self, event: &Event) -> AnnouncementResult { | 45 | pub async fn validate(&self, event: &Event) -> AnnouncementResult { |
| 38 | // First, try normal validation (announcement lists service) | 46 | // First, try validation (GRASP-01 + GRASP-05) |
| 39 | match validate_announcement(event, &self.ctx.domain) { | 47 | let validation_result = |
| 40 | Ok(_) => AnnouncementResult::Accept, | 48 | validate_announcement(event, &self.ctx.domain, &self.archive_config); |
| 41 | Err(validation_err) => { | ||
| 42 | // Validation failed - check if this is a recursive maintainer announcement | ||
| 43 | // GRASP-01 Exception: Accept announcements from recursive maintainers | ||
| 44 | // even without listing the service, for chain discovery and GRASP-02 sync | ||
| 45 | 49 | ||
| 46 | // Try to parse the announcement to get identifier | 50 | match validation_result { |
| 51 | AnnouncementResult::Reject(reason) => { | ||
| 52 | // Validation failed - check maintainer exception | ||
| 53 | // GRASP-01 Exception: Accept announcements from recursive maintainers | ||
| 47 | match RepositoryAnnouncement::from_event(event.clone()) { | 54 | match RepositoryAnnouncement::from_event(event.clone()) { |
| 48 | Ok(announcement) => { | 55 | Ok(announcement) => { |
| 49 | // Check if author is listed as maintainer in any existing announcement | ||
| 50 | match self | 56 | match self |
| 51 | .is_maintainer_in_any_announcement( | 57 | .is_maintainer_in_any_announcement( |
| 52 | &announcement.identifier, | 58 | &announcement.identifier, |
| @@ -55,16 +61,18 @@ impl AnnouncementPolicy { | |||
| 55 | .await | 61 | .await |
| 56 | { | 62 | { |
| 57 | Ok(true) => AnnouncementResult::AcceptMaintainer, | 63 | Ok(true) => AnnouncementResult::AcceptMaintainer, |
| 58 | Ok(false) => AnnouncementResult::Reject(validation_err.to_string()), | 64 | Ok(false) => AnnouncementResult::Reject(reason), |
| 59 | Err(_) => { | 65 | Err(_) => { |
| 60 | // Fail-secure: reject on database errors | 66 | // Fail-secure: reject on database errors |
| 61 | AnnouncementResult::Reject(validation_err.to_string()) | 67 | AnnouncementResult::Reject(reason) |
| 62 | } | 68 | } |
| 63 | } | 69 | } |
| 64 | } | 70 | } |
| 65 | Err(_) => AnnouncementResult::Reject(validation_err.to_string()), | 71 | Err(_) => AnnouncementResult::Reject(reason), |
| 66 | } | 72 | } |
| 67 | } | 73 | } |
| 74 | // Accept, AcceptArchive, or AcceptMaintainer - return as-is | ||
| 75 | result => result, | ||
| 68 | } | 76 | } |
| 69 | } | 77 | } |
| 70 | 78 | ||