lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

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 }