field_ios

In-the-field app for Radroots on iOS
git clone https://radroots.dev/git/field_ios.git
Log | Files | Refs | README | LICENSE

dto.rs (107414B)


      1 //! Focused, versioned value types owned by the native boundary.
      2 
      3 use crate::media_file::MEDIA_FILE_MAX_BYTES;
      4 use crate::{FfiDraftStatusRecord, FfiMediaFile};
      5 
      6 use radroots_blossom::{BlobDescriptor, MediaType, Sha256};
      7 use radroots_event::{
      8     calendar::{AuthoredCalendarDateEvent, AuthoredCalendarTimeEvent, CalendarDate},
      9     food::availability::{
     10         FoodAvailabilityDetails, FoodAvailabilityDetailsParts, FoodAvailabilityImage,
     11         FoodAvailabilityStatus, FoodContent, FoodCurrency, FoodIdentifier, FoodImageDimensions,
     12         FoodPrice, FoodPublishedAt, FoodQuantity, FoodText, FoodUnit,
     13     },
     14     media::AuthoredImage,
     15     post::{AuthoredPostImage, PostImageDimensions},
     16 };
     17 use tera_core::runtime::{
     18     app_info::AppInfoPlatform,
     19     info::{AppInfo, RuntimeBuildInfo, RuntimeInfo},
     20     product_surface::{
     21         AddCommandType, CardLifecycleState, CreateAsk, CreateEvent, CreateFoodAvailability,
     22         CreatePhotoUpdate, CreateUpdate, LocalNetwork, LocalNetworkRelayPolicy, MeSnapshot,
     23         MediaReference, Phase1AddCommand, Phase1CancellationPolicy, Phase1DraftEventTiming,
     24         Phase1DraftFormSnapshot, Phase1DraftKind, Phase1DraftMediaSnapshot,
     25         Phase1InboundMediaState, Phase1MediaPrerequisite, Phase1MediaStage, Phase1OutboxState,
     26         Phase1QueuePolicy, Phase1RelaySatisfaction, Phase1UploadIntent, ProfileSummary,
     27         SearchResult, SearchResultType, SupportingProfile, ThreadEntry, TodayCard, TodayCardType,
     28         TodayPage, TodayProjectionUpdate, TodayRefreshReceipt,
     29     },
     30     sdk::{
     31         SdkBlossomConfigurationRecord, SdkBlossomEvidenceRecord, SdkCapabilityRecord,
     32         SdkRelayAccessRecord, SdkRelayStatusRecord, SdkRelayStatusReportRecord, SdkShutdownRecord,
     33         SdkStorageStatusRecord,
     34     },
     35 };
     36 
     37 use crate::TeraAppError;
     38 
     39 mod viewer_calendar;
     40 pub use viewer_calendar::{FfiViewerCalendarContext, TODAY_PAGE_FFI_SCHEMA_VERSION};
     41 mod calendar;
     42 pub use calendar::{FfiCalendarTiming, FfiCivilDate, TODAY_CARD_FFI_SCHEMA_VERSION};
     43 
     44 mod today_sync;
     45 pub use today_sync::{
     46     FfiTodayDiscoveryRecord, FfiTodayRelaySyncState, FfiTodaySyncRecord, FfiTodaySyncTermination,
     47     FfiTodayTargetPageSummary, FfiTodayTargetSyncRecord, FfiTodayTargetSyncState,
     48 };
     49 
     50 pub const MOBILE_FFI_SCHEMA_VERSION: u16 = 1;
     51 pub const PREPARED_MEDIA_FFI_SCHEMA_VERSION: u16 = 2;
     52 /// Upload outputs separate canonical blob references from transport destinations.
     53 pub const UPLOAD_OUTPUT_FFI_SCHEMA_VERSION: u16 = 2;
     54 const MEDIA_REFERENCE_MAX_BYTES: usize = 256;
     55 
     56 /// Final four-state trade-evidence coverage vocabulary.
     57 #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
     58 pub enum FfiTradeEvidenceCoverage {
     59     Missing,
     60     Partial,
     61     ScopeSatisfied,
     62     Unsupported,
     63 }
     64 
     65 /// Final three-state trade-evidence outcome vocabulary.
     66 #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
     67 pub enum FfiTradeEvidenceOutcome {
     68     Valid,
     69     Invalid,
     70     Indeterminate,
     71 }
     72 
     73 /// Secret-free projection of one canonical evidence manifest.
     74 #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)]
     75 pub struct FfiTradeEvidenceManifestRecord {
     76     pub schema_version: u16,
     77     pub contract_id: String,
     78     pub contract_version: u16,
     79     pub trade_id: String,
     80     pub trade_generation: String,
     81     pub observed_at_unix_s: String,
     82     pub coverage: FfiTradeEvidenceCoverage,
     83     pub evidence_policy_digest: String,
     84     pub manifest_digest: String,
     85     pub canonical_bytes_hex: String,
     86 }
     87 
     88 /// Secret-free projection of one canonical RHI evidence report.
     89 #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)]
     90 pub struct FfiRhiEvidenceReportRecord {
     91     pub schema_version: u16,
     92     pub contract_id: String,
     93     pub contract_version: u16,
     94     pub issuer_pubkey: String,
     95     pub trade_id: String,
     96     pub claim_mutation_id: String,
     97     pub outcome: FfiTradeEvidenceOutcome,
     98     pub reason_codes: Vec<String>,
     99     pub projection_digest: String,
    100     pub evidence_manifest_digest: String,
    101     pub evidence_policy_digest: String,
    102     pub observed_at_unix_s: String,
    103     pub trade_generation: String,
    104     pub statement_digest: String,
    105     pub supersedes_report_id: Option<String>,
    106     pub supersedes_event_id: Option<String>,
    107     pub canonical_content: String,
    108 }
    109 
    110 /// Unsigned typed event plan ready for host-owned signing.
    111 #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)]
    112 pub struct FfiTypedEvidenceEventPlanRecord {
    113     pub schema_version: u16,
    114     pub contract_id: String,
    115     pub kind: u32,
    116     pub author_pubkey: String,
    117     pub created_at_unix_s: String,
    118     pub expected_event_id: String,
    119     pub tags: Vec<Vec<String>>,
    120     pub content: String,
    121 }
    122 
    123 /// Signed NIP-01 event input for verified attestation admission.
    124 #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)]
    125 pub struct FfiSignedEvidenceEventRecord {
    126     pub id: String,
    127     pub author_pubkey: String,
    128     pub created_at_unix_s: u64,
    129     pub kind: u32,
    130     pub tags: Vec<Vec<String>>,
    131     pub content: String,
    132     pub signature: String,
    133 }
    134 
    135 /// Secret-free supersession projection from one verified attestation.
    136 #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)]
    137 pub struct FfiRhiEvidenceAttestationSupersessionRecord {
    138     pub report_id: String,
    139     pub event_id: String,
    140 }
    141 
    142 /// Verified final RHI evidence attestation.
    143 #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)]
    144 pub struct FfiRhiEvidenceAttestationRecord {
    145     pub schema_version: u16,
    146     pub issuer_pubkey: String,
    147     pub trade_id: String,
    148     pub claim_mutation_id: String,
    149     pub outcome: FfiTradeEvidenceOutcome,
    150     pub observed_at_unix_s: String,
    151     pub trade_generation: String,
    152     pub statement_digest: String,
    153     pub supersession: Option<FfiRhiEvidenceAttestationSupersessionRecord>,
    154     pub canonical_content: String,
    155 }
    156 
    157 #[uniffi::export]
    158 pub fn parse_trade_evidence_manifest(
    159     canonical_bytes: Vec<u8>,
    160 ) -> Result<FfiTradeEvidenceManifestRecord, TeraAppError> {
    161     let manifest = radroots_sdk::trade::parse_evidence_manifest(&canonical_bytes)
    162         .map_err(|_| TeraAppError::invalid_argument("invalid_evidence_manifest"))?;
    163     Ok(FfiTradeEvidenceManifestRecord {
    164         schema_version: MOBILE_FFI_SCHEMA_VERSION,
    165         contract_id: manifest.contract_id().to_owned(),
    166         contract_version: manifest.contract_version(),
    167         trade_id: manifest.trade_id().to_string(),
    168         trade_generation: manifest.trade_generation().get().to_string(),
    169         observed_at_unix_s: manifest.observed_at_unix_s().to_string(),
    170         coverage: manifest.coverage().into(),
    171         evidence_policy_digest: manifest.evidence_policy_digest().to_hex(),
    172         manifest_digest: manifest.digest().to_hex(),
    173         canonical_bytes_hex: hex::encode(manifest.canonical_bytes()),
    174     })
    175 }
    176 
    177 #[uniffi::export]
    178 pub fn parse_rhi_evidence_report(
    179     canonical_content: String,
    180 ) -> Result<FfiRhiEvidenceReportRecord, TeraAppError> {
    181     let report = radroots_sdk::trade::parse_rhi_evidence_report(canonical_content.as_bytes())
    182         .map_err(|_| TeraAppError::invalid_argument("invalid_evidence_report"))?;
    183     let supersession = report.supersession();
    184     Ok(FfiRhiEvidenceReportRecord {
    185         schema_version: MOBILE_FFI_SCHEMA_VERSION,
    186         contract_id: report.contract_id().to_owned(),
    187         contract_version: report.contract_version(),
    188         issuer_pubkey: report.issuer_public_key().to_hex(),
    189         trade_id: report.trade_id().to_string(),
    190         claim_mutation_id: report.claim_mutation_id().to_string(),
    191         outcome: report.outcome().into(),
    192         reason_codes: report
    193             .reason_codes()
    194             .iter()
    195             .map(|code| code.as_str().to_owned())
    196             .collect(),
    197         projection_digest: report.projection_digest().to_hex(),
    198         evidence_manifest_digest: report.evidence_manifest_digest().to_hex(),
    199         evidence_policy_digest: report.evidence_policy_digest().to_hex(),
    200         observed_at_unix_s: report.observed_at_unix_s().to_string(),
    201         trade_generation: report.trade_generation().get().to_string(),
    202         statement_digest: report.statement_digest().to_hex(),
    203         supersedes_report_id: supersession.map(|value| value.report_id().to_hex()),
    204         supersedes_event_id: supersession.map(|value| value.event_id().to_hex()),
    205         canonical_content: report.canonical_content().to_owned(),
    206     })
    207 }
    208 
    209 #[uniffi::export]
    210 pub fn prepare_rhi_evidence_attestation(
    211     canonical_content: String,
    212     created_at_unix_s: u64,
    213 ) -> Result<FfiTypedEvidenceEventPlanRecord, TeraAppError> {
    214     let report = radroots_sdk::trade::parse_rhi_evidence_report(canonical_content.as_bytes())
    215         .map_err(|_| TeraAppError::invalid_argument("invalid_evidence_report"))?;
    216     let plan = radroots_sdk::trade::prepare_rhi_evidence_attestation(&report, created_at_unix_s)
    217         .map_err(|_| TeraAppError::invalid_argument("invalid_evidence_attestation_plan"))?;
    218     Ok(FfiTypedEvidenceEventPlanRecord {
    219         schema_version: MOBILE_FFI_SCHEMA_VERSION,
    220         contract_id: plan.body().contract().contract_id().as_str().to_owned(),
    221         kind: plan.body().kind(),
    222         author_pubkey: plan.author().to_hex(),
    223         created_at_unix_s: plan.created_at().to_string(),
    224         expected_event_id: plan.expected_event_id().to_hex(),
    225         tags: plan.body().tags().to_vec(),
    226         content: plan.body().content().to_owned(),
    227     })
    228 }
    229 
    230 #[uniffi::export]
    231 pub fn validate_rhi_evidence_attestation(
    232     event: FfiSignedEvidenceEventRecord,
    233 ) -> Result<FfiRhiEvidenceAttestationRecord, TeraAppError> {
    234     let event = radroots_event::envelope::EventEnvelope::new(
    235         radroots_event::envelope::EventEnvelopeParts {
    236             id: event.id,
    237             author: event.author_pubkey,
    238             created_at: event.created_at_unix_s,
    239             kind: event.kind,
    240             tags: event.tags,
    241             content: event.content,
    242             sig: event.signature,
    243         },
    244     )
    245     .map_err(|_| TeraAppError::invalid_argument("invalid_signed_event"))?;
    246     let attestation = radroots_sdk::trade::validate_rhi_evidence_attestation(event).map_err(
    247         |error| match error {
    248             radroots_sdk::trade::EvidenceAttestationValidationError::Signature => {
    249                 TeraAppError::invalid_argument("invalid_event_signature")
    250             }
    251             radroots_sdk::trade::EvidenceAttestationValidationError::Contract => {
    252                 TeraAppError::invalid_argument("invalid_evidence_attestation")
    253             }
    254         },
    255     )?;
    256     Ok(FfiRhiEvidenceAttestationRecord {
    257         schema_version: MOBILE_FFI_SCHEMA_VERSION,
    258         issuer_pubkey: attestation.issuer().to_hex(),
    259         trade_id: attestation.trade_id().to_string(),
    260         claim_mutation_id: attestation.claim_mutation_id().to_string(),
    261         outcome: match attestation.outcome() {
    262             radroots_sdk::trade::RadrootsRhiEvidenceAttestationOutcomeV1::Valid => {
    263                 FfiTradeEvidenceOutcome::Valid
    264             }
    265             radroots_sdk::trade::RadrootsRhiEvidenceAttestationOutcomeV1::Invalid => {
    266                 FfiTradeEvidenceOutcome::Invalid
    267             }
    268             radroots_sdk::trade::RadrootsRhiEvidenceAttestationOutcomeV1::Indeterminate => {
    269                 FfiTradeEvidenceOutcome::Indeterminate
    270             }
    271         },
    272         observed_at_unix_s: attestation.observed_at_unix_s().to_string(),
    273         trade_generation: attestation.trade_generation().get().to_string(),
    274         statement_digest: hex::encode(attestation.statement_digest()),
    275         supersession: attestation.supersession().map(|value| {
    276             FfiRhiEvidenceAttestationSupersessionRecord {
    277                 report_id: hex::encode(value.report_id()),
    278                 event_id: value.event_id().to_hex(),
    279             }
    280         }),
    281         canonical_content: attestation.canonical_content().to_owned(),
    282     })
    283 }
    284 
    285 impl From<radroots_sdk::trade::RadrootsTradeEvidenceCoverageV1> for FfiTradeEvidenceCoverage {
    286     fn from(value: radroots_sdk::trade::RadrootsTradeEvidenceCoverageV1) -> Self {
    287         match value {
    288             radroots_sdk::trade::RadrootsTradeEvidenceCoverageV1::Missing => Self::Missing,
    289             radroots_sdk::trade::RadrootsTradeEvidenceCoverageV1::Partial => Self::Partial,
    290             radroots_sdk::trade::RadrootsTradeEvidenceCoverageV1::ScopeSatisfied => {
    291                 Self::ScopeSatisfied
    292             }
    293             radroots_sdk::trade::RadrootsTradeEvidenceCoverageV1::Unsupported => Self::Unsupported,
    294         }
    295     }
    296 }
    297 
    298 impl From<radroots_sdk::trade::RadrootsTradeEvidenceOutcomeV1> for FfiTradeEvidenceOutcome {
    299     fn from(value: radroots_sdk::trade::RadrootsTradeEvidenceOutcomeV1) -> Self {
    300         match value {
    301             radroots_sdk::trade::RadrootsTradeEvidenceOutcomeV1::Valid => Self::Valid,
    302             radroots_sdk::trade::RadrootsTradeEvidenceOutcomeV1::Invalid => Self::Invalid,
    303             radroots_sdk::trade::RadrootsTradeEvidenceOutcomeV1::Indeterminate => {
    304                 Self::Indeterminate
    305             }
    306         }
    307     }
    308 }
    309 
    310 #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)]
    311 pub struct FfiBuildInfoRecord {
    312     pub schema_version: u16,
    313     pub crate_name: String,
    314     pub crate_version: String,
    315     pub rustc: Option<String>,
    316     pub profile: Option<String>,
    317     pub lib_revision: Option<String>,
    318     pub consumer_revision: Option<String>,
    319     pub build_time_unix: Option<u64>,
    320 }
    321 
    322 // These exhaustive field-for-field adapters are verified by the generated API
    323 // snapshot and Swift compilation. Excluding the mechanical projection glue
    324 // keeps the coverage gate focused on validation and behavior.
    325 #[cfg_attr(coverage_nightly, coverage(off))]
    326 impl From<RuntimeBuildInfo> for FfiBuildInfoRecord {
    327     fn from(value: RuntimeBuildInfo) -> Self {
    328         Self {
    329             schema_version: MOBILE_FFI_SCHEMA_VERSION,
    330             crate_name: value.crate_name,
    331             crate_version: value.crate_version,
    332             rustc: value.rustc,
    333             profile: value.profile,
    334             lib_revision: value.lib_revision,
    335             consumer_revision: value.consumer_revision,
    336             build_time_unix: value.build_time_unix,
    337         }
    338     }
    339 }
    340 
    341 #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)]
    342 pub struct FfiAppPlatformRecord {
    343     pub schema_version: u16,
    344     pub platform: Option<String>,
    345     pub bundle_id: Option<String>,
    346     pub version: Option<String>,
    347     pub build_number: Option<String>,
    348     pub build_sha: Option<String>,
    349 }
    350 
    351 #[cfg_attr(coverage_nightly, coverage(off))]
    352 impl From<AppInfoPlatform> for FfiAppPlatformRecord {
    353     fn from(value: AppInfoPlatform) -> Self {
    354         Self {
    355             schema_version: MOBILE_FFI_SCHEMA_VERSION,
    356             platform: value.platform,
    357             bundle_id: value.bundle_id,
    358             version: value.version,
    359             build_number: value.build_number,
    360             build_sha: value.build_sha,
    361         }
    362     }
    363 }
    364 
    365 #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)]
    366 pub struct FfiAppInfoRecord {
    367     pub schema_version: u16,
    368     pub build: FfiBuildInfoRecord,
    369     pub started_unix_ms: i64,
    370     pub uptime_millis: i64,
    371     pub shutting_down: bool,
    372     pub platform: Option<FfiAppPlatformRecord>,
    373 }
    374 
    375 #[cfg_attr(coverage_nightly, coverage(off))]
    376 impl From<AppInfo> for FfiAppInfoRecord {
    377     fn from(value: AppInfo) -> Self {
    378         Self {
    379             schema_version: MOBILE_FFI_SCHEMA_VERSION,
    380             build: value.build.into(),
    381             started_unix_ms: value.started_unix_ms,
    382             uptime_millis: value.uptime_millis,
    383             shutting_down: value.shutting_down,
    384             platform: value.platform.map(Into::into),
    385         }
    386     }
    387 }
    388 
    389 #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)]
    390 pub struct FfiRuntimeInfoRecord {
    391     pub schema_version: u16,
    392     pub app: FfiAppInfoRecord,
    393     pub sdk: FfiBuildInfoRecord,
    394     pub sdk_closed: bool,
    395 }
    396 
    397 #[cfg_attr(coverage_nightly, coverage(off))]
    398 impl From<RuntimeInfo> for FfiRuntimeInfoRecord {
    399     fn from(value: RuntimeInfo) -> Self {
    400         Self {
    401             schema_version: MOBILE_FFI_SCHEMA_VERSION,
    402             app: value.app.into(),
    403             sdk: value.sdk.into(),
    404             sdk_closed: value.sdk_closed,
    405         }
    406     }
    407 }
    408 
    409 #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)]
    410 pub struct FfiIdentityStatusRecord {
    411     pub schema_version: u16,
    412     pub public_key: String,
    413     pub host_signer_configured: bool,
    414 }
    415 
    416 #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
    417 pub enum FfiTodayCardType {
    418     Update,
    419     PhotoUpdate,
    420     Ask,
    421     Event,
    422     FoodAvailability,
    423 }
    424 
    425 #[cfg_attr(coverage_nightly, coverage(off))]
    426 impl From<TodayCardType> for FfiTodayCardType {
    427     fn from(value: TodayCardType) -> Self {
    428         match value {
    429             TodayCardType::Update => Self::Update,
    430             TodayCardType::PhotoUpdate => Self::PhotoUpdate,
    431             TodayCardType::Ask => Self::Ask,
    432             TodayCardType::Event => Self::Event,
    433             TodayCardType::FoodAvailability => Self::FoodAvailability,
    434         }
    435     }
    436 }
    437 
    438 #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
    439 pub enum FfiAddCommandType {
    440     CreateUpdate,
    441     CreatePhotoUpdate,
    442     CreateAsk,
    443     CreateEvent,
    444     CreateFoodAvailability,
    445 }
    446 
    447 #[cfg_attr(coverage_nightly, coverage(off))]
    448 impl From<AddCommandType> for FfiAddCommandType {
    449     fn from(value: AddCommandType) -> Self {
    450         match value {
    451             AddCommandType::CreateUpdate => Self::CreateUpdate,
    452             AddCommandType::CreatePhotoUpdate => Self::CreatePhotoUpdate,
    453             AddCommandType::CreateAsk => Self::CreateAsk,
    454             AddCommandType::CreateEvent => Self::CreateEvent,
    455             AddCommandType::CreateFoodAvailability => Self::CreateFoodAvailability,
    456         }
    457     }
    458 }
    459 
    460 #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)]
    461 pub struct FfiCardAddParityRecord {
    462     pub schema_version: u16,
    463     pub card_type: FfiTodayCardType,
    464     pub command_type: FfiAddCommandType,
    465 }
    466 
    467 #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)]
    468 pub struct FfiLocalNetworkRecord {
    469     pub schema_version: u16,
    470     pub id: String,
    471     pub label: String,
    472     pub relay_urls: Vec<String>,
    473     pub locality: Option<String>,
    474     pub followed_authors: Vec<String>,
    475     pub generation: u64,
    476 }
    477 
    478 impl TryFrom<FfiLocalNetworkRecord> for LocalNetwork {
    479     type Error = TeraAppError;
    480 
    481     fn try_from(value: FfiLocalNetworkRecord) -> Result<Self, Self::Error> {
    482         value.try_into_with_relay_policy(LocalNetworkRelayPolicy::Public)
    483     }
    484 }
    485 
    486 impl FfiLocalNetworkRecord {
    487     pub(crate) fn try_into_with_relay_policy(
    488         self,
    489         relay_policy: LocalNetworkRelayPolicy,
    490     ) -> Result<LocalNetwork, TeraAppError> {
    491         require_schema(self.schema_version)?;
    492         LocalNetwork::new_for_relay_policy(
    493             self.id,
    494             self.label,
    495             self.relay_urls,
    496             self.locality,
    497             self.followed_authors,
    498             self.generation,
    499             relay_policy,
    500         )
    501         .map_err(|_| TeraAppError::invalid_argument("invalid_local_network"))
    502     }
    503 }
    504 
    505 #[cfg_attr(coverage_nightly, coverage(off))]
    506 impl From<LocalNetwork> for FfiLocalNetworkRecord {
    507     fn from(value: LocalNetwork) -> Self {
    508         Self {
    509             schema_version: MOBILE_FFI_SCHEMA_VERSION,
    510             id: value.id.into(),
    511             label: value.label,
    512             relay_urls: value.relay_urls,
    513             locality: value.locality,
    514             followed_authors: value.followed_authors,
    515             generation: value.generation,
    516         }
    517     }
    518 }
    519 
    520 #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
    521 pub enum FfiMediaVerificationState {
    522     Pending,
    523     Verified,
    524     Failed,
    525     Unavailable,
    526 }
    527 
    528 #[cfg_attr(coverage_nightly, coverage(off))]
    529 impl From<&Phase1InboundMediaState> for FfiMediaVerificationState {
    530     fn from(value: &Phase1InboundMediaState) -> Self {
    531         match value {
    532             Phase1InboundMediaState::Pending(_) => Self::Pending,
    533             Phase1InboundMediaState::Verified(_) => Self::Verified,
    534             Phase1InboundMediaState::Failed(_) => Self::Failed,
    535             Phase1InboundMediaState::Unavailable => Self::Unavailable,
    536         }
    537     }
    538 }
    539 
    540 #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)]
    541 pub struct FfiMediaReferenceRecord {
    542     pub schema_version: u16,
    543     pub reference_fingerprint: String,
    544     pub url: String,
    545     pub sha256: Option<String>,
    546     pub media_type: Option<String>,
    547     pub width: Option<u32>,
    548     pub height: Option<u32>,
    549     pub byte_size: Option<u64>,
    550     pub alt: Option<String>,
    551     pub verification: FfiMediaVerificationState,
    552 }
    553 
    554 #[cfg_attr(coverage_nightly, coverage(off))]
    555 impl From<MediaReference> for FfiMediaReferenceRecord {
    556     fn from(value: MediaReference) -> Self {
    557         let structural = value.structural();
    558         Self {
    559             schema_version: MOBILE_FFI_SCHEMA_VERSION,
    560             reference_fingerprint: hex::encode(structural.fingerprint()),
    561             url: structural.source_url().to_owned(),
    562             sha256: structural.expected_sha256().map(str::to_owned),
    563             media_type: structural.expected_media_type().map(str::to_owned),
    564             width: structural.expected_width(),
    565             height: structural.expected_height(),
    566             byte_size: structural.expected_byte_size(),
    567             alt: structural.alt().map(str::to_owned),
    568             verification: value.retrieval().into(),
    569         }
    570     }
    571 }
    572 
    573 #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)]
    574 pub struct FfiProfileRecord {
    575     pub schema_version: u16,
    576     pub author_public_key: String,
    577     pub name: Option<String>,
    578     pub display_name: Option<String>,
    579     pub about: Option<String>,
    580     pub picture: Option<FfiMediaReferenceRecord>,
    581     pub banner: Option<FfiMediaReferenceRecord>,
    582     pub nip05: Option<String>,
    583     pub website: Option<String>,
    584     pub lightning_address: Option<String>,
    585 }
    586 
    587 #[cfg_attr(coverage_nightly, coverage(off))]
    588 impl From<ProfileSummary> for FfiProfileRecord {
    589     fn from(value: ProfileSummary) -> Self {
    590         Self {
    591             schema_version: MOBILE_FFI_SCHEMA_VERSION,
    592             author_public_key: value.author_pubkey,
    593             name: value.name,
    594             display_name: value.display_name,
    595             about: value.about,
    596             picture: value.picture.map(Into::into),
    597             banner: value.banner.map(Into::into),
    598             nip05: value.nip05,
    599             website: value.website,
    600             lightning_address: value.lightning_address,
    601         }
    602     }
    603 }
    604 
    605 #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
    606 pub enum FfiThreadProfile {
    607     Profile,
    608     Reply,
    609     Comment,
    610     Deletion,
    611 }
    612 
    613 #[cfg_attr(coverage_nightly, coverage(off))]
    614 impl From<SupportingProfile> for FfiThreadProfile {
    615     fn from(value: SupportingProfile) -> Self {
    616         match value {
    617             SupportingProfile::Profile => Self::Profile,
    618             SupportingProfile::Reply => Self::Reply,
    619             SupportingProfile::Comment => Self::Comment,
    620             SupportingProfile::Deletion => Self::Deletion,
    621         }
    622     }
    623 }
    624 
    625 #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)]
    626 pub struct FfiThreadEntryRecord {
    627     pub schema_version: u16,
    628     pub event_id: String,
    629     pub author_public_key: String,
    630     pub content: String,
    631     pub authored_at_unix_s: u64,
    632     pub profile: FfiThreadProfile,
    633     pub root: String,
    634     pub parent_event_id: String,
    635     pub author_profile: Option<FfiProfileRecord>,
    636 }
    637 
    638 #[cfg_attr(coverage_nightly, coverage(off))]
    639 impl From<ThreadEntry> for FfiThreadEntryRecord {
    640     fn from(value: ThreadEntry) -> Self {
    641         Self {
    642             schema_version: MOBILE_FFI_SCHEMA_VERSION,
    643             event_id: value.event_id,
    644             author_public_key: value.author_pubkey,
    645             content: value.content,
    646             authored_at_unix_s: value.authored_at,
    647             profile: value.reference.profile.into(),
    648             root: value.reference.root,
    649             parent_event_id: value.reference.parent_event_id,
    650             author_profile: value.author_profile.map(Into::into),
    651         }
    652     }
    653 }
    654 
    655 #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
    656 pub enum FfiCardLifecycleState {
    657     Active,
    658     Sold,
    659     Past,
    660 }
    661 
    662 #[cfg_attr(coverage_nightly, coverage(off))]
    663 impl From<CardLifecycleState> for FfiCardLifecycleState {
    664     fn from(value: CardLifecycleState) -> Self {
    665         match value {
    666             CardLifecycleState::Active => Self::Active,
    667             CardLifecycleState::Sold => Self::Sold,
    668             CardLifecycleState::Past => Self::Past,
    669         }
    670     }
    671 }
    672 
    673 #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)]
    674 pub struct FfiTodayCardRecord {
    675     pub schema_version: u16,
    676     pub card_id: String,
    677     pub card_type: FfiTodayCardType,
    678     pub source_event_id: String,
    679     pub source_address: Option<String>,
    680     pub author_public_key: String,
    681     pub contract_id: String,
    682     pub title: Option<String>,
    683     pub content: String,
    684     pub authored_at_unix_s: u64,
    685     pub effective_at_unix_s: u64,
    686     pub calendar_timing: Option<FfiCalendarTiming>,
    687     pub location: Option<String>,
    688     pub price_amount: Option<String>,
    689     pub price_currency: Option<String>,
    690     pub price_unit: Option<String>,
    691     pub quantity: Option<String>,
    692     pub food_summary: Option<String>,
    693     pub food_published_at_unix_s: Option<u64>,
    694     pub food_status: Option<String>,
    695     pub context_rank: u8,
    696     pub inclusion_reason: String,
    697     pub media: Vec<FfiMediaReferenceRecord>,
    698     pub lifecycle: FfiCardLifecycleState,
    699     pub rank_digest: Option<String>,
    700     pub author_profile: Option<FfiProfileRecord>,
    701     pub thread: Vec<FfiThreadEntryRecord>,
    702     pub local_source_draft_id: Option<String>,
    703     pub local_operation_id: Option<String>,
    704     pub local_operation_state: Option<String>,
    705 }
    706 
    707 #[cfg_attr(coverage_nightly, coverage(off))]
    708 impl From<TodayCard> for FfiTodayCardRecord {
    709     fn from(value: TodayCard) -> Self {
    710         let card = value.card;
    711         Self {
    712             schema_version: TODAY_CARD_FFI_SCHEMA_VERSION,
    713             card_id: card.card_id.to_hex(),
    714             card_type: card.card_type.into(),
    715             source_event_id: card.source_event_id,
    716             source_address: card.source_address,
    717             author_public_key: card.author_pubkey,
    718             contract_id: card.contract_id,
    719             title: card.title,
    720             content: card.content,
    721             authored_at_unix_s: card.authored_at,
    722             effective_at_unix_s: card.effective_at,
    723             calendar_timing: card.calendar_timing.map(Into::into),
    724             location: card.location,
    725             price_amount: card.price_amount,
    726             price_currency: card.price_currency,
    727             price_unit: card.price_unit,
    728             quantity: card.quantity,
    729             food_summary: card.food_summary,
    730             food_published_at_unix_s: card.food_published_at,
    731             food_status: card.food_status,
    732             context_rank: card.context_rank.value(),
    733             inclusion_reason: card.inclusion_reason,
    734             media: card.media.into_iter().map(Into::into).collect(),
    735             lifecycle: card.lifecycle.into(),
    736             rank_digest: card.rank.map(|rank| rank.digest_hex()),
    737             author_profile: value.author_profile.map(Into::into),
    738             thread: value.thread.into_iter().map(Into::into).collect(),
    739             local_source_draft_id: value
    740                 .local_overlay
    741                 .as_ref()
    742                 .and_then(|overlay| overlay.source_draft_id.clone()),
    743             local_operation_id: value
    744                 .local_overlay
    745                 .as_ref()
    746                 .map(|overlay| overlay.operation_id.clone()),
    747             local_operation_state: value.local_overlay.map(|overlay| overlay.state),
    748         }
    749     }
    750 }
    751 
    752 #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)]
    753 pub struct FfiTodayPageRecord {
    754     pub calendar: FfiViewerCalendarContext,
    755     pub projection_generation: u64,
    756     pub schema_version: u16,
    757     pub as_of_unix_s: u64,
    758     pub items: Vec<FfiTodayCardRecord>,
    759     pub next_cursor: Option<String>,
    760 }
    761 
    762 #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
    763 pub enum FfiTodayProjectionUpdate {
    764     Incremental,
    765     Rebuild,
    766 }
    767 
    768 #[cfg_attr(coverage_nightly, coverage(off))]
    769 impl From<FfiTodayProjectionUpdate> for TodayProjectionUpdate {
    770     fn from(value: FfiTodayProjectionUpdate) -> Self {
    771         match value {
    772             FfiTodayProjectionUpdate::Incremental => Self::Incremental,
    773             FfiTodayProjectionUpdate::Rebuild => Self::Rebuild,
    774         }
    775     }
    776 }
    777 
    778 #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)]
    779 pub struct FfiTodayRefreshRecord {
    780     pub schema_version: u16,
    781     pub update: FfiTodayProjectionUpdate,
    782     pub source_events: u64,
    783     pub visible_cards: u64,
    784     pub profiles: u64,
    785     pub thread_entries: u64,
    786     pub content_generation: u64,
    787     pub changed: bool,
    788 }
    789 
    790 #[cfg_attr(coverage_nightly, coverage(off))]
    791 impl From<TodayRefreshReceipt> for FfiTodayRefreshRecord {
    792     fn from(value: TodayRefreshReceipt) -> Self {
    793         Self {
    794             schema_version: MOBILE_FFI_SCHEMA_VERSION,
    795             update: match value.update {
    796                 TodayProjectionUpdate::Incremental => FfiTodayProjectionUpdate::Incremental,
    797                 TodayProjectionUpdate::Rebuild => FfiTodayProjectionUpdate::Rebuild,
    798             },
    799             source_events: value.source_events,
    800             visible_cards: value.visible_cards,
    801             profiles: value.profiles,
    802             thread_entries: value.thread_entries,
    803             content_generation: value.content_generation,
    804             changed: value.changed,
    805         }
    806     }
    807 }
    808 
    809 #[cfg_attr(coverage_nightly, coverage(off))]
    810 impl From<TodayPage> for FfiTodayPageRecord {
    811     fn from(value: TodayPage) -> Self {
    812         Self {
    813             calendar: value.calendar.into(),
    814             schema_version: TODAY_PAGE_FFI_SCHEMA_VERSION,
    815             projection_generation: value.projection_generation,
    816             as_of_unix_s: value.as_of,
    817             items: value.items.into_iter().map(Into::into).collect(),
    818             next_cursor: value.next_cursor,
    819         }
    820     }
    821 }
    822 
    823 #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
    824 pub enum FfiSearchResultType {
    825     Card,
    826     Profile,
    827 }
    828 
    829 #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)]
    830 pub struct FfiSearchResultRecord {
    831     pub schema_version: u16,
    832     pub result_type: FfiSearchResultType,
    833     pub stable_id: String,
    834     pub card: Option<FfiTodayCardRecord>,
    835     pub profile: Option<FfiProfileRecord>,
    836 }
    837 
    838 #[cfg_attr(coverage_nightly, coverage(off))]
    839 impl From<SearchResult> for FfiSearchResultRecord {
    840     fn from(value: SearchResult) -> Self {
    841         Self {
    842             schema_version: MOBILE_FFI_SCHEMA_VERSION,
    843             result_type: match value.result_type {
    844                 SearchResultType::Card => FfiSearchResultType::Card,
    845                 SearchResultType::Profile => FfiSearchResultType::Profile,
    846             },
    847             stable_id: value.stable_id,
    848             card: value.card.map(Into::into),
    849             profile: value.profile.map(Into::into),
    850         }
    851     }
    852 }
    853 
    854 #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)]
    855 pub struct FfiMeRecord {
    856     pub schema_version: u16,
    857     pub public_key: String,
    858     pub profile: Option<FfiProfileRecord>,
    859     pub cards: Vec<FfiTodayCardRecord>,
    860 }
    861 
    862 #[cfg_attr(coverage_nightly, coverage(off))]
    863 impl From<MeSnapshot> for FfiMeRecord {
    864     fn from(value: MeSnapshot) -> Self {
    865         Self {
    866             schema_version: MOBILE_FFI_SCHEMA_VERSION,
    867             public_key: value.public_key,
    868             profile: value.profile.map(Into::into),
    869             cards: value.cards.into_iter().map(Into::into).collect(),
    870         }
    871     }
    872 }
    873 
    874 #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
    875 pub enum FfiAddFieldKind {
    876     Text,
    877     MultilineText,
    878     Date,
    879     DateTime,
    880     Decimal,
    881     Choice,
    882     Location,
    883     Media,
    884 }
    885 
    886 #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)]
    887 pub struct FfiAddFieldRecord {
    888     pub schema_version: u16,
    889     pub id: String,
    890     pub label: String,
    891     pub kind: FfiAddFieldKind,
    892     pub required: bool,
    893     pub choices: Vec<String>,
    894     pub max_bytes: Option<u64>,
    895     pub max_items: Option<u16>,
    896 }
    897 
    898 #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)]
    899 pub struct FfiAddSchemaRecord {
    900     pub schema_version: u16,
    901     pub command_type: FfiAddCommandType,
    902     pub label: String,
    903     pub fields: Vec<FfiAddFieldRecord>,
    904 }
    905 
    906 pub fn add_schemas() -> Vec<FfiAddSchemaRecord> {
    907     use FfiAddCommandType as Command;
    908     use FfiAddFieldKind as Kind;
    909 
    910     let field =
    911         |id: &str, label: &str, kind, required, choices: &[&str], max_bytes| FfiAddFieldRecord {
    912             schema_version: MOBILE_FFI_SCHEMA_VERSION,
    913             id: id.to_owned(),
    914             label: label.to_owned(),
    915             kind,
    916             required,
    917             choices: choices.iter().map(|value| (*value).to_owned()).collect(),
    918             max_bytes,
    919             max_items: None,
    920         };
    921     let media_field = |label: &str, required: bool, max_items| FfiAddFieldRecord {
    922         max_items: Some(max_items),
    923         ..field(
    924             "media",
    925             label,
    926             Kind::Media,
    927             required,
    928             &[],
    929             Some(MEDIA_FILE_MAX_BYTES),
    930         )
    931     };
    932     vec![
    933         FfiAddSchemaRecord {
    934             schema_version: MOBILE_FFI_SCHEMA_VERSION,
    935             command_type: Command::CreateUpdate,
    936             label: "Update".to_owned(),
    937             fields: vec![field(
    938                 "content",
    939                 "Update",
    940                 Kind::MultilineText,
    941                 true,
    942                 &[],
    943                 Some(65_535),
    944             )],
    945         },
    946         FfiAddSchemaRecord {
    947             schema_version: MOBILE_FFI_SCHEMA_VERSION,
    948             command_type: Command::CreatePhotoUpdate,
    949             label: "Photo update".to_owned(),
    950             fields: vec![
    951                 field(
    952                     "content",
    953                     "Update",
    954                     Kind::MultilineText,
    955                     true,
    956                     &[],
    957                     Some(65_535),
    958                 ),
    959                 media_field("Photos", true, 20),
    960             ],
    961         },
    962         FfiAddSchemaRecord {
    963             schema_version: MOBILE_FFI_SCHEMA_VERSION,
    964             command_type: Command::CreateAsk,
    965             label: "Ask".to_owned(),
    966             fields: vec![
    967                 field(
    968                     "content",
    969                     "Question",
    970                     Kind::MultilineText,
    971                     true,
    972                     &[],
    973                     Some(65_535),
    974                 ),
    975                 media_field("Photos", false, 20),
    976             ],
    977         },
    978         FfiAddSchemaRecord {
    979             schema_version: MOBILE_FFI_SCHEMA_VERSION,
    980             command_type: Command::CreateEvent,
    981             label: "Event".to_owned(),
    982             fields: vec![
    983                 field("identifier", "Identifier", Kind::Text, true, &[], Some(256)),
    984                 field("title", "Title", Kind::Text, true, &[], Some(256)),
    985                 field(
    986                     "content",
    987                     "Description",
    988                     Kind::MultilineText,
    989                     false,
    990                     &[],
    991                     Some(65_535),
    992                 ),
    993                 field("event_start", "Starts", Kind::DateTime, true, &[], None),
    994                 field("event_end", "Ends", Kind::DateTime, false, &[], None),
    995                 field(
    996                     "location",
    997                     "Location",
    998                     Kind::Location,
    999                     false,
   1000                     &[],
   1001                     Some(256),
   1002                 ),
   1003                 media_field("Photo", false, 1),
   1004             ],
   1005         },
   1006         FfiAddSchemaRecord {
   1007             schema_version: MOBILE_FFI_SCHEMA_VERSION,
   1008             command_type: Command::CreateFoodAvailability,
   1009             label: "Food availability".to_owned(),
   1010             fields: vec![
   1011                 field("identifier", "Identifier", Kind::Text, true, &[], Some(256)),
   1012                 field("title", "Food", Kind::Text, true, &[], Some(256)),
   1013                 field("summary", "Summary", Kind::Text, true, &[], Some(256)),
   1014                 field(
   1015                     "content",
   1016                     "Details",
   1017                     Kind::MultilineText,
   1018                     true,
   1019                     &[],
   1020                     Some(65_535),
   1021                 ),
   1022                 field(
   1023                     "location",
   1024                     "Pickup location",
   1025                     Kind::Location,
   1026                     true,
   1027                     &[],
   1028                     Some(256),
   1029                 ),
   1030                 field("price_amount", "Price", Kind::Decimal, true, &[], Some(64)),
   1031                 field("currency", "Currency", Kind::Choice, true, &[], Some(3)),
   1032                 field(
   1033                     "unit",
   1034                     "Unit",
   1035                     Kind::Choice,
   1036                     true,
   1037                     &[
   1038                         "g", "kg", "lb", "oz", "each", "dozen", "bunch", "punnet", "bag", "basket",
   1039                     ],
   1040                     None,
   1041                 ),
   1042                 field(
   1043                     "quantity",
   1044                     "Available quantity",
   1045                     Kind::Decimal,
   1046                     false,
   1047                     &[],
   1048                     Some(64),
   1049                 ),
   1050                 media_field("Photos", false, 20),
   1051             ],
   1052         },
   1053     ]
   1054 }
   1055 
   1056 #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
   1057 pub enum FfiEventTimingKind {
   1058     AllDay,
   1059     Timed,
   1060 }
   1061 
   1062 #[derive(Clone, Debug, uniffi::Record)]
   1063 pub struct FfiPreparedMediaInput {
   1064     pub schema_version: u16,
   1065     pub opaque_reference: String,
   1066     pub file: std::sync::Arc<FfiMediaFile>,
   1067     pub sha256: String,
   1068     pub media_type: String,
   1069     pub byte_size: u64,
   1070     pub width: u32,
   1071     pub height: u32,
   1072     pub alt: String,
   1073     pub prepared_at_unix_s: u64,
   1074 }
   1075 
   1076 #[derive(Clone, Debug, uniffi::Record)]
   1077 pub struct FfiAddDraftInput {
   1078     pub schema_version: u16,
   1079     pub command_type: FfiAddCommandType,
   1080     pub content: String,
   1081     pub identifier: Option<String>,
   1082     pub title: Option<String>,
   1083     pub summary: Option<String>,
   1084     pub location: Option<String>,
   1085     pub event_timing: Option<FfiEventTimingKind>,
   1086     pub event_start_date: Option<String>,
   1087     pub event_end_date: Option<String>,
   1088     pub event_start_unix_s: Option<u64>,
   1089     pub event_end_unix_s: Option<u64>,
   1090     pub event_timezone: Option<String>,
   1091     pub price_amount: Option<String>,
   1092     pub currency: Option<String>,
   1093     pub unit: Option<String>,
   1094     pub quantity: Option<String>,
   1095     pub food_published_at_unix_s: Option<u64>,
   1096     pub food_status: Option<String>,
   1097     pub media: Vec<FfiPreparedMediaInput>,
   1098 }
   1099 
   1100 #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)]
   1101 pub struct FfiRetractionDraftInput {
   1102     pub schema_version: u16,
   1103     pub command_type: FfiAddCommandType,
   1104     pub target_card_id: String,
   1105     pub target_event_id: String,
   1106     pub target_kind: u32,
   1107     pub target_address: Option<String>,
   1108     pub reason: String,
   1109 }
   1110 
   1111 #[derive(Clone, Debug, uniffi::Record)]
   1112 pub struct FfiBlossomUploadInput {
   1113     pub schema_version: u16,
   1114     pub draft_id: String,
   1115     pub expected_revision: u64,
   1116     pub media: FfiPreparedMediaInput,
   1117     pub authorization_content: String,
   1118     pub authorization_created_at_unix_s: u64,
   1119     pub authorization_lifetime_seconds: u64,
   1120     pub operation_id: String,
   1121     pub artifact_id: String,
   1122     pub signing_deadline_unix_ms: u64,
   1123     pub signing_cancellation: FfiCancellationPolicy,
   1124     pub verified_at_unix_ms: u64,
   1125     pub updated_at_unix_ms: u64,
   1126 }
   1127 
   1128 /// Minimal host input for a Rust-planned exact-byte upload attempt.
   1129 #[derive(Clone, Debug, uniffi::Record)]
   1130 pub struct FfiBlossomUploadIntent {
   1131     pub schema_version: u16,
   1132     pub draft_id: String,
   1133     pub expected_revision: u64,
   1134     pub media: FfiPreparedMediaInput,
   1135 }
   1136 
   1137 /// Secret-bearing native job. Hosts may use the authorization header for the
   1138 /// immediate OS request but must not persist it in application metadata.
   1139 #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)]
   1140 pub struct FfiNativeUploadJobRecord {
   1141     pub schema_version: u16,
   1142     pub operation_id: String,
   1143     pub draft: FfiDraftStatusRecord,
   1144     pub remote_url: String,
   1145     pub upload_url: String,
   1146     pub authorization_header: String,
   1147     pub expected_sha256: String,
   1148     pub media_type: String,
   1149     pub byte_size: u64,
   1150 }
   1151 
   1152 #[derive(Clone, Debug, uniffi::Record)]
   1153 pub struct FfiNativeUploadCompletionInput {
   1154     pub schema_version: u16,
   1155     pub draft_id: String,
   1156     pub expected_revision: u64,
   1157     pub media: FfiPreparedMediaInput,
   1158     pub status_code: u16,
   1159     pub response_media_type: Option<String>,
   1160     pub response_content_encoding: Option<String>,
   1161     pub response_body: Vec<u8>,
   1162 }
   1163 
   1164 impl FfiAddDraftInput {
   1165     pub(crate) fn command_and_media(
   1166         self,
   1167         authored_at_unix_s: u64,
   1168         blossom: Option<&radroots_sdk::transport::BlossomSlot>,
   1169     ) -> Result<(Phase1AddCommand, Vec<Phase1MediaPrerequisite>), TeraAppError> {
   1170         self.command_media_and_form(authored_at_unix_s, blossom)
   1171             .map(|(command, media, _)| (command, media))
   1172     }
   1173 
   1174     pub(crate) fn command_media_and_form(
   1175         self,
   1176         authored_at_unix_s: u64,
   1177         blossom: Option<&radroots_sdk::transport::BlossomSlot>,
   1178     ) -> Result<
   1179         (
   1180             Phase1AddCommand,
   1181             Vec<Phase1MediaPrerequisite>,
   1182             Phase1DraftFormSnapshot,
   1183         ),
   1184         TeraAppError,
   1185     > {
   1186         require_schema(self.schema_version)?;
   1187         if authored_at_unix_s == 0 || self.media.len() > 20 {
   1188             return Err(TeraAppError::invalid_argument("invalid_add_draft"));
   1189         }
   1190         let prepared = self
   1191             .media
   1192             .iter()
   1193             .cloned()
   1194             .map(PreparedMedia::try_from)
   1195             .map(|media| {
   1196                 media.and_then(|media| {
   1197                     let blossom = blossom
   1198                         .ok_or_else(|| TeraAppError::invalid_argument("blossom_not_configured"))?;
   1199                     media.bind(blossom)
   1200                 })
   1201             })
   1202             .collect::<Result<Vec<BoundPreparedMedia>, _>>()?;
   1203         let prerequisites = prepared
   1204             .iter()
   1205             .map(|value| {
   1206                 Phase1MediaPrerequisite::new(
   1207                     value.media.opaque_reference.clone(),
   1208                     &value.descriptor,
   1209                 )
   1210             })
   1211             .collect::<Result<Vec<_>, _>>()
   1212             .map_err(|_| TeraAppError::invalid_argument("invalid_media_reference"))?;
   1213         let post_images = prepared
   1214             .iter()
   1215             .map(BoundPreparedMedia::post_image)
   1216             .collect::<Result<Vec<_>, _>>()?;
   1217         let form = self.form_snapshot(&prepared);
   1218         let command = match self.command_type {
   1219             FfiAddCommandType::CreateUpdate => {
   1220                 reject_media(&prepared)?;
   1221                 Phase1AddCommand::CreateUpdate(
   1222                     CreateUpdate::new(self.content)
   1223                         .map_err(|_| TeraAppError::invalid_argument("invalid_update"))?,
   1224                 )
   1225             }
   1226             FfiAddCommandType::CreatePhotoUpdate => Phase1AddCommand::CreatePhotoUpdate(
   1227                 CreatePhotoUpdate::new(
   1228                     content_with_media_references(self.content, &prepared)?,
   1229                     post_images,
   1230                 )
   1231                 .map_err(|_| TeraAppError::invalid_argument("invalid_photo_update"))?,
   1232             ),
   1233             FfiAddCommandType::CreateAsk => Phase1AddCommand::CreateAsk(
   1234                 CreateAsk::new(
   1235                     content_with_media_references(self.content, &prepared)?,
   1236                     post_images,
   1237                 )
   1238                 .map_err(|_| TeraAppError::invalid_argument("invalid_ask"))?,
   1239             ),
   1240             FfiAddCommandType::CreateEvent => {
   1241                 Phase1AddCommand::CreateEvent(event_command(&self, prepared.first())?)
   1242             }
   1243             FfiAddCommandType::CreateFoodAvailability => Phase1AddCommand::CreateFoodAvailability(
   1244                 food_command(self, authored_at_unix_s, &prepared)?,
   1245             ),
   1246         };
   1247         Ok((command, prerequisites, form))
   1248     }
   1249 
   1250     fn form_snapshot(&self, prepared: &[BoundPreparedMedia]) -> Phase1DraftFormSnapshot {
   1251         Phase1DraftFormSnapshot {
   1252             command_type: match self.command_type {
   1253                 FfiAddCommandType::CreateUpdate => AddCommandType::CreateUpdate,
   1254                 FfiAddCommandType::CreatePhotoUpdate => AddCommandType::CreatePhotoUpdate,
   1255                 FfiAddCommandType::CreateAsk => AddCommandType::CreateAsk,
   1256                 FfiAddCommandType::CreateEvent => AddCommandType::CreateEvent,
   1257                 FfiAddCommandType::CreateFoodAvailability => AddCommandType::CreateFoodAvailability,
   1258             },
   1259             content: self.content.clone(),
   1260             identifier: self.identifier.clone(),
   1261             title: self.title.clone(),
   1262             summary: self.summary.clone(),
   1263             location: self.location.clone(),
   1264             event_timing: self.event_timing.map(|value| match value {
   1265                 FfiEventTimingKind::AllDay => Phase1DraftEventTiming::AllDay,
   1266                 FfiEventTimingKind::Timed => Phase1DraftEventTiming::Timed,
   1267             }),
   1268             event_start_date: self.event_start_date.clone(),
   1269             event_end_date: self.event_end_date.clone(),
   1270             event_start_unix_s: self.event_start_unix_s,
   1271             event_end_unix_s: self.event_end_unix_s,
   1272             event_timezone: self.event_timezone.clone(),
   1273             price_amount: self.price_amount.clone(),
   1274             currency: self.currency.clone(),
   1275             unit: self.unit.clone(),
   1276             quantity: self.quantity.clone(),
   1277             food_published_at_unix_s: self.food_published_at_unix_s,
   1278             food_status: self.food_status.clone(),
   1279             media: prepared
   1280                 .iter()
   1281                 .map(|value| Phase1DraftMediaSnapshot {
   1282                     opaque_reference: value.media.opaque_reference.clone(),
   1283                     url: value.descriptor.url().as_str().to_owned(),
   1284                     sha256: value.media.sha256.to_hex(),
   1285                     media_type: value.media.media_type.as_str().to_owned(),
   1286                     byte_size: value.media.byte_size,
   1287                     width: value.media.width,
   1288                     height: value.media.height,
   1289                     alt: value.media.alt.clone(),
   1290                     prepared_at_unix_s: value.media.prepared_at_unix_s,
   1291                 })
   1292                 .collect(),
   1293         }
   1294     }
   1295 }
   1296 
   1297 pub(crate) struct PreparedMedia {
   1298     opaque_reference: String,
   1299     sha256: Sha256,
   1300     byte_size: u64,
   1301     prepared_at_unix_s: u64,
   1302     bytes: std::sync::Arc<[u8]>,
   1303     media_type: MediaType,
   1304     width: u32,
   1305     height: u32,
   1306     alt: String,
   1307 }
   1308 
   1309 struct BoundPreparedMedia {
   1310     media: PreparedMedia,
   1311     descriptor: radroots_blossom::ByteVerifiedDescriptor,
   1312 }
   1313 
   1314 impl TryFrom<FfiPreparedMediaInput> for PreparedMedia {
   1315     type Error = TeraAppError;
   1316 
   1317     fn try_from(value: FfiPreparedMediaInput) -> Result<Self, Self::Error> {
   1318         if value.schema_version != PREPARED_MEDIA_FFI_SCHEMA_VERSION {
   1319             return Err(TeraAppError::invalid_argument("unsupported_schema_version"));
   1320         }
   1321         if !opaque_media_reference_is_valid(&value.opaque_reference)
   1322             || value.byte_size == 0
   1323             || value.byte_size > MEDIA_FILE_MAX_BYTES
   1324             || value.width == 0
   1325             || value.height == 0
   1326             || value.prepared_at_unix_s == 0
   1327             || value.alt.trim().is_empty()
   1328             || value.alt.len() > 1_024
   1329         {
   1330             return Err(TeraAppError::invalid_argument("invalid_media_reference"));
   1331         }
   1332         let bytes = value.file.read(value.byte_size)?;
   1333         let media_type = MediaType::parse(&value.media_type)
   1334             .map_err(|_| TeraAppError::invalid_argument("invalid_media_type"))?;
   1335         let sha256 = Sha256::from_hex(&value.sha256)
   1336             .map_err(|_| TeraAppError::invalid_argument("invalid_media_digest"))?;
   1337         if Sha256::digest(&bytes) != sha256 {
   1338             return Err(TeraAppError::invalid_argument("media_verification_failed"));
   1339         }
   1340         let dimensions =
   1341             radroots_sdk::transport::BlossomImageDimensions::new(value.width, value.height)
   1342                 .map_err(|_| TeraAppError::invalid_argument("invalid_image_dimensions"))?;
   1343         let verified_at_unix_ms = value
   1344             .prepared_at_unix_s
   1345             .checked_mul(1_000)
   1346             .ok_or_else(|| TeraAppError::invalid_argument("invalid_media_reference"))?;
   1347         radroots_sdk::transport::BlossomUploadRequest::new(
   1348             bytes.clone().into(),
   1349             media_type.clone(),
   1350             dimensions,
   1351             verified_at_unix_ms,
   1352         )
   1353         .map_err(|_| TeraAppError::invalid_argument("media_verification_failed"))?;
   1354         Ok(Self {
   1355             opaque_reference: value.opaque_reference,
   1356             sha256,
   1357             byte_size: value.byte_size,
   1358             prepared_at_unix_s: value.prepared_at_unix_s,
   1359             bytes: bytes.into(),
   1360             media_type,
   1361             width: value.width,
   1362             height: value.height,
   1363             alt: value.alt,
   1364         })
   1365     }
   1366 }
   1367 
   1368 impl PreparedMedia {
   1369     pub(crate) fn into_recovery_media(
   1370         self,
   1371     ) -> Result<tera_core::runtime::product_surface::recovery_completion::RecoveryMedia, TeraAppError>
   1372     {
   1373         tera_core::runtime::product_surface::recovery_completion::RecoveryMedia::new(
   1374             self.opaque_reference,
   1375             self.bytes,
   1376             self.media_type,
   1377             self.width,
   1378             self.height,
   1379         )
   1380         .map_err(Into::into)
   1381     }
   1382 
   1383     pub(crate) fn into_submission_bytes(self) -> std::sync::Arc<[u8]> {
   1384         self.bytes
   1385     }
   1386 
   1387     pub(crate) fn into_submission_media(
   1388         self,
   1389         request: tera_core::runtime::product_surface::SubmissionReservationRequest,
   1390         expected_revision: u64,
   1391     ) -> Result<tera_core::runtime::product_surface::SubmissionMediaRequest, TeraAppError> {
   1392         tera_core::runtime::product_surface::SubmissionMediaRequest::new(
   1393             request,
   1394             expected_revision,
   1395             self.opaque_reference,
   1396             self.bytes,
   1397         )
   1398         .map_err(Into::into)
   1399     }
   1400 
   1401     pub(crate) fn into_authored_image(
   1402         self,
   1403         blossom: &radroots_sdk::transport::BlossomSlot,
   1404     ) -> Result<AuthoredImage, TeraAppError> {
   1405         self.bind(blossom)?.authored_image()
   1406     }
   1407 
   1408     pub(crate) fn into_upload_intent(
   1409         self,
   1410         draft_id: [u8; 16],
   1411         expected_revision: u64,
   1412     ) -> Result<Phase1UploadIntent, TeraAppError> {
   1413         Phase1UploadIntent::new(
   1414             draft_id,
   1415             expected_revision,
   1416             self.bytes,
   1417             self.media_type,
   1418             self.width,
   1419             self.height,
   1420         )
   1421         .map_err(|_| TeraAppError::invalid_argument("invalid_blossom_upload"))
   1422     }
   1423 
   1424     pub(crate) fn upload_request(
   1425         &self,
   1426         verified_at_unix_ms: u64,
   1427     ) -> Result<radroots_sdk::transport::BlossomUploadRequest, TeraAppError> {
   1428         let dimensions =
   1429             radroots_sdk::transport::BlossomImageDimensions::new(self.width, self.height)
   1430                 .map_err(|_| TeraAppError::invalid_argument("invalid_image_dimensions"))?;
   1431         radroots_sdk::transport::BlossomUploadRequest::new(
   1432             std::sync::Arc::clone(&self.bytes),
   1433             self.media_type.clone(),
   1434             dimensions,
   1435             verified_at_unix_ms,
   1436         )
   1437         .map_err(|_| TeraAppError::invalid_argument("invalid_blossom_upload"))
   1438     }
   1439 
   1440     fn bind(
   1441         self,
   1442         blossom: &radroots_sdk::transport::BlossomSlot,
   1443     ) -> Result<BoundPreparedMedia, TeraAppError> {
   1444         let verified_at_unix_ms = self
   1445             .prepared_at_unix_s
   1446             .checked_mul(1_000)
   1447             .ok_or_else(|| TeraAppError::invalid_argument("invalid_media_reference"))?;
   1448         let transaction = blossom
   1449             .prepare_upload(self.upload_request(verified_at_unix_ms)?)
   1450             .map_err(|error| TeraAppError::invalid_argument(error.code()))?;
   1451         let descriptor = BlobDescriptor::new(
   1452             transaction.expected_url().clone(),
   1453             self.sha256,
   1454             self.byte_size,
   1455             self.media_type.clone(),
   1456             self.prepared_at_unix_s,
   1457         )
   1458         .and_then(BlobDescriptor::approve_reference)
   1459         .and_then(|descriptor| descriptor.verify_bytes(&self.bytes, &self.media_type))
   1460         .map_err(|_| TeraAppError::invalid_argument("media_verification_failed"))?;
   1461         Ok(BoundPreparedMedia {
   1462             media: self,
   1463             descriptor,
   1464         })
   1465     }
   1466 }
   1467 
   1468 impl BoundPreparedMedia {
   1469     fn authored_image(&self) -> Result<AuthoredImage, TeraAppError> {
   1470         AuthoredImage::try_from_verified_descriptor(self.descriptor.clone())
   1471             .map_err(|_| TeraAppError::invalid_argument("invalid_image_media"))
   1472     }
   1473 
   1474     fn post_image(&self) -> Result<AuthoredPostImage, TeraAppError> {
   1475         AuthoredPostImage::new(
   1476             self.authored_image()?,
   1477             PostImageDimensions::new(self.media.width, self.media.height)
   1478                 .map_err(|_| TeraAppError::invalid_argument("invalid_image_dimensions"))?,
   1479             self.media.alt.clone(),
   1480         )
   1481         .map_err(|_| TeraAppError::invalid_argument("invalid_image"))
   1482     }
   1483 }
   1484 
   1485 fn event_command(
   1486     input: &FfiAddDraftInput,
   1487     image: Option<&BoundPreparedMedia>,
   1488 ) -> Result<CreateEvent, TeraAppError> {
   1489     if input.media.len() > 1 {
   1490         return Err(TeraAppError::invalid_argument("event_image_limit"));
   1491     }
   1492     let identifier = required(input.identifier.as_deref(), "event_identifier_required")?;
   1493     let title = required(input.title.as_deref(), "event_title_required")?;
   1494     let timing = input
   1495         .event_timing
   1496         .ok_or_else(|| TeraAppError::invalid_argument("event_timing_required"))?;
   1497     match timing {
   1498         FfiEventTimingKind::AllDay => {
   1499             let start = CalendarDate::parse(required(
   1500                 input.event_start_date.as_deref(),
   1501                 "event_start_date_required",
   1502             )?)
   1503             .map_err(|_| TeraAppError::invalid_argument("invalid_event_start_date"))?;
   1504             let mut event = AuthoredCalendarDateEvent::new(identifier, title, start)
   1505                 .map_err(|_| TeraAppError::invalid_argument("invalid_event"))?;
   1506             if let Some(end) = input.event_end_date.as_deref() {
   1507                 event =
   1508                     event
   1509                         .with_end(CalendarDate::parse(end).map_err(|_| {
   1510                             TeraAppError::invalid_argument("invalid_event_end_date")
   1511                         })?)
   1512                         .map_err(|_| TeraAppError::invalid_argument("invalid_event_range"))?;
   1513             }
   1514             if !input.content.is_empty() {
   1515                 event = event
   1516                     .with_description(input.content.clone())
   1517                     .map_err(|_| TeraAppError::invalid_argument("invalid_event_description"))?;
   1518             }
   1519             if let Some(location) = input.location.clone() {
   1520                 event = event
   1521                     .with_locations(vec![location])
   1522                     .map_err(|_| TeraAppError::invalid_argument("invalid_event_location"))?;
   1523             }
   1524             if let Some(image) = image {
   1525                 event = event
   1526                     .with_image(image.authored_image()?)
   1527                     .map_err(|_| TeraAppError::invalid_argument("invalid_event_image"))?;
   1528             }
   1529             Ok(CreateEvent::date(event))
   1530         }
   1531         FfiEventTimingKind::Timed => {
   1532             let start = input
   1533                 .event_start_unix_s
   1534                 .filter(|value| *value != 0)
   1535                 .ok_or_else(|| TeraAppError::invalid_argument("event_start_required"))?;
   1536             let mut event = AuthoredCalendarTimeEvent::new(identifier, title, start)
   1537                 .map_err(|_| TeraAppError::invalid_argument("invalid_event"))?;
   1538             if let Some(end) = input.event_end_unix_s {
   1539                 event = event
   1540                     .with_end(end)
   1541                     .map_err(|_| TeraAppError::invalid_argument("invalid_event_range"))?;
   1542             }
   1543             if let Some(timezone) = input.event_timezone.as_deref() {
   1544                 event = event
   1545                     .with_start_tzid(timezone)
   1546                     .map_err(|_| TeraAppError::invalid_argument("invalid_event_timezone"))?;
   1547             }
   1548             if !input.content.is_empty() {
   1549                 event = event
   1550                     .with_description(input.content.clone())
   1551                     .map_err(|_| TeraAppError::invalid_argument("invalid_event_description"))?;
   1552             }
   1553             if let Some(location) = input.location.clone() {
   1554                 event = event
   1555                     .with_locations(vec![location])
   1556                     .map_err(|_| TeraAppError::invalid_argument("invalid_event_location"))?;
   1557             }
   1558             if let Some(image) = image {
   1559                 event = event
   1560                     .with_image(image.authored_image()?)
   1561                     .map_err(|_| TeraAppError::invalid_argument("invalid_event_image"))?;
   1562             }
   1563             Ok(CreateEvent::time(event))
   1564         }
   1565     }
   1566 }
   1567 
   1568 fn food_command(
   1569     input: FfiAddDraftInput,
   1570     authored_at_unix_s: u64,
   1571     media: &[BoundPreparedMedia],
   1572 ) -> Result<CreateFoodAvailability, TeraAppError> {
   1573     let unit = FoodUnit::parse(required(input.unit.as_deref(), "food_unit_required")?)
   1574         .map_err(|_| TeraAppError::invalid_argument("invalid_food_unit"))?;
   1575     let images = media
   1576         .iter()
   1577         .map(|image| {
   1578             Ok(FoodAvailabilityImage::new(
   1579                 image.authored_image()?,
   1580                 FoodImageDimensions::new(image.media.width, image.media.height)
   1581                     .map_err(|_| TeraAppError::invalid_argument("invalid_image_dimensions"))?,
   1582             ))
   1583         })
   1584         .collect::<Result<Vec<_>, TeraAppError>>()?;
   1585     let status = input.food_status.as_deref().unwrap_or("active");
   1586     let details = FoodAvailabilityDetails::new(FoodAvailabilityDetailsParts {
   1587         content: FoodContent::new(input.content)
   1588             .map_err(|_| TeraAppError::invalid_argument("invalid_food_content"))?,
   1589         identifier: FoodIdentifier::parse(required(
   1590             input.identifier.as_deref(),
   1591             "food_identifier_required",
   1592         )?)
   1593         .map_err(|_| TeraAppError::invalid_argument("invalid_food_identifier"))?,
   1594         title: FoodText::new(required(input.title, "food_title_required")?)
   1595             .map_err(|_| TeraAppError::invalid_argument("invalid_food_title"))?,
   1596         summary: FoodText::new(required(input.summary, "food_summary_required")?)
   1597             .map_err(|_| TeraAppError::invalid_argument("invalid_food_summary"))?,
   1598         published_at: FoodPublishedAt::new(
   1599             input.food_published_at_unix_s.unwrap_or(authored_at_unix_s),
   1600         )
   1601         .map_err(|_| TeraAppError::invalid_argument("invalid_food_published_at"))?,
   1602         location: FoodText::new(required(input.location, "food_location_required")?)
   1603             .map_err(|_| TeraAppError::invalid_argument("invalid_food_location"))?,
   1604         price: FoodPrice::new(
   1605             required(input.price_amount, "food_price_required")?,
   1606             FoodCurrency::parse(required(input.currency, "food_currency_required")?)
   1607                 .map_err(|_| TeraAppError::invalid_argument("invalid_food_currency"))?,
   1608             unit,
   1609         )
   1610         .map_err(|_| TeraAppError::invalid_argument("invalid_food_price"))?,
   1611         quantity: input
   1612             .quantity
   1613             .map(|quantity| FoodQuantity::new(quantity, unit))
   1614             .transpose()
   1615             .map_err(|_| TeraAppError::invalid_argument("invalid_food_quantity"))?,
   1616         status: FoodAvailabilityStatus::parse(status)
   1617             .map_err(|_| TeraAppError::invalid_argument("invalid_food_status"))?,
   1618         images,
   1619     })
   1620     .map_err(|_| TeraAppError::invalid_argument("invalid_food_availability"))?;
   1621     Ok(CreateFoodAvailability::new(details))
   1622 }
   1623 
   1624 fn reject_media(media: &[BoundPreparedMedia]) -> Result<(), TeraAppError> {
   1625     if media.is_empty() {
   1626         Ok(())
   1627     } else {
   1628         Err(TeraAppError::invalid_argument("media_not_allowed"))
   1629     }
   1630 }
   1631 
   1632 fn content_with_media_references(
   1633     mut content: String,
   1634     media: &[BoundPreparedMedia],
   1635 ) -> Result<String, TeraAppError> {
   1636     if content.trim().is_empty() {
   1637         return Err(TeraAppError::invalid_argument("content_required"));
   1638     }
   1639     for item in media {
   1640         let url = item.descriptor.url().as_str();
   1641         match content.match_indices(url).count() {
   1642             0 => {
   1643                 if !content.ends_with('\n') {
   1644                     content.push('\n');
   1645                 }
   1646                 content.push_str(url);
   1647             }
   1648             1 => {}
   1649             _ => {
   1650                 return Err(TeraAppError::invalid_argument("duplicate_media_reference"));
   1651             }
   1652         }
   1653     }
   1654     Ok(content)
   1655 }
   1656 
   1657 fn required<T>(value: Option<T>, code: &'static str) -> Result<T, TeraAppError> {
   1658     value.ok_or_else(|| TeraAppError::invalid_argument(code))
   1659 }
   1660 
   1661 fn opaque_media_reference_is_valid(value: &str) -> bool {
   1662     value.len() > "media:".len()
   1663         && value.len() <= MEDIA_REFERENCE_MAX_BYTES
   1664         && value.starts_with("media:")
   1665         && value["media:".len()..].bytes().all(|byte| {
   1666             byte.is_ascii_lowercase() || byte.is_ascii_digit() || matches!(byte, b'-' | b'_')
   1667         })
   1668 }
   1669 
   1670 #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
   1671 pub enum FfiMediaStage {
   1672     Pending,
   1673     Preparing,
   1674     Uploading,
   1675     Verified,
   1676     Failed,
   1677     Orphaned,
   1678 }
   1679 
   1680 #[cfg_attr(coverage_nightly, coverage(off))]
   1681 impl From<Phase1MediaStage> for FfiMediaStage {
   1682     fn from(value: Phase1MediaStage) -> Self {
   1683         match value {
   1684             Phase1MediaStage::Pending => Self::Pending,
   1685             Phase1MediaStage::Preparing => Self::Preparing,
   1686             Phase1MediaStage::Uploading => Self::Uploading,
   1687             Phase1MediaStage::Verified => Self::Verified,
   1688             Phase1MediaStage::Failed => Self::Failed,
   1689             Phase1MediaStage::Orphaned => Self::Orphaned,
   1690         }
   1691     }
   1692 }
   1693 
   1694 #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)]
   1695 pub struct FfiDraftMediaRecord {
   1696     pub schema_version: u16,
   1697     pub url: String,
   1698     pub upload_url: Option<String>,
   1699     pub stage: FfiMediaStage,
   1700     pub upload_attempts: u8,
   1701     pub verified_at_unix_ms: Option<u64>,
   1702     pub possible_orphan: bool,
   1703     pub orphan_reason_code: Option<String>,
   1704     pub orphan_recorded_at_unix_ms: Option<u64>,
   1705 }
   1706 
   1707 #[cfg_attr(coverage_nightly, coverage(off))]
   1708 impl From<&Phase1MediaPrerequisite> for FfiDraftMediaRecord {
   1709     fn from(value: &Phase1MediaPrerequisite) -> Self {
   1710         let orphan = value.orphan();
   1711         Self {
   1712             schema_version: UPLOAD_OUTPUT_FFI_SCHEMA_VERSION,
   1713             url: value.url().to_owned(),
   1714             upload_url: radroots_blossom::BlobUrl::parse(value.url())
   1715                 .ok()
   1716                 .map(|url| url.upload_url()),
   1717             stage: value.stage().into(),
   1718             upload_attempts: value.upload_attempts(),
   1719             verified_at_unix_ms: value.verified_at_unix_ms(),
   1720             possible_orphan: orphan.is_some(),
   1721             orphan_reason_code: orphan.map(|value| value.reason_code().to_owned()),
   1722             orphan_recorded_at_unix_ms: orphan.map(|value| value.recorded_at_unix_ms()),
   1723         }
   1724     }
   1725 }
   1726 
   1727 #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
   1728 pub enum FfiOutboxState {
   1729     Draft,
   1730     MediaPreparing,
   1731     MediaUploading,
   1732     ReadyToSign,
   1733     Signing,
   1734     Signed,
   1735     Queued,
   1736     Delivering,
   1737     PartiallyDelivered,
   1738     Retryable,
   1739     Terminal,
   1740     Cancelled,
   1741     Complete,
   1742 }
   1743 
   1744 #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
   1745 pub enum FfiDraftKind {
   1746     Add,
   1747     Retraction,
   1748 }
   1749 
   1750 #[cfg_attr(coverage_nightly, coverage(off))]
   1751 impl From<Phase1DraftKind> for FfiDraftKind {
   1752     fn from(value: Phase1DraftKind) -> Self {
   1753         match value {
   1754             Phase1DraftKind::Add => Self::Add,
   1755             Phase1DraftKind::Retraction => Self::Retraction,
   1756         }
   1757     }
   1758 }
   1759 
   1760 #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)]
   1761 pub struct FfiDraftFormMediaRecord {
   1762     pub schema_version: u16,
   1763     pub opaque_reference: String,
   1764     pub url: String,
   1765     pub sha256: String,
   1766     pub media_type: String,
   1767     pub byte_size: u64,
   1768     pub width: u32,
   1769     pub height: u32,
   1770     pub alt: String,
   1771     pub prepared_at_unix_s: u64,
   1772 }
   1773 
   1774 #[cfg_attr(coverage_nightly, coverage(off))]
   1775 impl From<&Phase1DraftMediaSnapshot> for FfiDraftFormMediaRecord {
   1776     fn from(value: &Phase1DraftMediaSnapshot) -> Self {
   1777         Self {
   1778             schema_version: MOBILE_FFI_SCHEMA_VERSION,
   1779             opaque_reference: value.opaque_reference.clone(),
   1780             url: value.url.clone(),
   1781             sha256: value.sha256.clone(),
   1782             media_type: value.media_type.clone(),
   1783             byte_size: value.byte_size,
   1784             width: value.width,
   1785             height: value.height,
   1786             alt: value.alt.clone(),
   1787             prepared_at_unix_s: value.prepared_at_unix_s,
   1788         }
   1789     }
   1790 }
   1791 
   1792 #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)]
   1793 pub struct FfiDraftFormRecord {
   1794     pub schema_version: u16,
   1795     pub command_type: FfiAddCommandType,
   1796     pub content: String,
   1797     pub identifier: Option<String>,
   1798     pub title: Option<String>,
   1799     pub summary: Option<String>,
   1800     pub location: Option<String>,
   1801     pub event_timing: Option<FfiEventTimingKind>,
   1802     pub event_start_date: Option<String>,
   1803     pub event_end_date: Option<String>,
   1804     pub event_start_unix_s: Option<u64>,
   1805     pub event_end_unix_s: Option<u64>,
   1806     pub event_timezone: Option<String>,
   1807     pub price_amount: Option<String>,
   1808     pub currency: Option<String>,
   1809     pub unit: Option<String>,
   1810     pub quantity: Option<String>,
   1811     pub food_published_at_unix_s: Option<u64>,
   1812     pub food_status: Option<String>,
   1813     pub media: Vec<FfiDraftFormMediaRecord>,
   1814 }
   1815 
   1816 #[cfg_attr(coverage_nightly, coverage(off))]
   1817 impl From<&Phase1DraftFormSnapshot> for FfiDraftFormRecord {
   1818     fn from(value: &Phase1DraftFormSnapshot) -> Self {
   1819         Self {
   1820             schema_version: MOBILE_FFI_SCHEMA_VERSION,
   1821             command_type: value.command_type.into(),
   1822             content: value.content.clone(),
   1823             identifier: value.identifier.clone(),
   1824             title: value.title.clone(),
   1825             summary: value.summary.clone(),
   1826             location: value.location.clone(),
   1827             event_timing: value.event_timing.map(|value| match value {
   1828                 Phase1DraftEventTiming::AllDay => FfiEventTimingKind::AllDay,
   1829                 Phase1DraftEventTiming::Timed => FfiEventTimingKind::Timed,
   1830             }),
   1831             event_start_date: value.event_start_date.clone(),
   1832             event_end_date: value.event_end_date.clone(),
   1833             event_start_unix_s: value.event_start_unix_s,
   1834             event_end_unix_s: value.event_end_unix_s,
   1835             event_timezone: value.event_timezone.clone(),
   1836             price_amount: value.price_amount.clone(),
   1837             currency: value.currency.clone(),
   1838             unit: value.unit.clone(),
   1839             quantity: value.quantity.clone(),
   1840             food_published_at_unix_s: value.food_published_at_unix_s,
   1841             food_status: value.food_status.clone(),
   1842             media: value.media.iter().map(Into::into).collect(),
   1843         }
   1844     }
   1845 }
   1846 
   1847 #[cfg_attr(coverage_nightly, coverage(off))]
   1848 impl From<Phase1OutboxState> for FfiOutboxState {
   1849     fn from(value: Phase1OutboxState) -> Self {
   1850         match value {
   1851             Phase1OutboxState::Draft => Self::Draft,
   1852             Phase1OutboxState::MediaPreparing => Self::MediaPreparing,
   1853             Phase1OutboxState::MediaUploading => Self::MediaUploading,
   1854             Phase1OutboxState::ReadyToSign => Self::ReadyToSign,
   1855             Phase1OutboxState::Signing => Self::Signing,
   1856             Phase1OutboxState::Signed => Self::Signed,
   1857             Phase1OutboxState::Queued => Self::Queued,
   1858             Phase1OutboxState::Delivering => Self::Delivering,
   1859             Phase1OutboxState::PartiallyDelivered => Self::PartiallyDelivered,
   1860             Phase1OutboxState::Retryable => Self::Retryable,
   1861             Phase1OutboxState::Terminal => Self::Terminal,
   1862             Phase1OutboxState::Cancelled => Self::Cancelled,
   1863             Phase1OutboxState::Complete => Self::Complete,
   1864         }
   1865     }
   1866 }
   1867 
   1868 #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Record)]
   1869 pub struct FfiOperationSettlementRecord {
   1870     pub schema_version: u16,
   1871     pub artifacts: u16,
   1872     pub signed: u16,
   1873     pub admitted: u16,
   1874     pub pending: u16,
   1875     pub retryable: u16,
   1876     pub indeterminate: u16,
   1877     pub failed_terminal: u16,
   1878     pub cancelled: u16,
   1879     pub delivery_plans: u16,
   1880     pub delivery_satisfied: u16,
   1881     pub delivery_pending: u16,
   1882     pub delivery_retryable: u16,
   1883     pub delivery_exhausted: u16,
   1884     pub delivery_failed_terminal: u16,
   1885     pub delivery_cancelled: u16,
   1886 }
   1887 
   1888 #[cfg_attr(coverage_nightly, coverage(off))]
   1889 impl From<radroots_storage::authored::OperationSettlement> for FfiOperationSettlementRecord {
   1890     fn from(value: radroots_storage::authored::OperationSettlement) -> Self {
   1891         Self {
   1892             schema_version: MOBILE_FFI_SCHEMA_VERSION,
   1893             artifacts: value.artifacts(),
   1894             signed: value.signed(),
   1895             admitted: value.admitted(),
   1896             pending: value.pending(),
   1897             retryable: value.retryable(),
   1898             indeterminate: value.indeterminate(),
   1899             failed_terminal: value.failed_terminal(),
   1900             cancelled: value.cancelled(),
   1901             delivery_plans: value.delivery_plans(),
   1902             delivery_satisfied: value.delivery_satisfied(),
   1903             delivery_pending: value.delivery_pending(),
   1904             delivery_retryable: value.delivery_retryable(),
   1905             delivery_exhausted: value.delivery_exhausted(),
   1906             delivery_failed_terminal: value.delivery_failed_terminal(),
   1907             delivery_cancelled: value.delivery_cancelled(),
   1908         }
   1909     }
   1910 }
   1911 
   1912 #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
   1913 pub enum FfiRelaySatisfaction {
   1914     AnyAccepted,
   1915     AllAccepted,
   1916     AnyDelivered,
   1917     AllDelivered,
   1918 }
   1919 
   1920 #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
   1921 pub enum FfiCancellationPolicy {
   1922     PreservePublishedRequest,
   1923     LocalCooperative,
   1924 }
   1925 
   1926 impl FfiCancellationPolicy {
   1927     pub(crate) const fn core(self) -> Phase1CancellationPolicy {
   1928         match self {
   1929             Self::PreservePublishedRequest => Phase1CancellationPolicy::PreservePublishedRequest,
   1930             Self::LocalCooperative => Phase1CancellationPolicy::LocalCooperative,
   1931         }
   1932     }
   1933 }
   1934 
   1935 #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)]
   1936 pub struct FfiQueuePolicyRecord {
   1937     pub schema_version: u16,
   1938     pub relay_urls: Vec<String>,
   1939     pub satisfaction: FfiRelaySatisfaction,
   1940     pub delivery_deadline_unix_ms: u64,
   1941     pub cancellation: FfiCancellationPolicy,
   1942 }
   1943 
   1944 impl TryFrom<FfiQueuePolicyRecord> for Phase1QueuePolicy {
   1945     type Error = TeraAppError;
   1946 
   1947     fn try_from(value: FfiQueuePolicyRecord) -> Result<Self, Self::Error> {
   1948         require_schema(value.schema_version)?;
   1949         Phase1QueuePolicy::new(
   1950             value.relay_urls,
   1951             match value.satisfaction {
   1952                 FfiRelaySatisfaction::AnyAccepted => Phase1RelaySatisfaction::AnyAccepted,
   1953                 FfiRelaySatisfaction::AllAccepted => Phase1RelaySatisfaction::AllAccepted,
   1954                 FfiRelaySatisfaction::AnyDelivered => Phase1RelaySatisfaction::AnyDelivered,
   1955                 FfiRelaySatisfaction::AllDelivered => Phase1RelaySatisfaction::AllDelivered,
   1956             },
   1957             value.delivery_deadline_unix_ms,
   1958             match value.cancellation {
   1959                 FfiCancellationPolicy::PreservePublishedRequest => {
   1960                     Phase1CancellationPolicy::PreservePublishedRequest
   1961                 }
   1962                 FfiCancellationPolicy::LocalCooperative => {
   1963                     Phase1CancellationPolicy::LocalCooperative
   1964                 }
   1965             },
   1966         )
   1967         .map_err(|_| TeraAppError::invalid_argument("invalid_queue_policy"))
   1968     }
   1969 }
   1970 
   1971 #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)]
   1972 pub struct FfiCapabilityRecord {
   1973     pub schema_version: u16,
   1974     pub id: String,
   1975     pub compiled: bool,
   1976     pub configured: bool,
   1977     pub availability: String,
   1978     pub maturity: String,
   1979 }
   1980 
   1981 #[cfg_attr(coverage_nightly, coverage(off))]
   1982 impl From<SdkCapabilityRecord> for FfiCapabilityRecord {
   1983     fn from(value: SdkCapabilityRecord) -> Self {
   1984         Self {
   1985             schema_version: MOBILE_FFI_SCHEMA_VERSION,
   1986             id: value.id,
   1987             compiled: value.compiled,
   1988             configured: value.configured,
   1989             availability: value.availability,
   1990             maturity: value.maturity,
   1991         }
   1992     }
   1993 }
   1994 
   1995 #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)]
   1996 pub struct FfiStorageStatusRecord {
   1997     pub schema_version: u16,
   1998     pub backend: String,
   1999     pub open_mode: String,
   2000     pub shutdown: String,
   2001     pub integrity: String,
   2002 }
   2003 
   2004 #[cfg_attr(coverage_nightly, coverage(off))]
   2005 impl From<SdkStorageStatusRecord> for FfiStorageStatusRecord {
   2006     fn from(value: SdkStorageStatusRecord) -> Self {
   2007         Self {
   2008             schema_version: MOBILE_FFI_SCHEMA_VERSION,
   2009             backend: value.backend,
   2010             open_mode: value.open_mode,
   2011             shutdown: value.shutdown,
   2012             integrity: value.integrity,
   2013         }
   2014     }
   2015 }
   2016 
   2017 #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
   2018 pub enum FfiRelayAccessRecord {
   2019     ReadOnly,
   2020     ReadWrite,
   2021 }
   2022 
   2023 #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)]
   2024 pub struct FfiRelayStatusRecord {
   2025     pub schema_version: u16,
   2026     pub relay_url: String,
   2027     pub access: FfiRelayAccessRecord,
   2028     pub read_state: String,
   2029     pub write_state: String,
   2030     pub read_last_attempt_unix_ms: Option<u64>,
   2031     pub write_last_attempt_unix_ms: Option<u64>,
   2032     pub read_next_attempt_unix_ms: Option<u64>,
   2033     pub write_next_attempt_unix_ms: Option<u64>,
   2034 }
   2035 
   2036 #[cfg_attr(coverage_nightly, coverage(off))]
   2037 impl From<SdkRelayStatusRecord> for FfiRelayStatusRecord {
   2038     fn from(value: SdkRelayStatusRecord) -> Self {
   2039         Self {
   2040             schema_version: MOBILE_FFI_SCHEMA_VERSION,
   2041             relay_url: value.relay_url,
   2042             access: match value.access {
   2043                 SdkRelayAccessRecord::ReadOnly => FfiRelayAccessRecord::ReadOnly,
   2044                 SdkRelayAccessRecord::ReadWrite => FfiRelayAccessRecord::ReadWrite,
   2045             },
   2046             read_state: value.read_state,
   2047             write_state: value.write_state,
   2048             read_last_attempt_unix_ms: value.read_last_attempt_unix_ms,
   2049             write_last_attempt_unix_ms: value.write_last_attempt_unix_ms,
   2050             read_next_attempt_unix_ms: value.read_next_attempt_unix_ms,
   2051             write_next_attempt_unix_ms: value.write_next_attempt_unix_ms,
   2052         }
   2053     }
   2054 }
   2055 
   2056 #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)]
   2057 pub struct FfiRelayStatusReportRecord {
   2058     pub schema_version: u16,
   2059     pub profile: String,
   2060     pub state: String,
   2061     pub read_availability: String,
   2062     pub write_availability: String,
   2063     pub relays: Vec<FfiRelayStatusRecord>,
   2064 }
   2065 
   2066 #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
   2067 pub enum FfiBlossomHostKind {
   2068     Native,
   2069     Simulator,
   2070     PhysicalDevice,
   2071 }
   2072 
   2073 impl From<FfiBlossomHostKind> for radroots_sdk::transport::BlossomHostKind {
   2074     fn from(value: FfiBlossomHostKind) -> Self {
   2075         match value {
   2076             FfiBlossomHostKind::Native => Self::Native,
   2077             FfiBlossomHostKind::Simulator => Self::Simulator,
   2078             FfiBlossomHostKind::PhysicalDevice => Self::PhysicalDevice,
   2079         }
   2080     }
   2081 }
   2082 
   2083 #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
   2084 pub enum FfiBlossomEndpointAuthority {
   2085     PublicWebPki,
   2086     LoopbackDevelopment,
   2087     PrivateNetworkDevelopment,
   2088 }
   2089 
   2090 impl From<FfiBlossomEndpointAuthority> for radroots_sdk::transport::BlossomEndpointAuthority {
   2091     fn from(value: FfiBlossomEndpointAuthority) -> Self {
   2092         match value {
   2093             FfiBlossomEndpointAuthority::PublicWebPki => Self::PublicWebPki,
   2094             FfiBlossomEndpointAuthority::LoopbackDevelopment => Self::LoopbackDevelopment,
   2095             FfiBlossomEndpointAuthority::PrivateNetworkDevelopment => {
   2096                 Self::PrivateNetworkDevelopment
   2097             }
   2098         }
   2099     }
   2100 }
   2101 
   2102 #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)]
   2103 pub struct FfiBlossomConfigurationRecord {
   2104     pub schema_version: u16,
   2105     pub host_kind: String,
   2106     pub endpoint_authority: String,
   2107     pub primary_origin: String,
   2108     pub fallback_origins: Vec<String>,
   2109     pub config_fingerprint: String,
   2110 }
   2111 
   2112 #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)]
   2113 pub struct FfiBlossomEvidenceRecord {
   2114     pub schema_version: u16,
   2115     pub origin: String,
   2116     pub config_fingerprint: String,
   2117     pub state: String,
   2118     pub last_successful_state: String,
   2119     pub transport_security: String,
   2120     pub observed_at_unix_ms: Option<u64>,
   2121     pub http_status: Option<u16>,
   2122     pub error_code: Option<String>,
   2123     pub server_error_code: Option<String>,
   2124     pub error_phase: Option<String>,
   2125     pub retryable: bool,
   2126     pub possible_orphan: bool,
   2127     pub attempts: u8,
   2128 }
   2129 
   2130 #[cfg_attr(coverage_nightly, coverage(off))]
   2131 impl From<SdkBlossomConfigurationRecord> for FfiBlossomConfigurationRecord {
   2132     fn from(value: SdkBlossomConfigurationRecord) -> Self {
   2133         Self {
   2134             schema_version: MOBILE_FFI_SCHEMA_VERSION,
   2135             host_kind: value.host_kind,
   2136             endpoint_authority: value.endpoint_authority,
   2137             primary_origin: value.primary_origin,
   2138             fallback_origins: value.fallback_origins,
   2139             config_fingerprint: value.config_fingerprint,
   2140         }
   2141     }
   2142 }
   2143 
   2144 #[cfg_attr(coverage_nightly, coverage(off))]
   2145 impl From<SdkBlossomEvidenceRecord> for FfiBlossomEvidenceRecord {
   2146     fn from(value: SdkBlossomEvidenceRecord) -> Self {
   2147         Self {
   2148             schema_version: value.schema_version,
   2149             origin: value.origin,
   2150             config_fingerprint: value.config_fingerprint,
   2151             state: value.state,
   2152             last_successful_state: value.last_successful_state,
   2153             transport_security: value.transport_security,
   2154             observed_at_unix_ms: value.observed_at_unix_ms,
   2155             http_status: value.http_status,
   2156             error_code: value.error_code,
   2157             server_error_code: value.server_error_code,
   2158             error_phase: value.error_phase,
   2159             retryable: value.retryable,
   2160             possible_orphan: value.possible_orphan,
   2161             attempts: value.attempts,
   2162         }
   2163     }
   2164 }
   2165 
   2166 #[cfg_attr(coverage_nightly, coverage(off))]
   2167 impl From<SdkRelayStatusReportRecord> for FfiRelayStatusReportRecord {
   2168     fn from(value: SdkRelayStatusReportRecord) -> Self {
   2169         Self {
   2170             schema_version: MOBILE_FFI_SCHEMA_VERSION,
   2171             profile: value.profile,
   2172             state: value.state,
   2173             read_availability: value.read_availability,
   2174             write_availability: value.write_availability,
   2175             relays: value.relays.into_iter().map(Into::into).collect(),
   2176         }
   2177     }
   2178 }
   2179 
   2180 #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)]
   2181 pub struct FfiShutdownRecord {
   2182     pub schema_version: u16,
   2183     pub state: String,
   2184     pub already_closed: bool,
   2185 }
   2186 
   2187 #[cfg_attr(coverage_nightly, coverage(off))]
   2188 impl From<SdkShutdownRecord> for FfiShutdownRecord {
   2189     fn from(value: SdkShutdownRecord) -> Self {
   2190         Self {
   2191             schema_version: MOBILE_FFI_SCHEMA_VERSION,
   2192             state: value.state,
   2193             already_closed: value.already_closed,
   2194         }
   2195     }
   2196 }
   2197 
   2198 pub(crate) fn decode_id(value: &str, code: &'static str) -> Result<[u8; 16], TeraAppError> {
   2199     if value.len() != 32 {
   2200         return Err(TeraAppError::invalid_argument(code));
   2201     }
   2202     let bytes = hex::decode(value).map_err(|_| TeraAppError::invalid_argument(code))?;
   2203     bytes
   2204         .try_into()
   2205         .map_err(|_| TeraAppError::invalid_argument(code))
   2206 }
   2207 
   2208 fn require_schema(schema_version: u16) -> Result<(), TeraAppError> {
   2209     if schema_version == MOBILE_FFI_SCHEMA_VERSION {
   2210         Ok(())
   2211     } else {
   2212         Err(TeraAppError::invalid_argument("unsupported_schema_version"))
   2213     }
   2214 }
   2215 
   2216 #[cfg(test)]
   2217 mod tests {
   2218     use core::num::NonZeroU64;
   2219     use std::io::Write;
   2220     use std::os::fd::AsRawFd;
   2221 
   2222     use radroots_event::id::TradeId;
   2223 
   2224     use super::*;
   2225 
   2226     fn rhi_attestation_fixture() -> serde_json::Value {
   2227         let fixture: serde_json::Value = serde_json::from_str(include_str!(
   2228             "../../../test-fixtures/tera_ffi/authored_operations.v1.json"
   2229         ))
   2230         .expect("authored corpus");
   2231         fixture["vectors"]
   2232             .as_array()
   2233             .expect("operations")
   2234             .iter()
   2235             .find(|entry| entry["id"] == "typed_rhi_evidence_attestation_017")
   2236             .expect("RHI operation")
   2237             .get("expected")
   2238             .expect("expected")
   2239             .clone()
   2240     }
   2241 
   2242     #[test]
   2243     fn evidence_report_plan_and_verified_event_use_final_mobile_vocabulary() {
   2244         let expected = rhi_attestation_fixture();
   2245         let content = expected["content"].as_str().expect("content").to_owned();
   2246         let report = parse_rhi_evidence_report(content.clone()).expect("report");
   2247         assert_eq!(report.outcome, FfiTradeEvidenceOutcome::Indeterminate);
   2248         assert_eq!(report.trade_generation, "7");
   2249         assert_eq!(report.observed_at_unix_s, "1800000000");
   2250 
   2251         let plan = prepare_rhi_evidence_attestation(content, 1_784_347_200).expect("plan");
   2252         assert_eq!(plan.kind, 3_441);
   2253         assert_eq!(plan.created_at_unix_s, "1784347200");
   2254         assert_eq!(
   2255             plan.expected_event_id,
   2256             expected["event_id"].as_str().expect("event id")
   2257         );
   2258 
   2259         let raw: serde_json::Value =
   2260             serde_json::from_str(expected["raw_json"].as_str().expect("raw event"))
   2261                 .expect("raw event JSON");
   2262         let signed = FfiSignedEvidenceEventRecord {
   2263             id: raw["id"].as_str().expect("id").to_owned(),
   2264             author_pubkey: raw["pubkey"].as_str().expect("pubkey").to_owned(),
   2265             created_at_unix_s: raw["created_at"].as_u64().expect("created_at"),
   2266             kind: u32::try_from(raw["kind"].as_u64().expect("kind")).expect("u32 kind"),
   2267             tags: serde_json::from_value(raw["tags"].clone()).expect("tags"),
   2268             content: raw["content"].as_str().expect("content").to_owned(),
   2269             signature: raw["sig"].as_str().expect("signature").to_owned(),
   2270         };
   2271         let attestation = validate_rhi_evidence_attestation(signed).expect("attestation");
   2272         assert_eq!(attestation.outcome, FfiTradeEvidenceOutcome::Indeterminate);
   2273         assert_eq!(attestation.trade_generation, "7");
   2274     }
   2275 
   2276     #[test]
   2277     fn evidence_manifest_and_supersession_project_the_complete_vocabulary() {
   2278         use radroots_sdk::trade::{
   2279             RadrootsTradeEvidenceManifestSourceResultV1, RadrootsTradeEvidenceManifestV1,
   2280             RadrootsTradeEvidencePolicyDigestV1, RadrootsTradeEvidenceScopePrerequisitesV1,
   2281             RadrootsTradeEvidenceSourceCompletionV1, RadrootsTradeEvidenceSourceIdV1,
   2282             RadrootsTradeEvidenceSourceRequirementV1, RadrootsTradeEvidenceSourceResultDigestV1,
   2283             RadrootsTradeEvidenceSourceResultV1,
   2284         };
   2285 
   2286         assert_eq!(
   2287             FfiTradeEvidenceCoverage::from(
   2288                 radroots_sdk::trade::RadrootsTradeEvidenceCoverageV1::Missing
   2289             ),
   2290             FfiTradeEvidenceCoverage::Missing
   2291         );
   2292         assert_eq!(
   2293             FfiTradeEvidenceCoverage::from(
   2294                 radroots_sdk::trade::RadrootsTradeEvidenceCoverageV1::Partial
   2295             ),
   2296             FfiTradeEvidenceCoverage::Partial
   2297         );
   2298         assert_eq!(
   2299             FfiTradeEvidenceCoverage::from(
   2300                 radroots_sdk::trade::RadrootsTradeEvidenceCoverageV1::ScopeSatisfied
   2301             ),
   2302             FfiTradeEvidenceCoverage::ScopeSatisfied
   2303         );
   2304         assert_eq!(
   2305             FfiTradeEvidenceCoverage::from(
   2306                 radroots_sdk::trade::RadrootsTradeEvidenceCoverageV1::Unsupported
   2307             ),
   2308             FfiTradeEvidenceCoverage::Unsupported
   2309         );
   2310         assert_eq!(
   2311             FfiTradeEvidenceOutcome::from(
   2312                 radroots_sdk::trade::RadrootsTradeEvidenceOutcomeV1::Valid
   2313             ),
   2314             FfiTradeEvidenceOutcome::Valid
   2315         );
   2316         assert_eq!(
   2317             FfiTradeEvidenceOutcome::from(
   2318                 radroots_sdk::trade::RadrootsTradeEvidenceOutcomeV1::Invalid
   2319             ),
   2320             FfiTradeEvidenceOutcome::Invalid
   2321         );
   2322         assert_eq!(
   2323             FfiTradeEvidenceOutcome::from(
   2324                 radroots_sdk::trade::RadrootsTradeEvidenceOutcomeV1::Indeterminate
   2325             ),
   2326             FfiTradeEvidenceOutcome::Indeterminate
   2327         );
   2328 
   2329         let source = RadrootsTradeEvidenceManifestSourceResultV1::new(
   2330             RadrootsTradeEvidenceSourceIdV1::parse("typed_source").expect("source id"),
   2331             RadrootsTradeEvidenceSourceResultV1::new(
   2332                 RadrootsTradeEvidenceSourceRequirementV1::Required,
   2333                 RadrootsTradeEvidenceSourceCompletionV1::Complete,
   2334                 0,
   2335             )
   2336             .expect("source result"),
   2337             RadrootsTradeEvidenceSourceResultDigestV1::from_bytes([0x33; 32]),
   2338         );
   2339         let manifest = RadrootsTradeEvidenceManifestV1::new(
   2340             TradeId::from_bytes([0x11; 16]),
   2341             NonZeroU64::new(1).expect("nonzero generation"),
   2342             RadrootsTradeEvidencePolicyDigestV1::from_bytes([0x22; 32]),
   2343             1_800_000_000,
   2344             RadrootsTradeEvidenceScopePrerequisitesV1::Satisfied,
   2345             [source],
   2346             [],
   2347         )
   2348         .expect("manifest");
   2349         let projected = parse_trade_evidence_manifest(manifest.canonical_bytes().to_vec())
   2350             .expect("manifest projection");
   2351         assert_eq!(projected.coverage, FfiTradeEvidenceCoverage::ScopeSatisfied);
   2352         assert_eq!(
   2353             projected.canonical_bytes_hex,
   2354             hex::encode(manifest.canonical_bytes())
   2355         );
   2356 
   2357         let decisions: serde_json::Value = serde_json::from_str(include_str!(
   2358             "../../../test-fixtures/tera_ffi/evidence_attestation_decision.v1.json"
   2359         ))
   2360         .expect("RHI decision corpus");
   2361         let superseding = decisions["vectors"]
   2362             .as_array()
   2363             .expect("RHI decision vectors")
   2364             .iter()
   2365             .find(|entry| entry["id"] == "rhi_evidence_attestation_superseding_002")
   2366             .expect("superseding vector");
   2367         let report = parse_rhi_evidence_report(
   2368             superseding["expected"]["canonical_event_content_utf8"]
   2369                 .as_str()
   2370                 .expect("canonical event content")
   2371                 .to_owned(),
   2372         )
   2373         .expect("superseding report");
   2374         assert_eq!(report.outcome, FfiTradeEvidenceOutcome::Valid);
   2375         assert_eq!(
   2376             report.supersedes_report_id.as_deref(),
   2377             Some("7777777777777777777777777777777777777777777777777777777777777777")
   2378         );
   2379         assert_eq!(
   2380             report.supersedes_event_id.as_deref(),
   2381             Some("8888888888888888888888888888888888888888888888888888888888888888")
   2382         );
   2383     }
   2384 
   2385     #[test]
   2386     fn transport_policy_enums_map_every_closed_variant() {
   2387         assert_eq!(
   2388             FfiCancellationPolicy::PreservePublishedRequest.core(),
   2389             Phase1CancellationPolicy::PreservePublishedRequest
   2390         );
   2391         assert_eq!(
   2392             FfiCancellationPolicy::LocalCooperative.core(),
   2393             Phase1CancellationPolicy::LocalCooperative
   2394         );
   2395         assert_eq!(
   2396             radroots_sdk::transport::BlossomHostKind::from(FfiBlossomHostKind::Native),
   2397             radroots_sdk::transport::BlossomHostKind::Native
   2398         );
   2399         assert_eq!(
   2400             radroots_sdk::transport::BlossomHostKind::from(FfiBlossomHostKind::Simulator),
   2401             radroots_sdk::transport::BlossomHostKind::Simulator
   2402         );
   2403         assert_eq!(
   2404             radroots_sdk::transport::BlossomHostKind::from(FfiBlossomHostKind::PhysicalDevice),
   2405             radroots_sdk::transport::BlossomHostKind::PhysicalDevice
   2406         );
   2407     }
   2408 
   2409     #[test]
   2410     fn evidence_errors_do_not_render_untrusted_content() {
   2411         let secret = "mobile-private-evidence";
   2412         let error = parse_rhi_evidence_report(secret.to_owned()).expect_err("malformed report");
   2413         assert!(!error.to_string().contains(secret));
   2414         assert!(!format!("{error:?}").contains(secret));
   2415     }
   2416 
   2417     fn png(width: u32, height: u32) -> Vec<u8> {
   2418         let mut bytes = b"\x89PNG\r\n\x1a\n\0\0\0\rIHDR".to_vec();
   2419         bytes.extend_from_slice(&width.to_be_bytes());
   2420         bytes.extend_from_slice(&height.to_be_bytes());
   2421         bytes
   2422     }
   2423 
   2424     fn blossom_slot() -> radroots_sdk::transport::BlossomSlot {
   2425         let profile = radroots_sdk::transport::BlossomProfile::new(
   2426             radroots_sdk::transport::BlossomHostKind::Simulator,
   2427             radroots_sdk::transport::BlossomEndpointAuthority::LoopbackDevelopment,
   2428             "http://127.0.0.1:3000",
   2429             std::iter::empty::<&str>(),
   2430         )
   2431         .unwrap();
   2432         let slot = radroots_sdk::transport::BlossomSlot::new();
   2433         slot.configure(radroots_sdk::transport::BlossomConfig::from_profile(
   2434             profile,
   2435         ))
   2436         .unwrap();
   2437         slot
   2438     }
   2439 
   2440     fn photo_input(file_descriptor: u64, bytes: &[u8], digest: String) -> FfiAddDraftInput {
   2441         FfiAddDraftInput {
   2442             schema_version: MOBILE_FFI_SCHEMA_VERSION,
   2443             command_type: FfiAddCommandType::CreatePhotoUpdate,
   2444             content: "Fresh carrots from this morning.".to_owned(),
   2445             identifier: None,
   2446             title: None,
   2447             summary: None,
   2448             location: None,
   2449             event_timing: None,
   2450             event_start_date: None,
   2451             event_end_date: None,
   2452             event_start_unix_s: None,
   2453             event_end_unix_s: None,
   2454             event_timezone: None,
   2455             price_amount: None,
   2456             currency: None,
   2457             unit: None,
   2458             quantity: None,
   2459             food_published_at_unix_s: None,
   2460             food_status: None,
   2461             media: vec![FfiPreparedMediaInput {
   2462                 schema_version: PREPARED_MEDIA_FFI_SCHEMA_VERSION,
   2463                 opaque_reference: "media:carrots-01".to_owned(),
   2464                 file: std::sync::Arc::new(
   2465                     FfiMediaFile::new(file_descriptor, bytes.len() as u64).expect("admitted file"),
   2466                 ),
   2467                 sha256: digest,
   2468                 media_type: "image/png".to_owned(),
   2469                 byte_size: bytes.len() as u64,
   2470                 width: 2,
   2471                 height: 2,
   2472                 alt: "A basket of carrots".to_owned(),
   2473                 prepared_at_unix_s: 1_800_000_000,
   2474             }],
   2475         }
   2476     }
   2477 
   2478     fn text_input(command_type: FfiAddCommandType) -> FfiAddDraftInput {
   2479         FfiAddDraftInput {
   2480             schema_version: MOBILE_FFI_SCHEMA_VERSION,
   2481             command_type,
   2482             content: "Fresh from the farm".to_owned(),
   2483             identifier: None,
   2484             title: None,
   2485             summary: None,
   2486             location: None,
   2487             event_timing: None,
   2488             event_start_date: None,
   2489             event_end_date: None,
   2490             event_start_unix_s: None,
   2491             event_end_unix_s: None,
   2492             event_timezone: None,
   2493             price_amount: None,
   2494             currency: None,
   2495             unit: None,
   2496             quantity: None,
   2497             food_published_at_unix_s: None,
   2498             food_status: None,
   2499             media: Vec::new(),
   2500         }
   2501     }
   2502 
   2503     #[test]
   2504     fn exact_five_add_inputs_build_their_typed_core_commands() {
   2505         let (update, media) = text_input(FfiAddCommandType::CreateUpdate)
   2506             .command_and_media(1_800_000_000, None)
   2507             .expect("update");
   2508         assert!(matches!(update, Phase1AddCommand::CreateUpdate(_)));
   2509         assert!(media.is_empty());
   2510 
   2511         let (ask, media) = text_input(FfiAddCommandType::CreateAsk)
   2512             .command_and_media(1_800_000_000, None)
   2513             .expect("ask");
   2514         assert!(matches!(ask, Phase1AddCommand::CreateAsk(_)));
   2515         assert!(media.is_empty());
   2516 
   2517         let mut all_day = text_input(FfiAddCommandType::CreateEvent);
   2518         all_day.identifier = Some("market-day".to_owned());
   2519         all_day.title = Some("Farmers market".to_owned());
   2520         all_day.location = Some("Town square".to_owned());
   2521         all_day.event_timing = Some(FfiEventTimingKind::AllDay);
   2522         all_day.event_start_date = Some("2026-08-08".to_owned());
   2523         all_day.event_end_date = Some("2026-08-09".to_owned());
   2524         let (event, media) = all_day
   2525             .command_and_media(1_800_000_000, None)
   2526             .expect("all-day event");
   2527         assert!(matches!(event, Phase1AddCommand::CreateEvent(_)));
   2528         assert!(media.is_empty());
   2529 
   2530         let mut timed = text_input(FfiAddCommandType::CreateEvent);
   2531         timed.identifier = Some("harvest-tour".to_owned());
   2532         timed.title = Some("Harvest tour".to_owned());
   2533         timed.event_timing = Some(FfiEventTimingKind::Timed);
   2534         timed.event_start_unix_s = Some(1_800_000_000);
   2535         timed.event_end_unix_s = Some(1_800_003_600);
   2536         timed.event_timezone = Some("America/Vancouver".to_owned());
   2537         let (event, media) = timed
   2538             .command_and_media(1_800_000_000, None)
   2539             .expect("timed event");
   2540         assert!(matches!(event, Phase1AddCommand::CreateEvent(_)));
   2541         assert!(media.is_empty());
   2542 
   2543         let bytes = png(2, 2);
   2544         let mut file = tempfile::NamedTempFile::new().expect("media file");
   2545         file.write_all(&bytes).expect("write media");
   2546         file.flush().expect("flush media");
   2547         let digest = Sha256::digest(&bytes).to_hex();
   2548         let mut food = photo_input(file.as_file().as_raw_fd() as u64, &bytes, digest);
   2549         food.command_type = FfiAddCommandType::CreateFoodAvailability;
   2550         food.identifier = Some("carrots-2026-08".to_owned());
   2551         food.title = Some("Carrots".to_owned());
   2552         food.summary = Some("Fresh bunches".to_owned());
   2553         food.location = Some("Victoria".to_owned());
   2554         food.price_amount = Some("4.5".to_owned());
   2555         food.currency = Some("CAD".to_owned());
   2556         food.unit = Some("bunch".to_owned());
   2557         food.quantity = Some("12".to_owned());
   2558         food.food_status = Some("active".to_owned());
   2559         let blossom = blossom_slot();
   2560         let (food, media) = food
   2561             .command_and_media(1_800_000_000, Some(&blossom))
   2562             .expect("food availability");
   2563         assert!(matches!(food, Phase1AddCommand::CreateFoodAvailability(_)));
   2564         assert_eq!(media.len(), 1);
   2565     }
   2566 
   2567     #[test]
   2568     fn add_validation_reports_schema_shape_media_and_required_field_failures() {
   2569         let mut wrong_schema = text_input(FfiAddCommandType::CreateUpdate);
   2570         wrong_schema.schema_version = MOBILE_FFI_SCHEMA_VERSION + 1;
   2571         assert_eq!(
   2572             wrong_schema
   2573                 .command_and_media(1_800_000_000, None)
   2574                 .expect_err("schema")
   2575                 .report()
   2576                 .code,
   2577             "unsupported_schema_version"
   2578         );
   2579         assert_eq!(
   2580             text_input(FfiAddCommandType::CreateUpdate)
   2581                 .command_and_media(0, None)
   2582                 .expect_err("authored time")
   2583                 .report()
   2584                 .code,
   2585             "invalid_add_draft"
   2586         );
   2587         assert_eq!(
   2588             text_input(FfiAddCommandType::CreateEvent)
   2589                 .command_and_media(1_800_000_000, None)
   2590                 .expect_err("event identity")
   2591                 .report()
   2592                 .code,
   2593             "event_identifier_required"
   2594         );
   2595         let mut food = text_input(FfiAddCommandType::CreateFoodAvailability);
   2596         food.unit = Some("crate".to_owned());
   2597         assert_eq!(
   2598             food.command_and_media(1_800_000_000, None)
   2599                 .expect_err("food unit")
   2600                 .report()
   2601                 .code,
   2602             "invalid_food_unit"
   2603         );
   2604     }
   2605 
   2606     #[test]
   2607     fn prepared_media_accepts_only_the_exact_bounded_file_descriptor_bytes() {
   2608         let bytes = png(2, 2);
   2609         let mut file = tempfile::NamedTempFile::new().expect("media file");
   2610         file.write_all(&bytes).expect("write media");
   2611         file.flush().expect("flush media");
   2612         let digest = Sha256::digest(&bytes).to_hex();
   2613         let input = photo_input(file.as_file().as_raw_fd() as u64, &bytes, digest.clone());
   2614         let blossom = blossom_slot();
   2615 
   2616         let (command, media) = input
   2617             .command_and_media(1_800_000_000, Some(&blossom))
   2618             .expect("verified media input");
   2619         assert!(matches!(command, Phase1AddCommand::CreatePhotoUpdate(_)));
   2620         assert_eq!(media.len(), 1);
   2621         assert_eq!(
   2622             media[0].url(),
   2623             format!("http://127.0.0.1:3000/{digest}.png")
   2624         );
   2625         assert_eq!(
   2626             file.as_file().metadata().expect("caller-owned media").len(),
   2627             bytes.len() as u64
   2628         );
   2629     }
   2630 
   2631     #[test]
   2632     fn prepared_media_rejects_file_descriptors_outside_the_platform_range() {
   2633         assert_eq!(
   2634             FfiMediaFile::new(u64::MAX, 1)
   2635                 .expect_err("out-of-range descriptor")
   2636                 .report()
   2637                 .code,
   2638             "media_handle_unavailable"
   2639         );
   2640     }
   2641 
   2642     #[cfg(unix)]
   2643     #[test]
   2644     fn prepared_media_rejects_unavailable_in_range_file_descriptors() {
   2645         assert_eq!(
   2646             FfiMediaFile::new(i32::MAX as u64, 1)
   2647                 .expect_err("unavailable in-range descriptor")
   2648                 .report()
   2649                 .code,
   2650             "media_handle_unavailable"
   2651         );
   2652     }
   2653 
   2654     #[test]
   2655     fn prepared_media_rejects_digest_tamper_and_path_like_references() {
   2656         let bytes = png(2, 2);
   2657         let mut file = tempfile::NamedTempFile::new().expect("media file");
   2658         file.write_all(&bytes).expect("write media");
   2659         file.flush().expect("flush media");
   2660 
   2661         let tampered = photo_input(
   2662             file.as_file().as_raw_fd() as u64,
   2663             &bytes,
   2664             Sha256::digest(b"other").to_hex(),
   2665         );
   2666         let blossom = blossom_slot();
   2667         assert_eq!(
   2668             tampered
   2669                 .command_and_media(1_800_000_000, Some(&blossom))
   2670                 .expect_err("digest mismatch")
   2671                 .report()
   2672                 .code,
   2673             "media_verification_failed"
   2674         );
   2675 
   2676         let mut path_like = photo_input(
   2677             file.as_file().as_raw_fd() as u64,
   2678             &bytes,
   2679             Sha256::digest(&bytes).to_hex(),
   2680         );
   2681         path_like.media[0].opaque_reference = "file:/private/media.jpg".to_owned();
   2682         assert_eq!(
   2683             path_like
   2684                 .command_and_media(1_800_000_000, Some(&blossom))
   2685                 .expect_err("path-like reference")
   2686                 .report()
   2687                 .code,
   2688             "invalid_media_reference"
   2689         );
   2690     }
   2691 
   2692     #[test]
   2693     fn prepared_media_constructs_a_bounded_verified_upload_request() {
   2694         let bytes = b"\x89PNG\r\n\x1a\n\0\0\0\rIHDR\0\0\0\x02\0\0\0\x02";
   2695         let mut file = tempfile::NamedTempFile::new().expect("media file");
   2696         file.write_all(bytes).expect("write media");
   2697         file.flush().expect("flush media");
   2698         let digest = Sha256::digest(bytes).to_hex();
   2699         let mut input = photo_input(file.as_file().as_raw_fd() as u64, bytes, digest.clone());
   2700         input.media[0].media_type = "image/png".to_owned();
   2701 
   2702         let prepared = PreparedMedia::try_from(input.media.remove(0)).expect("prepared media");
   2703         let request = prepared
   2704             .upload_request(1_800_000_000_000)
   2705             .expect("bounded upload request");
   2706         assert_eq!(request.sha256().to_hex(), digest);
   2707         assert_eq!(request.byte_size(), bytes.len() as u64);
   2708         assert_eq!(request.media_type().as_str(), "image/png");
   2709         assert_eq!(request.dimensions().width(), 2);
   2710         assert_eq!(request.dimensions().height(), 2);
   2711     }
   2712 
   2713     #[test]
   2714     fn media_validation_executes_every_bounded_shape_guard() {
   2715         let bytes = png(2, 2);
   2716         let mut file = tempfile::NamedTempFile::new().expect("media file");
   2717         file.write_all(&bytes).expect("write media");
   2718         file.flush().expect("flush media");
   2719         let digest = Sha256::digest(&bytes).to_hex();
   2720         let valid = photo_input(file.as_file().as_raw_fd() as u64, &bytes, digest).media[0].clone();
   2721 
   2722         let mut old_schema = valid.clone();
   2723         old_schema.schema_version = MOBILE_FFI_SCHEMA_VERSION;
   2724         assert_eq!(
   2725             PreparedMedia::try_from(old_schema)
   2726                 .err()
   2727                 .expect("borrowed schema rejected")
   2728                 .report()
   2729                 .code,
   2730             "unsupported_schema_version"
   2731         );
   2732 
   2733         let mut invalid_values = Vec::new();
   2734         let mut value = valid.clone();
   2735         value.byte_size = 0;
   2736         invalid_values.push(value);
   2737         let mut value = valid.clone();
   2738         value.byte_size = MEDIA_FILE_MAX_BYTES + 1;
   2739         invalid_values.push(value);
   2740         let mut value = valid.clone();
   2741         value.width = 0;
   2742         invalid_values.push(value);
   2743         let mut value = valid.clone();
   2744         value.height = 0;
   2745         invalid_values.push(value);
   2746         let mut value = valid.clone();
   2747         value.prepared_at_unix_s = 0;
   2748         invalid_values.push(value);
   2749         let mut value = valid.clone();
   2750         value.alt = "   ".to_owned();
   2751         invalid_values.push(value);
   2752         let mut value = valid.clone();
   2753         value.alt = "a".repeat(1_025);
   2754         invalid_values.push(value);
   2755 
   2756         for value in invalid_values {
   2757             assert_eq!(
   2758                 PreparedMedia::try_from(value)
   2759                     .err()
   2760                     .expect("invalid bounded media")
   2761                     .report()
   2762                     .code,
   2763                 "invalid_media_reference"
   2764             );
   2765         }
   2766 
   2767         let mut wrong_size = valid.clone();
   2768         wrong_size.byte_size += 1;
   2769         assert_eq!(
   2770             PreparedMedia::try_from(wrong_size)
   2771                 .err()
   2772                 .expect("descriptor size")
   2773                 .report()
   2774                 .code,
   2775             "media_size_mismatch"
   2776         );
   2777         for reference in ["media:", "Media:item", "media:BAD", "media:item/path"] {
   2778             assert!(!opaque_media_reference_is_valid(reference));
   2779         }
   2780         assert!(opaque_media_reference_is_valid("media:item_01-a"));
   2781         assert!(!opaque_media_reference_is_valid(&format!(
   2782             "media:{}",
   2783             "a".repeat(MEDIA_REFERENCE_MAX_BYTES)
   2784         )));
   2785     }
   2786 
   2787     #[test]
   2788     fn event_and_post_optional_branches_remain_strict_and_complete() {
   2789         let mut minimal_date = text_input(FfiAddCommandType::CreateEvent);
   2790         minimal_date.content.clear();
   2791         minimal_date.identifier = Some("minimal-date".to_owned());
   2792         minimal_date.title = Some("Minimal date".to_owned());
   2793         minimal_date.event_timing = Some(FfiEventTimingKind::AllDay);
   2794         minimal_date.event_start_date = Some("2026-08-08".to_owned());
   2795         assert!(minimal_date.command_and_media(1_800_000_000, None).is_ok());
   2796 
   2797         let mut minimal_time = text_input(FfiAddCommandType::CreateEvent);
   2798         minimal_time.content.clear();
   2799         minimal_time.identifier = Some("minimal-time".to_owned());
   2800         minimal_time.title = Some("Minimal time".to_owned());
   2801         minimal_time.event_timing = Some(FfiEventTimingKind::Timed);
   2802         minimal_time.event_start_unix_s = Some(1_800_000_000);
   2803         assert!(minimal_time.command_and_media(1_800_000_000, None).is_ok());
   2804 
   2805         let bytes = png(2, 2);
   2806         let mut file = tempfile::NamedTempFile::new().expect("media file");
   2807         file.write_all(&bytes).expect("write media");
   2808         file.flush().expect("flush media");
   2809         let digest = Sha256::digest(&bytes).to_hex();
   2810         let mut event = photo_input(file.as_file().as_raw_fd() as u64, &bytes, digest);
   2811         event.command_type = FfiAddCommandType::CreateEvent;
   2812         event.identifier = Some("event-image".to_owned());
   2813         event.title = Some("Event image".to_owned());
   2814         event.event_timing = Some(FfiEventTimingKind::Timed);
   2815         event.event_start_unix_s = Some(1_800_000_000);
   2816         let blossom = blossom_slot();
   2817         assert!(
   2818             event
   2819                 .clone()
   2820                 .command_and_media(1_800_000_000, Some(&blossom))
   2821                 .is_ok()
   2822         );
   2823 
   2824         let mut second = event.media[0].clone();
   2825         second.opaque_reference = "media:event-image-two".to_owned();
   2826         event.media.push(second);
   2827         assert_eq!(
   2828             event
   2829                 .command_and_media(1_800_000_000, Some(&blossom))
   2830                 .expect_err("event media limit")
   2831                 .report()
   2832                 .code,
   2833             "event_image_limit"
   2834         );
   2835     }
   2836 
   2837     #[test]
   2838     fn content_references_and_identifier_decoding_cover_all_outcomes() {
   2839         let bytes = png(2, 2);
   2840         let mut file = tempfile::NamedTempFile::new().expect("media file");
   2841         file.write_all(&bytes).expect("write media");
   2842         file.flush().expect("flush media");
   2843         let digest = Sha256::digest(&bytes).to_hex();
   2844         let input = photo_input(file.as_file().as_raw_fd() as u64, &bytes, digest);
   2845         let blossom = blossom_slot();
   2846         let prepared = PreparedMedia::try_from(input.media[0].clone())
   2847             .expect("prepared media")
   2848             .bind(&blossom)
   2849             .expect("bound media");
   2850         let url = prepared.descriptor.url().as_str().to_owned();
   2851 
   2852         assert_eq!(
   2853             content_with_media_references("   ".to_owned(), std::slice::from_ref(&prepared))
   2854                 .expect_err("blank content")
   2855                 .report()
   2856                 .code,
   2857             "content_required"
   2858         );
   2859         assert_eq!(
   2860             content_with_media_references(
   2861                 format!("caption\n{url}"),
   2862                 std::slice::from_ref(&prepared)
   2863             )
   2864             .expect("one existing reference")
   2865             .match_indices(&url)
   2866             .count(),
   2867             1
   2868         );
   2869         assert!(
   2870             content_with_media_references("caption\n".to_owned(), std::slice::from_ref(&prepared))
   2871                 .expect("newline append")
   2872                 .ends_with(&url)
   2873         );
   2874         assert_eq!(
   2875             content_with_media_references(
   2876                 format!("{url}\n{url}"),
   2877                 std::slice::from_ref(&prepared),
   2878             )
   2879             .expect_err("duplicate reference")
   2880             .report()
   2881             .code,
   2882             "duplicate_media_reference"
   2883         );
   2884 
   2885         let mut update = text_input(FfiAddCommandType::CreateUpdate);
   2886         update.media.push(input.media[0].clone());
   2887         assert_eq!(
   2888             update
   2889                 .command_and_media(1_800_000_000, Some(&blossom))
   2890                 .expect_err("update media")
   2891                 .report()
   2892                 .code,
   2893             "media_not_allowed"
   2894         );
   2895         let mut over_limit = text_input(FfiAddCommandType::CreateUpdate);
   2896         over_limit.media = vec![input.media[0].clone(); 21];
   2897         assert_eq!(
   2898             over_limit
   2899                 .command_and_media(1_800_000_000, None)
   2900                 .expect_err("media count")
   2901                 .report()
   2902                 .code,
   2903             "invalid_add_draft"
   2904         );
   2905 
   2906         assert_eq!(decode_id(&"01".repeat(16), "bad").expect("id"), [1; 16]);
   2907         assert_eq!(
   2908             decode_id("01", "bad").expect_err("short id").report().code,
   2909             "bad"
   2910         );
   2911         assert_eq!(
   2912             decode_id(&"gg".repeat(16), "bad")
   2913                 .expect_err("non-hex id")
   2914                 .report()
   2915                 .code,
   2916             "bad"
   2917         );
   2918     }
   2919 }