upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/nostr/policy
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/policy
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/policy')
-rw-r--r--src/nostr/policy/announcement.rs42
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 @@
5use nostr_relay_builder::prelude::{Alphabet, Event, Filter, Kind, PublicKey, SingleLetterTag}; 5use nostr_relay_builder::prelude::{Alphabet, Event, Filter, Kind, PublicKey, SingleLetterTag};
6 6
7use super::PolicyContext; 7use super::PolicyContext;
8use crate::config::ArchiveConfig;
8use crate::nostr::events::{validate_announcement, RepositoryAnnouncement}; 9use 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)]
12pub enum AnnouncementResult { 13pub 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)]
23pub struct AnnouncementPolicy { 26pub struct AnnouncementPolicy {
24 ctx: PolicyContext, 27 ctx: PolicyContext,
28 archive_config: ArchiveConfig,
25} 29}
26 30
27impl AnnouncementPolicy { 31impl 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