source_boundary.rs (24934B)
1 use std::{ 2 fs, 3 path::{Path, PathBuf}, 4 }; 5 6 struct ForbiddenConcept { 7 pattern: &'static str, 8 reason: &'static str, 9 } 10 11 const FORBIDDEN_DAEMON_TRANSPORT_CONCEPTS: &[ForbiddenConcept] = &[ 12 ForbiddenConcept { 13 pattern: "\"radrootsd_proxy\"", 14 reason: "proxy-as-transport targets are removed", 15 }, 16 ForbiddenConcept { 17 pattern: "radrootsd.publish_proxy.v1", 18 reason: "daemon publish proxy v1 is removed", 19 }, 20 ForbiddenConcept { 21 pattern: "publish.relays.resolve", 22 reason: "relay-resolution RPC is replaced by transport publish target policy", 23 }, 24 ForbiddenConcept { 25 pattern: "\"publish.event\"", 26 reason: "publish.event is replaced by transport.publish.event", 27 }, 28 ForbiddenConcept { 29 pattern: "\"transport_kinds\"", 30 reason: "capabilities must expose per-transport readiness instead of kind-only lists", 31 }, 32 ForbiddenConcept { 33 pattern: "allowed_relay_policy", 34 reason: "relay policy is Nostr-specific and must not be a generic transport API", 35 }, 36 ForbiddenConcept { 37 pattern: "relay_policy", 38 reason: "relay policy is Nostr-specific and must not be a generic transport API", 39 }, 40 ForbiddenConcept { 41 pattern: "PublishRelayPolicy", 42 reason: "old relay-shaped publish policy names must not return", 43 }, 44 ForbiddenConcept { 45 pattern: "PublishRelayOutcome", 46 reason: "old relay-shaped publish outcome names must not return", 47 }, 48 ForbiddenConcept { 49 pattern: "PublishRelaySource", 50 reason: "old relay-shaped publish source names must not return", 51 }, 52 ForbiddenConcept { 53 pattern: "radroots_relay_transport", 54 reason: "daemon must depend on radroots_transport_nostr directly without a dependency alias", 55 }, 56 ]; 57 58 const FORBIDDEN_FOUNDATION_HARDENING_RETIRED_CONCEPTS: &[ForbiddenConcept] = &[ 59 ForbiddenConcept { 60 pattern: "WireEventParts", 61 reason: "event construction must use current Radroots NIP-01 wire part names", 62 }, 63 ForbiddenConcept { 64 pattern: "RadrootsFrozenEventDraft", 65 reason: "event draft surfaces must use the current RadrootsEventDraft name", 66 }, 67 ForbiddenConcept { 68 pattern: "RadrootsNostrEvent", 69 reason: "product-level event surfaces must use protocol-neutral event names", 70 }, 71 ForbiddenConcept { 72 pattern: "RadrootsNostrEventRef", 73 reason: "product-level event references must use RadrootsEventRef", 74 }, 75 ForbiddenConcept { 76 pattern: "RadrootsNostrEventPtr", 77 reason: "product-level event pointers must use RadrootsEventPtr", 78 }, 79 ForbiddenConcept { 80 pattern: "SignedNostrEvent", 81 reason: "generic signed-event surfaces must use product-neutral signed-event names", 82 }, 83 ForbiddenConcept { 84 pattern: "RadrootsEventIndexIndexCheckpoint", 85 reason: "event-index checkpoint names must not duplicate the index noun", 86 }, 87 ForbiddenConcept { 88 pattern: "RadrootsEventsIndexed", 89 reason: "event-indexed APIs must use the singular event-index crate family", 90 }, 91 ForbiddenConcept { 92 pattern: "RADROOTS_EVENTS_VERSION", 93 reason: "event contract version constants must use the current singular event namespace", 94 }, 95 ForbiddenConcept { 96 pattern: "radroots_events", 97 reason: "daemon surfaces must use the current singular event crate names", 98 }, 99 ForbiddenConcept { 100 pattern: "radroots_events_codec", 101 reason: "daemon event codec surfaces must use the current singular event-codec name", 102 }, 103 ForbiddenConcept { 104 pattern: "radroots_events_indexed", 105 reason: "daemon event index surfaces must use the current singular event-index name", 106 }, 107 ForbiddenConcept { 108 pattern: "radroots_local_events", 109 reason: "daemon storage surfaces must not reintroduce retired local-events names", 110 }, 111 ForbiddenConcept { 112 pattern: "radroots_local_store", 113 reason: "daemon storage surfaces must not reintroduce retired local-store names", 114 }, 115 ForbiddenConcept { 116 pattern: "radroots_types", 117 reason: "daemon type surfaces must use current crate ownership instead of retired types crates", 118 }, 119 ForbiddenConcept { 120 pattern: "radroots_types_bindings", 121 reason: "daemon generated surfaces must not reintroduce retired types-binding names", 122 }, 123 ForbiddenConcept { 124 pattern: "radroots_nostr_ndb", 125 reason: "daemon Nostr database surfaces must not reintroduce retired ndb names", 126 }, 127 ForbiddenConcept { 128 pattern: "radroots_replica_db", 129 reason: "daemon replica surfaces must use current replica-store ownership", 130 }, 131 ForbiddenConcept { 132 pattern: "radroots_replica_db_schema", 133 reason: "daemon replica schema surfaces must use current replica-schema ownership", 134 }, 135 ForbiddenConcept { 136 pattern: "radroots_sp1_guest_trade", 137 reason: "daemon trade SP1 surfaces must use the current trade_sp1 crate names", 138 }, 139 ForbiddenConcept { 140 pattern: "radroots_sp1_host_trade", 141 reason: "daemon trade SP1 surfaces must use the current trade_sp1 crate names", 142 }, 143 ]; 144 145 const FORBIDDEN_FOUNDATION_HARDENING_DOC_CONCEPTS: &[ForbiddenConcept] = &[ 146 ForbiddenConcept { 147 pattern: "Nostr event timestamp", 148 reason: "daemon docs must describe event-envelope timestamps without generic Nostr wording", 149 }, 150 ForbiddenConcept { 151 pattern: "Forwarded satisfies Delivered", 152 reason: "daemon docs must not imply forwarded evidence is strict delivery", 153 }, 154 ForbiddenConcept { 155 pattern: "StoredByGateway satisfies Delivered", 156 reason: "daemon docs must not imply gateway storage evidence is strict delivery", 157 }, 158 ForbiddenConcept { 159 pattern: "Seen satisfies Delivered", 160 reason: "daemon docs must not imply seen evidence is strict delivery", 161 }, 162 ]; 163 164 #[test] 165 fn transport_publish_sources_reject_removed_protocol_identifiers() { 166 let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); 167 let src_dir = manifest_dir.join("src"); 168 let mut findings = Vec::new(); 169 170 for path in rust_source_files(src_dir.as_path()) { 171 let source = read_source(path.as_path()); 172 let relative_path = relative_path(manifest_dir, path.as_path()); 173 174 for concept in FORBIDDEN_DAEMON_TRANSPORT_CONCEPTS { 175 if contains_forbidden_concept(source.as_str(), concept.pattern) { 176 findings.push(format!( 177 "{} contains removed daemon transport concept `{}`: {}", 178 relative_path, concept.pattern, concept.reason 179 )); 180 } 181 } 182 183 for line in removed_reticulum_endpoint_alias_lines(source.as_str()) { 184 findings.push(format!( 185 "{relative_path}:{line} contains removed Reticulum endpoint alias" 186 )); 187 } 188 } 189 190 let manifest_source = read_source(manifest_dir.join("Cargo.toml").as_path()); 191 for concept in FORBIDDEN_DAEMON_TRANSPORT_CONCEPTS { 192 if contains_forbidden_concept(manifest_source.as_str(), concept.pattern) { 193 findings.push(format!( 194 "Cargo.toml contains removed daemon transport concept `{}`: {}", 195 concept.pattern, concept.reason 196 )); 197 } 198 } 199 assert!( 200 manifest_source.contains( 201 "radroots_transport_nostr = { git = \"https://github.com/radrootslabs/lib.git\", rev = \"3563f3b5a4331eb2cb3f925cafc9de524d844228\", version = \"=0.1.0-alpha\"" 202 ), 203 "Cargo.toml must pin radroots_transport_nostr to the approved Lib revision and exact version" 204 ); 205 assert!( 206 !manifest_source.contains("package = \"radroots_transport_nostr\""), 207 "Cargo.toml must not disguise radroots_transport_nostr through a package alias" 208 ); 209 assert!( 210 !manifest_source.contains("path = \"../lib"), 211 "Cargo.toml must not retain sibling source dependencies" 212 ); 213 assert!( 214 !manifest_source.contains("radroots_runtime"), 215 "daemon lifecycle and path policy must remain host-owned" 216 ); 217 218 assert!( 219 findings.is_empty(), 220 "daemon transport source-boundary violations:\n{}", 221 findings.join("\n") 222 ); 223 } 224 225 #[test] 226 fn foundation_hardening_sources_reject_retired_names_and_ambiguous_docs() { 227 let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); 228 let mut findings = Vec::new(); 229 230 for path in foundation_hardening_guard_files(manifest_dir) { 231 let source_raw = read_source(path.as_path()); 232 let source = if path.extension().and_then(|extension| extension.to_str()) == Some("rs") { 233 production_source(source_raw.as_str()) 234 } else { 235 source_raw.as_str() 236 }; 237 let relative_path = relative_path(manifest_dir, path.as_path()); 238 239 for concept in FORBIDDEN_FOUNDATION_HARDENING_RETIRED_CONCEPTS { 240 if foundation_hardening_retired_concept_allowed(relative_path.as_str(), concept.pattern) 241 { 242 continue; 243 } 244 if contains_forbidden_concept(source, concept.pattern) { 245 findings.push(format!( 246 "{} contains retired Foundation Hardening concept `{}`: {}", 247 relative_path, concept.pattern, concept.reason 248 )); 249 } 250 } 251 252 if is_doc_surface(path.as_path()) { 253 for concept in FORBIDDEN_FOUNDATION_HARDENING_DOC_CONCEPTS { 254 if source.contains(concept.pattern) { 255 findings.push(format!( 256 "{} contains ambiguous Foundation Hardening wording `{}`: {}", 257 relative_path, concept.pattern, concept.reason 258 )); 259 } 260 } 261 } 262 } 263 264 assert!( 265 findings.is_empty(), 266 "daemon Foundation Hardening V1 source-boundary violations:\n{}", 267 findings.join("\n") 268 ); 269 } 270 271 #[test] 272 fn transport_publish_sources_reject_removed_execution_transport_targets() { 273 let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); 274 use radroots_protocol::radrootsd::transport_publish::v5::{ 275 DeliveryPolicy, Error, EventRequest, Target, TargetPolicy, 276 }; 277 let request = EventRequest { 278 raw_event_json: "{}".to_owned(), 279 target_policy: TargetPolicy::explicit_targets(vec![Target { 280 transport_kind: "proxy".to_owned(), 281 endpoint_uri: "proxy:publish".to_owned(), 282 target_scope: None, 283 target_label: None, 284 reticulum_behavior: None, 285 }]), 286 delivery_policy: DeliveryPolicy::Any, 287 idempotency_key: None, 288 timeout_ms: None, 289 }; 290 assert_eq!( 291 request.validate(20), 292 Err(Error::InvalidTransportKind { index: 0 }) 293 ); 294 295 let daemon_source = read_source(manifest_dir.join("src/core/transport_publish.rs").as_path()); 296 for required in [ 297 "publish_event_rejects_removed_execution_kind_before_recording_job", 298 "publish_event_rejects_removed_execution_target_before_recording_job", 299 "!matches!(canonical.as_str(), \"local\" | \"nostr\" | \"reticulum\")", 300 ] { 301 assert!( 302 daemon_source.contains(required), 303 "daemon transport publish tests must prove removed execution transport targets are rejected before job recording" 304 ); 305 } 306 } 307 308 #[test] 309 fn transport_publish_sources_require_principal_explicit_kind_scope() { 310 let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); 311 let daemon_source = read_source(manifest_dir.join("src/core/transport_publish.rs").as_path()); 312 313 for required in [ 314 "allowed_explicit_transport_kinds_json", 315 "pub allowed_explicit_transport_kinds: Vec<String>,", 316 "parse_explicit_transport_kind", 317 "principal must include at least one allowed explicit transport kind", 318 "principal is not allowed to use explicit transport target kind", 319 "publish_event_records_explicit_nostr_target_when_kind_allowed", 320 "publish_event_rejects_explicit_target_kind_not_allowed_before_recording_job", 321 ] { 322 assert!( 323 daemon_source.contains(required), 324 "daemon transport publish sources must retain explicit target kind-scope witness `{required}`" 325 ); 326 } 327 } 328 329 #[test] 330 fn transport_publish_sources_reject_runtime_principal_schema_repair() { 331 let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); 332 let daemon_source = read_source(manifest_dir.join("src/core/transport_publish.rs").as_path()); 333 334 for required in [ 335 "validate_transport_publish_schema", 336 "TransportPublishError::Schema", 337 "transport_store_open_validates_current_principal_schema", 338 "transport_store_open_rejects_legacy_principal_schema_without_explicit_kind_allowlist", 339 ] { 340 assert!( 341 daemon_source.contains(required), 342 "daemon transport publish source must retain strict schema validation witness `{required}`" 343 ); 344 } 345 346 for forbidden in [ 347 concat!("ensure_transport_publish", "_schema"), 348 concat!("ALTER TABLE ", "transport_publish_principals"), 349 ] { 350 assert!( 351 !daemon_source.contains(forbidden), 352 "daemon transport publish source must not contain runtime schema repair `{forbidden}`" 353 ); 354 } 355 } 356 357 #[test] 358 fn transport_publish_store_egress_requires_protocol_validation() { 359 let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); 360 let daemon_source = read_source(manifest_dir.join("src/core/transport_publish.rs").as_path()); 361 362 for required in [ 363 "fn finalize_job_row_for_egress", 364 "job.view.targets = self.target_outcomes(job.view.job_id.as_str())?;", 365 "finalize_job_view(&mut job.view);", 366 "job.view\n .validate()", 367 "TransportPublishError::InvalidPublishJobState", 368 "target_scope TEXT NOT NULL", 369 "target_label TEXT", 370 "PRIMARY KEY(job_id, transport_kind, endpoint_uri, target_scope)", 371 "storage_target_scope_to_protocol", 372 "publish_outcomes_from_receipt", 373 "store_egress_rejects_malformed_target_counts_for_get_list_and_dedupe", 374 "store_egress_rejects_explicit_target_outcome_drift", 375 "store_egress_rejects_recovered_explicit_target_snapshot_drift", 376 ] { 377 assert!( 378 daemon_source.contains(required), 379 "daemon transport publish store must retain validated public egress witness `{required}`" 380 ); 381 } 382 } 383 384 #[test] 385 fn transport_publish_required_targets_stay_fingerprint_exact() { 386 let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); 387 let daemon_source = read_source(manifest_dir.join("src/core/transport_publish.rs").as_path()); 388 for required in [ 389 "validate_delivery_policy_for_resolution", 390 "let target_fingerprints = resolution.target_fingerprints()?;", 391 ".any(|actual| actual.as_str() == required.as_str())", 392 "fn required_outcomes_for_policy", 393 "target_outcome_fingerprint(outcome, index)", 394 "fingerprint.as_str() == required.as_str()", 395 "let required_outcomes = required_outcomes_for_policy(targets, outcomes);", 396 "required_outcomes.len() == targets.len()", 397 ] { 398 assert!( 399 daemon_source.contains(required), 400 "daemon transport publish must retain exact required-target witness `{required}`" 401 ); 402 } 403 404 assert!( 405 !daemon_source.contains("SatisfactionPolicy"), 406 "daemon V5 delivery policy must not be duplicated in the transport adapter" 407 ); 408 409 use radroots_protocol::radrootsd::transport_publish::v5::{DeliveryPolicy, TargetFingerprint}; 410 let target = TargetFingerprint::parse("a".repeat(64)).expect("fingerprint"); 411 let policy = DeliveryPolicy::required_targets(vec![target]).expect("required targets"); 412 assert_eq!(policy.required_target_count(9), 1); 413 } 414 415 #[test] 416 fn transport_publish_capabilities_expose_per_transport_readiness() { 417 let methods_source = read_source( 418 Path::new(env!("CARGO_MANIFEST_DIR")) 419 .join("src/transport/jsonrpc/methods/mod.rs") 420 .as_path(), 421 ); 422 for required in [ 423 "transport.publish.capabilities", 424 "serde_json::from_str(response.get())", 425 r#"result["api_version"]"#, 426 "radrootsd.transport_publish.v5", 427 r#"result["publish"]["raw_event_json_ingress"]"#, 428 r#"transport["transport"] == "reticulum""#, 429 r#"reticulum["configured"]"#, 430 r#"reticulum["implementation"]"#, 431 r#"reticulum["usable_for_delivery"]"#, 432 r#"reticulum["capabilities"]"#, 433 r#""deliver": false"#, 434 r#""fetch": false"#, 435 r#""discovery": false"#, 436 r#""gateway_forwarding": false"#, 437 r#""receipt_observation": false"#, 438 "RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE", 439 ] { 440 assert!( 441 methods_source.contains(required), 442 "daemon capabilities tests must retain transport readiness witness `{required}`" 443 ); 444 } 445 446 use radroots_protocol::radrootsd::transport_publish::v5::{ 447 Capabilities, Implementation, RETICULUM_UNAVAILABLE_MESSAGE, 448 }; 449 let capabilities = Capabilities::v5(1024, 20); 450 let reticulum = capabilities 451 .publish 452 .transports 453 .iter() 454 .find(|transport| transport.transport == "reticulum") 455 .expect("reticulum capability"); 456 assert!(reticulum.configured); 457 assert_eq!(reticulum.implementation, Implementation::Real); 458 assert!(!reticulum.usable_for_delivery); 459 assert!(!reticulum.capabilities.deliver); 460 assert_eq!(reticulum.message, RETICULUM_UNAVAILABLE_MESSAGE); 461 } 462 463 #[test] 464 fn transport_publish_reticulum_unavailable_outcomes_use_shared_unavailable_message() { 465 let daemon_source = read_source( 466 Path::new(env!("CARGO_MANIFEST_DIR")) 467 .join("src/core/transport_publish.rs") 468 .as_path(), 469 ); 470 for required in [ 471 "RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE", 472 "message: Some(RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE.to_owned())", 473 "publish_event_records_reticulum_unavailable_as_deferred_until_implemented", 474 ] { 475 assert!( 476 daemon_source.contains(required), 477 "daemon Reticulum unavailable publish outcomes must retain shared unavailable-message witness `{required}`" 478 ); 479 } 480 481 let retired_message = [ 482 "reticulum transport is registered for preview", 483 "not routable by radrootsd", 484 ] 485 .join(" but "); 486 assert!( 487 !daemon_source.contains(retired_message.as_str()), 488 "daemon Reticulum unavailable publish outcomes must not revive local unavailable-message copy" 489 ); 490 } 491 492 fn rust_source_files(root: &Path) -> Vec<PathBuf> { 493 let mut paths = Vec::new(); 494 collect_rust_source_files(root, &mut paths); 495 paths.sort(); 496 paths 497 } 498 499 fn foundation_hardening_guard_files(manifest_dir: &Path) -> Vec<PathBuf> { 500 let mut paths = Vec::new(); 501 502 for path in [ 503 manifest_dir.join("Cargo.toml"), 504 manifest_dir.join("README"), 505 manifest_dir.join("README.md"), 506 ] { 507 if path.exists() { 508 paths.push(path); 509 } 510 } 511 512 for relative_root in ["src", "docs"] { 513 let root = manifest_dir.join(relative_root); 514 if root.exists() { 515 collect_foundation_hardening_guard_files(root.as_path(), &mut paths); 516 } 517 } 518 519 paths.sort(); 520 paths 521 } 522 523 fn foundation_hardening_retired_concept_allowed(relative_path: &str, pattern: &str) -> bool { 524 pattern == "RadrootsNostrEvent" && daemon_nostr_protocol_context(relative_path) 525 } 526 527 fn daemon_nostr_protocol_context(relative_path: &str) -> bool { 528 relative_path == "src/core/transport_publish.rs" 529 || relative_path.starts_with("src/transport/nostr/") 530 || relative_path.contains("/nip46/") 531 } 532 533 fn collect_foundation_hardening_guard_files(root: &Path, paths: &mut Vec<PathBuf>) { 534 for entry in fs::read_dir(root) 535 .unwrap_or_else(|error| panic!("failed to read {}: {error}", root.display())) 536 { 537 let path = entry.expect("guard entry").path(); 538 if path.is_dir() { 539 collect_foundation_hardening_guard_files(path.as_path(), paths); 540 continue; 541 } 542 543 let guarded_extension = matches!( 544 path.extension().and_then(|extension| extension.to_str()), 545 Some("rs") | Some("toml") | Some("md") 546 ); 547 if guarded_extension 548 || matches!( 549 path.file_name().and_then(|file_name| file_name.to_str()), 550 Some("README") | Some("README.md") 551 ) 552 { 553 paths.push(path); 554 } 555 } 556 } 557 558 fn collect_rust_source_files(root: &Path, paths: &mut Vec<PathBuf>) { 559 for entry in fs::read_dir(root) 560 .unwrap_or_else(|error| panic!("failed to read {}: {error}", root.display())) 561 { 562 let entry = entry.expect("read source entry"); 563 let path = entry.path(); 564 if path.is_dir() { 565 collect_rust_source_files(path.as_path(), paths); 566 } else if path.extension().and_then(|extension| extension.to_str()) == Some("rs") { 567 paths.push(path); 568 } 569 } 570 } 571 572 fn read_source(path: &Path) -> String { 573 fs::read_to_string(path) 574 .unwrap_or_else(|error| panic!("failed to read source {}: {error}", path.display())) 575 } 576 577 fn relative_path(root: &Path, path: &Path) -> String { 578 path.strip_prefix(root) 579 .expect("source path is under repo root") 580 .to_string_lossy() 581 .replace('\\', "/") 582 } 583 584 fn production_source(source: &str) -> &str { 585 source 586 .find("\n#[cfg(test)]") 587 .map_or(source, |index| &source[..index]) 588 } 589 590 fn is_doc_surface(path: &Path) -> bool { 591 matches!( 592 path.file_name().and_then(|file_name| file_name.to_str()), 593 Some("README") | Some("README.md") 594 ) || matches!( 595 path.extension().and_then(|extension| extension.to_str()), 596 Some("md") 597 ) 598 } 599 600 fn contains_forbidden_concept(source: &str, pattern: &str) -> bool { 601 if !pattern.chars().all(is_rust_identifier_character) { 602 return source.contains(pattern); 603 } 604 605 source.match_indices(pattern).any(|(index, _)| { 606 let before = source[..index].chars().next_back(); 607 let after = source[index + pattern.len()..].chars().next(); 608 before.is_none_or(|character| !is_rust_identifier_character(character)) 609 && after.is_none_or(|character| !is_rust_identifier_character(character)) 610 }) 611 } 612 613 fn removed_reticulum_endpoint_alias_lines(source: &str) -> Vec<usize> { 614 let removed_endpoint = ["reticulum", ":", "preview"].concat(); 615 source 616 .match_indices(removed_endpoint.as_str()) 617 .filter_map(|(index, _)| { 618 let after = source[index + removed_endpoint.len()..].chars().next(); 619 (after != Some('-')).then(|| line_number(source, index)) 620 }) 621 .collect() 622 } 623 624 fn is_rust_identifier_character(character: char) -> bool { 625 character == '_' || character.is_ascii_alphanumeric() 626 } 627 628 fn line_number(source: &str, index: usize) -> usize { 629 source[..index] 630 .bytes() 631 .filter(|byte| *byte == b'\n') 632 .count() 633 + 1 634 } 635 636 #[test] 637 fn nip46_transport_uses_completed_events_and_exact_sign_event_binding() { 638 let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); 639 let protocol = read_source( 640 manifest_dir 641 .join("src/transport/nostr/protocol.rs") 642 .as_path(), 643 ); 644 let client = read_source( 645 manifest_dir 646 .join("src/transport/jsonrpc/nip46/client.rs") 647 .as_path(), 648 ); 649 let production_sources = [ 650 "src/transport/nostr/listener.rs", 651 "src/transport/jsonrpc/nip46/client.rs", 652 "src/transport/jsonrpc/methods/nip46/connect.rs", 653 "src/transport/jsonrpc/methods/nip46/session_authorize.rs", 654 ] 655 .map(|path| { 656 let source = read_source(manifest_dir.join(path).as_path()); 657 production_source(source.as_str()).to_owned() 658 }) 659 .join("\n"); 660 661 assert!(protocol.contains("EventBuilder::nostr_connect")); 662 assert!(protocol.contains(".sign_with_keys(sender_keys)")); 663 assert!(!production_sources.contains("EventBuilder")); 664 assert!(!production_sources.contains("radroots_nostr_build_event")); 665 assert!(!production_sources.contains(".send_event_builder(")); 666 for required in [ 667 "unsigned.verify_id()", 668 "let expected_event_id = unsigned.id()", 669 "event.pubkey != expected_public_key", 670 "event.id != expected_event_id", 671 ".verify()", 672 ] { 673 assert!( 674 client.contains(required), 675 "NIP-46 client must retain exact sign_event binding witness `{required}`" 676 ); 677 } 678 }