diff options
Diffstat (limited to 'src/nostr')
| -rw-r--r-- | src/nostr/builder.rs | 159 | ||||
| -rw-r--r-- | src/nostr/events.rs | 16 | ||||
| -rw-r--r-- | src/nostr/policy/state.rs | 9 |
3 files changed, 155 insertions, 29 deletions
diff --git a/src/nostr/builder.rs b/src/nostr/builder.rs index 34014db..3baa2ff 100644 --- a/src/nostr/builder.rs +++ b/src/nostr/builder.rs | |||
| @@ -98,6 +98,62 @@ impl Nip34WritePolicy { | |||
| 98 | self.ctx.set_local_relay(relay); | 98 | self.ctx.set_local_relay(relay); |
| 99 | } | 99 | } |
| 100 | 100 | ||
| 101 | /// Extract repository identifier from event's 'd' tag. | ||
| 102 | /// | ||
| 103 | /// Used for structured logging when parsing fails - we try to extract | ||
| 104 | /// the identifier even if full parsing failed. | ||
| 105 | fn extract_identifier_from_event(event: &Event) -> String { | ||
| 106 | use nostr_relay_builder::prelude::TagKind; | ||
| 107 | event | ||
| 108 | .tags | ||
| 109 | .iter() | ||
| 110 | .find(|t| t.kind() == TagKind::d()) | ||
| 111 | .and_then(|t| t.content()) | ||
| 112 | .map(|s| s.to_string()) | ||
| 113 | .unwrap_or_else(|| "unknown".to_string()) | ||
| 114 | } | ||
| 115 | |||
| 116 | /// Extract ALL repository identifiers from PR event's 'a' tags. | ||
| 117 | /// | ||
| 118 | /// PR events can reference multiple repositories via multiple 'a' tags | ||
| 119 | /// (e.g., when there are multiple maintainers). Each tag has format | ||
| 120 | /// `30617:<owner_pubkey>:<identifier>`. | ||
| 121 | /// | ||
| 122 | /// Returns a vector of unique identifiers, or `["unknown"]` if none found. | ||
| 123 | fn extract_repos_from_pr_event(event: &Event) -> Vec<String> { | ||
| 124 | let repos: Vec<String> = event | ||
| 125 | .tags | ||
| 126 | .iter() | ||
| 127 | .filter_map(|tag| { | ||
| 128 | let tag_vec = tag.clone().to_vec(); | ||
| 129 | if tag_vec.len() >= 2 && tag_vec[0] == "a" && tag_vec[1].starts_with("30617:") { | ||
| 130 | // Format: 30617:<owner_pubkey>:<identifier> | ||
| 131 | let parts: Vec<&str> = tag_vec[1].split(':').collect(); | ||
| 132 | if parts.len() >= 3 { | ||
| 133 | Some(parts[2].to_string()) | ||
| 134 | } else { | ||
| 135 | None | ||
| 136 | } | ||
| 137 | } else { | ||
| 138 | None | ||
| 139 | } | ||
| 140 | }) | ||
| 141 | .collect(); | ||
| 142 | |||
| 143 | // Deduplicate while preserving order | ||
| 144 | let mut seen = std::collections::HashSet::new(); | ||
| 145 | let unique_repos: Vec<String> = repos | ||
| 146 | .into_iter() | ||
| 147 | .filter(|r| seen.insert(r.clone())) | ||
| 148 | .collect(); | ||
| 149 | |||
| 150 | if unique_repos.is_empty() { | ||
| 151 | vec!["unknown".to_string()] | ||
| 152 | } else { | ||
| 153 | unique_repos | ||
| 154 | } | ||
| 155 | } | ||
| 156 | |||
| 101 | /// Handle repository announcement event | 157 | /// Handle repository announcement event |
| 102 | async fn handle_announcement(&self, event: &Event) -> WritePolicyResult { | 158 | async fn handle_announcement(&self, event: &Event) -> WritePolicyResult { |
| 103 | let event_id_str = event.id.to_bech32().unwrap_or_else(|_| event.id.to_hex()); | 159 | let event_id_str = event.id.to_bech32().unwrap_or_else(|_| event.id.to_hex()); |
| @@ -129,10 +185,21 @@ impl Nip34WritePolicy { | |||
| 129 | WritePolicyResult::Accept | 185 | WritePolicyResult::Accept |
| 130 | } | 186 | } |
| 131 | Err(e) => { | 187 | Err(e) => { |
| 188 | let npub = event | ||
| 189 | .pubkey | ||
| 190 | .to_bech32() | ||
| 191 | .unwrap_or_else(|_| event.pubkey.to_hex()); | ||
| 192 | let event_id_short = &event.id.to_hex()[..12]; | ||
| 193 | // Try to extract repo identifier from 'd' tag even if parsing failed | ||
| 194 | let repo = Self::extract_identifier_from_event(event); | ||
| 195 | // Structured log for migration scripts | ||
| 132 | tracing::warn!( | 196 | tracing::warn!( |
| 133 | "Failed to parse repository announcement {}: {}", | 197 | "[PARSE_FAIL] kind={} event_id={}... reason=\"{}\" repo={} npub={}", |
| 134 | event_id_str, | 198 | event.kind.as_u16(), |
| 135 | e | 199 | event_id_short, |
| 200 | e, | ||
| 201 | repo, | ||
| 202 | npub | ||
| 136 | ); | 203 | ); |
| 137 | WritePolicyResult::reject(format!("Failed to parse announcement: {}", e)) | 204 | WritePolicyResult::reject(format!("Failed to parse announcement: {}", e)) |
| 138 | } | 205 | } |
| @@ -157,10 +224,21 @@ impl Nip34WritePolicy { | |||
| 157 | WritePolicyResult::Accept | 224 | WritePolicyResult::Accept |
| 158 | } | 225 | } |
| 159 | Err(e) => { | 226 | Err(e) => { |
| 227 | let npub = event | ||
| 228 | .pubkey | ||
| 229 | .to_bech32() | ||
| 230 | .unwrap_or_else(|_| event.pubkey.to_hex()); | ||
| 231 | let event_id_short = &event.id.to_hex()[..12]; | ||
| 232 | // Try to extract repo identifier from 'd' tag even if parsing failed | ||
| 233 | let repo = Self::extract_identifier_from_event(event); | ||
| 234 | // Structured log for migration scripts | ||
| 160 | tracing::warn!( | 235 | tracing::warn!( |
| 161 | "Failed to parse maintainer announcement {}: {}", | 236 | "[PARSE_FAIL] kind={} event_id={}... reason=\"{}\" repo={} npub={}", |
| 162 | event_id_str, | 237 | event.kind.as_u16(), |
| 163 | e | 238 | event_id_short, |
| 239 | e, | ||
| 240 | repo, | ||
| 241 | npub | ||
| 164 | ); | 242 | ); |
| 165 | WritePolicyResult::reject(format!("Failed to parse announcement: {}", e)) | 243 | WritePolicyResult::reject(format!("Failed to parse announcement: {}", e)) |
| 166 | } | 244 | } |
| @@ -183,8 +261,6 @@ impl Nip34WritePolicy { | |||
| 183 | /// * `event` - The state event to validate | 261 | /// * `event` - The state event to validate |
| 184 | /// * `is_synced` - True if this event came from proactive sync (vs user-submitted) | 262 | /// * `is_synced` - True if this event came from proactive sync (vs user-submitted) |
| 185 | async fn handle_state(&self, event: &Event, is_synced: bool) -> WritePolicyResult { | 263 | async fn handle_state(&self, event: &Event, is_synced: bool) -> WritePolicyResult { |
| 186 | let event_id_str = event.id.to_bech32().unwrap_or_else(|_| event.id.to_hex()); | ||
| 187 | |||
| 188 | match self.state_policy.validate(event) { | 264 | match self.state_policy.validate(event) { |
| 189 | StateResult::Accept => { | 265 | StateResult::Accept => { |
| 190 | // Process state alignment asynchronously | 266 | // Process state alignment asynchronously |
| @@ -195,7 +271,22 @@ impl Nip34WritePolicy { | |||
| 195 | { | 271 | { |
| 196 | Ok(poilicy_result) => poilicy_result, | 272 | Ok(poilicy_result) => poilicy_result, |
| 197 | Err(e) => { | 273 | Err(e) => { |
| 198 | tracing::warn!("Failed to process state event {}: {}", event_id_str, e); | 274 | let npub = event |
| 275 | .pubkey | ||
| 276 | .to_bech32() | ||
| 277 | .unwrap_or_else(|_| event.pubkey.to_hex()); | ||
| 278 | let event_id_short = &event.id.to_hex()[..12]; | ||
| 279 | // Try to extract repo identifier from 'd' tag even if parsing failed | ||
| 280 | let repo = Self::extract_identifier_from_event(event); | ||
| 281 | // Structured log for migration scripts | ||
| 282 | tracing::warn!( | ||
| 283 | "[PARSE_FAIL] kind={} event_id={}... reason=\"{}\" repo={} npub={}", | ||
| 284 | event.kind.as_u16(), | ||
| 285 | event_id_short, | ||
| 286 | e, | ||
| 287 | repo, | ||
| 288 | npub | ||
| 289 | ); | ||
| 199 | // reject if processing failed | 290 | // reject if processing failed |
| 200 | WritePolicyResult::Reject { | 291 | WritePolicyResult::Reject { |
| 201 | status: false, | 292 | status: false, |
| @@ -205,7 +296,22 @@ impl Nip34WritePolicy { | |||
| 205 | } | 296 | } |
| 206 | } | 297 | } |
| 207 | StateResult::Reject(reason) => { | 298 | StateResult::Reject(reason) => { |
| 208 | tracing::warn!("Rejected repository state {}: {}", event_id_str, reason); | 299 | let npub = event |
| 300 | .pubkey | ||
| 301 | .to_bech32() | ||
| 302 | .unwrap_or_else(|_| event.pubkey.to_hex()); | ||
| 303 | let event_id_short = &event.id.to_hex()[..12]; | ||
| 304 | // Try to extract repo identifier from 'd' tag even if parsing failed | ||
| 305 | let repo = Self::extract_identifier_from_event(event); | ||
| 306 | // Structured log for migration scripts | ||
| 307 | tracing::warn!( | ||
| 308 | "[PARSE_FAIL] kind={} event_id={}... reason=\"{}\" repo={} npub={}", | ||
| 309 | event.kind.as_u16(), | ||
| 310 | event_id_short, | ||
| 311 | reason, | ||
| 312 | repo, | ||
| 313 | npub | ||
| 314 | ); | ||
| 209 | WritePolicyResult::reject(reason) | 315 | WritePolicyResult::reject(reason) |
| 210 | } | 316 | } |
| 211 | } | 317 | } |
| @@ -303,9 +409,12 @@ impl Nip34WritePolicy { | |||
| 303 | ); | 409 | ); |
| 304 | 410 | ||
| 305 | // Add to purgatory | 411 | // Add to purgatory |
| 306 | self.ctx | 412 | self.ctx.purgatory.add_pr( |
| 307 | .purgatory | 413 | event.clone(), |
| 308 | .add_pr(event.clone(), event.id.to_hex(), commit.clone()); | 414 | event.id.to_hex(), |
| 415 | commit.clone(), | ||
| 416 | is_synced, | ||
| 417 | ); | ||
| 309 | 418 | ||
| 310 | WritePolicyResult::Reject { | 419 | WritePolicyResult::Reject { |
| 311 | status: true, // Client sees OK | 420 | status: true, // Client sees OK |
| @@ -323,11 +432,25 @@ impl Nip34WritePolicy { | |||
| 323 | } | 432 | } |
| 324 | Err(e) => { | 433 | Err(e) => { |
| 325 | // Error checking git data - reject event | 434 | // Error checking git data - reject event |
| 326 | tracing::warn!( | 435 | let npub = event |
| 327 | "Failed to check git data for PR event {}: {}", | 436 | .pubkey |
| 328 | event_id_str, | 437 | .to_bech32() |
| 329 | e | 438 | .unwrap_or_else(|_| event.pubkey.to_hex()); |
| 330 | ); | 439 | let event_id_short = &event.id.to_hex()[..12]; |
| 440 | // Extract ALL repo identifiers from 'a' tags for PR events | ||
| 441 | // (PR events can reference multiple repos when there are multiple maintainers) | ||
| 442 | let repos = Self::extract_repos_from_pr_event(event); | ||
| 443 | // Structured log for migration scripts - log once per repo | ||
| 444 | for repo in &repos { | ||
| 445 | tracing::warn!( | ||
| 446 | "[PARSE_FAIL] kind={} event_id={}... reason=\"git data check failed: {}\" repo={} npub={}", | ||
| 447 | event.kind.as_u16(), | ||
| 448 | event_id_short, | ||
| 449 | e, | ||
| 450 | repo, | ||
| 451 | npub | ||
| 452 | ); | ||
| 453 | } | ||
| 331 | WritePolicyResult::reject(format!("Failed to check git data: {}", e)) | 454 | WritePolicyResult::reject(format!("Failed to check git data: {}", e)) |
| 332 | } | 455 | } |
| 333 | } | 456 | } |
diff --git a/src/nostr/events.rs b/src/nostr/events.rs index 718633e..a441742 100644 --- a/src/nostr/events.rs +++ b/src/nostr/events.rs | |||
| @@ -419,14 +419,14 @@ pub fn validate_announcement( | |||
| 419 | // GRASP-01: Normal mode - accept if announcement lists our service AND matches repository whitelist (if enabled) | 419 | // GRASP-01: Normal mode - accept if announcement lists our service AND matches repository whitelist (if enabled) |
| 420 | if lists_service && !archive_config.read_only { | 420 | if lists_service && !archive_config.read_only { |
| 421 | // Check repository whitelist if enabled | 421 | // Check repository whitelist if enabled |
| 422 | if repository_config.enabled() { | 422 | if repository_config.enabled() |
| 423 | if !repository_config.matches(&npub, &announcement.identifier) { | 423 | && !repository_config.matches(&npub, &announcement.identifier) |
| 424 | return AnnouncementResult::Reject(format!( | 424 | { |
| 425 | "Announcement lists service but does not match repository whitelist. \ | 425 | return AnnouncementResult::Reject(format!( |
| 426 | Repository {}/{} not in whitelist", | 426 | "Announcement lists service but does not match repository whitelist. \ |
| 427 | npub, announcement.identifier | 427 | Repository {}/{} not in whitelist", |
| 428 | )); | 428 | npub, announcement.identifier |
| 429 | } | 429 | )); |
| 430 | } | 430 | } |
| 431 | return AnnouncementResult::Accept; | 431 | return AnnouncementResult::Accept; |
| 432 | } | 432 | } |
diff --git a/src/nostr/policy/state.rs b/src/nostr/policy/state.rs index f94f004..3411077 100644 --- a/src/nostr/policy/state.rs +++ b/src/nostr/policy/state.rs | |||
| @@ -205,9 +205,12 @@ impl StatePolicy { | |||
| 205 | 205 | ||
| 206 | // If no git data - add to purgatory | 206 | // If no git data - add to purgatory |
| 207 | // (add_state automatically enqueues for background sync) | 207 | // (add_state automatically enqueues for background sync) |
| 208 | self.ctx | 208 | self.ctx.purgatory.add_state( |
| 209 | .purgatory | 209 | event.clone(), |
| 210 | .add_state(event.clone(), state.identifier.clone(), event.pubkey); | 210 | state.identifier.clone(), |
| 211 | event.pubkey, | ||
| 212 | is_synced, | ||
| 213 | ); | ||
| 211 | 214 | ||
| 212 | tracing::info!( | 215 | tracing::info!( |
| 213 | "state event added to purgatory: eventid: {}, identifier: {}", | 216 | "state event added to purgatory: eventid: {}, identifier: {}", |