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>2026-01-12 17:40:25 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2026-01-12 17:40:25 +0000
commitc29191b1e1239e931c575a926ec9480e594476d6 (patch)
tree6fcb776ba34b6fab766ceb613997b07b18e780df /src/nostr/builder.rs
parent2b8992631b9dedcfd4ea44e8565b14ac8a5ed8ea (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.rs53
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())