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 }