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/builder.rs | |
| 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/builder.rs')
| -rw-r--r-- | src/nostr/builder.rs | 53 |
1 files changed, 50 insertions, 3 deletions
diff --git a/src/nostr/builder.rs b/src/nostr/builder.rs index c010854..deee641 100644 --- a/src/nostr/builder.rs +++ b/src/nostr/builder.rs | |||
| @@ -55,10 +55,11 @@ impl Nip34WritePolicy { | |||
| 55 | database: SharedDatabase, | 55 | database: SharedDatabase, |
| 56 | git_data_path: impl Into<std::path::PathBuf>, | 56 | git_data_path: impl Into<std::path::PathBuf>, |
| 57 | purgatory: std::sync::Arc<crate::purgatory::Purgatory>, | 57 | purgatory: std::sync::Arc<crate::purgatory::Purgatory>, |
| 58 | archive_config: crate::config::ArchiveConfig, | ||
| 58 | ) -> Self { | 59 | ) -> Self { |
| 59 | let ctx = PolicyContext::new(domain, database, git_data_path, purgatory); | 60 | let ctx = PolicyContext::new(domain, database, git_data_path, purgatory); |
| 60 | Self { | 61 | Self { |
| 61 | announcement_policy: AnnouncementPolicy::new(ctx.clone()), | 62 | announcement_policy: AnnouncementPolicy::new(ctx.clone(), archive_config), |
| 62 | state_policy: StatePolicy::new(ctx.clone()), | 63 | state_policy: StatePolicy::new(ctx.clone()), |
| 63 | pr_event_policy: PrEventPolicy::new(ctx.clone()), | 64 | pr_event_policy: PrEventPolicy::new(ctx.clone()), |
| 64 | related_event_policy: RelatedEventPolicy::new(ctx.clone()), | 65 | related_event_policy: RelatedEventPolicy::new(ctx.clone()), |
| @@ -147,6 +148,34 @@ impl Nip34WritePolicy { | |||
| 147 | } | 148 | } |
| 148 | } | 149 | } |
| 149 | } | 150 | } |
| 151 | AnnouncementResult::AcceptArchive => { | ||
| 152 | // GRASP-05: Archive mode - accept announcement but don't create bare repository | ||
| 153 | match RepositoryAnnouncement::from_event(event.clone()) { | ||
| 154 | Ok(announcement) => { | ||
| 155 | tracing::info!( | ||
| 156 | "Accepted archive announcement {} for {}/{} (GRASP-05 read-only mirror)", | ||
| 157 | event_id_str, | ||
| 158 | announcement.owner_npub(), | ||
| 159 | announcement.identifier | ||
| 160 | ); | ||
| 161 | // Don't create bare repository for archived announcements | ||
| 162 | |||
| 163 | // Check purgatory for state events that might now be authorized | ||
| 164 | self.check_purgatory_state_events_for_identifier(&announcement.identifier) | ||
| 165 | .await; | ||
| 166 | |||
| 167 | WritePolicyResult::Accept | ||
| 168 | } | ||
| 169 | Err(e) => { | ||
| 170 | tracing::warn!( | ||
| 171 | "Failed to parse archive announcement {}: {}", | ||
| 172 | event_id_str, | ||
| 173 | e | ||
| 174 | ); | ||
| 175 | WritePolicyResult::reject(format!("Failed to parse announcement: {}", e)) | ||
| 176 | } | ||
| 177 | } | ||
| 178 | } | ||
| 150 | AnnouncementResult::Reject(reason) => { | 179 | AnnouncementResult::Reject(reason) => { |
| 151 | tracing::warn!( | 180 | tracing::warn!( |
| 152 | "Rejected repository announcement {}: {}", | 181 | "Rejected repository announcement {}: {}", |
| @@ -539,9 +568,27 @@ pub async fn create_relay( | |||
| 539 | // Clone Arc for the write policy so both relay and policy can access the database | 568 | // Clone Arc for the write policy so both relay and policy can access the database |
| 540 | let git_data_path = config.effective_git_data_path(); | 569 | let git_data_path = config.effective_git_data_path(); |
| 541 | 570 | ||
| 571 | // Parse archive configuration | ||
| 572 | let archive_config = config | ||
| 573 | .archive_config() | ||
| 574 | .map_err(|e| anyhow::anyhow!("Failed to parse archive configuration: {}", e))?; | ||
| 575 | |||
| 576 | if archive_config.enabled() { | ||
| 577 | tracing::info!( | ||
| 578 | "GRASP-05 archive mode enabled: archive_all={}, whitelist_entries={}", | ||
| 579 | archive_config.archive_all, | ||
| 580 | archive_config.whitelist.len() | ||
| 581 | ); | ||
| 582 | } | ||
| 583 | |||
| 542 | // Create write policy with purgatory integration | 584 | // Create write policy with purgatory integration |
| 543 | let write_policy = | 585 | let write_policy = Nip34WritePolicy::new( |
| 544 | Nip34WritePolicy::new(&config.domain, database.clone(), &git_data_path, purgatory); | 586 | &config.domain, |
| 587 | database.clone(), | ||
| 588 | &git_data_path, | ||
| 589 | purgatory, | ||
| 590 | archive_config, | ||
| 591 | ); | ||
| 545 | 592 | ||
| 546 | let relay = LocalRelayBuilder::default() | 593 | let relay = LocalRelayBuilder::default() |
| 547 | .database(database.clone()) | 594 | .database(database.clone()) |