diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2025-12-01 11:56:49 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2025-12-01 11:58:34 +0000 |
| commit | 7a78815e29b01c83f3d0ec195ba717a2eba8cd37 (patch) | |
| tree | 4c5ccd9b812f1d1d75ed218501192ddc5459fd12 /src/nostr | |
| parent | e6ceab90de1acad154624022a6036efac18abab6 (diff) | |
reject push when refs/nostr/<event-id> doesnt match known event and delete incorrect ref on event receive
Diffstat (limited to 'src/nostr')
| -rw-r--r-- | src/nostr/builder.rs | 389 | ||||
| -rw-r--r-- | src/nostr/events.rs | 6 |
2 files changed, 339 insertions, 56 deletions
diff --git a/src/nostr/builder.rs b/src/nostr/builder.rs index 7aa2b97..8e9926a 100644 --- a/src/nostr/builder.rs +++ b/src/nostr/builder.rs | |||
| @@ -14,8 +14,8 @@ use nostr_relay_builder::prelude::*; | |||
| 14 | use crate::config::{Config, DatabaseBackend}; | 14 | use crate::config::{Config, DatabaseBackend}; |
| 15 | use crate::git; | 15 | use crate::git; |
| 16 | use crate::nostr::events::{ | 16 | use crate::nostr::events::{ |
| 17 | validate_announcement, validate_state, RepositoryAnnouncement, RepositoryState, | 17 | validate_announcement, validate_state, RepositoryAnnouncement, RepositoryState, KIND_PR, |
| 18 | KIND_REPOSITORY_ANNOUNCEMENT, KIND_REPOSITORY_STATE, | 18 | KIND_PR_UPDATE, KIND_REPOSITORY_ANNOUNCEMENT, KIND_REPOSITORY_STATE, |
| 19 | }; | 19 | }; |
| 20 | 20 | ||
| 21 | /// NIP-34 Write Policy with Full GRASP-01 Event Validation | 21 | /// NIP-34 Write Policy with Full GRASP-01 Event Validation |
| @@ -36,7 +36,11 @@ pub struct Nip34WritePolicy { | |||
| 36 | } | 36 | } |
| 37 | 37 | ||
| 38 | impl Nip34WritePolicy { | 38 | impl Nip34WritePolicy { |
| 39 | pub fn new(domain: impl Into<String>, database: Arc<MemoryDatabase>, git_data_path: impl Into<PathBuf>) -> Self { | 39 | pub fn new( |
| 40 | domain: impl Into<String>, | ||
| 41 | database: Arc<MemoryDatabase>, | ||
| 42 | git_data_path: impl Into<PathBuf>, | ||
| 43 | ) -> Self { | ||
| 40 | Self { | 44 | Self { |
| 41 | domain: domain.into(), | 45 | domain: domain.into(), |
| 42 | database, | 46 | database, |
| @@ -48,7 +52,7 @@ impl Nip34WritePolicy { | |||
| 48 | /// Path format: <git_data_path>/<npub>/<identifier>.git | 52 | /// Path format: <git_data_path>/<npub>/<identifier>.git |
| 49 | fn ensure_bare_repository(&self, announcement: &RepositoryAnnouncement) -> Result<(), String> { | 53 | fn ensure_bare_repository(&self, announcement: &RepositoryAnnouncement) -> Result<(), String> { |
| 50 | let repo_path = self.git_data_path.join(&announcement.repo_path()); | 54 | let repo_path = self.git_data_path.join(&announcement.repo_path()); |
| 51 | 55 | ||
| 52 | // Check if repository already exists | 56 | // Check if repository already exists |
| 53 | if repo_path.exists() { | 57 | if repo_path.exists() { |
| 54 | tracing::debug!("Repository already exists at {}", repo_path.display()); | 58 | tracing::debug!("Repository already exists at {}", repo_path.display()); |
| @@ -56,13 +60,12 @@ impl Nip34WritePolicy { | |||
| 56 | } | 60 | } |
| 57 | 61 | ||
| 58 | // Create parent directory (npub directory) | 62 | // Create parent directory (npub directory) |
| 59 | let parent = repo_path.parent().ok_or_else(|| { | 63 | let parent = repo_path |
| 60 | format!("Invalid repository path: {}", repo_path.display()) | 64 | .parent() |
| 61 | })?; | 65 | .ok_or_else(|| format!("Invalid repository path: {}", repo_path.display()))?; |
| 62 | 66 | ||
| 63 | std::fs::create_dir_all(parent).map_err(|e| { | 67 | std::fs::create_dir_all(parent) |
| 64 | format!("Failed to create directory {}: {}", parent.display(), e) | 68 | .map_err(|e| format!("Failed to create directory {}: {}", parent.display(), e))?; |
| 65 | })?; | ||
| 66 | 69 | ||
| 67 | // Initialize bare repository using git command | 70 | // Initialize bare repository using git command |
| 68 | let output = std::process::Command::new("git") | 71 | let output = std::process::Command::new("git") |
| @@ -165,7 +168,11 @@ impl Nip34WritePolicy { | |||
| 165 | tracing::debug!( | 168 | tracing::debug!( |
| 166 | "Found authorized announcement for {}: owner={}, maintainer={}", | 169 | "Found authorized announcement for {}: owner={}, maintainer={}", |
| 167 | identifier, | 170 | identifier, |
| 168 | if is_owner { event.pubkey.to_hex() } else { "n/a".to_string() }, | 171 | if is_owner { |
| 172 | event.pubkey.to_hex() | ||
| 173 | } else { | ||
| 174 | "n/a".to_string() | ||
| 175 | }, | ||
| 169 | is_maintainer | 176 | is_maintainer |
| 170 | ); | 177 | ); |
| 171 | authorized.push(announcement); | 178 | authorized.push(announcement); |
| @@ -198,10 +205,7 @@ impl Nip34WritePolicy { | |||
| 198 | let head_ref = match &state.head { | 205 | let head_ref = match &state.head { |
| 199 | Some(h) => h, | 206 | Some(h) => h, |
| 200 | None => { | 207 | None => { |
| 201 | tracing::debug!( | 208 | tracing::debug!("State event for {} has no HEAD reference", state.identifier); |
| 202 | "State event for {} has no HEAD reference", | ||
| 203 | state.identifier | ||
| 204 | ); | ||
| 205 | return Ok(0); | 209 | return Ok(0); |
| 206 | } | 210 | } |
| 207 | }; | 211 | }; |
| @@ -232,11 +236,9 @@ impl Nip34WritePolicy { | |||
| 232 | }; | 236 | }; |
| 233 | 237 | ||
| 234 | // Find all announcements where state author is authorized | 238 | // Find all announcements where state author is authorized |
| 235 | let announcements = Self::find_authorized_announcements( | 239 | let announcements = |
| 236 | database, | 240 | Self::find_authorized_announcements(database, &state.identifier, &state.event.pubkey) |
| 237 | &state.identifier, | 241 | .await?; |
| 238 | &state.event.pubkey, | ||
| 239 | ).await?; | ||
| 240 | 242 | ||
| 241 | if announcements.is_empty() { | 243 | if announcements.is_empty() { |
| 242 | tracing::debug!( | 244 | tracing::debug!( |
| @@ -271,7 +273,7 @@ impl Nip34WritePolicy { | |||
| 271 | } | 273 | } |
| 272 | 274 | ||
| 273 | // Build repository path: <git_data_path>/<owner_npub>/<identifier>.git | 275 | // Build repository path: <git_data_path>/<owner_npub>/<identifier>.git |
| 274 | let repo_path = self.git_data_path.join(&announcement.repo_path()); | 276 | let repo_path = self.git_data_path.join(announcement.repo_path().clone()); |
| 275 | 277 | ||
| 276 | match git::try_set_head_if_available(&repo_path, head_ref, head_commit) { | 278 | match git::try_set_head_if_available(&repo_path, head_ref, head_commit) { |
| 277 | Ok(true) => { | 279 | Ok(true) => { |
| @@ -291,11 +293,7 @@ impl Nip34WritePolicy { | |||
| 291 | ); | 293 | ); |
| 292 | } | 294 | } |
| 293 | Err(e) => { | 295 | Err(e) => { |
| 294 | tracing::warn!( | 296 | tracing::warn!("Failed to set HEAD in {}: {}", repo_path.display(), e); |
| 295 | "Failed to set HEAD in {}: {}", | ||
| 296 | repo_path.display(), | ||
| 297 | e | ||
| 298 | ); | ||
| 299 | } | 297 | } |
| 300 | } | 298 | } |
| 301 | } | 299 | } |
| @@ -338,6 +336,191 @@ impl Nip34WritePolicy { | |||
| 338 | (addressable_refs, event_refs) | 336 | (addressable_refs, event_refs) |
| 339 | } | 337 | } |
| 340 | 338 | ||
| 339 | /// Validate refs/nostr/<event-id> ref against a PR or PR Update event's `c` tag | ||
| 340 | /// | ||
| 341 | /// When a PR event (kind 1618) or PR Update event (kind 1619) is received, | ||
| 342 | /// this checks if a corresponding refs/nostr/<event-id> ref exists in the | ||
| 343 | /// repository and validates that it points to the correct commit (from the | ||
| 344 | /// `c` tag). If the ref exists but points to a different commit, the ref is | ||
| 345 | /// deleted. | ||
| 346 | /// | ||
| 347 | /// PR and PR Update events can have multiple `a` tags to update multiple | ||
| 348 | /// repositories simultaneously. | ||
| 349 | /// | ||
| 350 | /// This is part of GRASP-01 compliance: ensuring refs/nostr refs are consistent | ||
| 351 | /// with their corresponding events. | ||
| 352 | /// | ||
| 353 | /// # Arguments | ||
| 354 | /// * `database` - Database for looking up repository announcements | ||
| 355 | /// * `event` - The PR event (kind 1618) or PR Update event (kind 1619) | ||
| 356 | /// | ||
| 357 | /// # Returns | ||
| 358 | /// Ok(Some(n)) if n refs were deleted, Ok(None) if no action taken, Err on failure | ||
| 359 | async fn validate_pr_nostr_ref( | ||
| 360 | &self, | ||
| 361 | database: &Arc<MemoryDatabase>, | ||
| 362 | event: &Event, | ||
| 363 | ) -> Result<Option<usize>, String> { | ||
| 364 | let event_id = event.id.to_hex(); | ||
| 365 | |||
| 366 | // Extract the `c` tag (commit hash) from the PR event | ||
| 367 | let expected_commit = event.tags.iter().find_map(|tag| { | ||
| 368 | let tag_vec = tag.clone().to_vec(); | ||
| 369 | if tag_vec.len() >= 2 && tag_vec[0] == "c" { | ||
| 370 | Some(tag_vec[1].clone()) | ||
| 371 | } else { | ||
| 372 | None | ||
| 373 | } | ||
| 374 | }); | ||
| 375 | |||
| 376 | let expected_commit = match expected_commit { | ||
| 377 | Some(c) => c, | ||
| 378 | None => { | ||
| 379 | tracing::debug!( | ||
| 380 | "PR event {} has no 'c' tag, skipping ref validation", | ||
| 381 | event_id | ||
| 382 | ); | ||
| 383 | return Ok(None); | ||
| 384 | } | ||
| 385 | }; | ||
| 386 | |||
| 387 | // Extract ALL `a` tags (repository references) from the PR event | ||
| 388 | // PR events can reference multiple repositories | ||
| 389 | // Format: 30617:<pubkey>:<identifier> | ||
| 390 | let repo_refs: Vec<String> = event | ||
| 391 | .tags | ||
| 392 | .iter() | ||
| 393 | .filter_map(|tag| { | ||
| 394 | let tag_vec = tag.clone().to_vec(); | ||
| 395 | if tag_vec.len() >= 2 && tag_vec[0] == "a" && tag_vec[1].starts_with("30617:") { | ||
| 396 | Some(tag_vec[1].clone()) | ||
| 397 | } else { | ||
| 398 | None | ||
| 399 | } | ||
| 400 | }) | ||
| 401 | .collect(); | ||
| 402 | |||
| 403 | if repo_refs.is_empty() { | ||
| 404 | tracing::debug!( | ||
| 405 | "PR event {} has no repo 'a' tags, skipping ref validation", | ||
| 406 | event_id | ||
| 407 | ); | ||
| 408 | return Ok(None); | ||
| 409 | } | ||
| 410 | |||
| 411 | let mut deleted_count = 0; | ||
| 412 | |||
| 413 | // Process each repository reference | ||
| 414 | for repo_ref in repo_refs { | ||
| 415 | // Parse the repo reference: 30617:<pubkey>:<identifier> | ||
| 416 | let parts: Vec<&str> = repo_ref.split(':').collect(); | ||
| 417 | if parts.len() < 3 { | ||
| 418 | tracing::debug!( | ||
| 419 | "PR event {} has invalid 'a' tag format: {}", | ||
| 420 | event_id, | ||
| 421 | repo_ref | ||
| 422 | ); | ||
| 423 | continue; | ||
| 424 | } | ||
| 425 | |||
| 426 | let repo_pubkey = match PublicKey::from_hex(parts[1]) { | ||
| 427 | Ok(pk) => pk, | ||
| 428 | Err(_) => { | ||
| 429 | tracing::debug!( | ||
| 430 | "PR event {} has invalid pubkey in 'a' tag: {}", | ||
| 431 | event_id, | ||
| 432 | parts[1] | ||
| 433 | ); | ||
| 434 | continue; | ||
| 435 | } | ||
| 436 | }; | ||
| 437 | let identifier = parts[2]; | ||
| 438 | |||
| 439 | // Look up repository announcement to get the npub for path | ||
| 440 | let filter = Filter::new() | ||
| 441 | .kind(Kind::from(KIND_REPOSITORY_ANNOUNCEMENT)) | ||
| 442 | .author(repo_pubkey) | ||
| 443 | .custom_tag( | ||
| 444 | SingleLetterTag::lowercase(Alphabet::D), | ||
| 445 | identifier.to_string(), | ||
| 446 | ); | ||
| 447 | |||
| 448 | let announcements: Vec<Event> = match database.query(filter).await { | ||
| 449 | Ok(events) => events.into_iter().collect(), | ||
| 450 | Err(e) => { | ||
| 451 | tracing::warn!( | ||
| 452 | "Failed to query for repository announcement for PR {}: {}", | ||
| 453 | event_id, | ||
| 454 | e | ||
| 455 | ); | ||
| 456 | continue; | ||
| 457 | } | ||
| 458 | }; | ||
| 459 | |||
| 460 | if announcements.is_empty() { | ||
| 461 | tracing::debug!( | ||
| 462 | "No repository announcement found for PR event {} (repo {}:{})", | ||
| 463 | event_id, | ||
| 464 | repo_pubkey.to_hex(), | ||
| 465 | identifier | ||
| 466 | ); | ||
| 467 | continue; | ||
| 468 | } | ||
| 469 | |||
| 470 | // Process each matching announcement (there could be multiple) | ||
| 471 | for announcement_event in announcements { | ||
| 472 | let announcement = match RepositoryAnnouncement::from_event(announcement_event) { | ||
| 473 | Ok(a) => a, | ||
| 474 | Err(e) => { | ||
| 475 | tracing::warn!( | ||
| 476 | "Failed to parse announcement for PR {} validation: {}", | ||
| 477 | event_id, | ||
| 478 | e | ||
| 479 | ); | ||
| 480 | continue; | ||
| 481 | } | ||
| 482 | }; | ||
| 483 | |||
| 484 | // Build repository path | ||
| 485 | let repo_path = self.git_data_path.join(&announcement.repo_path()); | ||
| 486 | |||
| 487 | // Validate the ref | ||
| 488 | match git::validate_nostr_ref(&repo_path, &event_id, &expected_commit) { | ||
| 489 | Ok(true) => { | ||
| 490 | tracing::info!( | ||
| 491 | "Deleted mismatched refs/nostr/{} in {} (expected commit {})", | ||
| 492 | event_id, | ||
| 493 | repo_path.display(), | ||
| 494 | expected_commit | ||
| 495 | ); | ||
| 496 | deleted_count += 1; | ||
| 497 | } | ||
| 498 | Ok(false) => { | ||
| 499 | tracing::debug!( | ||
| 500 | "refs/nostr/{} in {} is valid or doesn't exist", | ||
| 501 | event_id, | ||
| 502 | repo_path.display() | ||
| 503 | ); | ||
| 504 | } | ||
| 505 | Err(e) => { | ||
| 506 | tracing::warn!( | ||
| 507 | "Failed to validate refs/nostr/{} in {}: {}", | ||
| 508 | event_id, | ||
| 509 | repo_path.display(), | ||
| 510 | e | ||
| 511 | ); | ||
| 512 | } | ||
| 513 | } | ||
| 514 | } | ||
| 515 | } | ||
| 516 | |||
| 517 | if deleted_count > 0 { | ||
| 518 | Ok(Some(deleted_count)) | ||
| 519 | } else { | ||
| 520 | Ok(None) | ||
| 521 | } | ||
| 522 | } | ||
| 523 | |||
| 341 | /// Check if any addressable events (repositories) exist in database | 524 | /// Check if any addressable events (repositories) exist in database |
| 342 | /// Returns the first matching addressable reference found, or None if none match | 525 | /// Returns the first matching addressable reference found, or None if none match |
| 343 | async fn find_accepted_repository( | 526 | async fn find_accepted_repository( |
| @@ -377,16 +560,17 @@ impl Nip34WritePolicy { | |||
| 377 | use std::collections::HashMap; | 560 | use std::collections::HashMap; |
| 378 | let mut by_kind: HashMap<u16, Vec<_>> = HashMap::new(); | 561 | let mut by_kind: HashMap<u16, Vec<_>> = HashMap::new(); |
| 379 | for (addr, kind, pubkey, identifier) in parsed_refs { | 562 | for (addr, kind, pubkey, identifier) in parsed_refs { |
| 380 | by_kind.entry(kind).or_default().push((addr, pubkey, identifier)); | 563 | by_kind |
| 564 | .entry(kind) | ||
| 565 | .or_default() | ||
| 566 | .push((addr, pubkey, identifier)); | ||
| 381 | } | 567 | } |
| 382 | 568 | ||
| 383 | // Query each kind group | 569 | // Query each kind group |
| 384 | for (kind, refs) in by_kind { | 570 | for (kind, refs) in by_kind { |
| 385 | let authors: Vec<PublicKey> = refs.iter().map(|(_, pk, _)| *pk).collect(); | 571 | let authors: Vec<PublicKey> = refs.iter().map(|(_, pk, _)| *pk).collect(); |
| 386 | 572 | ||
| 387 | let filter = Filter::new() | 573 | let filter = Filter::new().kind(Kind::from(kind)).authors(authors); |
| 388 | .kind(Kind::from(kind)) | ||
| 389 | .authors(authors); | ||
| 390 | 574 | ||
| 391 | match database.query(filter).await { | 575 | match database.query(filter).await { |
| 392 | Ok(events) => { | 576 | Ok(events) => { |
| @@ -445,7 +629,7 @@ impl Nip34WritePolicy { | |||
| 445 | event: &Event, | 629 | event: &Event, |
| 446 | ) -> Result<bool, String> { | 630 | ) -> Result<bool, String> { |
| 447 | let kind_u16 = event.kind.as_u16(); | 631 | let kind_u16 = event.kind.as_u16(); |
| 448 | 632 | ||
| 449 | // Check if this is any kind of replaceable event | 633 | // Check if this is any kind of replaceable event |
| 450 | let is_regular_replaceable = kind_u16 >= 10000 && kind_u16 < 20000; | 634 | let is_regular_replaceable = kind_u16 >= 10000 && kind_u16 < 20000; |
| 451 | let is_parameterized_replaceable = kind_u16 >= 30000 && kind_u16 < 40000; | 635 | let is_parameterized_replaceable = kind_u16 >= 30000 && kind_u16 < 40000; |
| @@ -454,7 +638,9 @@ impl Nip34WritePolicy { | |||
| 454 | // Build the appropriate address format based on event type | 638 | // Build the appropriate address format based on event type |
| 455 | let address = if is_parameterized_replaceable { | 639 | let address = if is_parameterized_replaceable { |
| 456 | // For parameterized replaceable: kind:pubkey:d-identifier format (2 colons) | 640 | // For parameterized replaceable: kind:pubkey:d-identifier format (2 colons) |
| 457 | let identifier = event.tags.iter() | 641 | let identifier = event |
| 642 | .tags | ||
| 643 | .iter() | ||
| 458 | .find_map(|tag| { | 644 | .find_map(|tag| { |
| 459 | let tag_vec = tag.clone().to_vec(); | 645 | let tag_vec = tag.clone().to_vec(); |
| 460 | if tag_vec.len() >= 2 && tag_vec[0] == "d" { | 646 | if tag_vec.len() >= 2 && tag_vec[0] == "d" { |
| @@ -464,12 +650,17 @@ impl Nip34WritePolicy { | |||
| 464 | } | 650 | } |
| 465 | }) | 651 | }) |
| 466 | .unwrap_or_default(); // Empty string if no 'd' tag | 652 | .unwrap_or_default(); // Empty string if no 'd' tag |
| 467 | format!("{}:{}:{}", event.kind.as_u16(), event.pubkey.to_hex(), identifier) | 653 | format!( |
| 654 | "{}:{}:{}", | ||
| 655 | event.kind.as_u16(), | ||
| 656 | event.pubkey.to_hex(), | ||
| 657 | identifier | ||
| 658 | ) | ||
| 468 | } else { | 659 | } else { |
| 469 | // For regular replaceable: kind:pubkey format (1 colon) | 660 | // For regular replaceable: kind:pubkey format (1 colon) |
| 470 | format!("{}:{}", event.kind.as_u16(), event.pubkey.to_hex()) | 661 | format!("{}:{}", event.kind.as_u16(), event.pubkey.to_hex()) |
| 471 | }; | 662 | }; |
| 472 | 663 | ||
| 473 | // Check addressable reference tags: a, A, q (with address format) | 664 | // Check addressable reference tags: a, A, q (with address format) |
| 474 | let addressable_tags = [ | 665 | let addressable_tags = [ |
| 475 | SingleLetterTag::lowercase(Alphabet::A), // 'a' - addressable event reference | 666 | SingleLetterTag::lowercase(Alphabet::A), // 'a' - addressable event reference |
| @@ -479,7 +670,7 @@ impl Nip34WritePolicy { | |||
| 479 | 670 | ||
| 480 | for tag_type in &addressable_tags { | 671 | for tag_type in &addressable_tags { |
| 481 | let filter = Filter::new().custom_tag(tag_type.clone(), address.clone()); | 672 | let filter = Filter::new().custom_tag(tag_type.clone(), address.clone()); |
| 482 | 673 | ||
| 483 | match database.query(filter).await { | 674 | match database.query(filter).await { |
| 484 | Ok(events) => { | 675 | Ok(events) => { |
| 485 | if !events.is_empty() { | 676 | if !events.is_empty() { |
| @@ -492,7 +683,7 @@ impl Nip34WritePolicy { | |||
| 492 | } else { | 683 | } else { |
| 493 | // For regular events, check event ID reference tags: e, E, q (with hex ID) | 684 | // For regular events, check event ID reference tags: e, E, q (with hex ID) |
| 494 | let event_id_hex = event.id.to_hex(); | 685 | let event_id_hex = event.id.to_hex(); |
| 495 | 686 | ||
| 496 | let event_id_tags = [ | 687 | let event_id_tags = [ |
| 497 | SingleLetterTag::lowercase(Alphabet::E), // 'e' - standard event reference | 688 | SingleLetterTag::lowercase(Alphabet::E), // 'e' - standard event reference |
| 498 | SingleLetterTag::uppercase(Alphabet::E), // 'E' - NIP-22 root event reference | 689 | SingleLetterTag::uppercase(Alphabet::E), // 'E' - NIP-22 root event reference |
| @@ -501,7 +692,7 @@ impl Nip34WritePolicy { | |||
| 501 | 692 | ||
| 502 | for tag_type in &event_id_tags { | 693 | for tag_type in &event_id_tags { |
| 503 | let filter = Filter::new().custom_tag(tag_type.clone(), event_id_hex.clone()); | 694 | let filter = Filter::new().custom_tag(tag_type.clone(), event_id_hex.clone()); |
| 504 | 695 | ||
| 505 | match database.query(filter).await { | 696 | match database.query(filter).await { |
| 506 | Ok(events) => { | 697 | Ok(events) => { |
| 507 | if !events.is_empty() { | 698 | if !events.is_empty() { |
| @@ -545,7 +736,7 @@ impl WritePolicy for Nip34WritePolicy { | |||
| 545 | // Note: We still accept the event even if repo creation fails | 736 | // Note: We still accept the event even if repo creation fails |
| 546 | // The git operation failure shouldn't prevent event acceptance | 737 | // The git operation failure shouldn't prevent event acceptance |
| 547 | } | 738 | } |
| 548 | 739 | ||
| 549 | tracing::debug!( | 740 | tracing::debug!( |
| 550 | "Accepted repository announcement: {}", | 741 | "Accepted repository announcement: {}", |
| 551 | event_id_str | 742 | event_id_str |
| @@ -563,11 +754,7 @@ impl WritePolicy for Nip34WritePolicy { | |||
| 563 | } | 754 | } |
| 564 | } | 755 | } |
| 565 | Err(e) => { | 756 | Err(e) => { |
| 566 | tracing::warn!( | 757 | tracing::warn!("Rejected repository announcement {}: {}", event_id_str, e); |
| 567 | "Rejected repository announcement {}: {}", | ||
| 568 | event_id_str, | ||
| 569 | e | ||
| 570 | ); | ||
| 571 | PolicyResult::Reject(e.to_string()) | 758 | PolicyResult::Reject(e.to_string()) |
| 572 | } | 759 | } |
| 573 | }, | 760 | }, |
| @@ -577,7 +764,10 @@ impl WritePolicy for Nip34WritePolicy { | |||
| 577 | match RepositoryState::from_event(event.clone()) { | 764 | match RepositoryState::from_event(event.clone()) { |
| 578 | Ok(state) => { | 765 | Ok(state) => { |
| 579 | // Try to set HEAD for all authorized repos if this is the latest state | 766 | // Try to set HEAD for all authorized repos if this is the latest state |
| 580 | match self.try_set_head_for_authorized_repos(&database, &state).await { | 767 | match self |
| 768 | .try_set_head_for_authorized_repos(&database, &state) | ||
| 769 | .await | ||
| 770 | { | ||
| 581 | Ok(count) if count > 0 => { | 771 | Ok(count) if count > 0 => { |
| 582 | tracing::info!( | 772 | tracing::info!( |
| 583 | "Set HEAD from state event {} for {} repo(s) with identifier {}", | 773 | "Set HEAD from state event {} for {} repo(s) with identifier {}", |
| @@ -600,11 +790,8 @@ impl WritePolicy for Nip34WritePolicy { | |||
| 600 | ); | 790 | ); |
| 601 | } | 791 | } |
| 602 | } | 792 | } |
| 603 | 793 | ||
| 604 | tracing::debug!( | 794 | tracing::debug!("Accepted repository state: {}", event_id_str); |
| 605 | "Accepted repository state: {}", | ||
| 606 | event_id_str | ||
| 607 | ); | ||
| 608 | PolicyResult::Accept | 795 | PolicyResult::Accept |
| 609 | } | 796 | } |
| 610 | Err(e) => { | 797 | Err(e) => { |
| @@ -620,14 +807,104 @@ impl WritePolicy for Nip34WritePolicy { | |||
| 620 | } | 807 | } |
| 621 | } | 808 | } |
| 622 | Err(e) => { | 809 | Err(e) => { |
| 810 | tracing::warn!("Rejected repository state {}: {}", event_id_str, e); | ||
| 811 | PolicyResult::Reject(e.to_string()) | ||
| 812 | } | ||
| 813 | }, | ||
| 814 | // KIND_PR (1618) and KIND_PR_UPDATE (1619): Validate refs/nostr/<event-id> refs before acceptance | ||
| 815 | KIND_PR | KIND_PR_UPDATE => { | ||
| 816 | // Validate refs/nostr refs for this PR event | ||
| 817 | // This deletes any refs/nostr/<event-id> that points to wrong commit | ||
| 818 | if let Err(e) = self.validate_pr_nostr_ref(&database, event).await { | ||
| 623 | tracing::warn!( | 819 | tracing::warn!( |
| 624 | "Rejected repository state {}: {}", | 820 | "Failed to validate refs/nostr for PR event {}: {}", |
| 625 | event_id_str, | 821 | event_id_str, |
| 626 | e | 822 | e |
| 627 | ); | 823 | ); |
| 628 | PolicyResult::Reject(e.to_string()) | 824 | // Don't reject - just log the error and proceed with normal validation |
| 629 | } | 825 | } |
| 630 | }, | 826 | |
| 827 | // Continue with standard reference checking (same as default case) | ||
| 828 | let (addressable_refs, event_refs) = Self::extract_reference_tags(event); | ||
| 829 | |||
| 830 | // Check 1: Does this event reference an accepted repository? | ||
| 831 | match Self::find_accepted_repository(&database, &addressable_refs).await { | ||
| 832 | Ok(Some(addr_ref)) => { | ||
| 833 | tracing::debug!( | ||
| 834 | "Accepted PR event {}: references accepted repository {}", | ||
| 835 | event_id_str, | ||
| 836 | addr_ref | ||
| 837 | ); | ||
| 838 | return PolicyResult::Accept; | ||
| 839 | } | ||
| 840 | Ok(None) => { | ||
| 841 | // No matching repositories, continue to next check | ||
| 842 | } | ||
| 843 | Err(e) => { | ||
| 844 | tracing::warn!( | ||
| 845 | "Database query failed for PR {}, rejecting (fail-secure): {}", | ||
| 846 | event_id_str, | ||
| 847 | e | ||
| 848 | ); | ||
| 849 | return PolicyResult::Reject(format!("Database query failed: {}", e)); | ||
| 850 | } | ||
| 851 | } | ||
| 852 | |||
| 853 | // Check 2: Does this event reference an accepted event? | ||
| 854 | match Self::find_accepted_event(&database, &event_refs).await { | ||
| 855 | Ok(Some(event_ref)) => { | ||
| 856 | tracing::debug!( | ||
| 857 | "Accepted PR event {}: references accepted event {}", | ||
| 858 | event_id_str, | ||
| 859 | event_ref | ||
| 860 | ); | ||
| 861 | return PolicyResult::Accept; | ||
| 862 | } | ||
| 863 | Ok(None) => { | ||
| 864 | // No matching events, continue to next check | ||
| 865 | } | ||
| 866 | Err(e) => { | ||
| 867 | tracing::warn!( | ||
| 868 | "Database query failed for PR {}, rejecting (fail-secure): {}", | ||
| 869 | event_id_str, | ||
| 870 | e | ||
| 871 | ); | ||
| 872 | return PolicyResult::Reject(format!("Database query failed: {}", e)); | ||
| 873 | } | ||
| 874 | } | ||
| 875 | |||
| 876 | // Check 3: Is this event referenced by an accepted event? | ||
| 877 | match Self::is_referenced_by_accepted(&database, event).await { | ||
| 878 | Ok(true) => { | ||
| 879 | tracing::debug!( | ||
| 880 | "Accepted PR event {}: referenced by accepted event", | ||
| 881 | event_id_str | ||
| 882 | ); | ||
| 883 | return PolicyResult::Accept; | ||
| 884 | } | ||
| 885 | Ok(false) => { | ||
| 886 | // No forward references found, continue to rejection | ||
| 887 | } | ||
| 888 | Err(e) => { | ||
| 889 | tracing::warn!( | ||
| 890 | "Database query failed for PR {}, rejecting (fail-secure): {}", | ||
| 891 | event_id_str, | ||
| 892 | e | ||
| 893 | ); | ||
| 894 | return PolicyResult::Reject(format!("Database query failed: {}", e)); | ||
| 895 | } | ||
| 896 | } | ||
| 897 | |||
| 898 | // No valid references found - reject as orphan event | ||
| 899 | tracing::info!( | ||
| 900 | "Rejected orphan PR event {}: no references to accepted repos or events", | ||
| 901 | event_id_str | ||
| 902 | ); | ||
| 903 | PolicyResult::Reject( | ||
| 904 | "PR event must reference an accepted repository or accepted event" | ||
| 905 | .to_string(), | ||
| 906 | ) | ||
| 907 | } | ||
| 631 | // GRASP-01: Check if event references accepted repositories or events | 908 | // GRASP-01: Check if event references accepted repositories or events |
| 632 | _ => { | 909 | _ => { |
| 633 | // Extract all reference tags from event | 910 | // Extract all reference tags from event |
| @@ -709,7 +986,7 @@ impl WritePolicy for Nip34WritePolicy { | |||
| 709 | event_refs.len() | 986 | event_refs.len() |
| 710 | ); | 987 | ); |
| 711 | PolicyResult::Reject( | 988 | PolicyResult::Reject( |
| 712 | "Event must reference an accepted repository or accepted event".to_string() | 989 | "Event must reference an accepted repository or accepted event".to_string(), |
| 713 | ) | 990 | ) |
| 714 | } | 991 | } |
| 715 | } | 992 | } |
| @@ -786,4 +1063,4 @@ pub fn create_relay(config: &Config) -> Result<RelayWithDatabase> { | |||
| 786 | relay: LocalRelay::new(builder), | 1063 | relay: LocalRelay::new(builder), |
| 787 | database, | 1064 | database, |
| 788 | }) | 1065 | }) |
| 789 | } \ No newline at end of file | 1066 | } |
diff --git a/src/nostr/events.rs b/src/nostr/events.rs index 97688b1..6a62ccd 100644 --- a/src/nostr/events.rs +++ b/src/nostr/events.rs | |||
| @@ -15,6 +15,12 @@ pub const KIND_REPOSITORY_ANNOUNCEMENT: u16 = 30617; | |||
| 15 | /// NIP-34 Repository State Announcement (kind 30618) | 15 | /// NIP-34 Repository State Announcement (kind 30618) |
| 16 | pub const KIND_REPOSITORY_STATE: u16 = 30618; | 16 | pub const KIND_REPOSITORY_STATE: u16 = 30618; |
| 17 | 17 | ||
| 18 | /// NIP-34 Pull Request (kind 1618) - has `c` tag for commit | ||
| 19 | pub const KIND_PR: u16 = 1618; | ||
| 20 | |||
| 21 | /// NIP-34 Pull Request Update (kind 1619) - has `c` tag for commit | ||
| 22 | pub const KIND_PR_UPDATE: u16 = 1619; | ||
| 23 | |||
| 18 | /// Repository announcement details extracted from NIP-34 event | 24 | /// Repository announcement details extracted from NIP-34 event |
| 19 | #[derive(Debug, Clone)] | 25 | #[derive(Debug, Clone)] |
| 20 | pub struct RepositoryAnnouncement { | 26 | pub struct RepositoryAnnouncement { |