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:
Diffstat (limited to 'src/nostr/policy')
-rw-r--r--src/nostr/policy/announcement.rs117
-rw-r--r--src/nostr/policy/state.rs10
2 files changed, 119 insertions, 8 deletions
diff --git a/src/nostr/policy/announcement.rs b/src/nostr/policy/announcement.rs
index 15a6e58..1118497 100644
--- a/src/nostr/policy/announcement.rs
+++ b/src/nostr/policy/announcement.rs
@@ -3,6 +3,7 @@
3/// Handles validation of NIP-34 repository announcements (kind 30617) 3/// Handles validation of NIP-34 repository announcements (kind 30617)
4/// according to GRASP-01 specification. 4/// according to GRASP-01 specification.
5use nostr_relay_builder::prelude::{Alphabet, Event, Filter, Kind, PublicKey, SingleLetterTag}; 5use nostr_relay_builder::prelude::{Alphabet, Event, Filter, Kind, PublicKey, SingleLetterTag};
6use std::collections::HashSet;
6 7
7use super::PolicyContext; 8use super::PolicyContext;
8use crate::config::Config; 9use crate::config::Config;
@@ -11,12 +12,14 @@ use crate::nostr::events::{validate_announcement, RepositoryAnnouncement};
11/// Result of announcement policy evaluation 12/// Result of announcement policy evaluation
12#[derive(Debug, Clone, PartialEq)] 13#[derive(Debug, Clone, PartialEq)]
13pub enum AnnouncementResult { 14pub enum AnnouncementResult {
14 /// Accept: Event lists our service (GRASP-01 compliant) 15 /// Accept: Event lists our service (GRASP-01 compliant) - replacement announcement
15 Accept, 16 Accept,
16 /// Accept as maintainer: Event accepted via maintainer exception (multi-maintainer) 17 /// Accept as maintainer: Event accepted via maintainer exception (multi-maintainer)
17 AcceptMaintainer, 18 AcceptMaintainer,
18 /// Accept as archive: Event accepted via GRASP-05 archive whitelist (read-only) 19 /// Accept as archive: Event accepted via GRASP-05 archive whitelist (read-only)
19 AcceptArchive, 20 AcceptArchive,
21 /// Accept to purgatory: New announcement, waiting for git data
22 AcceptPurgatory,
20 /// Reject: Event fails validation with reason 23 /// Reject: Event fails validation with reason
21 Reject(String), 24 Reject(String),
22} 25}
@@ -35,10 +38,12 @@ impl AnnouncementPolicy {
35 38
36 /// Validate a repository announcement event 39 /// Validate a repository announcement event
37 /// 40 ///
38 /// Returns `Accept` if the announcement lists the service properly, 41 /// Returns:
39 /// `AcceptMaintainer` if accepted via maintainer exception, 42 /// - `Accept` if this is a replacement announcement (active announcement exists)
40 /// `AcceptArchive` if accepted via GRASP-05 archive config, 43 /// - `AcceptPurgatory` if this is a new announcement (no active announcement exists)
41 /// or `Reject` with reason. 44 /// - `AcceptMaintainer` if accepted via maintainer exception
45 /// - `AcceptArchive` if accepted via GRASP-05 archive config
46 /// - `Reject` with reason if validation fails
42 pub async fn validate(&self, event: &Event) -> AnnouncementResult { 47 pub async fn validate(&self, event: &Event) -> AnnouncementResult {
43 // First, try validation (GRASP-01 + GRASP-05) 48 // First, try validation (GRASP-01 + GRASP-05)
44 let validation_result = validate_announcement(event, &self.config); 49 let validation_result = validate_announcement(event, &self.config);
@@ -67,11 +72,111 @@ impl AnnouncementPolicy {
67 Err(_) => AnnouncementResult::Reject(reason), 72 Err(_) => AnnouncementResult::Reject(reason),
68 } 73 }
69 } 74 }
70 // Accept, AcceptArchive, or AcceptMaintainer - return as-is 75 AnnouncementResult::Accept | AnnouncementResult::AcceptArchive => {
76 // Parse announcement to check for existing active announcement
77 match RepositoryAnnouncement::from_event(event.clone()) {
78 Ok(announcement) => {
79 // Check if there's already an active announcement for this (pubkey, identifier)
80 match self
81 .has_active_announcement(&event.pubkey, &announcement.identifier)
82 .await
83 {
84 Ok(true) => {
85 // Replacement announcement - accept immediately
86 tracing::debug!(
87 identifier = %announcement.identifier,
88 "Replacement announcement - accepting immediately"
89 );
90 validation_result
91 }
92 Ok(false) => {
93 // New announcement - route to purgatory
94 tracing::debug!(
95 identifier = %announcement.identifier,
96 "New announcement - routing to purgatory"
97 );
98 AnnouncementResult::AcceptPurgatory
99 }
100 Err(e) => {
101 tracing::warn!(
102 error = %e,
103 "Failed to check for existing announcement - rejecting"
104 );
105 AnnouncementResult::Reject(format!(
106 "Database error checking existing announcement: {}",
107 e
108 ))
109 }
110 }
111 }
112 Err(e) => AnnouncementResult::Reject(format!(
113 "Failed to parse announcement: {}",
114 e
115 )),
116 }
117 }
118 // AcceptPurgatory shouldn't come from validate_announcement, but handle it
71 result => result, 119 result => result,
72 } 120 }
73 } 121 }
74 122
123 /// Check if there's an active announcement in the database for this (pubkey, identifier)
124 async fn has_active_announcement(
125 &self,
126 pubkey: &PublicKey,
127 identifier: &str,
128 ) -> Result<bool, String> {
129 let filter = Filter::new()
130 .kind(Kind::GitRepoAnnouncement)
131 .author(*pubkey)
132 .custom_tag(
133 SingleLetterTag::lowercase(Alphabet::D),
134 identifier.to_string(),
135 );
136
137 let events: Vec<Event> = match self.ctx.database.query(filter).await {
138 Ok(events) => events.into_iter().collect(),
139 Err(e) => return Err(format!("Database query failed: {}", e)),
140 };
141
142 Ok(!events.is_empty())
143 }
144
145 /// Add an announcement to purgatory
146 ///
147 /// Creates the bare repository and stores the announcement in purgatory
148 /// until git data arrives.
149 pub fn add_to_purgatory(&self, event: &Event) -> Result<(), String> {
150 let announcement = RepositoryAnnouncement::from_event(event.clone())
151 .map_err(|e| format!("Failed to parse announcement: {}", e))?;
152
153 // Create bare repository
154 self.ensure_bare_repository(&announcement)?;
155
156 // Build repo path
157 let repo_path = self.ctx.git_data_path.join(announcement.repo_path());
158
159 // Extract relays from announcement
160 let relays: HashSet<String> = announcement.relays.iter().cloned().collect();
161
162 // Add to purgatory
163 self.ctx.purgatory.add_announcement(
164 event.clone(),
165 announcement.identifier.clone(),
166 event.pubkey,
167 repo_path,
168 relays,
169 );
170
171 tracing::info!(
172 identifier = %announcement.identifier,
173 event_id = %event.id,
174 "Added announcement to purgatory"
175 );
176
177 Ok(())
178 }
179
75 /// Create a bare git repository if it doesn't exist 180 /// Create a bare git repository if it doesn't exist
76 /// Path format: <git_data_path>/<npub>/<identifier>.git 181 /// Path format: <git_data_path>/<npub>/<identifier>.git
77 pub fn ensure_bare_repository( 182 pub fn ensure_bare_repository(
diff --git a/src/nostr/policy/state.rs b/src/nostr/policy/state.rs
index f94f004..4bfb513 100644
--- a/src/nostr/policy/state.rs
+++ b/src/nostr/policy/state.rs
@@ -10,7 +10,7 @@ use nostr_relay_builder::prelude::Event;
10 10
11use super::PolicyContext; 11use super::PolicyContext;
12use crate::git; 12use crate::git;
13use crate::git::authorization::fetch_repository_data; 13use crate::git::authorization::fetch_repository_data_with_purgatory;
14use crate::nostr::events::{validate_state, RepositoryAnnouncement, RepositoryState}; 14use crate::nostr::events::{validate_state, RepositoryAnnouncement, RepositoryState};
15 15
16/// Result of state policy evaluation 16/// Result of state policy evaluation
@@ -76,7 +76,13 @@ impl StatePolicy {
76 } 76 }
77 77
78 // Get all repositories and state events from db with identifier 78 // Get all repositories and state events from db with identifier
79 let db_repo_data = fetch_repository_data(&self.ctx.database, &state.identifier).await?; 79 // Include purgatory announcements for authorization
80 let db_repo_data = fetch_repository_data_with_purgatory(
81 &self.ctx.database,
82 &self.ctx.purgatory,
83 &state.identifier,
84 )
85 .await?;
80 86
81 // CRITICAL: Check if author is authorized via maintainer set 87 // CRITICAL: Check if author is authorized via maintainer set
82 // State events MUST be rejected if author is not in maintainer set of any accepted announcement 88 // State events MUST be rejected if author is not in maintainer set of any accepted announcement