transport.rs (121873B)
1 //! Explicit user-facing transport profile composition. 2 //! 3 //! Profiles retain canonical `radroots_transport` identities, targets, 4 //! policies, and statuses. They select no adapter implicitly and never replace 5 //! an unavailable selection with another transport. 6 7 use radroots_transport::{ 8 Error, SinkStatus, SourceStatus, TargetSet, TransportId, 9 capability::{Availability, Maturity, SinkCapabilities, SourceCapabilities}, 10 policy::SatisfactionPolicy, 11 }; 12 #[cfg(any(feature = "blossom", feature = "nostr"))] 13 use std::sync::{Arc, RwLock}; 14 15 #[cfg(feature = "blossom")] 16 use std::{ 17 collections::BTreeSet, 18 net::{IpAddr, Ipv4Addr, Ipv6Addr}, 19 sync::atomic::{AtomicBool, Ordering}, 20 time::{Duration, SystemTime, UNIX_EPOCH}, 21 }; 22 23 #[cfg(feature = "blossom")] 24 use radroots_blossom::{ 25 BlobUrl, ByteVerifiedDescriptor, MediaType, Sha256, 26 authorization::{AuthoredUploadClaim, AuthorizationContent, ServerDomain}, 27 }; 28 29 #[cfg(feature = "nostr")] 30 pub use radroots_transport_nostr::{ 31 ReconnectBackoff, RelayAccess, RelayAggregateState, RelayCapabilityEvidence, RelayCursor, 32 RelayEndpoint, RelayEvidenceState, RelayProfile, RelayProfileKind, RelayStatus, 33 RelayStatusReport, RelayUrl, RelayUrlPolicy, 34 }; 35 36 const PREVIEW_UNAVAILABLE_MESSAGE: &str = "preview transport is unavailable in this SDK release"; 37 38 #[cfg(feature = "blossom")] 39 const MAX_BLOSSOM_ENDPOINTS: usize = 16; 40 #[cfg(feature = "blossom")] 41 const MAX_BLOSSOM_BLOB_BYTES: u64 = 100 * 1024 * 1024; 42 #[cfg(feature = "blossom")] 43 const MAX_BLOSSOM_DESCRIPTOR_BYTES: usize = 64 * 1024; 44 #[cfg(feature = "blossom")] 45 const MAX_BLOSSOM_REDIRECTS: u8 = 5; 46 #[cfg(feature = "blossom")] 47 const MAX_BLOSSOM_ATTEMPTS: u8 = 5; 48 #[cfg(feature = "blossom")] 49 const MAX_BLOSSOM_TIMEOUT: Duration = Duration::from_secs(120); 50 #[cfg(feature = "blossom")] 51 const MAX_BLOSSOM_RETRY_DELAY: Duration = Duration::from_secs(30); 52 #[cfg(feature = "blossom")] 53 const MAX_BLOSSOM_IMAGE_EDGE: u32 = 16_384; 54 #[cfg(feature = "blossom")] 55 const MAX_BLOSSOM_IMAGE_PIXELS: u64 = 100_000_000; 56 57 /// Host environment executing Blossom operations. 58 #[cfg(feature = "blossom")] 59 #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] 60 #[non_exhaustive] 61 pub enum BlossomHostKind { 62 /// A non-mobile native host. 63 Native, 64 /// An Apple or Android simulator. 65 Simulator, 66 /// A physical mobile device. 67 PhysicalDevice, 68 } 69 70 /// Network authority applied independently to configured Blossom origins. 71 #[cfg(feature = "blossom")] 72 #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] 73 #[non_exhaustive] 74 pub enum BlossomEndpointAuthority { 75 /// Public HTTPS authenticated by the platform WebPKI roots. 76 PublicWebPki, 77 /// Development-only HTTP or HTTPS resolving exclusively to loopback. 78 LoopbackDevelopment, 79 /// Development-only HTTP or HTTPS using an exact RFC1918 or ULA address. 80 PrivateNetworkDevelopment, 81 } 82 83 /// One canonical configured Blossom origin. 84 #[cfg(feature = "blossom")] 85 #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] 86 pub struct BlossomEndpoint { 87 origin: String, 88 host: String, 89 port: u16, 90 authority: BlossomEndpointAuthority, 91 } 92 93 #[cfg(feature = "blossom")] 94 impl BlossomEndpoint { 95 fn parse( 96 value: impl AsRef<str>, 97 authority: BlossomEndpointAuthority, 98 ) -> Result<Self, BlossomError> { 99 let value = value.as_ref(); 100 if value.is_empty() || !value.is_ascii() || value.chars().any(char::is_whitespace) { 101 return Err(BlossomError::configuration( 102 BlossomErrorKind::InvalidEndpoint, 103 )); 104 } 105 let parsed = reqwest::Url::parse(value) 106 .map_err(|_| BlossomError::configuration(BlossomErrorKind::InvalidEndpoint))?; 107 if !parsed.username().is_empty() 108 || parsed.password().is_some() 109 || parsed.query().is_some() 110 || parsed.fragment().is_some() 111 || parsed.path() != "/" 112 { 113 return Err(BlossomError::configuration( 114 BlossomErrorKind::InvalidEndpoint, 115 )); 116 } 117 let host = parsed 118 .host_str() 119 .ok_or_else(|| BlossomError::configuration(BlossomErrorKind::InvalidEndpoint))? 120 .to_owned(); 121 let port = parsed 122 .port_or_known_default() 123 .ok_or_else(|| BlossomError::configuration(BlossomErrorKind::InvalidEndpoint))?; 124 if port == 0 || !endpoint_scheme_is_allowed(parsed.scheme(), authority) { 125 return Err(BlossomError::configuration( 126 BlossomErrorKind::EndpointSchemeDenied, 127 )); 128 } 129 validate_blossom_host(host.as_str(), authority)?; 130 ServerDomain::parse(host.as_str()) 131 .map_err(|_| BlossomError::configuration(BlossomErrorKind::InvalidEndpoint))?; 132 // HTTP(S) URLs always have a tuple origin after the scheme and host 133 // checks above, so `ascii_serialization` cannot be the opaque `null` 134 // origin here. 135 let origin = parsed.origin().ascii_serialization(); 136 Ok(Self { 137 origin, 138 host, 139 port, 140 authority, 141 }) 142 } 143 144 /// Returns the canonical origin without a trailing slash. 145 #[must_use] 146 pub fn origin(&self) -> &str { 147 self.origin.as_str() 148 } 149 150 /// Returns the BUD-11 server-domain spelling. 151 #[must_use] 152 pub fn host(&self) -> &str { 153 self.host.as_str() 154 } 155 156 /// Returns the resolved connection port. 157 #[must_use] 158 pub const fn port(&self) -> u16 { 159 self.port 160 } 161 162 /// Returns the authority used before and after DNS resolution. 163 #[must_use] 164 pub const fn authority(&self) -> BlossomEndpointAuthority { 165 self.authority 166 } 167 168 pub(crate) fn upload_url(&self) -> String { 169 format!("{}/upload", self.origin) 170 } 171 172 pub(crate) fn server_domain(&self) -> Result<ServerDomain, BlossomError> { 173 ServerDomain::parse(self.host.as_str()) 174 .map_err(|_| BlossomError::configuration(BlossomErrorKind::InvalidEndpoint)) 175 } 176 177 pub(crate) fn accepts_blob_url(&self, value: &BlobUrl) -> bool { 178 reqwest::Url::parse(value.as_str()) 179 .is_ok_and(|url| url.origin().ascii_serialization() == self.origin) 180 } 181 182 pub(crate) fn validate_resolved_addresses( 183 &self, 184 addresses: impl IntoIterator<Item = IpAddr>, 185 ) -> Result<(), BlossomError> { 186 let mut found = false; 187 for address in addresses { 188 found = true; 189 if !blossom_authority_accepts_address(self.authority, address) { 190 return Err(BlossomError::configuration( 191 BlossomErrorKind::ResolvedAddressDenied, 192 )); 193 } 194 } 195 if !found { 196 return Err(BlossomError::configuration( 197 BlossomErrorKind::ResolutionFailed, 198 )); 199 } 200 Ok(()) 201 } 202 } 203 204 /// Complete validated Blossom origin set for one host environment. 205 #[cfg(feature = "blossom")] 206 #[derive(Clone, Debug, Eq, PartialEq)] 207 pub struct BlossomProfile { 208 host_kind: BlossomHostKind, 209 authority: BlossomEndpointAuthority, 210 primary: BlossomEndpoint, 211 fallbacks: Vec<BlossomEndpoint>, 212 } 213 214 #[cfg(feature = "blossom")] 215 impl BlossomProfile { 216 /// Configures one primary origin and an ordered, explicitly bounded fallback set. 217 pub fn new<I, S>( 218 host_kind: BlossomHostKind, 219 authority: BlossomEndpointAuthority, 220 primary_origin: impl AsRef<str>, 221 fallback_origins: I, 222 ) -> Result<Self, BlossomError> 223 where 224 I: IntoIterator<Item = S>, 225 S: AsRef<str>, 226 { 227 validate_host_authority(host_kind, authority)?; 228 let primary = BlossomEndpoint::parse(primary_origin, authority)?; 229 // The public constructor accepts any iterator, so bound collection 230 // before parsing to prevent an oversized caller from allocating an 231 // unbounded temporary vector only to be rejected afterward. 232 let fallbacks = fallback_origins 233 .into_iter() 234 .take(MAX_BLOSSOM_ENDPOINTS) 235 .map(|origin| BlossomEndpoint::parse(origin, authority)) 236 .collect::<Result<Vec<_>, _>>()?; 237 if fallbacks.len().saturating_add(1) > MAX_BLOSSOM_ENDPOINTS { 238 return Err(BlossomError::configuration( 239 BlossomErrorKind::InvalidEndpointCount, 240 )); 241 } 242 let unique = std::iter::once(&primary) 243 .chain(fallbacks.iter()) 244 .map(BlossomEndpoint::origin) 245 .collect::<BTreeSet<_>>(); 246 if unique.len() != fallbacks.len().saturating_add(1) { 247 return Err(BlossomError::configuration( 248 BlossomErrorKind::DuplicateEndpoint, 249 )); 250 } 251 Ok(Self { 252 host_kind, 253 authority, 254 primary, 255 fallbacks, 256 }) 257 } 258 259 #[must_use] 260 pub const fn host_kind(&self) -> BlossomHostKind { 261 self.host_kind 262 } 263 264 #[must_use] 265 pub const fn authority(&self) -> BlossomEndpointAuthority { 266 self.authority 267 } 268 269 #[must_use] 270 pub const fn primary(&self) -> &BlossomEndpoint { 271 &self.primary 272 } 273 274 #[must_use] 275 pub fn fallbacks(&self) -> &[BlossomEndpoint] { 276 self.fallbacks.as_slice() 277 } 278 279 pub(crate) fn endpoint_for_blob(&self, url: &BlobUrl) -> Option<&BlossomEndpoint> { 280 std::iter::once(&self.primary) 281 .chain(self.fallbacks.iter()) 282 .find(|endpoint| endpoint.accepts_blob_url(url)) 283 } 284 } 285 286 /// Bounded HTTP, response, retry, and redirect policy for Blossom operations. 287 #[cfg(feature = "blossom")] 288 #[derive(Clone, Debug, Eq, PartialEq)] 289 pub struct BlossomConfig { 290 profile: BlossomProfile, 291 max_blob_bytes: u64, 292 max_descriptor_bytes: usize, 293 max_redirects: u8, 294 max_attempts: u8, 295 connect_timeout: Duration, 296 request_timeout: Duration, 297 initial_retry_delay: Duration, 298 } 299 300 #[cfg(feature = "blossom")] 301 impl BlossomConfig { 302 #[must_use] 303 pub fn from_profile(profile: BlossomProfile) -> Self { 304 Self { 305 profile, 306 max_blob_bytes: 20 * 1024 * 1024, 307 max_descriptor_bytes: 16 * 1024, 308 max_redirects: 3, 309 max_attempts: 3, 310 connect_timeout: Duration::from_secs(10), 311 request_timeout: Duration::from_secs(60), 312 initial_retry_delay: Duration::from_millis(250), 313 } 314 } 315 316 pub fn with_limits( 317 mut self, 318 max_blob_bytes: u64, 319 max_descriptor_bytes: usize, 320 max_redirects: u8, 321 ) -> Result<Self, BlossomError> { 322 if max_blob_bytes == 0 323 || max_blob_bytes > MAX_BLOSSOM_BLOB_BYTES 324 || max_descriptor_bytes == 0 325 || max_descriptor_bytes > MAX_BLOSSOM_DESCRIPTOR_BYTES 326 || max_redirects > MAX_BLOSSOM_REDIRECTS 327 { 328 return Err(BlossomError::configuration(BlossomErrorKind::InvalidLimits)); 329 } 330 self.max_blob_bytes = max_blob_bytes; 331 self.max_descriptor_bytes = max_descriptor_bytes; 332 self.max_redirects = max_redirects; 333 Ok(self) 334 } 335 336 pub fn with_network_policy( 337 mut self, 338 connect_timeout: Duration, 339 request_timeout: Duration, 340 max_attempts: u8, 341 initial_retry_delay: Duration, 342 ) -> Result<Self, BlossomError> { 343 if connect_timeout.is_zero() 344 || connect_timeout > MAX_BLOSSOM_TIMEOUT 345 || request_timeout.is_zero() 346 || request_timeout > MAX_BLOSSOM_TIMEOUT 347 || max_attempts == 0 348 || max_attempts > MAX_BLOSSOM_ATTEMPTS 349 || initial_retry_delay.is_zero() 350 || initial_retry_delay > MAX_BLOSSOM_RETRY_DELAY 351 { 352 return Err(BlossomError::configuration(BlossomErrorKind::InvalidLimits)); 353 } 354 self.connect_timeout = connect_timeout; 355 self.request_timeout = request_timeout; 356 self.max_attempts = max_attempts; 357 self.initial_retry_delay = initial_retry_delay; 358 Ok(self) 359 } 360 361 #[must_use] 362 pub const fn profile(&self) -> &BlossomProfile { 363 &self.profile 364 } 365 366 /// Returns the stable identity of every setting that can affect an operation. 367 #[must_use] 368 pub fn fingerprint(&self) -> BlossomConfigFingerprint { 369 let mut material = Vec::new(); 370 material.extend_from_slice(b"radroots-blossom-config-v1\0"); 371 material.push(match self.profile.host_kind { 372 BlossomHostKind::Native => 0, 373 BlossomHostKind::Simulator => 1, 374 BlossomHostKind::PhysicalDevice => 2, 375 }); 376 material.push(match self.profile.authority { 377 BlossomEndpointAuthority::PublicWebPki => 0, 378 BlossomEndpointAuthority::LoopbackDevelopment => 1, 379 BlossomEndpointAuthority::PrivateNetworkDevelopment => 2, 380 }); 381 append_fingerprint_field(&mut material, self.profile.primary.origin.as_bytes()); 382 material.extend_from_slice(&(self.profile.fallbacks.len() as u64).to_be_bytes()); 383 for endpoint in &self.profile.fallbacks { 384 append_fingerprint_field(&mut material, endpoint.origin.as_bytes()); 385 } 386 material.extend_from_slice(&self.max_blob_bytes.to_be_bytes()); 387 material.extend_from_slice(&(self.max_descriptor_bytes as u64).to_be_bytes()); 388 material.push(self.max_redirects); 389 material.push(self.max_attempts); 390 material.extend_from_slice(&(self.connect_timeout.as_millis() as u64).to_be_bytes()); 391 material.extend_from_slice(&(self.request_timeout.as_millis() as u64).to_be_bytes()); 392 material.extend_from_slice(&(self.initial_retry_delay.as_millis() as u64).to_be_bytes()); 393 BlossomConfigFingerprint(Sha256::digest(material.as_slice())) 394 } 395 396 pub(crate) const fn max_blob_bytes(&self) -> u64 { 397 self.max_blob_bytes 398 } 399 400 pub(crate) const fn max_descriptor_bytes(&self) -> usize { 401 self.max_descriptor_bytes 402 } 403 404 pub(crate) const fn max_redirects(&self) -> u8 { 405 self.max_redirects 406 } 407 408 pub(crate) const fn max_attempts(&self) -> u8 { 409 self.max_attempts 410 } 411 412 pub(crate) const fn connect_timeout(&self) -> Duration { 413 self.connect_timeout 414 } 415 416 pub(crate) const fn request_timeout(&self) -> Duration { 417 self.request_timeout 418 } 419 420 pub(crate) const fn initial_retry_delay(&self) -> Duration { 421 self.initial_retry_delay 422 } 423 424 pub(crate) fn retry_delay(&self, attempt: u8) -> Duration { 425 let exponent = u32::from(attempt.saturating_sub(1)).min(16); 426 self.initial_retry_delay() 427 .saturating_mul(1_u32 << exponent) 428 .min(MAX_BLOSSOM_RETRY_DELAY) 429 } 430 } 431 432 /// Stable, non-secret identity of a completely validated Blossom configuration. 433 #[cfg(feature = "blossom")] 434 #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] 435 pub struct BlossomConfigFingerprint(Sha256); 436 437 #[cfg(feature = "blossom")] 438 impl BlossomConfigFingerprint { 439 #[must_use] 440 pub const fn as_bytes(&self) -> &[u8; 32] { 441 self.0.as_bytes() 442 } 443 444 #[must_use] 445 pub fn to_hex(self) -> String { 446 self.0.to_hex() 447 } 448 } 449 450 #[cfg(feature = "blossom")] 451 impl std::fmt::Display for BlossomConfigFingerprint { 452 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 453 std::fmt::Display::fmt(&self.0, formatter) 454 } 455 } 456 457 #[cfg(feature = "blossom")] 458 fn append_fingerprint_field(material: &mut Vec<u8>, value: &[u8]) { 459 material.extend_from_slice(&(value.len() as u64).to_be_bytes()); 460 material.extend_from_slice(value); 461 } 462 463 #[cfg(feature = "blossom")] 464 pub(crate) fn blossom_now_unix_ms() -> u64 { 465 SystemTime::now() 466 .duration_since(UNIX_EPOCH) 467 .ok() 468 .and_then(|duration| u64::try_from(duration.as_millis()).ok()) 469 .unwrap_or(u64::MAX) 470 .max(1) 471 } 472 473 /// Nonzero dimensions verified from the final image bytes. 474 #[cfg(feature = "blossom")] 475 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 476 pub struct BlossomImageDimensions { 477 width: u32, 478 height: u32, 479 } 480 481 #[cfg(feature = "blossom")] 482 impl BlossomImageDimensions { 483 pub const fn new(width: u32, height: u32) -> Result<Self, BlossomError> { 484 if width == 0 485 || height == 0 486 || width > MAX_BLOSSOM_IMAGE_EDGE 487 || height > MAX_BLOSSOM_IMAGE_EDGE 488 || width as u64 * height as u64 > MAX_BLOSSOM_IMAGE_PIXELS 489 { 490 return Err(BlossomError::configuration( 491 BlossomErrorKind::InvalidDimensions, 492 )); 493 } 494 Ok(Self { width, height }) 495 } 496 497 #[must_use] 498 pub const fn width(self) -> u32 { 499 self.width 500 } 501 502 #[must_use] 503 pub const fn height(self) -> u32 { 504 self.height 505 } 506 } 507 508 /// Exact final image bytes from which Rust derives the BUD-02 destination. 509 #[cfg(feature = "blossom")] 510 #[derive(Clone)] 511 pub struct BlossomUploadRequest { 512 sha256: Sha256, 513 bytes: Arc<[u8]>, 514 media_type: MediaType, 515 dimensions: BlossomImageDimensions, 516 verified_at_unix_ms: u64, 517 } 518 519 #[cfg(feature = "blossom")] 520 impl BlossomUploadRequest { 521 pub fn new( 522 bytes: Arc<[u8]>, 523 media_type: MediaType, 524 dimensions: BlossomImageDimensions, 525 verified_at_unix_ms: u64, 526 ) -> Result<Self, BlossomError> { 527 if bytes.is_empty() || verified_at_unix_ms == 0 { 528 return Err(BlossomError::configuration( 529 BlossomErrorKind::InvalidRequest, 530 )); 531 } 532 crate::adapters::blossom::verify_image(bytes.as_ref(), &media_type, dimensions)?; 533 Ok(Self { 534 sha256: Sha256::digest(bytes.as_ref()), 535 bytes, 536 media_type, 537 dimensions, 538 verified_at_unix_ms, 539 }) 540 } 541 542 #[must_use] 543 pub fn media_type(&self) -> &MediaType { 544 &self.media_type 545 } 546 547 #[must_use] 548 pub const fn dimensions(&self) -> BlossomImageDimensions { 549 self.dimensions 550 } 551 552 #[must_use] 553 pub const fn sha256(&self) -> Sha256 { 554 self.sha256 555 } 556 557 #[must_use] 558 pub fn byte_size(&self) -> u64 { 559 self.bytes.len() as u64 560 } 561 562 pub(crate) fn bytes(&self) -> &[u8] { 563 self.bytes.as_ref() 564 } 565 566 pub(crate) const fn verified_at_unix_ms(&self) -> u64 { 567 self.verified_at_unix_ms 568 } 569 } 570 571 /// Expected signed metadata for one bounded BUD-01 image retrieval. 572 #[cfg(feature = "blossom")] 573 #[derive(Clone, Debug, Eq, PartialEq)] 574 pub struct BlossomInboundRequest { 575 url: BlobUrl, 576 expected_media_type: Option<MediaType>, 577 expected_byte_size: Option<u64>, 578 expected_dimensions: Option<BlossomImageDimensions>, 579 } 580 581 #[cfg(feature = "blossom")] 582 impl BlossomInboundRequest { 583 pub fn new( 584 url: BlobUrl, 585 expected_media_type: Option<MediaType>, 586 expected_byte_size: Option<u64>, 587 expected_dimensions: Option<BlossomImageDimensions>, 588 ) -> Result<Self, BlossomError> { 589 url.clone() 590 .approve() 591 .map_err(|_| BlossomError::configuration(BlossomErrorKind::InvalidRequest))?; 592 if expected_byte_size == Some(0) { 593 return Err(BlossomError::configuration( 594 BlossomErrorKind::InvalidRequest, 595 )); 596 } 597 if let Some(media_type) = &expected_media_type { 598 let extension = canonical_image_extension(media_type)?; 599 if url 600 .hash_path() 601 .extension() 602 .is_none_or(|value| value.as_str() != extension) 603 { 604 return Err(BlossomError::configuration( 605 BlossomErrorKind::MediaTypeMismatch, 606 )); 607 } 608 } 609 Ok(Self { 610 url, 611 expected_media_type, 612 expected_byte_size, 613 expected_dimensions, 614 }) 615 } 616 617 #[must_use] 618 pub const fn url(&self) -> &BlobUrl { 619 &self.url 620 } 621 622 #[must_use] 623 pub const fn expected_media_type(&self) -> Option<&MediaType> { 624 self.expected_media_type.as_ref() 625 } 626 627 #[must_use] 628 pub const fn expected_byte_size(&self) -> Option<u64> { 629 self.expected_byte_size 630 } 631 632 #[must_use] 633 pub const fn expected_dimensions(&self) -> Option<BlossomImageDimensions> { 634 self.expected_dimensions 635 } 636 } 637 638 /// Immutable upload plan binding exact bytes to one complete configuration. 639 #[cfg(feature = "blossom")] 640 #[derive(Clone)] 641 pub struct BlossomUploadTransaction { 642 config: BlossomConfig, 643 config_fingerprint: BlossomConfigFingerprint, 644 endpoint: BlossomEndpoint, 645 expected_url: BlobUrl, 646 request: BlossomUploadRequest, 647 } 648 649 /// Security property observed for the configured primary transport. 650 #[cfg(feature = "blossom")] 651 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 652 #[non_exhaustive] 653 pub enum BlossomTransportSecurity { 654 /// Public HTTPS authenticated by the bundled platform WebPKI roots. 655 PublicWebPki, 656 /// Development HTTPS without a public-origin availability claim. 657 DevelopmentTls, 658 /// Development-only cleartext loopback or exact-private-network HTTP. 659 DevelopmentCleartext, 660 } 661 662 /// Latest redacted evidence state for the configured primary Blossom origin. 663 #[cfg(feature = "blossom")] 664 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 665 #[non_exhaustive] 666 pub enum BlossomEvidenceState { 667 ConfiguredUnobserved, 668 DnsPolicyValidated, 669 TlsHttpObserved, 670 UploadVerified, 671 RetrievalVerified, 672 RetryableFailure, 673 TerminalFailure, 674 } 675 676 /// Versioned, passive, secret-safe evidence for one exact configuration. 677 #[cfg(feature = "blossom")] 678 #[derive(Clone, Debug, Eq, PartialEq)] 679 pub struct BlossomEndpointEvidence { 680 origin: String, 681 config_fingerprint: BlossomConfigFingerprint, 682 state: BlossomEvidenceState, 683 last_successful_state: BlossomEvidenceState, 684 transport_security: BlossomTransportSecurity, 685 observed_at_unix_ms: Option<u64>, 686 http_status: Option<u16>, 687 error_code: Option<&'static str>, 688 server_error_code: Option<String>, 689 error_phase: Option<BlossomPhase>, 690 retryable: bool, 691 possible_orphan: bool, 692 attempts: u8, 693 } 694 695 #[cfg(feature = "blossom")] 696 impl BlossomEndpointEvidence { 697 const SCHEMA_VERSION: u16 = 2; 698 699 fn configured(config: &BlossomConfig) -> Self { 700 let primary = config.profile().primary(); 701 Self { 702 origin: primary.origin().to_owned(), 703 config_fingerprint: config.fingerprint(), 704 state: BlossomEvidenceState::ConfiguredUnobserved, 705 last_successful_state: BlossomEvidenceState::ConfiguredUnobserved, 706 transport_security: match ( 707 primary.origin().starts_with("https://"), 708 primary.authority(), 709 ) { 710 (true, BlossomEndpointAuthority::PublicWebPki) => { 711 BlossomTransportSecurity::PublicWebPki 712 } 713 (true, _) => BlossomTransportSecurity::DevelopmentTls, 714 (false, _) => BlossomTransportSecurity::DevelopmentCleartext, 715 }, 716 observed_at_unix_ms: None, 717 http_status: None, 718 error_code: None, 719 server_error_code: None, 720 error_phase: None, 721 retryable: false, 722 possible_orphan: false, 723 attempts: 0, 724 } 725 } 726 727 #[must_use] 728 pub const fn schema_version(&self) -> u16 { 729 Self::SCHEMA_VERSION 730 } 731 732 #[must_use] 733 pub fn origin(&self) -> &str { 734 self.origin.as_str() 735 } 736 737 #[must_use] 738 pub const fn config_fingerprint(&self) -> BlossomConfigFingerprint { 739 self.config_fingerprint 740 } 741 742 #[must_use] 743 pub const fn state(&self) -> BlossomEvidenceState { 744 self.state 745 } 746 747 #[must_use] 748 pub const fn last_successful_state(&self) -> BlossomEvidenceState { 749 self.last_successful_state 750 } 751 752 #[must_use] 753 pub const fn transport_security(&self) -> BlossomTransportSecurity { 754 self.transport_security 755 } 756 757 #[must_use] 758 pub const fn observed_at_unix_ms(&self) -> Option<u64> { 759 self.observed_at_unix_ms 760 } 761 762 #[must_use] 763 pub const fn http_status(&self) -> Option<u16> { 764 self.http_status 765 } 766 767 #[must_use] 768 pub const fn error_code(&self) -> Option<&'static str> { 769 self.error_code 770 } 771 772 /// Bounded, validated public error identifier returned by the server. 773 #[must_use] 774 pub fn server_error_code(&self) -> Option<&str> { 775 self.server_error_code.as_deref() 776 } 777 778 #[must_use] 779 pub const fn error_phase(&self) -> Option<BlossomPhase> { 780 self.error_phase 781 } 782 783 #[must_use] 784 pub const fn retryable(&self) -> bool { 785 self.retryable 786 } 787 788 #[must_use] 789 pub const fn possible_orphan(&self) -> bool { 790 self.possible_orphan 791 } 792 793 #[must_use] 794 pub const fn attempts(&self) -> u8 { 795 self.attempts 796 } 797 798 fn record_success(&mut self, state: BlossomEvidenceState, http_status: Option<u16>) { 799 self.state = state; 800 self.last_successful_state = state; 801 self.observed_at_unix_ms = Some(blossom_now_unix_ms()); 802 self.http_status = http_status; 803 self.error_code = None; 804 self.server_error_code = None; 805 self.error_phase = None; 806 self.retryable = false; 807 self.possible_orphan = false; 808 self.attempts = 0; 809 } 810 811 fn record_failure(&mut self, error: &BlossomError) { 812 self.state = if error.retryable() { 813 BlossomEvidenceState::RetryableFailure 814 } else { 815 BlossomEvidenceState::TerminalFailure 816 }; 817 self.observed_at_unix_ms = Some(blossom_now_unix_ms()); 818 self.http_status = error.http_status(); 819 self.error_code = Some(error.code()); 820 self.server_error_code = error.server_error_code().map(str::to_owned); 821 self.error_phase = Some(error.phase()); 822 self.retryable = error.retryable(); 823 self.possible_orphan = error.possible_orphan(); 824 self.attempts = error.attempts(); 825 } 826 } 827 828 #[cfg(feature = "blossom")] 829 impl BlossomUploadTransaction { 830 /// Returns the frozen policy's minimum delay before another attempt. 831 /// Zero or exhausted completed-attempt counts admit no retry. This is a 832 /// pure policy query, not evidence of native inactivity, renewed signing 833 /// access, or absence of remote effects. The caller owns those prerequisites 834 /// and the durable attempt count; current slot configuration cannot change 835 /// this transaction's budget. 836 #[must_use] 837 pub fn retry_delay_after(&self, completed_attempts: u8) -> Option<Duration> { 838 (completed_attempts > 0 && completed_attempts < self.config.max_attempts()) 839 .then(|| self.config.retry_delay(completed_attempts)) 840 } 841 842 #[must_use] 843 pub const fn config_fingerprint(&self) -> BlossomConfigFingerprint { 844 self.config_fingerprint 845 } 846 847 #[must_use] 848 pub const fn expected_url(&self) -> &BlobUrl { 849 &self.expected_url 850 } 851 852 #[must_use] 853 pub const fn request(&self) -> &BlossomUploadRequest { 854 &self.request 855 } 856 857 pub(crate) const fn config(&self) -> &BlossomConfig { 858 &self.config 859 } 860 861 pub(crate) const fn endpoint(&self) -> &BlossomEndpoint { 862 &self.endpoint 863 } 864 865 pub(crate) fn into_request(self) -> BlossomUploadRequest { 866 self.request 867 } 868 } 869 870 #[cfg(feature = "blossom")] 871 impl std::fmt::Debug for BlossomUploadTransaction { 872 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 873 formatter 874 .debug_struct("BlossomUploadTransaction") 875 .field("config_fingerprint", &self.config_fingerprint) 876 .field("endpoint", &self.endpoint) 877 .field("expected_url", &self.expected_url) 878 .field("request", &self.request) 879 .finish() 880 } 881 } 882 883 #[cfg(feature = "blossom")] 884 impl std::fmt::Debug for BlossomUploadRequest { 885 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 886 formatter 887 .debug_struct("BlossomUploadRequest") 888 .field("sha256", &self.sha256()) 889 .field("byte_size", &self.byte_size()) 890 .field("media_type", &self.media_type) 891 .field("dimensions", &self.dimensions) 892 .field("bytes", &"<redacted>") 893 .finish() 894 } 895 } 896 897 /// Cooperative cancellation shared by upload, retry, and retrieval phases. 898 #[cfg(feature = "blossom")] 899 #[derive(Clone, Debug, Default)] 900 pub struct BlossomCancellation { 901 state: Arc<BlossomCancellationState>, 902 } 903 904 #[cfg(feature = "blossom")] 905 #[derive(Debug, Default)] 906 struct BlossomCancellationState { 907 cancelled: AtomicBool, 908 notify: tokio::sync::Notify, 909 } 910 911 #[cfg(feature = "blossom")] 912 impl BlossomCancellation { 913 pub fn cancel(&self) { 914 self.state.cancelled.store(true, Ordering::Release); 915 self.state.notify.notify_waiters(); 916 } 917 918 #[must_use] 919 pub fn is_cancelled(&self) -> bool { 920 self.state.cancelled.load(Ordering::Acquire) 921 } 922 923 pub(crate) async fn cancelled(&self) { 924 loop { 925 let notified = self.state.notify.notified(); 926 if self.is_cancelled() { 927 return; 928 } 929 notified.await; 930 } 931 } 932 } 933 934 /// Exact operation phase associated with one redacted Blossom failure. 935 #[cfg(feature = "blossom")] 936 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 937 #[non_exhaustive] 938 pub enum BlossomPhase { 939 Configuration, 940 Probe, 941 Authorization, 942 Upload, 943 Descriptor, 944 Retrieval, 945 Verification, 946 } 947 948 /// Stable, secret-safe Blossom failure classification. 949 #[cfg(feature = "blossom")] 950 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 951 #[non_exhaustive] 952 pub enum BlossomErrorKind { 953 InvalidEndpoint, 954 EndpointSchemeDenied, 955 InvalidEndpointCount, 956 DuplicateEndpoint, 957 EndpointNotConfigured, 958 ConfigurationChanged, 959 ResolutionFailed, 960 ResolvedAddressDenied, 961 InvalidLimits, 962 InvalidRequest, 963 InvalidDimensions, 964 UnsupportedMediaType, 965 MediaTypeMismatch, 966 InvalidImageBytes, 967 DimensionMismatch, 968 Authorization, 969 Transport, 970 Timeout, 971 Cancelled, 972 HttpStatus, 973 UnsafeRedirect, 974 RedirectLimit, 975 ContentEncodingDenied, 976 ResponseTooLarge, 977 ResponseSizeMismatch, 978 ResponseHashMismatch, 979 InvalidDescriptor, 980 DescriptorMismatch, 981 RetrievedBytesMismatch, 982 } 983 984 /// Redacted recoverable state for one Blossom operation failure. 985 #[cfg(feature = "blossom")] 986 #[derive(Clone, Eq, PartialEq)] 987 pub struct BlossomError { 988 kind: BlossomErrorKind, 989 phase: BlossomPhase, 990 retryable: bool, 991 possible_orphan: bool, 992 attempts: u8, 993 http_status: Option<u16>, 994 server_error_code: Option<String>, 995 } 996 997 #[cfg(feature = "blossom")] 998 impl BlossomError { 999 pub(crate) const fn new( 1000 kind: BlossomErrorKind, 1001 phase: BlossomPhase, 1002 retryable: bool, 1003 possible_orphan: bool, 1004 attempts: u8, 1005 ) -> Self { 1006 Self { 1007 kind, 1008 phase, 1009 retryable, 1010 possible_orphan, 1011 attempts, 1012 http_status: None, 1013 server_error_code: None, 1014 } 1015 } 1016 1017 const fn configuration(kind: BlossomErrorKind) -> Self { 1018 Self::new(kind, BlossomPhase::Configuration, false, false, 0) 1019 } 1020 1021 #[must_use] 1022 pub const fn kind(&self) -> BlossomErrorKind { 1023 self.kind 1024 } 1025 1026 #[must_use] 1027 pub const fn phase(&self) -> BlossomPhase { 1028 self.phase 1029 } 1030 1031 #[must_use] 1032 pub const fn retryable(&self) -> bool { 1033 self.retryable 1034 } 1035 1036 #[must_use] 1037 pub const fn possible_orphan(&self) -> bool { 1038 self.possible_orphan 1039 } 1040 1041 #[must_use] 1042 pub const fn attempts(&self) -> u8 { 1043 self.attempts 1044 } 1045 1046 #[must_use] 1047 pub const fn http_status(&self) -> Option<u16> { 1048 self.http_status 1049 } 1050 1051 /// Bounded, validated public error identifier returned by the server. 1052 #[must_use] 1053 pub fn server_error_code(&self) -> Option<&str> { 1054 self.server_error_code.as_deref() 1055 } 1056 1057 #[must_use] 1058 pub const fn code(&self) -> &'static str { 1059 match self.kind { 1060 BlossomErrorKind::InvalidEndpoint => "blossom_invalid_endpoint", 1061 BlossomErrorKind::EndpointSchemeDenied => "blossom_endpoint_scheme_denied", 1062 BlossomErrorKind::InvalidEndpointCount => "blossom_invalid_endpoint_count", 1063 BlossomErrorKind::DuplicateEndpoint => "blossom_duplicate_endpoint", 1064 BlossomErrorKind::EndpointNotConfigured => "blossom_endpoint_not_configured", 1065 BlossomErrorKind::ConfigurationChanged => "blossom_configuration_changed", 1066 BlossomErrorKind::ResolutionFailed => "blossom_resolution_failed", 1067 BlossomErrorKind::ResolvedAddressDenied => "blossom_resolved_address_denied", 1068 BlossomErrorKind::InvalidLimits => "blossom_invalid_limits", 1069 BlossomErrorKind::InvalidRequest => "blossom_invalid_request", 1070 BlossomErrorKind::InvalidDimensions => "blossom_invalid_dimensions", 1071 BlossomErrorKind::UnsupportedMediaType => "blossom_unsupported_media_type", 1072 BlossomErrorKind::MediaTypeMismatch => "blossom_media_type_mismatch", 1073 BlossomErrorKind::InvalidImageBytes => "blossom_invalid_image_bytes", 1074 BlossomErrorKind::DimensionMismatch => "blossom_dimension_mismatch", 1075 BlossomErrorKind::Authorization => "blossom_authorization_failed", 1076 BlossomErrorKind::Transport => "blossom_transport_failed", 1077 BlossomErrorKind::Timeout => "blossom_timeout", 1078 BlossomErrorKind::Cancelled => "blossom_cancelled", 1079 BlossomErrorKind::HttpStatus => "blossom_http_status", 1080 BlossomErrorKind::UnsafeRedirect => "blossom_unsafe_redirect", 1081 BlossomErrorKind::RedirectLimit => "blossom_redirect_limit", 1082 BlossomErrorKind::ContentEncodingDenied => "blossom_content_encoding_denied", 1083 BlossomErrorKind::ResponseTooLarge => "blossom_response_too_large", 1084 BlossomErrorKind::ResponseSizeMismatch => "blossom_response_size_mismatch", 1085 BlossomErrorKind::ResponseHashMismatch => "blossom_response_hash_mismatch", 1086 BlossomErrorKind::InvalidDescriptor => "blossom_invalid_descriptor", 1087 BlossomErrorKind::DescriptorMismatch => "blossom_descriptor_mismatch", 1088 BlossomErrorKind::RetrievedBytesMismatch => "blossom_retrieved_bytes_mismatch", 1089 } 1090 } 1091 1092 pub(crate) const fn with_operation(mut self, possible_orphan: bool, attempts: u8) -> Self { 1093 self.possible_orphan |= possible_orphan; 1094 self.attempts = attempts; 1095 self 1096 } 1097 1098 pub(crate) const fn with_http_status(mut self, status: u16) -> Self { 1099 self.http_status = Some(status); 1100 self 1101 } 1102 1103 pub(crate) fn with_server_error_code(mut self, code: String) -> Self { 1104 self.server_error_code = Some(code); 1105 self 1106 } 1107 } 1108 1109 #[cfg(feature = "blossom")] 1110 impl std::fmt::Display for BlossomError { 1111 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 1112 formatter.write_str(self.code()) 1113 } 1114 } 1115 1116 #[cfg(feature = "blossom")] 1117 impl std::fmt::Debug for BlossomError { 1118 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 1119 formatter 1120 .debug_struct("BlossomError") 1121 .field("kind", &self.kind) 1122 .field("phase", &self.phase) 1123 .field("retryable", &self.retryable) 1124 .field("possible_orphan", &self.possible_orphan) 1125 .field("attempts", &self.attempts) 1126 .field("http_status", &self.http_status) 1127 .field("server_error_code", &self.server_error_code) 1128 .finish() 1129 } 1130 } 1131 1132 #[cfg(feature = "blossom")] 1133 impl std::error::Error for BlossomError {} 1134 1135 /// Successful BUD-02 upload plus bounded BUD-01 retrieval verification. 1136 #[cfg(feature = "blossom")] 1137 #[derive(Clone, Debug, Eq, PartialEq)] 1138 pub struct BlossomUploadReceipt { 1139 descriptor: ByteVerifiedDescriptor, 1140 dimensions: BlossomImageDimensions, 1141 attempts: u8, 1142 verified_at_unix_ms: u64, 1143 } 1144 1145 /// Exact verified image bytes returned by a bounded BUD-01 retrieval. 1146 #[cfg(feature = "blossom")] 1147 #[derive(Clone)] 1148 pub struct BlossomInboundReceipt { 1149 final_url: BlobUrl, 1150 commitment: radroots_blossom::descriptor::ByteCommitment, 1151 dimensions: BlossomImageDimensions, 1152 bytes: Arc<[u8]>, 1153 config_fingerprint: BlossomConfigFingerprint, 1154 attempts: u8, 1155 verified_at_unix_ms: u64, 1156 } 1157 1158 #[cfg(feature = "blossom")] 1159 impl std::fmt::Debug for BlossomInboundReceipt { 1160 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 1161 formatter 1162 .debug_struct("BlossomInboundReceipt") 1163 .field("final_url", &self.final_url) 1164 .field("commitment", &self.commitment) 1165 .field("dimensions", &self.dimensions) 1166 .field("bytes", &"<redacted>") 1167 .field("config_fingerprint", &self.config_fingerprint) 1168 .field("attempts", &self.attempts) 1169 .field("verified_at_unix_ms", &self.verified_at_unix_ms) 1170 .finish() 1171 } 1172 } 1173 1174 #[cfg(feature = "blossom")] 1175 impl BlossomInboundReceipt { 1176 pub(crate) fn new( 1177 final_url: BlobUrl, 1178 commitment: radroots_blossom::descriptor::ByteCommitment, 1179 dimensions: BlossomImageDimensions, 1180 bytes: Arc<[u8]>, 1181 config_fingerprint: BlossomConfigFingerprint, 1182 attempts: u8, 1183 verified_at_unix_ms: u64, 1184 ) -> Self { 1185 Self { 1186 final_url, 1187 commitment, 1188 dimensions, 1189 bytes, 1190 config_fingerprint, 1191 attempts, 1192 verified_at_unix_ms, 1193 } 1194 } 1195 1196 #[must_use] 1197 pub const fn final_url(&self) -> &BlobUrl { 1198 &self.final_url 1199 } 1200 1201 #[must_use] 1202 pub const fn commitment(&self) -> &radroots_blossom::descriptor::ByteCommitment { 1203 &self.commitment 1204 } 1205 1206 #[must_use] 1207 pub const fn dimensions(&self) -> BlossomImageDimensions { 1208 self.dimensions 1209 } 1210 1211 #[must_use] 1212 pub fn bytes(&self) -> &[u8] { 1213 self.bytes.as_ref() 1214 } 1215 1216 #[must_use] 1217 pub const fn config_fingerprint(&self) -> BlossomConfigFingerprint { 1218 self.config_fingerprint 1219 } 1220 1221 #[must_use] 1222 pub const fn attempts(&self) -> u8 { 1223 self.attempts 1224 } 1225 1226 #[must_use] 1227 pub const fn verified_at_unix_ms(&self) -> u64 { 1228 self.verified_at_unix_ms 1229 } 1230 } 1231 1232 #[cfg(feature = "blossom")] 1233 impl BlossomUploadReceipt { 1234 pub(crate) const fn new( 1235 descriptor: ByteVerifiedDescriptor, 1236 dimensions: BlossomImageDimensions, 1237 attempts: u8, 1238 verified_at_unix_ms: u64, 1239 ) -> Self { 1240 Self { 1241 descriptor, 1242 dimensions, 1243 attempts, 1244 verified_at_unix_ms, 1245 } 1246 } 1247 1248 #[must_use] 1249 pub const fn descriptor(&self) -> &ByteVerifiedDescriptor { 1250 &self.descriptor 1251 } 1252 1253 #[must_use] 1254 pub const fn dimensions(&self) -> BlossomImageDimensions { 1255 self.dimensions 1256 } 1257 1258 #[must_use] 1259 pub const fn attempts(&self) -> u8 { 1260 self.attempts 1261 } 1262 1263 #[must_use] 1264 pub const fn verified_at_unix_ms(&self) -> u64 { 1265 self.verified_at_unix_ms 1266 } 1267 1268 #[must_use] 1269 pub fn into_descriptor(self) -> ByteVerifiedDescriptor { 1270 self.descriptor 1271 } 1272 } 1273 1274 /// Host-reconfigurable Blossom HTTP adapter slot. 1275 #[cfg(feature = "blossom")] 1276 #[derive(Clone, Default)] 1277 pub struct BlossomSlot { 1278 state: Arc<RwLock<BlossomSlotState>>, 1279 } 1280 1281 #[cfg(feature = "blossom")] 1282 #[derive(Default)] 1283 struct BlossomSlotState { 1284 config: Option<BlossomConfig>, 1285 evidence: Option<BlossomEndpointEvidence>, 1286 } 1287 1288 #[cfg(feature = "blossom")] 1289 impl BlossomSlot { 1290 #[must_use] 1291 pub fn new() -> Self { 1292 Self::default() 1293 } 1294 1295 /// Atomically installs completely validated inert configuration. 1296 pub fn configure(&self, config: BlossomConfig) -> Result<(), BlossomError> { 1297 let evidence = BlossomEndpointEvidence::configured(&config); 1298 let mut state = self 1299 .state 1300 .write() 1301 .map_err(|_| BlossomError::configuration(BlossomErrorKind::EndpointNotConfigured))?; 1302 state.config = Some(config); 1303 state.evidence = Some(evidence); 1304 Ok(()) 1305 } 1306 1307 pub fn clear(&self) { 1308 if let Ok(mut state) = self.state.write() { 1309 state.config = None; 1310 state.evidence = None; 1311 } 1312 } 1313 1314 #[must_use] 1315 pub fn host_kind(&self) -> Option<BlossomHostKind> { 1316 self.snapshot().map(|config| config.profile.host_kind()) 1317 } 1318 1319 #[must_use] 1320 pub fn endpoint_authority(&self) -> Option<BlossomEndpointAuthority> { 1321 self.snapshot().map(|config| config.profile.authority()) 1322 } 1323 1324 #[must_use] 1325 pub fn config_fingerprint(&self) -> Option<BlossomConfigFingerprint> { 1326 self.snapshot().map(|config| config.fingerprint()) 1327 } 1328 1329 /// Returns the configured inert profile without performing network I/O. 1330 #[must_use] 1331 pub fn profile(&self) -> Option<BlossomProfile> { 1332 self.snapshot().map(|config| config.profile) 1333 } 1334 1335 /// Atomically returns the inert profile and its exact configuration identity. 1336 #[must_use] 1337 pub fn configuration(&self) -> Option<(BlossomProfile, BlossomConfigFingerprint)> { 1338 self.snapshot().map(|config| { 1339 let fingerprint = config.fingerprint(); 1340 (config.profile, fingerprint) 1341 }) 1342 } 1343 1344 /// Returns the latest passive evidence without performing network I/O. 1345 #[must_use] 1346 pub fn evidence(&self) -> Option<BlossomEndpointEvidence> { 1347 self.state 1348 .read() 1349 .ok() 1350 .and_then(|state| state.evidence.clone()) 1351 } 1352 1353 /// Performs a bounded non-mutating primary-origin probe. 1354 pub async fn probe( 1355 &self, 1356 cancellation: BlossomCancellation, 1357 ) -> Result<BlossomEndpointEvidence, BlossomError> { 1358 let config = self 1359 .snapshot() 1360 .ok_or_else(|| BlossomError::configuration(BlossomErrorKind::EndpointNotConfigured))?; 1361 let fingerprint = config.fingerprint(); 1362 let result = crate::adapters::blossom::probe( 1363 config.clone(), 1364 config.profile().primary().clone(), 1365 cancellation, 1366 ) 1367 .await; 1368 match result { 1369 Ok(observation) => { 1370 self.record_evidence(fingerprint, BlossomPhase::Probe, false, |evidence| { 1371 evidence.record_success( 1372 BlossomEvidenceState::TlsHttpObserved, 1373 Some(observation.http_status), 1374 ); 1375 }) 1376 } 1377 Err(failure) => { 1378 self.record_evidence(fingerprint, BlossomPhase::Probe, false, |evidence| { 1379 if failure.dns_policy_validated { 1380 evidence.last_successful_state = BlossomEvidenceState::DnsPolicyValidated; 1381 } 1382 evidence.record_failure(&failure.error); 1383 })?; 1384 Err(failure.error) 1385 } 1386 } 1387 } 1388 1389 /// Binds verified bytes to the configured primary origin without network I/O. 1390 pub fn prepare_upload( 1391 &self, 1392 request: BlossomUploadRequest, 1393 ) -> Result<BlossomUploadTransaction, BlossomError> { 1394 let config = self 1395 .snapshot() 1396 .ok_or_else(|| BlossomError::configuration(BlossomErrorKind::EndpointNotConfigured))?; 1397 let config_fingerprint = config.fingerprint(); 1398 let endpoint = config.profile.primary.clone(); 1399 let extension = canonical_image_extension(request.media_type())?; 1400 let expected_url = BlobUrl::parse( 1401 format!("{}/{}.{}", endpoint.origin(), request.sha256(), extension).as_str(), 1402 ) 1403 .map_err(|_| BlossomError::configuration(BlossomErrorKind::InvalidEndpoint))?; 1404 Ok(BlossomUploadTransaction { 1405 config, 1406 config_fingerprint, 1407 endpoint, 1408 expected_url, 1409 request, 1410 }) 1411 } 1412 1413 /// Builds the exact BUD-11 claim for this configured upload destination. 1414 pub fn authored_upload_claim( 1415 &self, 1416 transaction: &BlossomUploadTransaction, 1417 content: AuthorizationContent, 1418 created_at_unix_s: u64, 1419 lifetime_seconds: u64, 1420 ) -> Result<AuthoredUploadClaim, BlossomError> { 1421 self.validate_transaction(transaction)?; 1422 AuthoredUploadClaim::new( 1423 content, 1424 transaction.endpoint.server_domain()?, 1425 transaction.request.sha256(), 1426 created_at_unix_s, 1427 lifetime_seconds, 1428 ) 1429 .map_err(|_| { 1430 BlossomError::new( 1431 BlossomErrorKind::Authorization, 1432 BlossomPhase::Authorization, 1433 false, 1434 false, 1435 0, 1436 ) 1437 }) 1438 } 1439 1440 /// Uploads exact bytes and verifies the returned descriptor and a full GET. 1441 pub async fn upload( 1442 &self, 1443 transaction: BlossomUploadTransaction, 1444 authorization: crate::signing::AuthorizationHeader, 1445 cancellation: BlossomCancellation, 1446 ) -> Result<BlossomUploadReceipt, BlossomError> { 1447 self.validate_transaction(&transaction)?; 1448 let fingerprint = transaction.config_fingerprint(); 1449 let result = 1450 crate::adapters::blossom::upload(transaction, authorization, cancellation).await; 1451 match result { 1452 Ok(receipt) => { 1453 self.record_evidence(fingerprint, BlossomPhase::Verification, true, |evidence| { 1454 evidence.record_success(BlossomEvidenceState::RetrievalVerified, None); 1455 })?; 1456 Ok(receipt) 1457 } 1458 Err(error) => { 1459 self.record_evidence(fingerprint, BlossomPhase::Verification, true, |evidence| { 1460 if error.possible_orphan() 1461 && matches!( 1462 error.phase(), 1463 BlossomPhase::Retrieval | BlossomPhase::Verification 1464 ) 1465 { 1466 evidence.last_successful_state = BlossomEvidenceState::UploadVerified; 1467 } 1468 evidence.record_failure(&error); 1469 })?; 1470 Err(error) 1471 } 1472 } 1473 } 1474 1475 /// Verifies a host-executed BUD-02 response, then performs the canonical 1476 /// BUD-01 exact-byte retrieval before returning an upload receipt. 1477 /// Later verification failure or cancellation preserves the native upload's 1478 /// possible remote effect and includes that upload in the attempt count. 1479 pub async fn complete_native_upload( 1480 &self, 1481 transaction: BlossomUploadTransaction, 1482 status_code: u16, 1483 response_media_type: Option<&str>, 1484 response_content_encoding: Option<&str>, 1485 response_body: &[u8], 1486 cancellation: BlossomCancellation, 1487 ) -> Result<BlossomUploadReceipt, BlossomError> { 1488 self.validate_transaction(&transaction)?; 1489 let fingerprint = transaction.config_fingerprint(); 1490 let result = crate::adapters::blossom::complete_native_upload( 1491 transaction, 1492 status_code, 1493 response_media_type, 1494 response_content_encoding, 1495 response_body, 1496 cancellation, 1497 ) 1498 .await; 1499 match result { 1500 Ok(receipt) => { 1501 self.record_evidence(fingerprint, BlossomPhase::Verification, true, |evidence| { 1502 evidence.record_success(BlossomEvidenceState::RetrievalVerified, None); 1503 })?; 1504 Ok(receipt) 1505 } 1506 Err(error) => { 1507 self.record_evidence(fingerprint, BlossomPhase::Verification, true, |evidence| { 1508 if error.possible_orphan() { 1509 evidence.last_successful_state = BlossomEvidenceState::UploadVerified; 1510 } 1511 evidence.record_failure(&error); 1512 })?; 1513 Err(error) 1514 } 1515 } 1516 } 1517 1518 /// Retrieves and verifies one immutable BUD-01 image under the exact 1519 /// configured DNS, TLS, redirect, retry, and byte limits. 1520 pub async fn retrieve( 1521 &self, 1522 request: BlossomInboundRequest, 1523 cancellation: BlossomCancellation, 1524 ) -> Result<BlossomInboundReceipt, BlossomError> { 1525 let config = self 1526 .snapshot() 1527 .ok_or_else(|| BlossomError::configuration(BlossomErrorKind::EndpointNotConfigured))?; 1528 let fingerprint = config.fingerprint(); 1529 if config.profile().endpoint_for_blob(request.url()).is_none() { 1530 return Err(BlossomError::configuration( 1531 BlossomErrorKind::EndpointNotConfigured, 1532 )); 1533 } 1534 let result = crate::adapters::blossom::retrieve(config, request, cancellation).await; 1535 match result { 1536 Ok(receipt) => { 1537 self.record_evidence(fingerprint, BlossomPhase::Verification, false, |evidence| { 1538 evidence.record_success(BlossomEvidenceState::RetrievalVerified, None); 1539 })?; 1540 Ok(receipt) 1541 } 1542 Err(error) => { 1543 self.record_evidence(fingerprint, BlossomPhase::Verification, false, |evidence| { 1544 evidence.record_failure(&error); 1545 })?; 1546 Err(error) 1547 } 1548 } 1549 } 1550 1551 fn validate_transaction( 1552 &self, 1553 transaction: &BlossomUploadTransaction, 1554 ) -> Result<(), BlossomError> { 1555 let fingerprint = self 1556 .config_fingerprint() 1557 .ok_or_else(|| BlossomError::configuration(BlossomErrorKind::EndpointNotConfigured))?; 1558 if fingerprint != transaction.config_fingerprint { 1559 return Err(BlossomError::configuration( 1560 BlossomErrorKind::ConfigurationChanged, 1561 )); 1562 } 1563 Ok(()) 1564 } 1565 1566 fn snapshot(&self) -> Option<BlossomConfig> { 1567 self.state 1568 .read() 1569 .ok() 1570 .and_then(|state| state.config.clone()) 1571 } 1572 1573 fn record_evidence( 1574 &self, 1575 fingerprint: BlossomConfigFingerprint, 1576 drift_phase: BlossomPhase, 1577 drift_possible_orphan: bool, 1578 update: impl FnOnce(&mut BlossomEndpointEvidence), 1579 ) -> Result<BlossomEndpointEvidence, BlossomError> { 1580 let mut state = self 1581 .state 1582 .write() 1583 .map_err(|_| BlossomError::configuration(BlossomErrorKind::EndpointNotConfigured))?; 1584 let current = state 1585 .config 1586 .as_ref() 1587 .ok_or_else(|| BlossomError::configuration(BlossomErrorKind::EndpointNotConfigured))?; 1588 if current.fingerprint() != fingerprint { 1589 return Err(BlossomError::new( 1590 BlossomErrorKind::ConfigurationChanged, 1591 drift_phase, 1592 false, 1593 drift_possible_orphan, 1594 0, 1595 )); 1596 } 1597 let evidence = state 1598 .evidence 1599 .as_mut() 1600 .ok_or_else(|| BlossomError::configuration(BlossomErrorKind::EndpointNotConfigured))?; 1601 update(evidence); 1602 Ok(evidence.clone()) 1603 } 1604 } 1605 1606 #[cfg(feature = "blossom")] 1607 impl std::fmt::Debug for BlossomSlot { 1608 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 1609 formatter 1610 .debug_struct("BlossomSlot") 1611 .field("configured", &self.host_kind().is_some()) 1612 .finish() 1613 } 1614 } 1615 1616 #[cfg(feature = "blossom")] 1617 fn endpoint_scheme_is_allowed(scheme: &str, authority: BlossomEndpointAuthority) -> bool { 1618 scheme == "https" 1619 || scheme == "http" 1620 && matches!( 1621 authority, 1622 BlossomEndpointAuthority::LoopbackDevelopment 1623 | BlossomEndpointAuthority::PrivateNetworkDevelopment 1624 ) 1625 } 1626 1627 #[cfg(feature = "blossom")] 1628 fn validate_host_authority( 1629 host_kind: BlossomHostKind, 1630 authority: BlossomEndpointAuthority, 1631 ) -> Result<(), BlossomError> { 1632 let accepted = match authority { 1633 BlossomEndpointAuthority::PublicWebPki => true, 1634 BlossomEndpointAuthority::LoopbackDevelopment => host_kind == BlossomHostKind::Simulator, 1635 BlossomEndpointAuthority::PrivateNetworkDevelopment => { 1636 host_kind == BlossomHostKind::PhysicalDevice 1637 } 1638 }; 1639 if accepted { 1640 Ok(()) 1641 } else { 1642 Err(BlossomError::configuration( 1643 BlossomErrorKind::InvalidEndpoint, 1644 )) 1645 } 1646 } 1647 1648 #[cfg(feature = "blossom")] 1649 fn validate_blossom_host( 1650 host: &str, 1651 authority: BlossomEndpointAuthority, 1652 ) -> Result<(), BlossomError> { 1653 let address = host.trim_matches(['[', ']']).parse::<IpAddr>().ok(); 1654 let accepted = match (authority, address) { 1655 (BlossomEndpointAuthority::PublicWebPki, Some(address)) => public_blossom_address(address), 1656 (BlossomEndpointAuthority::PublicWebPki, None) => public_blossom_hostname(host), 1657 (BlossomEndpointAuthority::LoopbackDevelopment, Some(address)) => address.is_loopback(), 1658 (BlossomEndpointAuthority::LoopbackDevelopment, None) => host == "localhost", 1659 (BlossomEndpointAuthority::PrivateNetworkDevelopment, Some(address)) => { 1660 trusted_blossom_address(address) 1661 } 1662 (BlossomEndpointAuthority::PrivateNetworkDevelopment, None) => false, 1663 }; 1664 if accepted { 1665 Ok(()) 1666 } else { 1667 Err(BlossomError::configuration( 1668 BlossomErrorKind::ResolvedAddressDenied, 1669 )) 1670 } 1671 } 1672 1673 #[cfg(feature = "blossom")] 1674 fn public_blossom_hostname(host: &str) -> bool { 1675 host.contains('.') 1676 && !host.ends_with(".localhost") 1677 && !host.ends_with(".local") 1678 && !host.ends_with(".home.arpa") 1679 } 1680 1681 #[cfg(feature = "blossom")] 1682 fn blossom_authority_accepts_address(authority: BlossomEndpointAuthority, address: IpAddr) -> bool { 1683 match authority { 1684 BlossomEndpointAuthority::PublicWebPki => public_blossom_address(address), 1685 BlossomEndpointAuthority::LoopbackDevelopment => address.is_loopback(), 1686 BlossomEndpointAuthority::PrivateNetworkDevelopment => trusted_blossom_address(address), 1687 } 1688 } 1689 1690 #[cfg(feature = "blossom")] 1691 pub(crate) fn canonical_image_extension( 1692 media_type: &MediaType, 1693 ) -> Result<&'static str, BlossomError> { 1694 match media_type.as_str() { 1695 "image/png" => Ok("png"), 1696 "image/jpeg" => Ok("jpg"), 1697 "image/gif" => Ok("gif"), 1698 "image/webp" => Ok("webp"), 1699 _ => Err(BlossomError::new( 1700 BlossomErrorKind::UnsupportedMediaType, 1701 BlossomPhase::Verification, 1702 false, 1703 false, 1704 0, 1705 )), 1706 } 1707 } 1708 1709 #[cfg(feature = "blossom")] 1710 fn public_blossom_address(address: IpAddr) -> bool { 1711 match address { 1712 IpAddr::V4(address) => public_blossom_ipv4(address), 1713 IpAddr::V6(address) => public_blossom_ipv6(address), 1714 } 1715 } 1716 1717 #[cfg(feature = "blossom")] 1718 fn trusted_blossom_address(address: IpAddr) -> bool { 1719 match address { 1720 IpAddr::V4(address) => address.is_private(), 1721 IpAddr::V6(address) => address.segments()[0] & 0xfe00 == 0xfc00, 1722 } 1723 } 1724 1725 #[cfg(feature = "blossom")] 1726 fn public_blossom_ipv4(address: Ipv4Addr) -> bool { 1727 let octets = address.octets(); 1728 !(octets[0] == 0 1729 || address.is_loopback() 1730 || address.is_private() 1731 || address.is_link_local() 1732 || address.is_multicast() 1733 || address.is_documentation() 1734 || octets[0] == 100 && (64..=127).contains(&octets[1]) 1735 || octets[0] == 192 && octets[1] == 0 && octets[2] == 0 1736 || octets[0] == 192 && octets[1] == 88 && octets[2] == 99 1737 || octets[0] == 198 && matches!(octets[1], 18 | 19) 1738 || octets[0] >= 240) 1739 } 1740 1741 #[cfg(feature = "blossom")] 1742 fn public_blossom_ipv6(address: Ipv6Addr) -> bool { 1743 if let Some(mapped) = address.to_ipv4_mapped() { 1744 return public_blossom_ipv4(mapped); 1745 } 1746 let segments = address.segments(); 1747 (segments[0] & 0xe000) == 0x2000 1748 && !(segments[0] == 0x2001 && segments[1] <= 0x01ff) 1749 && !(segments[0] == 0x2001 && segments[1] == 0x0db8) 1750 && segments[0] != 0x2002 1751 && !(segments[0] == 0x3fff && (segments[1] & 0xf000) == 0) 1752 } 1753 1754 /// A side-effect-free transport selection for a client operation. 1755 #[derive(Clone, Debug, Eq, PartialEq)] 1756 pub struct Profile { 1757 selection: Selection, 1758 } 1759 1760 #[derive(Clone, Debug, Eq, PartialEq)] 1761 enum Selection { 1762 LocalOnly, 1763 Delivery { 1764 targets: TargetSet, 1765 satisfaction: SatisfactionPolicy, 1766 }, 1767 UnavailablePreview { 1768 source: SourceStatus, 1769 sink: SinkStatus, 1770 }, 1771 } 1772 1773 impl Profile { 1774 /// Selects local persistence only, with no transport target or fallback. 1775 #[must_use] 1776 pub const fn local_only() -> Self { 1777 Self { 1778 selection: Selection::LocalOnly, 1779 } 1780 } 1781 1782 /// Selects an exact bounded target set and canonical satisfaction policy. 1783 /// 1784 /// Impossible quorum and required-target policies are rejected here by the 1785 /// owning transport contract. Construction performs no network operation. 1786 pub fn delivery(targets: TargetSet, satisfaction: SatisfactionPolicy) -> Result<Self, Error> { 1787 satisfaction.validate_for(&targets)?; 1788 Ok(Self { 1789 selection: Selection::Delivery { 1790 targets, 1791 satisfaction, 1792 }, 1793 }) 1794 } 1795 1796 /// Describes a preview transport that is intentionally not selectable. 1797 /// 1798 /// Both canonical capability directions remain explicitly unconfigured 1799 /// and unavailable. The profile has no targets and therefore cannot fall 1800 /// back to local, Nostr, daemon, or another transport. 1801 #[must_use] 1802 pub fn unavailable_preview(transport_id: TransportId) -> Self { 1803 Self { 1804 selection: Selection::UnavailablePreview { 1805 source: SourceStatus::new( 1806 transport_id, 1807 false, 1808 Maturity::Preview, 1809 Availability::Unavailable, 1810 SourceCapabilities::NONE, 1811 PREVIEW_UNAVAILABLE_MESSAGE, 1812 ), 1813 sink: SinkStatus::new( 1814 transport_id, 1815 false, 1816 Maturity::Preview, 1817 Availability::Unavailable, 1818 SinkCapabilities::NONE, 1819 PREVIEW_UNAVAILABLE_MESSAGE, 1820 ), 1821 }, 1822 } 1823 } 1824 1825 /// Returns whether this profile authorizes no transport operation. 1826 #[must_use] 1827 pub const fn is_local_only(&self) -> bool { 1828 matches!(self.selection, Selection::LocalOnly) 1829 } 1830 1831 /// Returns the exact selected targets, if delivery is authorized. 1832 #[must_use] 1833 pub const fn targets(&self) -> Option<&TargetSet> { 1834 match &self.selection { 1835 Selection::Delivery { targets, .. } => Some(targets), 1836 Selection::LocalOnly | Selection::UnavailablePreview { .. } => None, 1837 } 1838 } 1839 1840 /// Returns the exact selected satisfaction policy, if delivery is authorized. 1841 #[must_use] 1842 pub const fn satisfaction(&self) -> Option<&SatisfactionPolicy> { 1843 match &self.selection { 1844 Selection::Delivery { satisfaction, .. } => Some(satisfaction), 1845 Selection::LocalOnly | Selection::UnavailablePreview { .. } => None, 1846 } 1847 } 1848 1849 /// Returns canonical source status for an unavailable preview. 1850 #[must_use] 1851 pub const fn source_status(&self) -> Option<&SourceStatus> { 1852 match &self.selection { 1853 Selection::UnavailablePreview { source, .. } => Some(source), 1854 Selection::LocalOnly | Selection::Delivery { .. } => None, 1855 } 1856 } 1857 1858 /// Returns canonical sink status for an unavailable preview. 1859 #[must_use] 1860 pub const fn sink_status(&self) -> Option<&SinkStatus> { 1861 match &self.selection { 1862 Selection::UnavailablePreview { sink, .. } => Some(sink), 1863 Selection::LocalOnly | Selection::Delivery { .. } => None, 1864 } 1865 } 1866 } 1867 1868 impl Default for Profile { 1869 fn default() -> Self { 1870 Self::local_only() 1871 } 1872 } 1873 1874 /// Host-configured, client-shareable Nostr transport slot. 1875 /// 1876 /// Reconfiguration validates the complete relay set before atomically 1877 /// replacing the active adapter. Construction, clearing, and target 1878 /// inspection perform no network I/O. 1879 #[cfg(feature = "nostr")] 1880 #[derive(Clone)] 1881 pub struct NostrSlot { 1882 state: Arc<RwLock<Option<NostrState>>>, 1883 } 1884 1885 #[cfg(feature = "nostr")] 1886 #[derive(Clone)] 1887 struct NostrState { 1888 transport: Arc<radroots_transport_nostr::NostrTransport>, 1889 read_targets: TargetSet, 1890 write_targets: Option<TargetSet>, 1891 } 1892 1893 #[cfg(feature = "nostr")] 1894 impl NostrSlot { 1895 /// Creates an inert slot with no selected host profile. 1896 #[must_use] 1897 pub fn new() -> Self { 1898 Self { 1899 state: Arc::new(RwLock::new(None)), 1900 } 1901 } 1902 1903 /// Atomically installs one completely validated relay profile. 1904 pub fn configure(&self, profile: RelayProfile) -> crate::Result<()> { 1905 let config = radroots_transport_nostr::Config::from_profile(profile); 1906 let read_targets = TargetSet::new( 1907 config 1908 .read_relays() 1909 .map(radroots_transport_nostr::RelayUrl::to_target) 1910 .collect::<Result<Vec<_>, _>>() 1911 .map_err(crate::Error::invalid_host_configuration)?, 1912 ) 1913 .map_err(|_| crate::Error::invalid_host_configuration_without_source())?; 1914 let write_targets = { 1915 let targets = config 1916 .write_relays() 1917 .map(radroots_transport_nostr::RelayUrl::to_target) 1918 .collect::<Result<Vec<_>, _>>() 1919 .map_err(crate::Error::invalid_host_configuration)?; 1920 if targets.is_empty() { 1921 None 1922 } else { 1923 Some( 1924 TargetSet::new(targets) 1925 .map_err(|_| crate::Error::invalid_host_configuration_without_source())?, 1926 ) 1927 } 1928 }; 1929 let state = NostrState { 1930 transport: Arc::new(radroots_transport_nostr::NostrTransport::new(config)), 1931 read_targets, 1932 write_targets, 1933 }; 1934 let mut current = self 1935 .state 1936 .write() 1937 .map_err(|_| crate::Error::shared_operation_unavailable())?; 1938 *current = Some(state); 1939 Ok(()) 1940 } 1941 1942 /// Removes the active adapter without starting or stopping background work. 1943 pub fn clear(&self) { 1944 if let Ok(mut state) = self.state.write() { 1945 *state = None; 1946 } 1947 } 1948 1949 /// Returns the currently selected canonical read targets. 1950 #[must_use] 1951 pub fn read_targets(&self) -> Option<TargetSet> { 1952 self.snapshot().map(|state| state.read_targets) 1953 } 1954 1955 /// Returns writable targets, or `None` when the profile is intentionally 1956 /// read-only. 1957 #[must_use] 1958 pub fn write_targets(&self) -> Option<TargetSet> { 1959 self.snapshot().and_then(|state| state.write_targets) 1960 } 1961 1962 /// Returns passive per-relay evidence without probing or opening sockets. 1963 #[must_use] 1964 pub fn relay_status(&self) -> Option<RelayStatusReport> { 1965 self.snapshot().map(|state| state.transport.relay_status()) 1966 } 1967 1968 fn snapshot(&self) -> Option<NostrState> { 1969 self.state.read().ok().and_then(|state| state.clone()) 1970 } 1971 } 1972 1973 #[cfg(feature = "nostr")] 1974 impl radroots_transport::EventSource for NostrSlot { 1975 fn status( 1976 &self, 1977 ) -> radroots_transport::BoxFuture<'_, Result<SourceStatus, radroots_transport::Error>> { 1978 Box::pin(async move { 1979 match self.snapshot() { 1980 Some(state) => { 1981 radroots_transport::EventSource::status(state.transport.as_ref()).await 1982 } 1983 None => Ok(SourceStatus::new( 1984 TransportId::NOSTR, 1985 false, 1986 Maturity::Stable, 1987 Availability::Unavailable, 1988 SourceCapabilities::FETCH, 1989 "Nostr transport is not configured", 1990 )), 1991 } 1992 }) 1993 } 1994 1995 fn fetch( 1996 &self, 1997 request: radroots_transport::FetchRequest, 1998 ) -> radroots_transport::BoxFuture< 1999 '_, 2000 Result<radroots_transport::FetchPage, radroots_transport::Error>, 2001 > { 2002 Box::pin(async move { 2003 let state = self 2004 .snapshot() 2005 .ok_or(radroots_transport::Error::UnsupportedOperation)?; 2006 radroots_transport::EventSource::fetch(state.transport.as_ref(), request).await 2007 }) 2008 } 2009 } 2010 2011 #[cfg(feature = "nostr")] 2012 impl radroots_transport::EventSink for NostrSlot { 2013 fn status( 2014 &self, 2015 ) -> radroots_transport::BoxFuture<'_, Result<SinkStatus, radroots_transport::Error>> { 2016 Box::pin(async move { 2017 match self.snapshot() { 2018 Some(state) => { 2019 radroots_transport::EventSink::status(state.transport.as_ref()).await 2020 } 2021 None => Ok(SinkStatus::new( 2022 TransportId::NOSTR, 2023 false, 2024 Maturity::Stable, 2025 Availability::Unavailable, 2026 SinkCapabilities::DELIVER, 2027 "Nostr transport is not configured", 2028 )), 2029 } 2030 }) 2031 } 2032 2033 fn deliver( 2034 &self, 2035 request: radroots_transport::DeliveryRequest, 2036 ) -> radroots_transport::BoxFuture< 2037 '_, 2038 Result<radroots_transport::DeliveryReceipt, radroots_transport::SinkFailure>, 2039 > { 2040 Box::pin(async move { 2041 let Some(state) = self.snapshot() else { 2042 return Err(radroots_transport::SinkFailure::for_request( 2043 &request, 2044 "nostr_transport_not_configured", 2045 radroots_transport::outcome::Retryability::Terminal, 2046 None, 2047 None, 2048 Vec::new(), 2049 ) 2050 .expect("static unconfigured sink failure is valid")); 2051 }; 2052 radroots_transport::EventSink::deliver(state.transport.as_ref(), request).await 2053 }) 2054 } 2055 2056 fn deliver_selected( 2057 &self, 2058 request: radroots_transport::DeliveryRequest, 2059 selected: radroots_transport::TargetSet, 2060 ) -> radroots_transport::BoxFuture< 2061 '_, 2062 Result<radroots_transport::DeliveryReceipt, radroots_transport::SinkFailure>, 2063 > { 2064 Box::pin(async move { 2065 if request.validate_target_selection(&selected).is_err() { 2066 return Err(radroots_transport::SinkFailure::invalid_contract(&request)); 2067 } 2068 let Some(state) = self.snapshot() else { 2069 return Err(radroots_transport::SinkFailure::for_request( 2070 &request, 2071 "nostr_transport_not_configured", 2072 radroots_transport::outcome::Retryability::Terminal, 2073 None, 2074 None, 2075 Vec::new(), 2076 ) 2077 .expect("static unconfigured sink failure is valid")); 2078 }; 2079 radroots_transport::EventSink::deliver_selected( 2080 state.transport.as_ref(), 2081 request, 2082 selected, 2083 ) 2084 .await 2085 }) 2086 } 2087 } 2088 2089 #[cfg(feature = "nostr")] 2090 impl std::fmt::Debug for NostrSlot { 2091 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 2092 formatter 2093 .debug_struct("NostrSlot") 2094 .field("configured", &self.read_targets().is_some()) 2095 .field("writable", &self.write_targets().is_some()) 2096 .finish() 2097 } 2098 } 2099 2100 #[cfg(feature = "nostr")] 2101 impl Default for NostrSlot { 2102 fn default() -> Self { 2103 Self::new() 2104 } 2105 } 2106 2107 /// Explicit daemon adapter authentication configuration. 2108 #[cfg(feature = "radrootsd")] 2109 #[derive(Clone, Eq, PartialEq)] 2110 #[non_exhaustive] 2111 pub enum DaemonAuth { 2112 /// Sends no authorization header. 2113 None, 2114 /// Sends the supplied bearer credential only when delivery is invoked. 2115 BearerToken(String), 2116 } 2117 2118 #[cfg(feature = "radrootsd")] 2119 impl std::fmt::Debug for DaemonAuth { 2120 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 2121 match self { 2122 Self::None => formatter.write_str("None"), 2123 Self::BearerToken(_) => formatter.write_str("BearerToken(<redacted>)"), 2124 } 2125 } 2126 } 2127 2128 /// Explicit daemon endpoint and request deadline configuration. 2129 #[cfg(feature = "radrootsd")] 2130 #[derive(Clone, Debug, Eq, PartialEq)] 2131 pub struct DaemonConfig { 2132 endpoint: String, 2133 auth: DaemonAuth, 2134 timeout: core::time::Duration, 2135 } 2136 2137 #[cfg(feature = "radrootsd")] 2138 impl DaemonConfig { 2139 /// Creates inert configuration; no client is built and no request is sent. 2140 #[must_use] 2141 pub fn new(endpoint: impl Into<String>) -> Self { 2142 Self { 2143 endpoint: endpoint.into(), 2144 auth: DaemonAuth::None, 2145 timeout: core::time::Duration::from_secs(10), 2146 } 2147 } 2148 2149 /// Selects explicit authentication for later invocation. 2150 #[must_use] 2151 pub fn with_auth(mut self, auth: DaemonAuth) -> Self { 2152 self.auth = auth; 2153 self 2154 } 2155 2156 /// Selects the complete HTTP/RPC request deadline. 2157 #[must_use] 2158 pub const fn with_timeout(mut self, timeout: core::time::Duration) -> Self { 2159 self.timeout = timeout; 2160 self 2161 } 2162 } 2163 2164 /// Stable secret-safe daemon execution failure class. 2165 #[cfg(feature = "radrootsd")] 2166 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 2167 #[non_exhaustive] 2168 pub enum DaemonErrorKind { 2169 /// The explicit authentication value cannot be represented safely. 2170 Authentication, 2171 /// The versioned protocol rejected the request. 2172 InvalidRequest, 2173 /// HTTP transport or timeout failed. 2174 Transport, 2175 /// The daemon returned a JSON-RPC error. 2176 Rpc, 2177 /// The response was malformed or did not match the request. 2178 InvalidResponse, 2179 } 2180 2181 /// One redacted daemon failure retaining a private source chain. 2182 #[cfg(feature = "radrootsd")] 2183 pub struct DaemonError { 2184 kind: DaemonErrorKind, 2185 source: crate::adapters::radrootsd::RadrootsdError, 2186 } 2187 2188 #[cfg(feature = "radrootsd")] 2189 impl DaemonError { 2190 /// Returns the stable failure class. 2191 #[must_use] 2192 pub const fn kind(&self) -> DaemonErrorKind { 2193 self.kind 2194 } 2195 2196 fn from_private(source: crate::adapters::radrootsd::RadrootsdError) -> Self { 2197 use crate::adapters::radrootsd::RadrootsdError; 2198 let kind = match &source { 2199 RadrootsdError::InvalidAuthHeader(_) => DaemonErrorKind::Authentication, 2200 RadrootsdError::InvalidRequest(_) => DaemonErrorKind::InvalidRequest, 2201 RadrootsdError::Http(_) => DaemonErrorKind::Transport, 2202 RadrootsdError::JsonRpc { .. } => DaemonErrorKind::Rpc, 2203 RadrootsdError::MalformedResponse(_) => DaemonErrorKind::InvalidResponse, 2204 }; 2205 Self { kind, source } 2206 } 2207 } 2208 2209 #[cfg(feature = "radrootsd")] 2210 impl std::fmt::Display for DaemonError { 2211 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 2212 formatter.write_str(match self.kind { 2213 DaemonErrorKind::Authentication => "daemon authentication configuration is invalid", 2214 DaemonErrorKind::InvalidRequest => "daemon delivery request is invalid", 2215 DaemonErrorKind::Transport => "daemon transport failed", 2216 DaemonErrorKind::Rpc => "daemon RPC failed", 2217 DaemonErrorKind::InvalidResponse => "daemon response is invalid", 2218 }) 2219 } 2220 } 2221 2222 #[cfg(feature = "radrootsd")] 2223 impl std::fmt::Debug for DaemonError { 2224 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 2225 formatter 2226 .debug_struct("DaemonError") 2227 .field("kind", &self.kind) 2228 .finish_non_exhaustive() 2229 } 2230 } 2231 2232 #[cfg(feature = "radrootsd")] 2233 impl std::error::Error for DaemonError { 2234 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 2235 Some(&self.source) 2236 } 2237 } 2238 2239 /// Explicitly configured daemon execution adapter. 2240 /// 2241 /// Construction is inert. Network contact occurs only in [`Self::deliver`]. 2242 #[cfg(feature = "radrootsd")] 2243 #[derive(Clone, Debug, Eq, PartialEq)] 2244 pub struct DaemonDelivery { 2245 adapter: crate::adapters::radrootsd::RadrootsdPublishAdapter, 2246 } 2247 2248 #[cfg(feature = "radrootsd")] 2249 impl DaemonDelivery { 2250 /// Creates an inert adapter from explicit host configuration. 2251 #[must_use] 2252 pub fn new(config: DaemonConfig) -> Self { 2253 let auth = match config.auth { 2254 DaemonAuth::None => crate::adapters::radrootsd::RadrootsdAuth::None, 2255 DaemonAuth::BearerToken(token) => { 2256 crate::adapters::radrootsd::RadrootsdAuth::BearerToken(token) 2257 } 2258 }; 2259 Self { 2260 adapter: crate::adapters::radrootsd::RadrootsdPublishAdapter::new( 2261 crate::adapters::radrootsd::RadrootsdPublishConfig::new(config.endpoint) 2262 .with_auth(auth) 2263 .with_timeout(config.timeout), 2264 ), 2265 } 2266 } 2267 2268 /// Invokes the generation-5 daemon transport-publish contract. 2269 pub async fn deliver( 2270 &self, 2271 signed_event: radroots_event::SignedEvent, 2272 target_policy: radroots_protocol::radrootsd::transport_publish::v5::TargetPolicy, 2273 delivery_policy: radroots_protocol::radrootsd::transport_publish::v5::DeliveryPolicy, 2274 idempotency_key: Option<String>, 2275 timeout_ms: Option<u64>, 2276 ) -> Result<radroots_protocol::radrootsd::transport_publish::v5::EventResponse, DaemonError> 2277 { 2278 self.adapter 2279 .publish_signed_event(crate::adapters::radrootsd::RadrootsdPublishRequest { 2280 signed_event, 2281 target_policy, 2282 delivery_policy, 2283 idempotency_key, 2284 timeout_ms, 2285 }) 2286 .await 2287 .map_err(DaemonError::from_private) 2288 } 2289 } 2290 2291 #[cfg(test)] 2292 mod tests { 2293 use radroots_transport::{ 2294 Error, TARGET_SET_MAX_ITEMS, Target, 2295 capability::{Availability, Maturity}, 2296 policy::{SatisfactionClass, SatisfactionPolicy, TargetPolicy}, 2297 target::TargetFingerprint, 2298 }; 2299 2300 use super::*; 2301 2302 fn target(index: usize) -> Target { 2303 Target::nostr_relay(format!("wss://relay-{index}.example")).expect("target") 2304 } 2305 2306 #[cfg(feature = "nostr")] 2307 fn signed_event() -> radroots_event::SignedEvent { 2308 let raw = r#"{"id":"56bfc78223bb2221bad82b539efdec1ade0f56d0eb0e1f592fd387df4b2ceee0","pubkey":"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df","created_at":1700000001,"kind":0,"tags":[],"content":"{}","sig":"dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"}"#; 2309 let wire = radroots_event::wire::v1::Nip01EventWire::parse_json(raw).expect("wire event"); 2310 radroots_event::SignedEvent::from_wire_verified_id(wire, raw).expect("signed event") 2311 } 2312 2313 #[test] 2314 fn delivery_profile_preserves_canonical_targets_and_policy() { 2315 let targets = TargetSet::new(vec![target(1), target(2)]).expect("target set"); 2316 let policy = SatisfactionPolicy::new(SatisfactionClass::Accepted, TargetPolicy::all()); 2317 let profile = Profile::delivery(targets.clone(), policy.clone()).expect("profile"); 2318 2319 assert_eq!(profile.targets(), Some(&targets)); 2320 assert_eq!(profile.satisfaction(), Some(&policy)); 2321 assert!(!profile.is_local_only()); 2322 assert!(profile.source_status().is_none()); 2323 assert!(profile.sink_status().is_none()); 2324 } 2325 2326 #[test] 2327 fn canonical_target_and_policy_bounds_fail_during_profile_construction() { 2328 assert_eq!(TargetSet::new(Vec::new()), Err(Error::EmptyTargetSet)); 2329 assert_eq!( 2330 TargetSet::new((0..=TARGET_SET_MAX_ITEMS).map(target).collect()), 2331 Err(Error::TargetSetTooLarge) 2332 ); 2333 2334 let targets = TargetSet::new(vec![target(1)]).expect("target set"); 2335 let quorum = SatisfactionPolicy::new( 2336 SatisfactionClass::Delivered, 2337 TargetPolicy::quorum(2).expect("non-zero quorum"), 2338 ); 2339 assert_eq!( 2340 Profile::delivery(targets.clone(), quorum), 2341 Err(Error::InvalidSatisfactionPolicy) 2342 ); 2343 2344 let missing = 2345 TargetFingerprint::from_target(target(2).kind(), target(2).uri(), target(2).scope()); 2346 let required = SatisfactionPolicy::new( 2347 SatisfactionClass::Accepted, 2348 TargetPolicy::required(vec![missing]).expect("required policy"), 2349 ); 2350 assert_eq!( 2351 Profile::delivery(targets, required), 2352 Err(Error::RequiredTargetNotRequested) 2353 ); 2354 } 2355 2356 #[test] 2357 fn preview_transport_is_explicitly_unavailable_and_unselectable() { 2358 let profile = Profile::unavailable_preview(TransportId::RETICULUM); 2359 let source = profile.source_status().expect("source status"); 2360 let sink = profile.sink_status().expect("sink status"); 2361 2362 assert_eq!(source.transport_id(), TransportId::RETICULUM); 2363 assert_eq!(sink.transport_id(), TransportId::RETICULUM); 2364 assert!(!source.is_configured()); 2365 assert!(!sink.is_configured()); 2366 assert_eq!(source.maturity(), Maturity::Preview); 2367 assert_eq!(sink.maturity(), Maturity::Preview); 2368 assert_eq!(source.availability(), Availability::Unavailable); 2369 assert_eq!(sink.availability(), Availability::Unavailable); 2370 assert!(!source.capabilities().can_fetch()); 2371 assert!(!sink.capabilities().can_deliver()); 2372 assert!(profile.targets().is_none()); 2373 assert!(profile.satisfaction().is_none()); 2374 } 2375 2376 #[test] 2377 fn local_and_preview_profiles_never_substitute_fallback_targets() { 2378 let local = Profile::local_only(); 2379 let preview = Profile::unavailable_preview(TransportId::RETICULUM); 2380 assert!(local.is_local_only()); 2381 assert!(local.targets().is_none()); 2382 assert!(preview.targets().is_none()); 2383 2384 let selected = TargetSet::new(vec![target(7)]).expect("selected targets"); 2385 let profile = Profile::delivery( 2386 selected.clone(), 2387 SatisfactionPolicy::new(SatisfactionClass::Accepted, TargetPolicy::any()), 2388 ) 2389 .expect("profile"); 2390 assert_eq!(profile.targets(), Some(&selected)); 2391 assert!( 2392 profile 2393 .targets() 2394 .expect("targets") 2395 .targets() 2396 .iter() 2397 .all(|target| *target.kind() == TransportId::NOSTR) 2398 ); 2399 } 2400 2401 #[test] 2402 fn default_profile_is_local_only() { 2403 assert_eq!(Profile::default(), Profile::local_only()); 2404 } 2405 2406 #[cfg(feature = "blossom")] 2407 #[test] 2408 fn blossom_profiles_enforce_environment_and_ssrf_boundaries() { 2409 assert!(public_blossom_profile("https://media.example").is_ok()); 2410 assert!(public_blossom_profile("http://media.example").is_err()); 2411 assert!(public_blossom_profile("https://127.0.0.1").is_err()); 2412 assert!(public_blossom_profile("https://10.0.0.1").is_err()); 2413 assert!(simulator_blossom_profile("http://127.0.0.1:3000").is_ok()); 2414 assert!(simulator_blossom_profile("http://localhost:3000").is_ok()); 2415 assert!(simulator_blossom_profile("http://media.example").is_err()); 2416 assert!(device_blossom_profile("https://10.0.0.10:8443").is_ok()); 2417 assert!(device_blossom_profile("http://10.0.0.10:8443").is_ok()); 2418 assert!(device_blossom_profile("http://8.8.8.8:8443").is_err()); 2419 assert!(device_blossom_profile("https://device.example:8443").is_err()); 2420 assert!(device_blossom_profile("https://127.0.0.1:8443").is_err()); 2421 } 2422 2423 #[cfg(feature = "blossom")] 2424 #[test] 2425 fn blossom_configuration_is_bounded_and_debug_is_secret_safe() { 2426 let profile = simulator_blossom_profile("http://127.0.0.1:3000").unwrap(); 2427 assert!( 2428 BlossomConfig::from_profile(profile.clone()) 2429 .with_limits(0, 1, 0) 2430 .is_err() 2431 ); 2432 assert!( 2433 BlossomConfig::from_profile(profile.clone()) 2434 .with_network_policy( 2435 Duration::from_secs(1), 2436 Duration::from_secs(1), 2437 0, 2438 Duration::from_millis(1), 2439 ) 2440 .is_err() 2441 ); 2442 let slot = BlossomSlot::new(); 2443 slot.configure(BlossomConfig::from_profile(profile)) 2444 .unwrap(); 2445 assert_eq!(slot.host_kind(), Some(BlossomHostKind::Simulator)); 2446 assert_eq!( 2447 slot.endpoint_authority(), 2448 Some(BlossomEndpointAuthority::LoopbackDevelopment) 2449 ); 2450 assert_eq!(format!("{slot:?}"), "BlossomSlot { configured: true }"); 2451 } 2452 2453 #[cfg(feature = "blossom")] 2454 #[test] 2455 fn blossom_evidence_is_versioned_passive_and_preserves_last_success() { 2456 let slot = BlossomSlot::new(); 2457 assert!(slot.profile().is_none()); 2458 assert!(slot.configuration().is_none()); 2459 assert!(slot.evidence().is_none()); 2460 2461 let public_config = BlossomConfig::from_profile( 2462 public_blossom_profile("https://media.example").expect("public profile"), 2463 ); 2464 let public_fingerprint = public_config.fingerprint(); 2465 slot.configure(public_config).expect("public config"); 2466 let initial = slot.evidence().expect("initial evidence"); 2467 assert_eq!(initial.schema_version(), 2); 2468 assert_eq!(initial.origin(), "https://media.example"); 2469 assert_eq!(initial.config_fingerprint(), public_fingerprint); 2470 assert_eq!(initial.state(), BlossomEvidenceState::ConfiguredUnobserved); 2471 assert_eq!( 2472 initial.last_successful_state(), 2473 BlossomEvidenceState::ConfiguredUnobserved 2474 ); 2475 assert_eq!( 2476 initial.transport_security(), 2477 BlossomTransportSecurity::PublicWebPki 2478 ); 2479 assert_eq!(initial.observed_at_unix_ms(), None); 2480 assert_eq!(initial.http_status(), None); 2481 assert_eq!(initial.error_code(), None); 2482 assert_eq!(initial.error_phase(), None); 2483 assert!(!initial.retryable()); 2484 assert!(!initial.possible_orphan()); 2485 assert_eq!(initial.attempts(), 0); 2486 assert_eq!(public_fingerprint.to_hex(), public_fingerprint.to_string()); 2487 assert_eq!( 2488 slot.profile().expect("profile").primary().origin(), 2489 initial.origin() 2490 ); 2491 assert_eq!( 2492 slot.configuration().expect("configuration").1, 2493 public_fingerprint 2494 ); 2495 2496 let private_tls = BlossomConfig::from_profile( 2497 device_blossom_profile("https://10.0.0.10:8443").expect("device profile"), 2498 ); 2499 let mut evidence = BlossomEndpointEvidence::configured(&private_tls); 2500 assert_eq!( 2501 evidence.transport_security(), 2502 BlossomTransportSecurity::DevelopmentTls 2503 ); 2504 evidence.record_success(BlossomEvidenceState::UploadVerified, Some(201)); 2505 assert_eq!(evidence.state(), BlossomEvidenceState::UploadVerified); 2506 assert_eq!( 2507 evidence.last_successful_state(), 2508 BlossomEvidenceState::UploadVerified 2509 ); 2510 assert!(evidence.observed_at_unix_ms().is_some()); 2511 assert_eq!(evidence.http_status(), Some(201)); 2512 2513 let failure = BlossomError::new( 2514 BlossomErrorKind::HttpStatus, 2515 BlossomPhase::Retrieval, 2516 false, 2517 true, 2518 2, 2519 ) 2520 .with_http_status(403); 2521 evidence.record_failure(&failure); 2522 assert_eq!(evidence.state(), BlossomEvidenceState::TerminalFailure); 2523 assert_eq!( 2524 evidence.last_successful_state(), 2525 BlossomEvidenceState::UploadVerified 2526 ); 2527 assert_eq!(evidence.http_status(), Some(403)); 2528 assert_eq!(evidence.error_code(), Some("blossom_http_status")); 2529 assert_eq!(evidence.server_error_code(), None); 2530 assert_eq!(evidence.error_phase(), Some(BlossomPhase::Retrieval)); 2531 assert!(!evidence.retryable()); 2532 assert!(evidence.possible_orphan()); 2533 assert_eq!(evidence.attempts(), 2); 2534 2535 let cleartext = BlossomEndpointEvidence::configured(&BlossomConfig::from_profile( 2536 simulator_blossom_profile("http://127.0.0.1:3000").expect("simulator profile"), 2537 )); 2538 assert_eq!( 2539 cleartext.transport_security(), 2540 BlossomTransportSecurity::DevelopmentCleartext 2541 ); 2542 let device_cleartext = BlossomEndpointEvidence::configured(&BlossomConfig::from_profile( 2543 device_blossom_profile("http://10.0.0.10:3000").expect("device profile"), 2544 )); 2545 assert_eq!( 2546 device_cleartext.transport_security(), 2547 BlossomTransportSecurity::DevelopmentCleartext 2548 ); 2549 2550 slot.clear(); 2551 assert!(slot.evidence().is_none()); 2552 } 2553 2554 #[cfg(feature = "blossom")] 2555 fn blossom_png(width: u32, height: u32) -> Vec<u8> { 2556 let mut bytes = b"\x89PNG\r\n\x1a\n\0\0\0\rIHDR".to_vec(); 2557 bytes.extend_from_slice(&width.to_be_bytes()); 2558 bytes.extend_from_slice(&height.to_be_bytes()); 2559 bytes 2560 } 2561 2562 #[cfg(feature = "blossom")] 2563 fn blossom_request(_origin: &str) -> BlossomUploadRequest { 2564 let bytes = blossom_png(2, 3); 2565 BlossomUploadRequest::new( 2566 Arc::from(bytes), 2567 MediaType::parse("image/png").expect("media type"), 2568 BlossomImageDimensions::new(2, 3).expect("dimensions"), 2569 1_900_000_000_000, 2570 ) 2571 .expect("request") 2572 } 2573 2574 #[cfg(feature = "blossom")] 2575 fn public_blossom_profile(origin: &str) -> Result<BlossomProfile, BlossomError> { 2576 BlossomProfile::new( 2577 BlossomHostKind::Native, 2578 BlossomEndpointAuthority::PublicWebPki, 2579 origin, 2580 std::iter::empty::<&str>(), 2581 ) 2582 } 2583 2584 #[cfg(feature = "blossom")] 2585 #[test] 2586 fn upload_retry_policy_is_bounded_and_frozen_in_the_transaction() { 2587 let profile = public_blossom_profile("https://media.example").unwrap(); 2588 for (maximum, initial_ms) in [(1, 250), (3, 250), (5, 20_000)] { 2589 let config = BlossomConfig::from_profile(profile.clone()) 2590 .with_network_policy( 2591 Duration::from_secs(10), 2592 Duration::from_secs(60), 2593 maximum, 2594 Duration::from_millis(initial_ms), 2595 ) 2596 .unwrap(); 2597 let slot = BlossomSlot::new(); 2598 slot.configure(config.clone()).unwrap(); 2599 let transaction = slot.prepare_upload(blossom_request("ignored")).unwrap(); 2600 for completed in 0..=u8::MAX { 2601 let expected = if completed > 0 && completed < maximum { 2602 Some(Duration::from_millis( 2603 (initial_ms * (1 << (completed - 1))).min(30_000), 2604 )) 2605 } else { 2606 None 2607 }; 2608 assert_eq!(transaction.retry_delay_after(completed), expected); 2609 } 2610 slot.configure( 2611 BlossomConfig::from_profile(profile.clone()) 2612 .with_network_policy( 2613 Duration::from_secs(10), 2614 Duration::from_secs(60), 2615 2, 2616 Duration::from_millis(7), 2617 ) 2618 .unwrap(), 2619 ) 2620 .unwrap(); 2621 let later = slot.prepare_upload(blossom_request("ignored")).unwrap(); 2622 assert_ne!(transaction.config_fingerprint(), later.config_fingerprint()); 2623 assert_eq!(later.retry_delay_after(1), Some(Duration::from_millis(7))); 2624 assert_eq!(later.retry_delay_after(2), None); 2625 assert_eq!(transaction.config_fingerprint(), config.fingerprint()); 2626 assert_eq!( 2627 transaction.retry_delay_after(1), 2628 (maximum > 1).then_some(Duration::from_millis(initial_ms)) 2629 ); 2630 assert_eq!(config.retry_delay(0), Duration::from_millis(initial_ms)); 2631 assert_eq!(config.retry_delay(u8::MAX), Duration::from_secs(30)); 2632 } 2633 } 2634 2635 #[cfg(feature = "blossom")] 2636 fn simulator_blossom_profile(origin: &str) -> Result<BlossomProfile, BlossomError> { 2637 BlossomProfile::new( 2638 BlossomHostKind::Simulator, 2639 BlossomEndpointAuthority::LoopbackDevelopment, 2640 origin, 2641 std::iter::empty::<&str>(), 2642 ) 2643 } 2644 2645 #[cfg(feature = "blossom")] 2646 fn device_blossom_profile(origin: &str) -> Result<BlossomProfile, BlossomError> { 2647 BlossomProfile::new( 2648 BlossomHostKind::PhysicalDevice, 2649 BlossomEndpointAuthority::PrivateNetworkDevelopment, 2650 origin, 2651 std::iter::empty::<&str>(), 2652 ) 2653 } 2654 2655 #[cfg(feature = "blossom")] 2656 #[test] 2657 fn blossom_profiles_expose_exact_identity_and_reject_malformed_sets() { 2658 let public = BlossomProfile::new( 2659 BlossomHostKind::PhysicalDevice, 2660 BlossomEndpointAuthority::PublicWebPki, 2661 "https://media.example:8443", 2662 ["https://fallback.example"], 2663 ) 2664 .expect("public"); 2665 assert_eq!(public.host_kind(), BlossomHostKind::PhysicalDevice); 2666 assert_eq!(public.authority(), BlossomEndpointAuthority::PublicWebPki); 2667 assert_eq!(public.fallbacks().len(), 1); 2668 let endpoint = public.primary(); 2669 assert_eq!(endpoint.origin(), "https://media.example:8443"); 2670 assert_eq!(endpoint.host(), "media.example"); 2671 assert_eq!(endpoint.port(), 8443); 2672 assert_eq!(endpoint.authority(), BlossomEndpointAuthority::PublicWebPki); 2673 2674 let request = blossom_request("https://media.example:8443"); 2675 let slot = BlossomSlot::new(); 2676 slot.configure(BlossomConfig::from_profile(public.clone())) 2677 .unwrap(); 2678 let transaction = slot.prepare_upload(request).unwrap(); 2679 assert!(endpoint.accepts_blob_url(transaction.expected_url())); 2680 assert_eq!( 2681 transaction 2682 .expected_url() 2683 .hash_path() 2684 .extension() 2685 .unwrap() 2686 .as_str(), 2687 "png" 2688 ); 2689 assert_eq!( 2690 transaction.expected_url().hash_path().hash(), 2691 transaction.request().sha256() 2692 ); 2693 assert_eq!(endpoint.upload_url(), "https://media.example:8443/upload"); 2694 assert_eq!(endpoint.server_domain().unwrap().as_str(), "media.example"); 2695 assert_eq!( 2696 public 2697 .endpoint_for_blob(transaction.expected_url()) 2698 .expect("configured endpoint"), 2699 endpoint 2700 ); 2701 2702 assert_eq!( 2703 BlossomProfile::new( 2704 BlossomHostKind::PhysicalDevice, 2705 BlossomEndpointAuthority::PrivateNetworkDevelopment, 2706 "https://10.0.0.10", 2707 std::iter::empty::<&str>(), 2708 ) 2709 .unwrap() 2710 .host_kind(), 2711 BlossomHostKind::PhysicalDevice 2712 ); 2713 assert_eq!( 2714 simulator_blossom_profile("http://localhost:3000") 2715 .unwrap() 2716 .host_kind(), 2717 BlossomHostKind::Simulator 2718 ); 2719 assert_eq!( 2720 BlossomProfile::new( 2721 BlossomHostKind::Native, 2722 BlossomEndpointAuthority::PublicWebPki, 2723 "", 2724 std::iter::empty::<&str>(), 2725 ) 2726 .expect_err("empty profile") 2727 .kind(), 2728 BlossomErrorKind::InvalidEndpoint 2729 ); 2730 assert_eq!( 2731 BlossomProfile::new( 2732 BlossomHostKind::Native, 2733 BlossomEndpointAuthority::PublicWebPki, 2734 "https://primary.example", 2735 std::iter::repeat_n("https://media.example", 16), 2736 ) 2737 .expect_err("bounded profile") 2738 .kind(), 2739 BlossomErrorKind::InvalidEndpointCount 2740 ); 2741 assert_eq!( 2742 BlossomProfile::new( 2743 BlossomHostKind::Native, 2744 BlossomEndpointAuthority::PublicWebPki, 2745 "https://media.example", 2746 ["https://media.example"], 2747 ) 2748 .expect_err("duplicate profile") 2749 .kind(), 2750 BlossomErrorKind::DuplicateEndpoint 2751 ); 2752 2753 for malformed in [ 2754 "", 2755 " https://media.example", 2756 "https://média.example", 2757 "https://user@media.example", 2758 "https://:password@media.example", 2759 "https://media.example/path", 2760 "https://media.example?query=1", 2761 "https://media.example#fragment", 2762 "ftp://media.example", 2763 "https://media.example:0", 2764 ] { 2765 assert!(public_blossom_profile(malformed).is_err(), "{malformed}"); 2766 } 2767 } 2768 2769 #[cfg(feature = "blossom")] 2770 #[test] 2771 fn blossom_upload_transactions_bind_primary_authority_and_complete_config() { 2772 let profile = BlossomProfile::new( 2773 BlossomHostKind::PhysicalDevice, 2774 BlossomEndpointAuthority::PublicWebPki, 2775 "https://media.example:443", 2776 ["https://fallback.example"], 2777 ) 2778 .unwrap(); 2779 assert_eq!(profile.primary().origin(), "https://media.example"); 2780 assert!( 2781 profile 2782 .primary() 2783 .validate_resolved_addresses([IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1))]) 2784 .is_err() 2785 ); 2786 2787 let slot = BlossomSlot::new(); 2788 let original = BlossomConfig::from_profile(profile); 2789 let original_fingerprint = original.fingerprint(); 2790 slot.configure(original).unwrap(); 2791 let transaction = slot 2792 .prepare_upload(blossom_request("caller-origin-is-ignored")) 2793 .unwrap(); 2794 assert_eq!(transaction.config_fingerprint(), original_fingerprint); 2795 assert!( 2796 transaction 2797 .expected_url() 2798 .as_str() 2799 .starts_with("https://media.example/") 2800 ); 2801 let claim = slot 2802 .authored_upload_claim( 2803 &transaction, 2804 AuthorizationContent::parse("Upload farm image").unwrap(), 2805 100, 2806 60, 2807 ) 2808 .unwrap(); 2809 assert_eq!(claim.server_domain().as_str(), "media.example"); 2810 2811 let changed = BlossomConfig::from_profile( 2812 BlossomProfile::new( 2813 BlossomHostKind::PhysicalDevice, 2814 BlossomEndpointAuthority::PublicWebPki, 2815 "https://other.example", 2816 ["https://fallback.example"], 2817 ) 2818 .unwrap(), 2819 ); 2820 assert_ne!(changed.fingerprint(), original_fingerprint); 2821 slot.configure(changed).unwrap(); 2822 assert_eq!( 2823 slot.authored_upload_claim( 2824 &transaction, 2825 AuthorizationContent::parse("Upload farm image").unwrap(), 2826 100, 2827 60, 2828 ) 2829 .expect_err("changed configuration"), 2830 BlossomError::configuration(BlossomErrorKind::ConfigurationChanged) 2831 ); 2832 assert_eq!( 2833 slot.validate_transaction(&transaction) 2834 .expect_err("changed upload destination") 2835 .kind(), 2836 BlossomErrorKind::ConfigurationChanged 2837 ); 2838 } 2839 2840 #[cfg(feature = "blossom")] 2841 #[test] 2842 fn blossom_limits_requests_and_errors_cover_the_complete_public_contract() { 2843 let profile = simulator_blossom_profile("http://127.0.0.1:3000").unwrap(); 2844 let valid = BlossomConfig::from_profile(profile.clone()) 2845 .with_limits(1, 1, 5) 2846 .unwrap() 2847 .with_network_policy( 2848 Duration::from_millis(1), 2849 Duration::from_millis(2), 2850 5, 2851 Duration::from_millis(3), 2852 ) 2853 .unwrap(); 2854 assert_eq!(valid.profile(), &profile); 2855 assert_eq!(valid.max_blob_bytes(), 1); 2856 assert_eq!(valid.max_descriptor_bytes(), 1); 2857 assert_eq!(valid.max_redirects(), 5); 2858 assert_eq!(valid.max_attempts(), 5); 2859 assert_eq!(valid.connect_timeout(), Duration::from_millis(1)); 2860 assert_eq!(valid.request_timeout(), Duration::from_millis(2)); 2861 assert_eq!(valid.initial_retry_delay(), Duration::from_millis(3)); 2862 2863 for (blob, descriptor, redirects) in [ 2864 (0, 1, 0), 2865 (MAX_BLOSSOM_BLOB_BYTES + 1, 1, 0), 2866 (1, 0, 0), 2867 (1, MAX_BLOSSOM_DESCRIPTOR_BYTES + 1, 0), 2868 (1, 1, MAX_BLOSSOM_REDIRECTS + 1), 2869 ] { 2870 assert!( 2871 BlossomConfig::from_profile(profile.clone()) 2872 .with_limits(blob, descriptor, redirects) 2873 .is_err() 2874 ); 2875 } 2876 for (connect, request, attempts, delay) in [ 2877 ( 2878 Duration::ZERO, 2879 Duration::from_secs(1), 2880 1, 2881 Duration::from_millis(1), 2882 ), 2883 ( 2884 MAX_BLOSSOM_TIMEOUT + Duration::from_secs(1), 2885 Duration::from_secs(1), 2886 1, 2887 Duration::from_millis(1), 2888 ), 2889 ( 2890 Duration::from_secs(1), 2891 Duration::ZERO, 2892 1, 2893 Duration::from_millis(1), 2894 ), 2895 ( 2896 Duration::from_secs(1), 2897 MAX_BLOSSOM_TIMEOUT + Duration::from_secs(1), 2898 1, 2899 Duration::from_millis(1), 2900 ), 2901 ( 2902 Duration::from_secs(1), 2903 Duration::from_secs(1), 2904 0, 2905 Duration::from_millis(1), 2906 ), 2907 ( 2908 Duration::from_secs(1), 2909 Duration::from_secs(1), 2910 MAX_BLOSSOM_ATTEMPTS + 1, 2911 Duration::from_millis(1), 2912 ), 2913 ( 2914 Duration::from_secs(1), 2915 Duration::from_secs(1), 2916 1, 2917 Duration::ZERO, 2918 ), 2919 ( 2920 Duration::from_secs(1), 2921 Duration::from_secs(1), 2922 1, 2923 MAX_BLOSSOM_RETRY_DELAY + Duration::from_secs(1), 2924 ), 2925 ] { 2926 assert!( 2927 BlossomConfig::from_profile(profile.clone()) 2928 .with_network_policy(connect, request, attempts, delay) 2929 .is_err() 2930 ); 2931 } 2932 2933 assert!(BlossomImageDimensions::new(0, 1).is_err()); 2934 assert!(BlossomImageDimensions::new(1, 0).is_err()); 2935 assert!(BlossomImageDimensions::new(16_385, 1).is_err()); 2936 assert!(BlossomImageDimensions::new(10_001, 10_000).is_err()); 2937 let dimensions = BlossomImageDimensions::new(2, 3).unwrap(); 2938 assert_eq!(dimensions.width(), 2); 2939 assert_eq!(dimensions.height(), 3); 2940 2941 let request = blossom_request("http://127.0.0.1:3000"); 2942 assert_eq!(request.media_type().as_str(), "image/png"); 2943 assert_eq!(request.dimensions(), dimensions); 2944 assert_eq!(request.byte_size(), request.bytes().len() as u64); 2945 assert_eq!(request.sha256(), Sha256::digest(request.bytes())); 2946 assert_eq!(request.verified_at_unix_ms(), 1_900_000_000_000); 2947 assert!(format!("{request:?}").contains("bytes: \"<redacted>\"")); 2948 2949 let media_type = MediaType::parse("image/png").unwrap(); 2950 assert!( 2951 BlossomUploadRequest::new(Arc::from([]), media_type.clone(), dimensions, 1,).is_err() 2952 ); 2953 let bytes = blossom_png(2, 3); 2954 assert!( 2955 BlossomUploadRequest::new( 2956 Arc::from(bytes.clone()), 2957 MediaType::parse("image/jpeg").unwrap(), 2958 dimensions, 2959 1, 2960 ) 2961 .is_err() 2962 ); 2963 assert!(BlossomUploadRequest::new(Arc::from(bytes), media_type, dimensions, 0).is_err()); 2964 2965 let all_kinds = [ 2966 ( 2967 BlossomErrorKind::InvalidEndpoint, 2968 "blossom_invalid_endpoint", 2969 ), 2970 ( 2971 BlossomErrorKind::EndpointSchemeDenied, 2972 "blossom_endpoint_scheme_denied", 2973 ), 2974 ( 2975 BlossomErrorKind::InvalidEndpointCount, 2976 "blossom_invalid_endpoint_count", 2977 ), 2978 ( 2979 BlossomErrorKind::DuplicateEndpoint, 2980 "blossom_duplicate_endpoint", 2981 ), 2982 ( 2983 BlossomErrorKind::EndpointNotConfigured, 2984 "blossom_endpoint_not_configured", 2985 ), 2986 ( 2987 BlossomErrorKind::ConfigurationChanged, 2988 "blossom_configuration_changed", 2989 ), 2990 ( 2991 BlossomErrorKind::ResolutionFailed, 2992 "blossom_resolution_failed", 2993 ), 2994 ( 2995 BlossomErrorKind::ResolvedAddressDenied, 2996 "blossom_resolved_address_denied", 2997 ), 2998 (BlossomErrorKind::InvalidLimits, "blossom_invalid_limits"), 2999 (BlossomErrorKind::InvalidRequest, "blossom_invalid_request"), 3000 ( 3001 BlossomErrorKind::InvalidDimensions, 3002 "blossom_invalid_dimensions", 3003 ), 3004 ( 3005 BlossomErrorKind::UnsupportedMediaType, 3006 "blossom_unsupported_media_type", 3007 ), 3008 ( 3009 BlossomErrorKind::MediaTypeMismatch, 3010 "blossom_media_type_mismatch", 3011 ), 3012 ( 3013 BlossomErrorKind::InvalidImageBytes, 3014 "blossom_invalid_image_bytes", 3015 ), 3016 ( 3017 BlossomErrorKind::DimensionMismatch, 3018 "blossom_dimension_mismatch", 3019 ), 3020 ( 3021 BlossomErrorKind::Authorization, 3022 "blossom_authorization_failed", 3023 ), 3024 (BlossomErrorKind::Transport, "blossom_transport_failed"), 3025 (BlossomErrorKind::Timeout, "blossom_timeout"), 3026 (BlossomErrorKind::Cancelled, "blossom_cancelled"), 3027 (BlossomErrorKind::HttpStatus, "blossom_http_status"), 3028 (BlossomErrorKind::UnsafeRedirect, "blossom_unsafe_redirect"), 3029 (BlossomErrorKind::RedirectLimit, "blossom_redirect_limit"), 3030 ( 3031 BlossomErrorKind::ContentEncodingDenied, 3032 "blossom_content_encoding_denied", 3033 ), 3034 ( 3035 BlossomErrorKind::ResponseTooLarge, 3036 "blossom_response_too_large", 3037 ), 3038 ( 3039 BlossomErrorKind::ResponseSizeMismatch, 3040 "blossom_response_size_mismatch", 3041 ), 3042 ( 3043 BlossomErrorKind::ResponseHashMismatch, 3044 "blossom_response_hash_mismatch", 3045 ), 3046 ( 3047 BlossomErrorKind::InvalidDescriptor, 3048 "blossom_invalid_descriptor", 3049 ), 3050 ( 3051 BlossomErrorKind::DescriptorMismatch, 3052 "blossom_descriptor_mismatch", 3053 ), 3054 ( 3055 BlossomErrorKind::RetrievedBytesMismatch, 3056 "blossom_retrieved_bytes_mismatch", 3057 ), 3058 ]; 3059 for (kind, code) in all_kinds { 3060 let error = BlossomError::new(kind, BlossomPhase::Verification, true, false, 2); 3061 assert_eq!(error.kind(), kind); 3062 assert_eq!(error.phase(), BlossomPhase::Verification); 3063 assert!(error.retryable()); 3064 assert!(!error.possible_orphan()); 3065 assert_eq!(error.attempts(), 2); 3066 assert_eq!(error.code(), code); 3067 assert_eq!(error.to_string(), code); 3068 assert!( 3069 format!("{error:?}").contains(code.trim_start_matches("blossom_")) 3070 || !code.is_empty() 3071 ); 3072 let updated = error.with_operation(true, 3); 3073 assert!(updated.possible_orphan()); 3074 assert_eq!(updated.attempts(), 3); 3075 } 3076 } 3077 3078 #[cfg(feature = "blossom")] 3079 #[test] 3080 fn blossom_address_policy_covers_public_simulator_and_device_networks() { 3081 use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; 3082 3083 let public_v4 = [ 3084 (Ipv4Addr::new(8, 8, 8, 8), true), 3085 (Ipv4Addr::new(0, 1, 2, 3), false), 3086 (Ipv4Addr::LOCALHOST, false), 3087 (Ipv4Addr::new(10, 0, 0, 1), false), 3088 (Ipv4Addr::new(169, 254, 1, 1), false), 3089 (Ipv4Addr::new(224, 0, 0, 1), false), 3090 (Ipv4Addr::new(192, 0, 2, 1), false), 3091 (Ipv4Addr::new(100, 64, 0, 1), false), 3092 (Ipv4Addr::new(100, 128, 0, 1), true), 3093 (Ipv4Addr::new(192, 0, 0, 1), false), 3094 (Ipv4Addr::new(192, 0, 1, 1), true), 3095 (Ipv4Addr::new(192, 88, 99, 1), false), 3096 (Ipv4Addr::new(192, 88, 98, 1), true), 3097 (Ipv4Addr::new(198, 18, 0, 1), false), 3098 (Ipv4Addr::new(240, 0, 0, 1), false), 3099 ]; 3100 for (address, accepted) in public_v4 { 3101 assert_eq!(public_blossom_ipv4(address), accepted, "{address}"); 3102 assert_eq!(public_blossom_address(IpAddr::V4(address)), accepted); 3103 } 3104 let public_v6 = [ 3105 ("2606:4700:4700::1111", true), 3106 ("::ffff:8.8.8.8", true), 3107 ("::ffff:127.0.0.1", false), 3108 ("2001:100::1", false), 3109 ("2001:db8::1", false), 3110 ("2002::1", false), 3111 ("3fff::1", false), 3112 ("4000::1", false), 3113 ("2001:db9::1", true), 3114 ]; 3115 for (text, accepted) in public_v6 { 3116 let address = text.parse::<Ipv6Addr>().unwrap(); 3117 assert_eq!(public_blossom_ipv6(address), accepted, "{text}"); 3118 assert_eq!(public_blossom_address(IpAddr::V6(address)), accepted); 3119 } 3120 3121 for (host, accepted) in [ 3122 ("media.example", true), 3123 ("localhost", false), 3124 ("farm.localhost", false), 3125 ("farm.local", false), 3126 ("farm.home.arpa", false), 3127 ("intranet", false), 3128 ] { 3129 assert_eq!(public_blossom_hostname(host), accepted, "{host}"); 3130 } 3131 3132 let simulator = simulator_blossom_profile("http://127.0.0.1:3000").unwrap(); 3133 let simulator_endpoint = simulator.primary(); 3134 assert!( 3135 simulator_endpoint 3136 .validate_resolved_addresses([IpAddr::V4(Ipv4Addr::LOCALHOST)]) 3137 .is_ok() 3138 ); 3139 assert!(simulator_endpoint.validate_resolved_addresses([]).is_err()); 3140 assert!( 3141 simulator_endpoint 3142 .validate_resolved_addresses([IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8))]) 3143 .is_err() 3144 ); 3145 3146 let public = public_blossom_profile("https://media.example").unwrap(); 3147 assert!( 3148 public 3149 .primary() 3150 .validate_resolved_addresses([IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8))]) 3151 .is_ok() 3152 ); 3153 let device = device_blossom_profile("https://10.0.0.10").unwrap(); 3154 assert!( 3155 device 3156 .primary() 3157 .validate_resolved_addresses([IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1))]) 3158 .is_ok() 3159 ); 3160 3161 for address in [ 3162 IpAddr::V4(Ipv4Addr::UNSPECIFIED), 3163 IpAddr::V4(Ipv4Addr::LOCALHOST), 3164 IpAddr::V4(Ipv4Addr::new(224, 0, 0, 1)), 3165 IpAddr::V4(Ipv4Addr::BROADCAST), 3166 IpAddr::V6(Ipv6Addr::UNSPECIFIED), 3167 IpAddr::V6(Ipv6Addr::LOCALHOST), 3168 IpAddr::V6("ff02::1".parse().unwrap()), 3169 ] { 3170 assert!(!trusted_blossom_address(address), "{address}"); 3171 } 3172 assert!(trusted_blossom_address(IpAddr::V4(Ipv4Addr::new( 3173 10, 0, 0, 1 3174 )))); 3175 assert!(trusted_blossom_address(IpAddr::V6( 3176 "fd00::1".parse().unwrap() 3177 ))); 3178 assert!(blossom_authority_accepts_address( 3179 BlossomEndpointAuthority::PublicWebPki, 3180 IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8)) 3181 )); 3182 assert!(blossom_authority_accepts_address( 3183 BlossomEndpointAuthority::LoopbackDevelopment, 3184 IpAddr::V4(Ipv4Addr::LOCALHOST) 3185 )); 3186 assert!(blossom_authority_accepts_address( 3187 BlossomEndpointAuthority::PrivateNetworkDevelopment, 3188 IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)) 3189 )); 3190 } 3191 3192 #[cfg(feature = "blossom")] 3193 #[test] 3194 fn blossom_device_policy_matches_the_shared_conformance_vectors() { 3195 let document: serde_json::Value = serde_json::from_str(include_str!( 3196 "../../../contracts/conformance/vectors/transport/device_network_policy.v1.json" 3197 )) 3198 .expect("device network policy vectors"); 3199 for vector in document["vectors"].as_array().expect("vectors") { 3200 let input = &vector["input"]; 3201 if input["surface"] != "blossom" { 3202 continue; 3203 } 3204 let endpoint = input["endpoint"].as_str().expect("endpoint"); 3205 let profile = match input["policy"].as_str().expect("policy") { 3206 "public" => public_blossom_profile(endpoint), 3207 "loopback" => simulator_blossom_profile(endpoint), 3208 "private_device" => device_blossom_profile(endpoint), 3209 other => panic!("unknown Blossom policy {other}"), 3210 }; 3211 assert_eq!( 3212 profile.is_ok(), 3213 vector["expected"]["accepted"].as_bool().expect("accepted"), 3214 "{}", 3215 vector["id"].as_str().expect("id") 3216 ); 3217 } 3218 } 3219 3220 #[cfg(feature = "blossom")] 3221 #[tokio::test] 3222 async fn blossom_cancellation_slot_claim_and_receipt_state_are_exact() { 3223 let cancellation = BlossomCancellation::default(); 3224 assert!(!cancellation.is_cancelled()); 3225 let waiting = cancellation.clone(); 3226 let waiter = tokio::spawn(async move { waiting.cancelled().await }); 3227 tokio::task::yield_now().await; 3228 cancellation.cancel(); 3229 waiter.await.unwrap(); 3230 cancellation.cancelled().await; 3231 assert!(cancellation.is_cancelled()); 3232 3233 let request = blossom_request("http://127.0.0.1:3000"); 3234 let slot = BlossomSlot::new(); 3235 assert!(slot.host_kind().is_none()); 3236 let content = AuthorizationContent::parse("Upload farm image").unwrap(); 3237 assert!(slot.prepare_upload(request.clone()).is_err()); 3238 slot.configure(BlossomConfig::from_profile( 3239 simulator_blossom_profile("http://127.0.0.1:3000").unwrap(), 3240 )) 3241 .unwrap(); 3242 let transaction = slot.prepare_upload(request.clone()).unwrap(); 3243 let claim = slot 3244 .authored_upload_claim(&transaction, content.clone(), 100, 60) 3245 .unwrap(); 3246 assert_eq!(claim.server_domain().as_str(), "127.0.0.1"); 3247 assert_eq!(claim.sha256(), request.sha256()); 3248 assert_eq!(claim.lifetime_seconds(), 60); 3249 assert_eq!( 3250 slot.authored_upload_claim(&transaction, content, 100, 0) 3251 .expect_err("invalid lifetime") 3252 .kind(), 3253 BlossomErrorKind::Authorization 3254 ); 3255 slot.clear(); 3256 assert!(slot.host_kind().is_none()); 3257 3258 let descriptor = radroots_blossom::BlobDescriptor::new( 3259 transaction.expected_url().clone(), 3260 request.sha256(), 3261 request.byte_size(), 3262 request.media_type().clone(), 3263 1, 3264 ) 3265 .unwrap() 3266 .approve_reference() 3267 .unwrap() 3268 .verify_bytes(request.bytes(), request.media_type()) 3269 .unwrap(); 3270 let receipt = BlossomUploadReceipt::new(descriptor, request.dimensions(), 2, 3); 3271 assert_eq!(receipt.descriptor().sha256(), request.sha256()); 3272 assert_eq!(receipt.dimensions(), request.dimensions()); 3273 assert_eq!(receipt.attempts(), 2); 3274 assert_eq!(receipt.verified_at_unix_ms(), 3); 3275 assert_eq!(receipt.into_descriptor().size(), request.byte_size()); 3276 3277 let poisoned = BlossomSlot::new(); 3278 let state = Arc::clone(&poisoned.state); 3279 let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { 3280 let _guard = state.write().expect("write lock"); 3281 panic!("poison Blossom slot"); 3282 })); 3283 poisoned.clear(); 3284 assert!( 3285 poisoned 3286 .configure(BlossomConfig::from_profile( 3287 simulator_blossom_profile("http://127.0.0.1:3000").unwrap(), 3288 )) 3289 .is_err() 3290 ); 3291 assert!(poisoned.host_kind().is_none()); 3292 } 3293 3294 #[cfg(feature = "radrootsd")] 3295 #[test] 3296 fn daemon_configuration_is_inert_explicit_and_redacted() { 3297 let config = DaemonConfig::new("http://127.0.0.1:1/rpc") 3298 .with_auth(DaemonAuth::BearerToken("secret-token".to_owned())) 3299 .with_timeout(core::time::Duration::from_millis(5)); 3300 let adapter = DaemonDelivery::new(config); 3301 3302 let debug = format!("{adapter:?}"); 3303 assert!(!debug.contains("secret-token")); 3304 assert!(!debug.contains("reqwest")); 3305 assert_eq!(format!("{:?}", DaemonAuth::None), "None"); 3306 assert_eq!( 3307 format!("{:?}", DaemonAuth::BearerToken("private".to_owned())), 3308 "BearerToken(<redacted>)" 3309 ); 3310 } 3311 3312 #[cfg(feature = "radrootsd")] 3313 #[test] 3314 fn daemon_errors_are_stably_classified_and_redacted() { 3315 use std::error::Error as _; 3316 3317 use crate::adapters::radrootsd::RadrootsdError; 3318 3319 let cases = [ 3320 ( 3321 RadrootsdError::InvalidAuthHeader("private".to_owned()), 3322 DaemonErrorKind::Authentication, 3323 "daemon authentication configuration is invalid", 3324 ), 3325 ( 3326 RadrootsdError::InvalidRequest("private".to_owned()), 3327 DaemonErrorKind::InvalidRequest, 3328 "daemon delivery request is invalid", 3329 ), 3330 ( 3331 RadrootsdError::Http("private".to_owned()), 3332 DaemonErrorKind::Transport, 3333 "daemon transport failed", 3334 ), 3335 ( 3336 RadrootsdError::JsonRpc { 3337 code: -1, 3338 message: "private".to_owned(), 3339 }, 3340 DaemonErrorKind::Rpc, 3341 "daemon RPC failed", 3342 ), 3343 ( 3344 RadrootsdError::MalformedResponse("private".to_owned()), 3345 DaemonErrorKind::InvalidResponse, 3346 "daemon response is invalid", 3347 ), 3348 ]; 3349 for (private, kind, display) in cases { 3350 let error = DaemonError::from_private(private); 3351 assert_eq!(error.kind(), kind); 3352 assert_eq!(error.to_string(), display); 3353 assert!(error.source().is_some()); 3354 assert!(!format!("{error:?}").contains("private")); 3355 } 3356 } 3357 3358 #[cfg(feature = "nostr")] 3359 #[test] 3360 fn nostr_slot_reconfiguration_is_atomic_directional_and_inert() { 3361 let slot = NostrSlot::new(); 3362 assert!(slot.read_targets().is_none()); 3363 assert!(slot.relay_status().is_none()); 3364 assert!( 3365 slot.configure( 3366 RelayProfile::explicit( 3367 RelayProfileKind::Simulator, 3368 [RelayEndpoint::new( 3369 "ws://127.0.0.1:7447", 3370 RelayUrlPolicy::Local, 3371 RelayAccess::ReadWrite, 3372 ) 3373 .expect("endpoint")], 3374 ) 3375 .expect("profile"), 3376 ) 3377 .is_ok() 3378 ); 3379 let original = slot.read_targets().expect("configured targets"); 3380 assert_eq!(slot.write_targets(), Some(original.clone())); 3381 let status = slot.relay_status().expect("status"); 3382 assert_eq!(status.profile_kind(), RelayProfileKind::Simulator); 3383 assert_eq!(status.read_availability(), Availability::Unavailable); 3384 assert_eq!(status.write_availability(), Availability::Unavailable); 3385 slot.clear(); 3386 assert!(slot.read_targets().is_none()); 3387 assert!(slot.write_targets().is_none()); 3388 assert!(format!("{slot:?}").contains("configured: false")); 3389 } 3390 3391 #[cfg(feature = "nostr")] 3392 #[test] 3393 fn poisoned_nostr_slot_fails_closed_for_every_host_operation() { 3394 let slot = NostrSlot::new(); 3395 let state = Arc::clone(&slot.state); 3396 let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { 3397 let _guard = state.write().expect("write lock"); 3398 panic!("poison transport slot"); 3399 })); 3400 3401 slot.clear(); 3402 assert!(slot.read_targets().is_none()); 3403 assert!( 3404 slot.configure( 3405 RelayProfile::explicit( 3406 RelayProfileKind::Simulator, 3407 [RelayEndpoint::new( 3408 "ws://127.0.0.1:7447", 3409 RelayUrlPolicy::Local, 3410 RelayAccess::ReadWrite, 3411 ) 3412 .expect("endpoint")], 3413 ) 3414 .expect("profile"), 3415 ) 3416 .is_err() 3417 ); 3418 } 3419 3420 #[cfg(feature = "nostr")] 3421 #[tokio::test] 3422 async fn empty_nostr_slot_reports_unavailable_and_rejects_operations() { 3423 use radroots_transport::{ 3424 DeliveryRequest, EventSink as _, EventSource as _, FetchRequest, sink::DeliveryPayload, 3425 source::FetchBounds, 3426 }; 3427 3428 let slot = NostrSlot::new(); 3429 let source = radroots_transport::EventSource::status(&slot) 3430 .await 3431 .expect("source status"); 3432 assert_eq!(source.availability(), Availability::Unavailable); 3433 3434 let targets = TargetSet::new(vec![target(1)]).expect("targets"); 3435 let fetch = FetchRequest::new( 3436 "fetch", 3437 targets.clone(), 3438 FetchBounds::new(1, 1).expect("bounds"), 3439 ) 3440 .expect("fetch"); 3441 assert_eq!(slot.fetch(fetch).await, Err(Error::UnsupportedOperation)); 3442 3443 let sink = radroots_transport::EventSink::status(&slot) 3444 .await 3445 .expect("sink status"); 3446 assert_eq!(sink.availability(), Availability::Unavailable); 3447 let deliver = DeliveryRequest::new( 3448 "deliver", 3449 DeliveryPayload::new(signed_event()), 3450 targets, 3451 SatisfactionPolicy::new(SatisfactionClass::Accepted, TargetPolicy::any()), 3452 1, 3453 ) 3454 .expect("delivery"); 3455 let failure = slot 3456 .deliver(deliver.clone()) 3457 .await 3458 .expect_err("unconfigured sink"); 3459 assert_eq!(failure.code(), "nostr_transport_not_configured"); 3460 let failure = slot 3461 .deliver_selected(deliver.clone(), deliver.target_set().clone()) 3462 .await 3463 .unwrap_err(); 3464 assert_eq!(failure.code(), "nostr_transport_not_configured"); 3465 failure.validate_for_request(&deliver).unwrap(); 3466 let foreign = TargetSet::new(vec![target(2)]).unwrap(); 3467 let failure = slot 3468 .deliver_selected(deliver.clone(), foreign) 3469 .await 3470 .unwrap_err(); 3471 assert_eq!(failure.code(), "invalid_transport_contract"); 3472 let subset_request = DeliveryRequest::new( 3473 "selected-delivery", 3474 DeliveryPayload::new(signed_event()), 3475 TargetSet::new(vec![target(1), target(2)]).unwrap(), 3476 SatisfactionPolicy::new(SatisfactionClass::Accepted, TargetPolicy::all()), 3477 1, 3478 ) 3479 .unwrap(); 3480 slot.configure( 3481 RelayProfile::explicit( 3482 RelayProfileKind::Public, 3483 [RelayEndpoint::new( 3484 "wss://one.example", 3485 RelayUrlPolicy::Public, 3486 RelayAccess::ReadWrite, 3487 ) 3488 .unwrap()], 3489 ) 3490 .unwrap(), 3491 ) 3492 .unwrap(); 3493 let receipt = slot 3494 .deliver_selected(deliver.clone(), deliver.target_set().clone()) 3495 .await 3496 .unwrap(); 3497 receipt.validate_for_request(&deliver).unwrap(); 3498 assert!( 3499 receipt 3500 .target_receipts() 3501 .iter() 3502 .all(|row| !row.was_attempted()) 3503 ); 3504 let subset = 3505 TargetSet::new(vec![subset_request.target_set().targets()[0].clone()]).unwrap(); 3506 let receipt = slot 3507 .deliver_selected(subset_request.clone(), subset) 3508 .await 3509 .unwrap(); 3510 receipt.validate_for_request(&subset_request).unwrap(); 3511 assert!(!receipt.target_receipts()[1].was_attempted()); 3512 assert_eq!( 3513 receipt.target_receipts()[1].outcome().code(), 3514 Some("target_not_selected") 3515 ); 3516 } 3517 }