lib.rs (26746B)
1 //! Private mobile FFI over the shared SDK engine. 2 3 #![forbid(unsafe_code)] 4 #![doc = include_str!("../README.md")] 5 6 use std::sync::Arc; 7 8 use radroots_sdk::{Client, ClientBuilder}; 9 10 uniffi::setup_scaffolding!("radroots_sdk"); 11 12 /// Generation-1 mobile DTOs. 13 pub mod v1 { 14 use super::*; 15 16 /// Stable capability maturity independent of runtime availability. 17 #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)] 18 pub enum CapabilityMaturity { 19 Stable, 20 Preview, 21 Experimental, 22 } 23 24 /// Current side-effect-free capability availability. 25 #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)] 26 pub enum CapabilityAvailability { 27 Available, 28 Degraded, 29 Unavailable, 30 Unsupported, 31 } 32 33 /// Versioned, presentation-independent capability observation. 34 #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)] 35 pub struct CapabilityStatus { 36 pub id: String, 37 pub compiled: bool, 38 pub configured: bool, 39 pub availability: CapabilityAvailability, 40 pub maturity: CapabilityMaturity, 41 } 42 43 /// Secret-safe SDK failure at the language boundary. 44 #[derive(Debug, thiserror::Error, uniffi::Error)] 45 pub enum Error { 46 #[error("SDK {code}: {message}")] 47 Sdk { 48 code: String, 49 message: String, 50 retryable: bool, 51 }, 52 } 53 54 impl From<radroots_sdk::Error> for Error { 55 fn from(error: radroots_sdk::Error) -> Self { 56 let descriptor = error.descriptor(); 57 Self::Sdk { 58 code: descriptor.code().as_str().to_owned(), 59 message: descriptor.message().to_owned(), 60 retryable: descriptor.retryable(), 61 } 62 } 63 } 64 65 impl Error { 66 fn contract(code: &'static str, message: &'static str) -> Self { 67 Self::Sdk { 68 code: code.to_owned(), 69 message: message.to_owned(), 70 retryable: false, 71 } 72 } 73 } 74 75 /// Final evidence coverage vocabulary. 76 #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)] 77 pub enum TradeEvidenceCoverage { 78 Missing, 79 Partial, 80 ScopeSatisfied, 81 Unsupported, 82 } 83 84 /// Final evidence outcome vocabulary. 85 #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)] 86 pub enum TradeEvidenceOutcome { 87 Valid, 88 Invalid, 89 Indeterminate, 90 } 91 92 /// Bounded projection of one canonical evidence manifest. 93 #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)] 94 pub struct TradeEvidenceManifest { 95 pub contract_id: String, 96 pub contract_version: u16, 97 pub trade_id: String, 98 pub trade_generation: String, 99 pub observed_at_unix_s: String, 100 pub coverage: TradeEvidenceCoverage, 101 pub evidence_policy_digest: String, 102 pub manifest_digest: String, 103 pub canonical_bytes_hex: String, 104 } 105 106 /// Exact immutable RHI supersession reference. 107 #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)] 108 pub struct RhiEvidenceSupersession { 109 pub report_id: String, 110 pub event_id: String, 111 } 112 113 /// Bounded projection of one canonical RHI evidence report. 114 #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)] 115 pub struct RhiEvidenceReport { 116 pub contract_id: String, 117 pub contract_version: u16, 118 pub issuer_pubkey: String, 119 pub trade_id: String, 120 pub claim_mutation_id: String, 121 pub outcome: TradeEvidenceOutcome, 122 pub reason_codes: Vec<String>, 123 pub projection_digest: String, 124 pub evidence_manifest_digest: String, 125 pub evidence_policy_digest: String, 126 pub observed_at_unix_s: String, 127 pub trade_generation: String, 128 pub statement_digest: String, 129 pub supersession: Option<RhiEvidenceSupersession>, 130 pub canonical_content: String, 131 } 132 133 /// One unsigned typed event plan ready for host-owned signing. 134 #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)] 135 pub struct TypedEvidenceEventPlan { 136 pub contract_id: String, 137 pub kind: u32, 138 pub author_pubkey: String, 139 pub created_at_unix_s: String, 140 pub expected_event_id: String, 141 pub tags: Vec<Vec<String>>, 142 pub content: String, 143 } 144 145 /// Signed NIP-01 event input for verified attestation admission. 146 #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)] 147 pub struct SignedEvent { 148 pub id: String, 149 pub author_pubkey: String, 150 pub created_at_unix_s: u64, 151 pub kind: u32, 152 pub tags: Vec<Vec<String>>, 153 pub content: String, 154 pub signature: String, 155 } 156 157 /// Verified final RHI attestation projection. 158 #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)] 159 pub struct RhiEvidenceAttestation { 160 pub issuer_pubkey: String, 161 pub trade_id: String, 162 pub claim_mutation_id: String, 163 pub outcome: TradeEvidenceOutcome, 164 pub observed_at_unix_s: String, 165 pub trade_generation: String, 166 pub statement_digest: String, 167 pub supersession: Option<RhiEvidenceSupersession>, 168 pub canonical_content: String, 169 } 170 171 #[uniffi::export] 172 pub fn parse_trade_evidence_manifest( 173 canonical_bytes: Vec<u8>, 174 ) -> Result<TradeEvidenceManifest, Error> { 175 let manifest = 176 radroots_sdk::trade::parse_evidence_manifest(&canonical_bytes).map_err(|_| { 177 Error::contract("invalid_evidence_manifest", "evidence manifest is invalid") 178 })?; 179 Ok(TradeEvidenceManifest::from(&manifest)) 180 } 181 182 #[uniffi::export] 183 pub fn parse_rhi_evidence_report( 184 canonical_content: String, 185 ) -> Result<RhiEvidenceReport, Error> { 186 let report = radroots_sdk::trade::parse_rhi_evidence_report(canonical_content.as_bytes()) 187 .map_err(|_| { 188 Error::contract("invalid_evidence_report", "evidence report is invalid") 189 })?; 190 Ok(RhiEvidenceReport::from(&report)) 191 } 192 193 #[uniffi::export] 194 pub fn prepare_rhi_evidence_attestation( 195 canonical_content: String, 196 created_at_unix_s: u64, 197 ) -> Result<TypedEvidenceEventPlan, Error> { 198 let report = radroots_sdk::trade::parse_rhi_evidence_report(canonical_content.as_bytes()) 199 .map_err(|_| { 200 Error::contract("invalid_evidence_report", "evidence report is invalid") 201 })?; 202 let plan = 203 radroots_sdk::trade::prepare_rhi_evidence_attestation(&report, created_at_unix_s) 204 .map_err(|_| { 205 Error::contract( 206 "invalid_evidence_attestation_plan", 207 "evidence attestation plan is invalid", 208 ) 209 })?; 210 Ok(TypedEvidenceEventPlan::from(&plan)) 211 } 212 213 #[uniffi::export] 214 pub fn validate_rhi_evidence_attestation( 215 event: SignedEvent, 216 ) -> Result<RhiEvidenceAttestation, Error> { 217 let event = radroots_event::envelope::EventEnvelope::new( 218 radroots_event::envelope::EventEnvelopeParts { 219 id: event.id, 220 author: event.author_pubkey, 221 created_at: event.created_at_unix_s, 222 kind: event.kind, 223 tags: event.tags, 224 content: event.content, 225 sig: event.signature, 226 }, 227 ) 228 .map_err(|_| Error::contract("invalid_signed_event", "signed event is invalid"))?; 229 let attestation = 230 radroots_sdk::trade::validate_rhi_evidence_attestation(event).map_err(|error| { 231 match error { 232 radroots_sdk::trade::EvidenceAttestationValidationError::Signature => { 233 Error::contract( 234 "invalid_event_signature", 235 "event signature validation failed", 236 ) 237 } 238 radroots_sdk::trade::EvidenceAttestationValidationError::Contract => { 239 Error::contract( 240 "invalid_evidence_attestation", 241 "evidence attestation is invalid", 242 ) 243 } 244 } 245 })?; 246 Ok(RhiEvidenceAttestation::from(&attestation)) 247 } 248 249 impl From<&radroots_sdk::trade::RadrootsTradeEvidenceManifestV1> for TradeEvidenceManifest { 250 fn from(value: &radroots_sdk::trade::RadrootsTradeEvidenceManifestV1) -> Self { 251 Self { 252 contract_id: value.contract_id().to_owned(), 253 contract_version: value.contract_version(), 254 trade_id: value.trade_id().to_string(), 255 trade_generation: value.trade_generation().get().to_string(), 256 observed_at_unix_s: value.observed_at_unix_s().to_string(), 257 coverage: value.coverage().into(), 258 evidence_policy_digest: value.evidence_policy_digest().to_hex(), 259 manifest_digest: value.digest().to_hex(), 260 canonical_bytes_hex: hex::encode(value.canonical_bytes()), 261 } 262 } 263 } 264 265 impl From<radroots_sdk::trade::RadrootsTradeEvidenceCoverageV1> for TradeEvidenceCoverage { 266 fn from(value: radroots_sdk::trade::RadrootsTradeEvidenceCoverageV1) -> Self { 267 match value { 268 radroots_sdk::trade::RadrootsTradeEvidenceCoverageV1::Missing => Self::Missing, 269 radroots_sdk::trade::RadrootsTradeEvidenceCoverageV1::Partial => Self::Partial, 270 radroots_sdk::trade::RadrootsTradeEvidenceCoverageV1::ScopeSatisfied => { 271 Self::ScopeSatisfied 272 } 273 radroots_sdk::trade::RadrootsTradeEvidenceCoverageV1::Unsupported => { 274 Self::Unsupported 275 } 276 } 277 } 278 } 279 280 impl From<radroots_sdk::trade::RadrootsTradeEvidenceOutcomeV1> for TradeEvidenceOutcome { 281 fn from(value: radroots_sdk::trade::RadrootsTradeEvidenceOutcomeV1) -> Self { 282 match value { 283 radroots_sdk::trade::RadrootsTradeEvidenceOutcomeV1::Valid => Self::Valid, 284 radroots_sdk::trade::RadrootsTradeEvidenceOutcomeV1::Invalid => Self::Invalid, 285 radroots_sdk::trade::RadrootsTradeEvidenceOutcomeV1::Indeterminate => { 286 Self::Indeterminate 287 } 288 } 289 } 290 } 291 292 impl From<&radroots_sdk::trade::RadrootsRhiEvidenceReportV1> for RhiEvidenceReport { 293 fn from(value: &radroots_sdk::trade::RadrootsRhiEvidenceReportV1) -> Self { 294 Self { 295 contract_id: value.contract_id().to_owned(), 296 contract_version: value.contract_version(), 297 issuer_pubkey: value.issuer_public_key().to_hex(), 298 trade_id: value.trade_id().to_string(), 299 claim_mutation_id: value.claim_mutation_id().to_string(), 300 outcome: value.outcome().into(), 301 reason_codes: value 302 .reason_codes() 303 .iter() 304 .map(|code| code.as_str().to_owned()) 305 .collect(), 306 projection_digest: value.projection_digest().to_hex(), 307 evidence_manifest_digest: value.evidence_manifest_digest().to_hex(), 308 evidence_policy_digest: value.evidence_policy_digest().to_hex(), 309 observed_at_unix_s: value.observed_at_unix_s().to_string(), 310 trade_generation: value.trade_generation().get().to_string(), 311 statement_digest: value.statement_digest().to_hex(), 312 supersession: value.supersession().map(RhiEvidenceSupersession::from), 313 canonical_content: value.canonical_content().to_owned(), 314 } 315 } 316 } 317 318 impl From<radroots_sdk::trade::RadrootsRhiEvidenceSupersessionV1> for RhiEvidenceSupersession { 319 fn from(value: radroots_sdk::trade::RadrootsRhiEvidenceSupersessionV1) -> Self { 320 Self { 321 report_id: value.report_id().to_hex(), 322 event_id: value.event_id().to_hex(), 323 } 324 } 325 } 326 327 impl From<&radroots_event_codec::authoring::AuthoredEventPlan> for TypedEvidenceEventPlan { 328 fn from(value: &radroots_event_codec::authoring::AuthoredEventPlan) -> Self { 329 Self { 330 contract_id: value.body().contract().contract_id().as_str().to_owned(), 331 kind: value.body().kind(), 332 author_pubkey: value.author().to_hex(), 333 created_at_unix_s: value.created_at().to_string(), 334 expected_event_id: value.expected_event_id().to_hex(), 335 tags: value.body().tags().to_vec(), 336 content: value.body().content().to_owned(), 337 } 338 } 339 } 340 341 impl From<&radroots_sdk::trade::RadrootsRhiEvidenceAttestationV1> for RhiEvidenceAttestation { 342 fn from(value: &radroots_sdk::trade::RadrootsRhiEvidenceAttestationV1) -> Self { 343 Self { 344 issuer_pubkey: value.issuer().to_hex(), 345 trade_id: value.trade_id().to_string(), 346 claim_mutation_id: value.claim_mutation_id().to_string(), 347 outcome: match value.outcome() { 348 radroots_sdk::trade::RadrootsRhiEvidenceAttestationOutcomeV1::Valid => { 349 TradeEvidenceOutcome::Valid 350 } 351 radroots_sdk::trade::RadrootsRhiEvidenceAttestationOutcomeV1::Invalid => { 352 TradeEvidenceOutcome::Invalid 353 } 354 radroots_sdk::trade::RadrootsRhiEvidenceAttestationOutcomeV1::Indeterminate => { 355 TradeEvidenceOutcome::Indeterminate 356 } 357 }, 358 observed_at_unix_s: value.observed_at_unix_s().to_string(), 359 trade_generation: value.trade_generation().get().to_string(), 360 statement_digest: hex::encode(value.statement_digest()), 361 supersession: value 362 .supersession() 363 .map(|supersession| RhiEvidenceSupersession { 364 report_id: hex::encode(supersession.report_id()), 365 event_id: supersession.event_id().to_hex(), 366 }), 367 canonical_content: value.canonical_content().to_owned(), 368 } 369 } 370 } 371 372 /// Thread-safe mobile handle delegating lifecycle to [`radroots_sdk::Client`]. 373 #[derive(uniffi::Object)] 374 pub struct MobileClient { 375 client: Client, 376 } 377 378 #[uniffi::export] 379 impl MobileClient { 380 /// Creates a deterministic, local-only memory client without I/O. 381 #[uniffi::constructor] 382 pub fn memory() -> Result<Arc<Self>, Error> { 383 let client = ClientBuilder::memory_default().build()?; 384 Ok(Arc::new(Self { client })) 385 } 386 387 /// Returns stable capability DTOs without probing host resources. 388 pub fn capabilities(&self) -> Vec<CapabilityStatus> { 389 self.client 390 .capabilities() 391 .iter() 392 .copied() 393 .map(CapabilityStatus::from) 394 .collect() 395 } 396 397 /// Returns whether explicit SDK close completed across all clones. 398 pub fn is_closed(&self) -> bool { 399 self.client.is_closed() 400 } 401 402 /// Explicitly closes SDK-owned resources without installing a runtime. 403 pub async fn close(&self) -> Result<(), Error> { 404 self.client.close().await.map_err(Error::from) 405 } 406 } 407 408 impl From<radroots_sdk::capability::CapabilityStatus> for CapabilityStatus { 409 fn from(status: radroots_sdk::capability::CapabilityStatus) -> Self { 410 Self { 411 id: status.id().as_str().to_owned(), 412 compiled: status.is_compiled(), 413 configured: status.is_configured(), 414 availability: match status.availability() { 415 radroots_sdk::capability::Availability::Available => { 416 CapabilityAvailability::Available 417 } 418 radroots_sdk::capability::Availability::Degraded => { 419 CapabilityAvailability::Degraded 420 } 421 radroots_sdk::capability::Availability::Unavailable => { 422 CapabilityAvailability::Unavailable 423 } 424 radroots_sdk::capability::Availability::Unsupported => { 425 CapabilityAvailability::Unsupported 426 } 427 }, 428 maturity: match status.maturity() { 429 radroots_sdk::capability::Maturity::Stable => CapabilityMaturity::Stable, 430 radroots_sdk::capability::Maturity::Preview => CapabilityMaturity::Preview, 431 radroots_sdk::capability::Maturity::Experimental => { 432 CapabilityMaturity::Experimental 433 } 434 }, 435 } 436 } 437 } 438 } 439 440 #[cfg(test)] 441 mod tests { 442 use core::num::NonZeroU64; 443 444 use radroots_event::id::TradeId; 445 446 use super::v1::{ 447 CapabilityAvailability, Error, MobileClient, SignedEvent, TradeEvidenceCoverage, 448 TradeEvidenceOutcome, parse_rhi_evidence_report, parse_trade_evidence_manifest, 449 prepare_rhi_evidence_attestation, validate_rhi_evidence_attestation, 450 }; 451 452 fn assert_send_sync<T: Send + Sync>() {} 453 454 #[tokio::test(flavor = "current_thread")] 455 async fn memory_client_delegates_capabilities_and_lifecycle() { 456 assert_send_sync::<MobileClient>(); 457 let client = MobileClient::memory().expect("memory client"); 458 let capabilities = client.capabilities(); 459 let storage = capabilities 460 .iter() 461 .find(|status| status.id == "storage.canonical") 462 .expect("canonical storage"); 463 assert!(storage.compiled); 464 assert!(storage.configured); 465 assert_eq!(storage.availability, CapabilityAvailability::Available); 466 assert!(!client.is_closed()); 467 468 client.close().await.expect("close"); 469 assert!(client.is_closed()); 470 client.close().await.expect("idempotent close"); 471 } 472 473 #[test] 474 fn sdk_error_mapping_is_versioned_and_secret_safe() { 475 let native = radroots_sdk::ClientBuilder::new() 476 .build() 477 .expect_err("missing storage"); 478 let error = Error::from(native); 479 let Error::Sdk { 480 code, 481 message, 482 retryable, 483 } = error; 484 assert_eq!(code, "missing_storage"); 485 assert_eq!(message, "SDK storage capability is not configured"); 486 assert!(!retryable); 487 } 488 489 #[test] 490 fn final_report_plan_and_signed_event_cross_the_ffi_without_numeric_narrowing() { 491 let fixture: serde_json::Value = serde_json::from_str(include_str!( 492 "../../../contracts/conformance/vectors/event/authored_operations.v1.json" 493 )) 494 .expect("authored corpus"); 495 let expected = fixture["vectors"] 496 .as_array() 497 .expect("operations") 498 .iter() 499 .find(|entry| entry["id"] == "typed_rhi_evidence_attestation_017") 500 .expect("RHI operation") 501 .get("expected") 502 .expect("expected"); 503 let content = expected["content"].as_str().expect("content").to_owned(); 504 505 let report = parse_rhi_evidence_report(content.clone()).expect("report"); 506 assert_eq!(report.outcome, TradeEvidenceOutcome::Indeterminate); 507 assert_eq!(report.trade_generation, "7"); 508 assert_eq!(report.observed_at_unix_s, "1800000000"); 509 assert_eq!(report.canonical_content, content); 510 511 let plan = prepare_rhi_evidence_attestation(content, 1_784_347_200).expect("plan"); 512 assert_eq!(plan.kind, 3_441); 513 assert_eq!(plan.created_at_unix_s, "1784347200"); 514 assert_eq!( 515 plan.expected_event_id, 516 expected["event_id"].as_str().expect("expected event id") 517 ); 518 519 let raw: serde_json::Value = 520 serde_json::from_str(expected["raw_json"].as_str().expect("raw event")) 521 .expect("raw event JSON"); 522 let signed = SignedEvent { 523 id: raw["id"].as_str().expect("id").to_owned(), 524 author_pubkey: raw["pubkey"].as_str().expect("pubkey").to_owned(), 525 created_at_unix_s: raw["created_at"].as_u64().expect("created_at"), 526 kind: u32::try_from(raw["kind"].as_u64().expect("kind")).expect("u32 kind"), 527 tags: serde_json::from_value(raw["tags"].clone()).expect("tags"), 528 content: raw["content"].as_str().expect("content").to_owned(), 529 signature: raw["sig"].as_str().expect("signature").to_owned(), 530 }; 531 let attestation = validate_rhi_evidence_attestation(signed).expect("signed attestation"); 532 assert_eq!(attestation.outcome, TradeEvidenceOutcome::Indeterminate); 533 assert_eq!(attestation.trade_generation, "7"); 534 } 535 536 #[test] 537 fn manifest_supersession_and_closed_evidence_vocabularies_cross_the_ffi() { 538 use radroots_sdk::trade::{ 539 RadrootsTradeEvidenceManifestSourceResultV1, RadrootsTradeEvidenceManifestV1, 540 RadrootsTradeEvidencePolicyDigestV1, RadrootsTradeEvidenceScopePrerequisitesV1, 541 RadrootsTradeEvidenceSourceCompletionV1, RadrootsTradeEvidenceSourceIdV1, 542 RadrootsTradeEvidenceSourceRequirementV1, RadrootsTradeEvidenceSourceResultDigestV1, 543 RadrootsTradeEvidenceSourceResultV1, 544 }; 545 546 for (source, expected) in [ 547 ( 548 radroots_sdk::trade::RadrootsTradeEvidenceCoverageV1::Missing, 549 TradeEvidenceCoverage::Missing, 550 ), 551 ( 552 radroots_sdk::trade::RadrootsTradeEvidenceCoverageV1::Partial, 553 TradeEvidenceCoverage::Partial, 554 ), 555 ( 556 radroots_sdk::trade::RadrootsTradeEvidenceCoverageV1::ScopeSatisfied, 557 TradeEvidenceCoverage::ScopeSatisfied, 558 ), 559 ( 560 radroots_sdk::trade::RadrootsTradeEvidenceCoverageV1::Unsupported, 561 TradeEvidenceCoverage::Unsupported, 562 ), 563 ] { 564 assert_eq!(TradeEvidenceCoverage::from(source), expected); 565 } 566 for (source, expected) in [ 567 ( 568 radroots_sdk::trade::RadrootsTradeEvidenceOutcomeV1::Valid, 569 TradeEvidenceOutcome::Valid, 570 ), 571 ( 572 radroots_sdk::trade::RadrootsTradeEvidenceOutcomeV1::Invalid, 573 TradeEvidenceOutcome::Invalid, 574 ), 575 ( 576 radroots_sdk::trade::RadrootsTradeEvidenceOutcomeV1::Indeterminate, 577 TradeEvidenceOutcome::Indeterminate, 578 ), 579 ] { 580 assert_eq!(TradeEvidenceOutcome::from(source), expected); 581 } 582 583 let source = RadrootsTradeEvidenceManifestSourceResultV1::new( 584 RadrootsTradeEvidenceSourceIdV1::parse("typed_source").expect("source id"), 585 RadrootsTradeEvidenceSourceResultV1::new( 586 RadrootsTradeEvidenceSourceRequirementV1::Required, 587 RadrootsTradeEvidenceSourceCompletionV1::Complete, 588 0, 589 ) 590 .expect("source result"), 591 RadrootsTradeEvidenceSourceResultDigestV1::from_bytes([0x33; 32]), 592 ); 593 let manifest = RadrootsTradeEvidenceManifestV1::new( 594 TradeId::from_bytes([0x11; 16]), 595 NonZeroU64::new(1).expect("nonzero generation"), 596 RadrootsTradeEvidencePolicyDigestV1::from_bytes([0x22; 32]), 597 1_800_000_000, 598 RadrootsTradeEvidenceScopePrerequisitesV1::Satisfied, 599 [source], 600 [], 601 ) 602 .expect("manifest"); 603 let projected = parse_trade_evidence_manifest(manifest.canonical_bytes().to_vec()) 604 .expect("manifest projection"); 605 assert_eq!(projected.coverage, TradeEvidenceCoverage::ScopeSatisfied); 606 assert_eq!( 607 projected.canonical_bytes_hex, 608 hex::encode(manifest.canonical_bytes()) 609 ); 610 assert!(parse_trade_evidence_manifest(Vec::new()).is_err()); 611 612 let decisions: serde_json::Value = serde_json::from_str(include_str!( 613 "../../../contracts/conformance/vectors/rhi/evidence_attestation_decision.v1.json" 614 )) 615 .expect("RHI decision corpus"); 616 let superseding = decisions["vectors"] 617 .as_array() 618 .expect("RHI decision vectors") 619 .iter() 620 .find(|entry| entry["id"] == "rhi_evidence_attestation_superseding_002") 621 .expect("superseding vector"); 622 let report = parse_rhi_evidence_report( 623 superseding["expected"]["canonical_event_content_utf8"] 624 .as_str() 625 .expect("canonical event content") 626 .to_owned(), 627 ) 628 .expect("superseding report"); 629 assert_eq!(report.outcome, TradeEvidenceOutcome::Valid); 630 let supersession = report.supersession.expect("supersession"); 631 assert_eq!( 632 supersession.report_id, 633 "7777777777777777777777777777777777777777777777777777777777777777" 634 ); 635 assert_eq!( 636 supersession.event_id, 637 "8888888888888888888888888888888888888888888888888888888888888888" 638 ); 639 } 640 641 #[test] 642 fn evidence_entry_points_classify_pre_plan_and_signed_event_failures() { 643 assert!(prepare_rhi_evidence_attestation("invalid".to_owned(), 0).is_err()); 644 assert!( 645 validate_rhi_evidence_attestation(SignedEvent { 646 id: String::new(), 647 author_pubkey: String::new(), 648 created_at_unix_s: 0, 649 kind: 0, 650 tags: Vec::new(), 651 content: String::new(), 652 signature: String::new(), 653 }) 654 .is_err() 655 ); 656 } 657 658 #[test] 659 fn ffi_contract_errors_are_bounded_and_secret_safe() { 660 let secret = "private-report-content"; 661 let error = parse_rhi_evidence_report(secret.to_owned()).expect_err("malformed report"); 662 let display = error.to_string(); 663 let debug = format!("{error:?}"); 664 assert!(!display.contains(secret)); 665 assert!(!debug.contains(secret)); 666 assert!(display.contains("invalid_evidence_report")); 667 } 668 }