field_ios

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

runtime.rs (45784B)


      1 use std::sync::Arc;
      2 #[path = "author_visibility.rs"]
      3 mod author_visibility;
      4 pub use author_visibility::{
      5     FfiAuthorVisibility, FfiAuthorVisibilityEntry, FfiAuthorVisibilityPolicy,
      6 };
      7 #[path = "cache_cleanup.rs"]
      8 mod cache_cleanup;
      9 #[path = "key_removal.rs"]
     10 mod key_removal;
     11 pub use cache_cleanup::FfiMediaCacheCleanup;
     12 #[path = "backup/runtime.rs"]
     13 mod backup;
     14 #[path = "restore/runtime.rs"]
     15 mod restore;
     16 pub use restore::{FfiRestoreStore, restore_local_application_backup};
     17 #[path = "revision.rs"]
     18 mod revision;
     19 pub use revision::FfiRevisionSourceRequest;
     20 
     21 #[path = "composer/runtime.rs"]
     22 mod composer;
     23 #[path = "draft_inventory/runtime.rs"]
     24 mod draft_inventory;
     25 #[path = "recovery_completion.rs"]
     26 mod recovery_completion;
     27 #[path = "recovery_inventory/runtime.rs"]
     28 mod recovery_inventory;
     29 #[path = "recovery_schedule.rs"]
     30 mod recovery_schedule;
     31 #[path = "recovery_status.rs"]
     32 mod recovery_status;
     33 pub use recovery_completion::{FfiRecoveryCompletionReceipt, FfiRecoveryUploadReceipt};
     34 pub use recovery_schedule::FfiNativeRecoverySchedule;
     35 pub use recovery_status::{FfiNativeRecoveryReason, FfiNativeRecoveryStatus};
     36 #[path = "submission/runtime.rs"]
     37 mod submission;
     38 
     39 use tera_core::runtime::product_surface::{
     40     LocalNetworkRelayPolicy, Phase1AddIntent, Phase1ExistingDraft, Phase1MediaCachePolicy,
     41     Phase1QueueIntent, Phase1ReviseIntent, ReplaceMobileSettings, TodayPageRequest,
     42     phase1_new_addressable_identifier, phase1_operation_now_unix_ms,
     43 };
     44 
     45 use crate::FfiRevisionStatusRecord;
     46 use crate::dto::PreparedMedia;
     47 use crate::operations::{
     48     FfiIdentityCommandRecord, FfiMediaCacheStatusRecord, FfiMediaOperation,
     49     FfiMobileSettingsRecord, FfiProfileMetadataInputRecord, FfiProfileStatusRecord,
     50     FfiReplaceSettingsRecord, FfiRevisionInputRecord, FfiSettingsTransitionRecord,
     51     FfiVerifiedMediaArtifactRecord, decode_artifact_id, decode_configuration,
     52     decode_reference_fingerprint,
     53 };
     54 use crate::signer::HostSignerAdapter;
     55 use crate::subscription::SubscriptionHub;
     56 use crate::{
     57     FfiAddDraftInput, FfiAddSchemaRecord, FfiBlossomConfigurationRecord,
     58     FfiBlossomEndpointAuthority, FfiBlossomEvidenceRecord, FfiBlossomHostKind,
     59     FfiBlossomUploadInput, FfiBlossomUploadIntent, FfiCapabilityRecord, FfiCardAddParityRecord,
     60     FfiDraftStatusRecord, FfiIdentityStatusRecord, FfiLocalNetworkRecord, FfiMeRecord,
     61     FfiQueuePolicyRecord, FfiRelayStatusReportRecord, FfiRetractionDraftInput,
     62     FfiRuntimeChangeKind, FfiRuntimeInfoRecord, FfiSearchResultRecord, FfiShutdownRecord,
     63     FfiStorageStatusRecord, FfiSubscriptionHandle, FfiTodayPageRecord, FfiTodayProjectionUpdate,
     64     FfiTodayRefreshRecord, FfiTodaySyncRecord, TeraAppError, TeraHostSigner, TeraRuntimeObserver,
     65     add_schemas, decode_id,
     66 };
     67 
     68 #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
     69 pub enum ProtectedDataAvailability {
     70     Available,
     71     Unavailable,
     72 }
     73 
     74 impl From<ProtectedDataAvailability> for tera_core::runtime::store::ProtectedDataAvailability {
     75     fn from(value: ProtectedDataAvailability) -> Self {
     76         match value {
     77             ProtectedDataAvailability::Available => Self::Available,
     78             ProtectedDataAvailability::Unavailable => Self::Unavailable,
     79         }
     80     }
     81 }
     82 
     83 /// Native boundary object delegating all product behavior to the ordinary Rust core.
     84 #[derive(uniffi::Object)]
     85 pub struct TeraRuntime {
     86     inner: tera_core::TeraRuntime,
     87     has_host_signer: bool,
     88     subscriptions: Arc<SubscriptionHub>,
     89 }
     90 
     91 #[cfg_attr(not(coverage_nightly), uniffi::export(async_runtime = "tokio"))]
     92 impl TeraRuntime {
     93     #[cfg_attr(not(coverage_nightly), uniffi::constructor)]
     94     pub async fn new(
     95         application_support_directory: String,
     96         public_key_hex: String,
     97         source_generation_hex: String,
     98         source_generation_created_at_unix_ms: u64,
     99         protected_data: ProtectedDataAvailability,
    100     ) -> Result<Self, TeraAppError> {
    101         build_runtime(
    102             application_support_directory,
    103             public_key_hex,
    104             source_generation_hex,
    105             source_generation_created_at_unix_ms,
    106             protected_data,
    107             None,
    108             RuntimeOpenOptions::default(),
    109         )
    110         .await
    111     }
    112 
    113     #[cfg_attr(not(coverage_nightly), uniffi::constructor)]
    114     pub async fn with_host_signer(
    115         application_support_directory: String,
    116         public_key_hex: String,
    117         source_generation_hex: String,
    118         source_generation_created_at_unix_ms: u64,
    119         protected_data: ProtectedDataAvailability,
    120         host_signer: Box<dyn TeraHostSigner>,
    121     ) -> Result<Self, TeraAppError> {
    122         build_runtime(
    123             application_support_directory,
    124             public_key_hex,
    125             source_generation_hex,
    126             source_generation_created_at_unix_ms,
    127             protected_data,
    128             Some(host_signer),
    129             RuntimeOpenOptions::default(),
    130         )
    131         .await
    132     }
    133 
    134     pub async fn shutdown(&self) -> Result<FfiShutdownRecord, TeraAppError> {
    135         self.subscriptions.close();
    136         let result = self
    137             .inner
    138             .shutdown()
    139             .await
    140             .map(Into::into)
    141             .map_err(Into::into);
    142         if result.is_ok() {
    143             self.subscriptions.drain().await;
    144         }
    145         result
    146     }
    147 
    148     pub fn uptime_millis(&self) -> i64 {
    149         self.inner.uptime_millis()
    150     }
    151 
    152     pub fn info(&self) -> FfiRuntimeInfoRecord {
    153         self.inner.info().into()
    154     }
    155 
    156     pub fn info_json(&self) -> String {
    157         self.inner.info_json()
    158     }
    159 
    160     pub fn set_app_info_platform(
    161         &self,
    162         platform: Option<String>,
    163         bundle_id: Option<String>,
    164         version: Option<String>,
    165         build_number: Option<String>,
    166         build_sha: Option<String>,
    167     ) {
    168         self.inner
    169             .set_app_info_platform(platform, bundle_id, version, build_number, build_sha);
    170     }
    171 
    172     pub fn identity_status(&self) -> Result<FfiIdentityStatusRecord, TeraAppError> {
    173         let public_key = self
    174             .inner
    175             .authenticated_store_public_key_hex()
    176             .ok_or_else(|| {
    177                 TeraAppError::failure(
    178                     "identity_unavailable",
    179                     "identity",
    180                     true,
    181                     &["unlock_identity"],
    182                     "The active identity is unavailable.",
    183                 )
    184             })?;
    185         Ok(FfiIdentityStatusRecord {
    186             schema_version: crate::MOBILE_FFI_SCHEMA_VERSION,
    187             public_key,
    188             host_signer_configured: self.has_host_signer,
    189         })
    190     }
    191 
    192     pub fn sdk_capabilities(&self) -> Vec<FfiCapabilityRecord> {
    193         self.inner
    194             .sdk_capabilities()
    195             .into_iter()
    196             .map(Into::into)
    197             .collect()
    198     }
    199 
    200     pub async fn sdk_storage_status(&self) -> Result<FfiStorageStatusRecord, TeraAppError> {
    201         self.inner
    202             .sdk_storage_status()
    203             .await
    204             .map(Into::into)
    205             .map_err(Into::into)
    206     }
    207 
    208     pub fn sdk_relay_status(&self) -> Result<Option<FfiRelayStatusReportRecord>, TeraAppError> {
    209         self.inner
    210             .sdk_relay_status()
    211             .map(|value| value.map(Into::into))
    212             .map_err(Into::into)
    213     }
    214 
    215     pub fn sdk_blossom_configuration(
    216         &self,
    217     ) -> Result<Option<FfiBlossomConfigurationRecord>, TeraAppError> {
    218         self.inner
    219             .sdk_blossom_configuration()
    220             .map(|value| value.map(Into::into))
    221             .map_err(Into::into)
    222     }
    223 
    224     pub fn sdk_blossom_evidence(&self) -> Result<Option<FfiBlossomEvidenceRecord>, TeraAppError> {
    225         self.inner
    226             .sdk_blossom_evidence()
    227             .map(|value| value.map(Into::into))
    228             .map_err(Into::into)
    229     }
    230 
    231     pub async fn probe_blossom(&self) -> Result<FfiBlossomEvidenceRecord, TeraAppError> {
    232         let evidence = self
    233             .inner
    234             .probe_blossom()
    235             .await
    236             .map(Into::into)
    237             .map_err(TeraAppError::from)?;
    238         self.subscriptions.notify(FfiRuntimeChangeKind::Media, None);
    239         Ok(evidence)
    240     }
    241 
    242     pub fn subscribe_changes(
    243         &self,
    244         observer: Box<dyn TeraRuntimeObserver>,
    245     ) -> Result<Arc<FfiSubscriptionHandle>, TeraAppError> {
    246         self.subscriptions.subscribe(observer)
    247     }
    248 
    249     pub async fn configure_public_relays(
    250         &self,
    251         writable_relays: Vec<String>,
    252     ) -> Result<(), TeraAppError> {
    253         self.inner
    254             .configure_public_relays(writable_relays)
    255             .await
    256             .map_err(TeraAppError::from)?;
    257         self.subscriptions.notify(FfiRuntimeChangeKind::Relay, None);
    258         Ok(())
    259     }
    260 
    261     pub async fn configure_simulator_relays(
    262         &self,
    263         loopback_relays: Vec<String>,
    264     ) -> Result<(), TeraAppError> {
    265         self.inner
    266             .configure_simulator_relays(loopback_relays)
    267             .await
    268             .map_err(TeraAppError::from)?;
    269         self.subscriptions.notify(FfiRuntimeChangeKind::Relay, None);
    270         Ok(())
    271     }
    272 
    273     pub async fn configure_device_relays(
    274         &self,
    275         writable_relays: Vec<String>,
    276     ) -> Result<(), TeraAppError> {
    277         self.inner
    278             .configure_device_relays(writable_relays)
    279             .await
    280             .map_err(TeraAppError::from)?;
    281         self.subscriptions.notify(FfiRuntimeChangeKind::Relay, None);
    282         Ok(())
    283     }
    284 
    285     pub async fn configure_blossom(
    286         &self,
    287         host_kind: FfiBlossomHostKind,
    288         endpoint_authority: FfiBlossomEndpointAuthority,
    289         primary_origin: String,
    290         fallback_origins: Vec<String>,
    291     ) -> Result<(), TeraAppError> {
    292         self.inner
    293             .configure_blossom(
    294                 host_kind.into(),
    295                 endpoint_authority.into(),
    296                 primary_origin,
    297                 fallback_origins,
    298             )
    299             .await
    300             .map_err(TeraAppError::from)?;
    301         self.subscriptions.notify(FfiRuntimeChangeKind::Media, None);
    302         Ok(())
    303     }
    304 
    305     pub fn phase1_card_add_parity(&self) -> Vec<FfiCardAddParityRecord> {
    306         self.inner
    307             .phase1_card_add_parity()
    308             .into_iter()
    309             .map(|value| FfiCardAddParityRecord {
    310                 schema_version: crate::MOBILE_FFI_SCHEMA_VERSION,
    311                 card_type: value.card_type.into(),
    312                 command_type: value.add_command_type.into(),
    313             })
    314             .collect()
    315     }
    316 
    317     pub fn phase1_add_schemas(&self) -> Vec<FfiAddSchemaRecord> {
    318         add_schemas()
    319     }
    320 
    321     pub fn phase1_local_network(
    322         &self,
    323         context: FfiLocalNetworkRecord,
    324     ) -> Result<FfiLocalNetworkRecord, TeraAppError> {
    325         self.local_network(context).map(Into::into)
    326     }
    327 
    328     pub async fn phase1_today_page(
    329         &self,
    330         context: FfiLocalNetworkRecord,
    331         limit: u16,
    332         as_of_unix_s: Option<u64>,
    333         cursor: Option<String>,
    334         viewer_time_zone: Option<String>,
    335     ) -> Result<FfiTodayPageRecord, TeraAppError> {
    336         if as_of_unix_s.is_some() == cursor.is_some()
    337             || viewer_time_zone.is_some() != as_of_unix_s.is_some()
    338         {
    339             return Err(TeraAppError::invalid_argument("invalid_today_page_request"));
    340         }
    341         let context = self.local_network(context)?;
    342         let request = match cursor {
    343             Some(cursor) => TodayPageRequest::after(limit, cursor),
    344             None => TodayPageRequest::first(
    345                 limit,
    346                 as_of_unix_s
    347                     .ok_or_else(|| TeraAppError::invalid_argument("today_as_of_required"))?,
    348                 viewer_time_zone
    349                     .as_deref()
    350                     .ok_or_else(|| TeraAppError::invalid_argument("today_viewer_zone_required"))?,
    351             ),
    352         };
    353         self.inner
    354             .phase1_today_page(&context, request)
    355             .await
    356             .map(Into::into)
    357             .map_err(Into::into)
    358     }
    359 
    360     /// Reconcile displayed identities against current application visibility.
    361     pub async fn phase1_today_reconcile(
    362         &self,
    363         context: FfiLocalNetworkRecord,
    364         as_of_unix_s: u64,
    365         card_ids: Vec<String>,
    366         expected_generation: Option<u64>,
    367         viewer_time_zone: String,
    368     ) -> Result<FfiTodayPageRecord, TeraAppError> {
    369         let context = self.local_network(context)?;
    370         let current = self
    371             .inner
    372             .phase1_today_reconcile(
    373                 &context,
    374                 as_of_unix_s,
    375                 &card_ids,
    376                 expected_generation,
    377                 &viewer_time_zone,
    378             )
    379             .await?;
    380         Ok(FfiTodayPageRecord {
    381             calendar: current.calendar.into(),
    382             schema_version: crate::TODAY_PAGE_FFI_SCHEMA_VERSION,
    383             projection_generation: current.projection_generation,
    384             as_of_unix_s,
    385             items: current.items.into_iter().map(Into::into).collect(),
    386             next_cursor: None,
    387         })
    388     }
    389 
    390     pub async fn phase1_refresh_today(
    391         &self,
    392         context: FfiLocalNetworkRecord,
    393         now_unix_s: u64,
    394         update: FfiTodayProjectionUpdate,
    395     ) -> Result<FfiTodayRefreshRecord, TeraAppError> {
    396         let context = self.local_network(context)?;
    397         let receipt = self
    398             .inner
    399             .phase1_refresh_today(&context, now_unix_s, update.into())
    400             .await
    401             .map_err(TeraAppError::from)?;
    402         self.subscriptions
    403             .notify_context(FfiRuntimeChangeKind::Today, Some(&context), None);
    404         Ok(receipt.into())
    405     }
    406 
    407     pub async fn phase1_sync_today(
    408         &self,
    409         context: FfiLocalNetworkRecord,
    410         now_unix_s: u64,
    411         update: FfiTodayProjectionUpdate,
    412     ) -> Result<FfiTodaySyncRecord, TeraAppError> {
    413         let context = self.local_network(context)?;
    414         let receipt = self
    415             .inner
    416             .phase1_sync_today(&context, now_unix_s, update.into())
    417             .await
    418             .map_err(TeraAppError::from)?;
    419         self.subscriptions
    420             .notify_context(FfiRuntimeChangeKind::Today, Some(&context), None);
    421         Ok(receipt.into())
    422     }
    423 
    424     pub async fn phase1_backfill_today(
    425         &self,
    426         context: FfiLocalNetworkRecord,
    427         now_unix_s: u64,
    428         cursor: String,
    429     ) -> Result<FfiTodaySyncRecord, TeraAppError> {
    430         let context = self.local_network(context)?;
    431         let receipt = self
    432             .inner
    433             .phase1_backfill_today(&context, now_unix_s, &cursor)
    434             .await
    435             .map_err(TeraAppError::from)?;
    436         self.subscriptions
    437             .notify_context(FfiRuntimeChangeKind::Today, Some(&context), None);
    438         Ok(receipt.into())
    439     }
    440 
    441     pub async fn phase1_search(
    442         &self,
    443         context: FfiLocalNetworkRecord,
    444         query: String,
    445         limit: u16,
    446         as_of_unix_s: u64,
    447         viewer_time_zone: String,
    448     ) -> Result<Vec<FfiSearchResultRecord>, TeraAppError> {
    449         let context = self.local_network(context)?;
    450         self.inner
    451             .phase1_search(&context, &query, limit, as_of_unix_s, &viewer_time_zone)
    452             .await
    453             .map(|results| results.into_iter().map(Into::into).collect())
    454             .map_err(Into::into)
    455     }
    456 
    457     pub async fn phase1_me(
    458         &self,
    459         context: FfiLocalNetworkRecord,
    460         as_of_unix_s: u64,
    461         viewer_time_zone: String,
    462     ) -> Result<FfiMeRecord, TeraAppError> {
    463         let context = self.local_network(context)?;
    464         let public_key = self
    465             .inner
    466             .authenticated_store_public_key_hex()
    467             .ok_or_else(|| {
    468                 TeraAppError::failure(
    469                     "identity_unavailable",
    470                     "identity",
    471                     true,
    472                     &["unlock_identity"],
    473                     "The active identity is unavailable.",
    474                 )
    475             })?;
    476         self.inner
    477             .phase1_me(&context, &public_key, as_of_unix_s, &viewer_time_zone)
    478             .await
    479             .map(Into::into)
    480             .map_err(Into::into)
    481     }
    482 
    483     pub fn phase1_validate_add_draft(
    484         &self,
    485         input: FfiAddDraftInput,
    486         authored_at_unix_s: u64,
    487     ) -> Result<(), TeraAppError> {
    488         let blossom = self.inner.sdk_blossom_slot().map_err(TeraAppError::from)?;
    489         input
    490             .command_and_media(authored_at_unix_s, blossom.as_ref())
    491             .map(|_| ())
    492     }
    493 
    494     /// Saves one new or existing Add form while Rust owns all identity and
    495     /// timestamp policy. Addressable identifiers are generated when omitted.
    496     pub async fn phase1_save_add_intent(
    497         &self,
    498         mut input: FfiAddDraftInput,
    499         existing_draft_id: Option<String>,
    500         expected_revision: Option<u64>,
    501     ) -> Result<FfiDraftStatusRecord, TeraAppError> {
    502         if input.identifier.is_none()
    503             && matches!(
    504                 input.command_type,
    505                 crate::FfiAddCommandType::CreateEvent
    506                     | crate::FfiAddCommandType::CreateFoodAvailability
    507             )
    508         {
    509             input.identifier = Some(phase1_new_addressable_identifier());
    510         }
    511         let authored_at_unix_s =
    512             phase1_operation_now_unix_ms().map_err(TeraAppError::from)? / 1_000;
    513         let blossom = self.inner.sdk_blossom_slot().map_err(TeraAppError::from)?;
    514         let (command, media, form) =
    515             input.command_media_and_form(authored_at_unix_s, blossom.as_ref())?;
    516         let existing = match (existing_draft_id, expected_revision) {
    517             (Some(draft_id), Some(revision)) => Some(
    518                 Phase1ExistingDraft::new(decode_id(&draft_id, "invalid_draft_id")?, revision)
    519                     .map_err(TeraAppError::from)?,
    520             ),
    521             (None, None) => None,
    522             _ => {
    523                 return Err(TeraAppError::invalid_argument("invalid_existing_draft"));
    524             }
    525         };
    526         let status = self
    527             .inner
    528             .phase1_save_add_intent(
    529                 Phase1AddIntent::new(command, media, form, existing).map_err(TeraAppError::from)?,
    530             )
    531             .await
    532             .map_err(TeraAppError::from)?;
    533         let draft_id = hex::encode(status.draft().draft_id().as_bytes());
    534         self.subscriptions
    535             .notify(FfiRuntimeChangeKind::Drafts, Some(draft_id));
    536         Ok(status.into())
    537     }
    538 
    539     #[allow(clippy::too_many_arguments)]
    540     pub async fn phase1_save_draft(
    541         &self,
    542         draft_id: String,
    543         input: FfiAddDraftInput,
    544         authored_at_unix_s: u64,
    545         expected_revision: Option<u64>,
    546         persisted_at_unix_ms: u64,
    547     ) -> Result<FfiDraftStatusRecord, TeraAppError> {
    548         let blossom = self.inner.sdk_blossom_slot().map_err(TeraAppError::from)?;
    549         let (command, media, form) =
    550             input.command_media_and_form(authored_at_unix_s, blossom.as_ref())?;
    551         let decoded_id = decode_id(&draft_id, "invalid_draft_id")?;
    552         let status = self
    553             .inner
    554             .phase1_save_draft_with_form(
    555                 decoded_id,
    556                 command,
    557                 authored_at_unix_s,
    558                 media,
    559                 form,
    560                 expected_revision,
    561                 persisted_at_unix_ms,
    562             )
    563             .await
    564             .map_err(TeraAppError::from)?;
    565         self.subscriptions
    566             .notify(FfiRuntimeChangeKind::Drafts, Some(draft_id));
    567         Ok(status.into())
    568     }
    569 
    570     pub async fn phase1_save_retraction_draft(
    571         &self,
    572         draft_id: String,
    573         input: FfiRetractionDraftInput,
    574         authored_at_unix_s: u64,
    575         persisted_at_unix_ms: u64,
    576     ) -> Result<FfiDraftStatusRecord, TeraAppError> {
    577         if input.schema_version != crate::MOBILE_FFI_SCHEMA_VERSION {
    578             return Err(TeraAppError::invalid_argument("unsupported_schema_version"));
    579         }
    580         let decoded_id = decode_id(&draft_id, "invalid_draft_id")?;
    581         let command_type = match input.command_type {
    582             crate::FfiAddCommandType::CreateUpdate => {
    583                 tera_core::runtime::product_surface::AddCommandType::CreateUpdate
    584             }
    585             crate::FfiAddCommandType::CreatePhotoUpdate => {
    586                 tera_core::runtime::product_surface::AddCommandType::CreatePhotoUpdate
    587             }
    588             crate::FfiAddCommandType::CreateAsk => {
    589                 tera_core::runtime::product_surface::AddCommandType::CreateAsk
    590             }
    591             crate::FfiAddCommandType::CreateEvent => {
    592                 tera_core::runtime::product_surface::AddCommandType::CreateEvent
    593             }
    594             crate::FfiAddCommandType::CreateFoodAvailability => {
    595                 tera_core::runtime::product_surface::AddCommandType::CreateFoodAvailability
    596             }
    597         };
    598         let card_id = tera_core::runtime::product_surface::CardId::parse(&input.target_card_id)
    599             .map_err(|_| TeraAppError::invalid_argument("invalid_card_id"))?;
    600         let status = self
    601             .inner
    602             .phase1_save_retraction_draft(
    603                 decoded_id,
    604                 command_type,
    605                 card_id,
    606                 &input.target_event_id,
    607                 input.target_kind,
    608                 input.target_address.as_deref(),
    609                 &input.reason,
    610                 authored_at_unix_s,
    611                 persisted_at_unix_ms,
    612             )
    613             .await
    614             .map_err(TeraAppError::from)?;
    615         self.subscriptions
    616             .notify(FfiRuntimeChangeKind::Drafts, Some(draft_id));
    617         Ok(status.into())
    618     }
    619 
    620     pub async fn phase1_draft_status(
    621         &self,
    622         draft_id: String,
    623     ) -> Result<FfiDraftStatusRecord, TeraAppError> {
    624         self.inner
    625             .phase1_draft_status(decode_id(&draft_id, "invalid_draft_id")?)
    626             .await
    627             .map(Into::into)
    628             .map_err(Into::into)
    629     }
    630 
    631     pub async fn phase1_draft_heads(
    632         &self,
    633         limit: u16,
    634     ) -> Result<Vec<FfiDraftStatusRecord>, TeraAppError> {
    635         self.inner
    636             .phase1_draft_heads(limit)
    637             .await
    638             .map(|values| values.into_iter().map(Into::into).collect())
    639             .map_err(Into::into)
    640     }
    641 
    642     pub async fn phase1_queue_draft(
    643         &self,
    644         draft_id: String,
    645         expected_revision: u64,
    646         policy: FfiQueuePolicyRecord,
    647         queued_at_unix_ms: u64,
    648     ) -> Result<FfiDraftStatusRecord, TeraAppError> {
    649         let decoded_id = decode_id(&draft_id, "invalid_draft_id")?;
    650         let status = self
    651             .inner
    652             .phase1_queue_draft(
    653                 decoded_id,
    654                 expected_revision,
    655                 policy.try_into()?,
    656                 queued_at_unix_ms,
    657             )
    658             .await
    659             .map_err(TeraAppError::from)?;
    660         self.subscriptions
    661             .notify(FfiRuntimeChangeKind::Drafts, Some(draft_id));
    662         Ok(status.into())
    663     }
    664 
    665     /// Queues with the Rust-owned active relay and settlement policy.
    666     pub async fn phase1_queue_add_intent(
    667         &self,
    668         draft_id: String,
    669         expected_revision: u64,
    670     ) -> Result<FfiDraftStatusRecord, TeraAppError> {
    671         let decoded_id = decode_id(&draft_id, "invalid_draft_id")?;
    672         let intent =
    673             Phase1QueueIntent::new(decoded_id, expected_revision).map_err(TeraAppError::from)?;
    674         let status = self
    675             .inner
    676             .phase1_queue_add_intent(intent)
    677             .await
    678             .map_err(TeraAppError::from)?;
    679         self.subscriptions
    680             .notify(FfiRuntimeChangeKind::Drafts, Some(draft_id));
    681         Ok(status.into())
    682     }
    683 
    684     pub async fn phase1_recover_draft_queue(
    685         &self,
    686         draft_id: String,
    687         recovered_at_unix_ms: u64,
    688     ) -> Result<FfiDraftStatusRecord, TeraAppError> {
    689         let decoded_id = decode_id(&draft_id, "invalid_draft_id")?;
    690         let status = self
    691             .inner
    692             .phase1_recover_draft_queue(decoded_id, recovered_at_unix_ms)
    693             .await
    694             .map_err(TeraAppError::from)?;
    695         self.subscriptions
    696             .notify(FfiRuntimeChangeKind::Drafts, Some(draft_id));
    697         Ok(status.into())
    698     }
    699 
    700     pub async fn phase1_recover_add_intent(
    701         &self,
    702         draft_id: String,
    703     ) -> Result<FfiDraftStatusRecord, TeraAppError> {
    704         let decoded_id = decode_id(&draft_id, "invalid_draft_id")?;
    705         let status = self
    706             .inner
    707             .phase1_recover_add_intent(decoded_id)
    708             .await
    709             .map_err(TeraAppError::from)?;
    710         self.subscriptions
    711             .notify(FfiRuntimeChangeKind::Drafts, Some(draft_id));
    712         Ok(status.into())
    713     }
    714 
    715     pub async fn phase1_sign_queued_draft(
    716         &self,
    717         draft_id: String,
    718         expected_revision: u64,
    719     ) -> Result<FfiDraftStatusRecord, TeraAppError> {
    720         let decoded_id = decode_id(&draft_id, "invalid_draft_id")?;
    721         let status = self
    722             .inner
    723             .phase1_sign_queued_draft(decoded_id, expected_revision)
    724             .await
    725             .map_err(TeraAppError::from)?;
    726         self.subscriptions
    727             .notify(FfiRuntimeChangeKind::Drafts, Some(draft_id));
    728         Ok(status.into())
    729     }
    730 
    731     pub async fn phase1_advance_draft(
    732         &self,
    733         draft_id: String,
    734         expected_revision: u64,
    735     ) -> Result<FfiDraftStatusRecord, TeraAppError> {
    736         let decoded_id = decode_id(&draft_id, "invalid_draft_id")?;
    737         let status = self
    738             .inner
    739             .phase1_advance_draft(decoded_id, expected_revision)
    740             .await
    741             .map_err(TeraAppError::from)?;
    742         self.subscriptions
    743             .notify(FfiRuntimeChangeKind::Drafts, Some(draft_id));
    744         Ok(status.into())
    745     }
    746 
    747     pub async fn phase1_upload_draft_media(
    748         &self,
    749         input: FfiBlossomUploadInput,
    750     ) -> Result<FfiDraftStatusRecord, TeraAppError> {
    751         if input.schema_version != crate::MOBILE_FFI_SCHEMA_VERSION {
    752             return Err(TeraAppError::invalid_argument("unsupported_schema_version"));
    753         }
    754         let draft_id = decode_id(&input.draft_id, "invalid_draft_id")?;
    755         let operation_id = decode_id(&input.operation_id, "invalid_operation_id")?;
    756         let artifact_id = decode_id(&input.artifact_id, "invalid_artifact_id")?;
    757         let media = PreparedMedia::try_from(input.media)?;
    758         let request = media.upload_request(input.verified_at_unix_ms)?;
    759         let content = radroots_blossom::authorization::AuthorizationContent::parse(
    760             &input.authorization_content,
    761         )
    762         .map_err(|_| TeraAppError::invalid_argument("invalid_blossom_authorization"))?;
    763         let status = self
    764             .inner
    765             .phase1_upload_draft_media(
    766                 draft_id,
    767                 input.expected_revision,
    768                 request,
    769                 content,
    770                 input.authorization_created_at_unix_s,
    771                 input.authorization_lifetime_seconds,
    772                 operation_id,
    773                 artifact_id,
    774                 input.signing_deadline_unix_ms,
    775                 input.signing_cancellation.core(),
    776                 radroots_sdk::transport::BlossomCancellation::default(),
    777                 input.updated_at_unix_ms,
    778             )
    779             .await
    780             .map_err(TeraAppError::from)?;
    781         self.subscriptions
    782             .notify(FfiRuntimeChangeKind::Media, Some(input.draft_id.clone()));
    783         self.subscriptions
    784             .notify(FfiRuntimeChangeKind::Drafts, Some(input.draft_id));
    785         Ok(status.into())
    786     }
    787 
    788     /// Runs a Rust-planned BUD-11/BUD-02/BUD-01 upload attempt. The host
    789     /// supplies only the selected bounded file handle and draft revision.
    790     pub async fn phase1_upload_add_media_intent(
    791         &self,
    792         input: FfiBlossomUploadIntent,
    793     ) -> Result<FfiDraftStatusRecord, TeraAppError> {
    794         if input.schema_version != crate::MOBILE_FFI_SCHEMA_VERSION {
    795             return Err(TeraAppError::invalid_argument("unsupported_schema_version"));
    796         }
    797         let draft_id = decode_id(&input.draft_id, "invalid_draft_id")?;
    798         let intent = PreparedMedia::try_from(input.media)?
    799             .into_upload_intent(draft_id, input.expected_revision)?;
    800         let status = self
    801             .inner
    802             .phase1_upload_add_media_intent(intent)
    803             .await
    804             .map_err(TeraAppError::from)?;
    805         self.subscriptions
    806             .notify(FfiRuntimeChangeKind::Media, Some(input.draft_id.clone()));
    807         self.subscriptions
    808             .notify(FfiRuntimeChangeKind::Drafts, Some(input.draft_id));
    809         Ok(status.into())
    810     }
    811 
    812     /// Persists the upload transition before returning an immutable native
    813     /// background-transfer job.
    814     pub async fn phase1_prepare_add_media_background(
    815         &self,
    816         input: crate::FfiBlossomUploadIntent,
    817     ) -> Result<crate::FfiNativeUploadJobRecord, TeraAppError> {
    818         if input.schema_version != crate::MOBILE_FFI_SCHEMA_VERSION {
    819             return Err(TeraAppError::invalid_argument("unsupported_schema_version"));
    820         }
    821         let draft_id = decode_id(&input.draft_id, "invalid_draft_id")?;
    822         let intent = PreparedMedia::try_from(input.media)?
    823             .into_upload_intent(draft_id, input.expected_revision)?;
    824         let (status, job) = self
    825             .inner
    826             .phase1_prepare_native_upload(intent)
    827             .await
    828             .map_err(TeraAppError::from)?;
    829         self.subscriptions
    830             .notify(FfiRuntimeChangeKind::Media, Some(input.draft_id.clone()));
    831         self.subscriptions
    832             .notify(FfiRuntimeChangeKind::Drafts, Some(input.draft_id));
    833         Ok(crate::FfiNativeUploadJobRecord {
    834             schema_version: crate::UPLOAD_OUTPUT_FFI_SCHEMA_VERSION,
    835             operation_id: hex::encode(job.operation_id()),
    836             draft: status.into(),
    837             remote_url: job.remote_url().to_owned(),
    838             upload_url: job.upload_url().to_owned(),
    839             authorization_header: job.authorization_header().to_owned(),
    840             expected_sha256: job.expected_sha256().to_owned(),
    841             media_type: job.media_type().to_owned(),
    842             byte_size: job.byte_size(),
    843         })
    844     }
    845 
    846     /// Accepts only bounded native HTTP evidence; Rust performs descriptor and
    847     /// exact-byte retrieval verification before advancing durable state.
    848     pub async fn phase1_complete_add_media_background(
    849         &self,
    850         input: crate::FfiNativeUploadCompletionInput,
    851     ) -> Result<FfiDraftStatusRecord, TeraAppError> {
    852         if input.schema_version != crate::MOBILE_FFI_SCHEMA_VERSION
    853             || input.response_body.len() > 16_384
    854         {
    855             return Err(TeraAppError::invalid_argument(
    856                 "invalid_native_upload_completion",
    857             ));
    858         }
    859         let draft_id = decode_id(&input.draft_id, "invalid_draft_id")?;
    860         let intent = PreparedMedia::try_from(input.media)?
    861             .into_upload_intent(draft_id, input.expected_revision)?;
    862         let status = self
    863             .inner
    864             .phase1_complete_native_upload(
    865                 intent,
    866                 input.status_code,
    867                 input.response_media_type.as_deref(),
    868                 input.response_content_encoding.as_deref(),
    869                 input.response_body.as_slice(),
    870             )
    871             .await
    872             .map_err(TeraAppError::from)?;
    873         self.subscriptions
    874             .notify(FfiRuntimeChangeKind::Media, Some(input.draft_id.clone()));
    875         self.subscriptions
    876             .notify(FfiRuntimeChangeKind::Drafts, Some(input.draft_id));
    877         Ok(status.into())
    878     }
    879 
    880     pub async fn phase1_cancel_draft(
    881         &self,
    882         draft_id: String,
    883         expected_revision: u64,
    884         cancelled_at_unix_ms: u64,
    885     ) -> Result<FfiDraftStatusRecord, TeraAppError> {
    886         let decoded_id = decode_id(&draft_id, "invalid_draft_id")?;
    887         let status = self
    888             .inner
    889             .phase1_cancel_draft(decoded_id, expected_revision, cancelled_at_unix_ms)
    890             .await
    891             .map_err(TeraAppError::from)?;
    892         self.subscriptions
    893             .notify(FfiRuntimeChangeKind::Drafts, Some(draft_id));
    894         Ok(status.into())
    895     }
    896 
    897     pub async fn phase1_cancel_add_intent(
    898         &self,
    899         draft_id: String,
    900         expected_revision: u64,
    901     ) -> Result<FfiDraftStatusRecord, TeraAppError> {
    902         let decoded_id = decode_id(&draft_id, "invalid_draft_id")?;
    903         let status = self
    904             .inner
    905             .phase1_cancel_add_intent(decoded_id, expected_revision)
    906             .await
    907             .map_err(TeraAppError::from)?;
    908         self.subscriptions
    909             .notify(FfiRuntimeChangeKind::Drafts, Some(draft_id));
    910         Ok(status.into())
    911     }
    912 
    913     pub async fn phase1_settings(&self) -> Result<FfiMobileSettingsRecord, TeraAppError> {
    914         self.inner
    915             .phase1_settings()
    916             .await
    917             .map(|settings| (&settings).into())
    918             .map_err(Into::into)
    919     }
    920 
    921     pub async fn phase1_apply_settings_to_runtime(
    922         &self,
    923     ) -> Result<FfiMobileSettingsRecord, TeraAppError> {
    924         let settings = self.inner.phase1_settings().await?;
    925         self.inner
    926             .configure_relay_preferences(settings.relays())
    927             .await
    928             .map_err(TeraAppError::from)?;
    929         self.inner
    930             .configure_blossom_preferences(settings.blossom())
    931             .await
    932             .map_err(TeraAppError::from)?;
    933         self.subscriptions
    934             .notify(FfiRuntimeChangeKind::Settings, None);
    935         Ok((&settings).into())
    936     }
    937 
    938     pub async fn phase1_replace_settings(
    939         &self,
    940         input: FfiReplaceSettingsRecord,
    941     ) -> Result<FfiSettingsTransitionRecord, TeraAppError> {
    942         let current = self.inner.phase1_settings().await?;
    943         let expected_revision = input.expected_revision;
    944         let next = input.apply(current)?;
    945         let transition = self
    946             .inner
    947             .phase1_replace_settings(ReplaceMobileSettings::new(expected_revision, next)?)
    948             .await?;
    949         self.subscriptions
    950             .notify(FfiRuntimeChangeKind::Settings, None);
    951         Ok(transition.into())
    952     }
    953 
    954     pub async fn phase1_apply_identity_command(
    955         &self,
    956         expected_revision: u64,
    957         command: FfiIdentityCommandRecord,
    958     ) -> Result<FfiSettingsTransitionRecord, TeraAppError> {
    959         let transition = self
    960             .inner
    961             .phase1_apply_identity_command(expected_revision, command.try_into()?)
    962             .await?;
    963         let identity_id = transition
    964             .settings
    965             .identity()
    966             .active_identity_id()
    967             .map(str::to_owned);
    968         self.subscriptions
    969             .notify(FfiRuntimeChangeKind::Identity, identity_id);
    970         Ok(transition.into())
    971     }
    972 
    973     pub async fn phase1_save_profile_metadata(
    974         &self,
    975         input: FfiProfileMetadataInputRecord,
    976     ) -> Result<FfiProfileStatusRecord, TeraAppError> {
    977         let blossom = self.inner.sdk_blossom_slot().map_err(TeraAppError::from)?;
    978         let status = self
    979             .inner
    980             .phase1_save_profile_metadata(input.command(blossom.as_ref())?)
    981             .await?;
    982         let operation_id = hex::encode(status.draft().draft_id().as_bytes());
    983         self.subscriptions
    984             .notify(FfiRuntimeChangeKind::Profile, Some(operation_id));
    985         Ok(status.into())
    986     }
    987 
    988     pub async fn phase1_profile_status(
    989         &self,
    990         operation_id: String,
    991     ) -> Result<FfiProfileStatusRecord, TeraAppError> {
    992         self.inner
    993             .phase1_profile_status(decode_id(&operation_id, "invalid_operation_id")?)
    994             .await
    995             .map(Into::into)
    996             .map_err(Into::into)
    997     }
    998 
    999     pub async fn phase1_advance_profile(
   1000         &self,
   1001         operation_id: String,
   1002     ) -> Result<FfiProfileStatusRecord, TeraAppError> {
   1003         let status = self
   1004             .inner
   1005             .phase1_advance_profile(decode_id(&operation_id, "invalid_operation_id")?)
   1006             .await?;
   1007         self.subscriptions
   1008             .notify(FfiRuntimeChangeKind::Profile, Some(operation_id));
   1009         Ok(status.into())
   1010     }
   1011 
   1012     pub async fn phase1_cancel_profile(
   1013         &self,
   1014         operation_id: String,
   1015         expected_revision: u64,
   1016     ) -> Result<FfiProfileStatusRecord, TeraAppError> {
   1017         let status = self
   1018             .inner
   1019             .phase1_cancel_profile(
   1020                 decode_id(&operation_id, "invalid_operation_id")?,
   1021                 expected_revision,
   1022             )
   1023             .await?;
   1024         self.subscriptions
   1025             .notify(FfiRuntimeChangeKind::Profile, Some(operation_id));
   1026         Ok(status.into())
   1027     }
   1028 
   1029     pub async fn phase1_save_revision_intent(
   1030         &self,
   1031         mut input: FfiRevisionInputRecord,
   1032     ) -> Result<FfiRevisionStatusRecord, TeraAppError> {
   1033         let target = input.target()?;
   1034         if input.replacement.identifier.is_none()
   1035             && matches!(
   1036                 input.replacement.command_type,
   1037                 crate::FfiAddCommandType::CreateEvent
   1038                     | crate::FfiAddCommandType::CreateFoodAvailability
   1039             )
   1040         {
   1041             input.replacement.identifier = Some(phase1_new_addressable_identifier());
   1042         }
   1043         let authored_at_unix_s = phase1_operation_now_unix_ms()? / 1_000;
   1044         let blossom = self.inner.sdk_blossom_slot().map_err(TeraAppError::from)?;
   1045         let (command, media, form) = input
   1046             .replacement
   1047             .command_media_and_form(authored_at_unix_s, blossom.as_ref())?;
   1048         let status = self
   1049             .inner
   1050             .prepare_revision_intent(
   1051                 decode_id(&input.request_id, "invalid_revision_request_id")?,
   1052                 Phase1ReviseIntent::new(target, command, media, form)?,
   1053             )
   1054             .await?;
   1055         let operation_id = hex::encode(status.replacement().draft().draft_id().as_bytes());
   1056         self.subscriptions
   1057             .notify(FfiRuntimeChangeKind::Drafts, Some(operation_id));
   1058         Ok(status.into())
   1059     }
   1060 
   1061     pub async fn phase1_revision_status(
   1062         &self,
   1063         operation_id: String,
   1064     ) -> Result<FfiRevisionStatusRecord, TeraAppError> {
   1065         self.inner
   1066             .phase1_revision_status(decode_id(&operation_id, "invalid_operation_id")?)
   1067             .await
   1068             .map(Into::into)
   1069             .map_err(Into::into)
   1070     }
   1071 
   1072     pub async fn phase1_advance_revision(
   1073         &self,
   1074         operation_id: String,
   1075     ) -> Result<FfiRevisionStatusRecord, TeraAppError> {
   1076         let status = self
   1077             .inner
   1078             .phase1_advance_revision(decode_id(&operation_id, "invalid_operation_id")?)
   1079             .await?;
   1080         self.subscriptions
   1081             .notify(FfiRuntimeChangeKind::Drafts, Some(operation_id));
   1082         Ok(status.into())
   1083     }
   1084 
   1085     pub async fn phase1_cancel_revision(
   1086         &self,
   1087         operation_id: String,
   1088     ) -> Result<FfiRevisionStatusRecord, TeraAppError> {
   1089         let status = self
   1090             .inner
   1091             .phase1_cancel_revision(decode_id(&operation_id, "invalid_operation_id")?)
   1092             .await?;
   1093         self.subscriptions
   1094             .notify(FfiRuntimeChangeKind::Drafts, Some(operation_id));
   1095         Ok(status.into())
   1096     }
   1097 
   1098     pub async fn phase1_retrieve_media(
   1099         &self,
   1100         context: FfiLocalNetworkRecord,
   1101         reference_fingerprint: String,
   1102         operation: Arc<FfiMediaOperation>,
   1103     ) -> Result<FfiVerifiedMediaArtifactRecord, TeraAppError> {
   1104         let context = self.local_network(context)?;
   1105         operation.claim()?;
   1106         let operation_id = operation.operation_id();
   1107         let settings = self.inner.phase1_settings().await?;
   1108         let policy = Phase1MediaCachePolicy::new(
   1109             settings.local_storage().media_cache_bytes(),
   1110             settings.local_storage().media_cache_artifacts(),
   1111         )
   1112         .map_err(|_| TeraAppError::invalid_argument("invalid_media_cache_policy"))?;
   1113         let artifact = self
   1114             .inner
   1115             .phase1_retrieve_media(
   1116                 &context,
   1117                 decode_reference_fingerprint(&reference_fingerprint)?,
   1118                 operation.id(),
   1119                 policy,
   1120                 operation.cancellation(),
   1121             )
   1122             .await
   1123             .map_err(|error| TeraAppError::from(error).with_operation_id(operation_id.clone()))?;
   1124         self.subscriptions.notify_context(
   1125             FfiRuntimeChangeKind::Media,
   1126             Some(&context),
   1127             Some(artifact.artifact_id().to_hex()),
   1128         );
   1129         Ok(FfiVerifiedMediaArtifactRecord::from_artifact(
   1130             artifact,
   1131             Some(operation_id),
   1132         ))
   1133     }
   1134 
   1135     pub async fn phase1_verified_media_artifact(
   1136         &self,
   1137         context: FfiLocalNetworkRecord,
   1138         artifact_id: String,
   1139     ) -> Result<Option<FfiVerifiedMediaArtifactRecord>, TeraAppError> {
   1140         let context = self.local_network(context)?;
   1141         self.inner
   1142             .phase1_verified_media_artifact(
   1143                 &context,
   1144                 decode_artifact_id(&artifact_id)?,
   1145                 phase1_operation_now_unix_ms()?,
   1146             )
   1147             .await
   1148             .map(|value| {
   1149                 value.map(|artifact| FfiVerifiedMediaArtifactRecord::from_artifact(artifact, None))
   1150             })
   1151             .map_err(Into::into)
   1152     }
   1153 
   1154     pub async fn phase1_media_cache_status(
   1155         &self,
   1156         context: FfiLocalNetworkRecord,
   1157     ) -> Result<FfiMediaCacheStatusRecord, TeraAppError> {
   1158         let context = self.local_network(context)?;
   1159         self.inner
   1160             .phase1_media_cache_status(&context)
   1161             .await
   1162             .map(Into::into)
   1163             .map_err(Into::into)
   1164     }
   1165 
   1166     pub async fn phase1_invalidate_media_artifact(
   1167         &self,
   1168         context: FfiLocalNetworkRecord,
   1169         artifact_id: String,
   1170     ) -> Result<bool, TeraAppError> {
   1171         let context = self.local_network(context)?;
   1172         let changed = self
   1173             .inner
   1174             .phase1_invalidate_media_artifact(&context, decode_artifact_id(&artifact_id)?)
   1175             .await?;
   1176         if changed {
   1177             self.subscriptions.notify_context(
   1178                 FfiRuntimeChangeKind::Media,
   1179                 Some(&context),
   1180                 Some(artifact_id),
   1181             );
   1182         }
   1183         Ok(changed)
   1184     }
   1185 
   1186     pub async fn phase1_invalidate_media_configuration(
   1187         &self,
   1188         context: FfiLocalNetworkRecord,
   1189         configuration_fingerprint: String,
   1190     ) -> Result<Vec<String>, TeraAppError> {
   1191         let context = self.local_network(context)?;
   1192         let removed = self
   1193             .inner
   1194             .phase1_invalidate_media_configuration(
   1195                 &context,
   1196                 decode_configuration(&configuration_fingerprint)?,
   1197             )
   1198             .await?;
   1199         self.subscriptions
   1200             .notify_context(FfiRuntimeChangeKind::Media, Some(&context), None);
   1201         Ok(removed.into_iter().map(|value| value.to_hex()).collect())
   1202     }
   1203 }
   1204 
   1205 impl TeraRuntime {
   1206     fn local_network(
   1207         &self,
   1208         context: FfiLocalNetworkRecord,
   1209     ) -> Result<tera_core::runtime::product_surface::LocalNetwork, TeraAppError> {
   1210         let profile = self.inner.sdk_relay_status()?.ok_or_else(|| {
   1211             TeraAppError::failure(
   1212                 "relay_profile_unavailable",
   1213                 "relay",
   1214                 true,
   1215                 &["configure_relay"],
   1216                 "The relay profile is unavailable.",
   1217             )
   1218         })?;
   1219         let relay_policy = match profile.profile.as_str() {
   1220             "public" => LocalNetworkRelayPolicy::Public,
   1221             "simulator_local" => LocalNetworkRelayPolicy::Simulator,
   1222             "device_development" => LocalNetworkRelayPolicy::Device,
   1223             _ => {
   1224                 return Err(TeraAppError::failure(
   1225                     "relay_profile_unsupported",
   1226                     "relay",
   1227                     false,
   1228                     &["configure_relay"],
   1229                     "The relay profile is unsupported.",
   1230                 ));
   1231             }
   1232         };
   1233         context.try_into_with_relay_policy(relay_policy)
   1234     }
   1235 }
   1236 
   1237 #[derive(Default)]
   1238 struct RuntimeOpenOptions {
   1239     local_backups: bool,
   1240     restore_guard: Option<tera_core::runtime::restore::ApplicationRestoreGuard>,
   1241 }
   1242 
   1243 async fn build_runtime(
   1244     application_support_directory: String,
   1245     public_key_hex: String,
   1246     source_generation_hex: String,
   1247     source_generation_created_at_unix_ms: u64,
   1248     protected_data: ProtectedDataAvailability,
   1249     host_signer: Option<Box<dyn TeraHostSigner>>,
   1250     options: RuntimeOpenOptions,
   1251 ) -> Result<TeraRuntime, TeraAppError> {
   1252     let store = tera_core::runtime::store::MobileUserStoreConfig::from_encoded(
   1253         application_support_directory,
   1254         public_key_hex.as_str(),
   1255         source_generation_hex.as_str(),
   1256         source_generation_created_at_unix_ms,
   1257         protected_data.into(),
   1258     )?;
   1259     let store = if options.local_backups {
   1260         store.with_local_backups()
   1261     } else {
   1262         store
   1263     };
   1264     let invalidations = tera_core::runtime::invalidation::RuntimeInvalidations::new(
   1265         store.public_key(),
   1266         store.source_generation(),
   1267         tera_core::runtime::invalidation::new_runtime_epoch(),
   1268     );
   1269     let has_host_signer = host_signer.is_some();
   1270     let mut builder = tera_core::runtime::builder::RuntimeBuilder::new(store);
   1271     if let Some(guard) = options.restore_guard {
   1272         builder = builder.restore_guard(guard);
   1273     }
   1274     if let Some(host_signer) = host_signer {
   1275         builder = builder.signer(Arc::new(HostSignerAdapter::new(host_signer)));
   1276     }
   1277     builder
   1278         .build()
   1279         .await
   1280         .map(|inner| TeraRuntime {
   1281             inner,
   1282             has_host_signer,
   1283             subscriptions: SubscriptionHub::new(invalidations),
   1284         })
   1285         .map_err(Into::into)
   1286 }