lib

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

commit 02aa4f43c1cb1022d1592379c00b86ea6b9af2b9
parent 18973e0aa62ae809b0495d5812b0c5ac8c7774ff
Author: triesap <tyson@radroots.org>
Date:   Mon, 10 Aug 2026 21:25:38 +0000

mobile: apply persisted network settings to runtime

Diffstat:
Mcrates/mobile_core/src/runtime/product_surface/settings.rs | 32++++++++++++++++++++++++++++----
Mcrates/mobile_core/src/runtime/sdk.rs | 27+++++++++++++++++++++++++++
Mcrates/mobile_ffi/src/runtime.rs | 15+++++++++++++++
Mcrates/mobile_ffi/tests/runtime_delegation.rs | 69+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
4 files changed, 133 insertions(+), 10 deletions(-)

diff --git a/crates/mobile_core/src/runtime/product_surface/settings.rs b/crates/mobile_core/src/runtime/product_surface/settings.rs @@ -803,6 +803,13 @@ impl MobileSettings { self.local_storage = local_storage; self } + + fn validate(&self) -> Result<(), SettingsError> { + if self.relays.environment() != self.blossom.environment() { + return Err(SettingsError::NetworkEnvironmentMismatch); + } + Ok(()) + } } #[derive(Clone, Debug, Eq, PartialEq)] @@ -816,6 +823,7 @@ impl ReplaceMobileSettings { if expected_revision == 0 || settings.revision != expected_revision { return Err(SettingsError::RevisionConflict); } + settings.validate()?; Ok(Self { expected_revision, settings, @@ -845,6 +853,8 @@ pub enum SettingsError { InvalidBlossomEndpoint, #[error("Blossom endpoint count is invalid")] InvalidBlossomEndpointCount, + #[error("relay and Blossom network environments do not match")] + NetworkEnvironmentMismatch, #[error("media cache byte quota is invalid")] InvalidMediaCacheBytes, #[error("media cache artifact quota is invalid")] @@ -872,6 +882,7 @@ impl SettingsError { Self::DuplicateRelayEndpoint => "duplicate_relay_endpoint", Self::InvalidBlossomEndpoint => "invalid_blossom_endpoint", Self::InvalidBlossomEndpointCount => "invalid_blossom_endpoint_count", + Self::NetworkEnvironmentMismatch => "network_environment_mismatch", Self::InvalidMediaCacheBytes => "invalid_media_cache_bytes", Self::InvalidMediaCacheArtifacts => "invalid_media_cache_artifacts", Self::RevisionConflict => "settings_revision_conflict", @@ -1129,14 +1140,16 @@ impl TryFrom<StoredSettingsV1> for MobileSettings { if value.schema_version != MOBILE_SETTINGS_SCHEMA_VERSION || value.revision == 0 { return Err(SettingsError::CorruptDocument); } - Ok(Self { + let settings = Self { revision: value.revision, identity: value.identity.try_into()?, relays: value.relays.try_into()?, blossom: value.blossom.try_into()?, media_network: value.media_network, local_storage: value.local_storage.try_into()?, - }) + }; + settings.validate()?; + Ok(settings) } } @@ -1147,7 +1160,7 @@ impl TryFrom<StoredSettingsV0> for MobileSettings { if value.schema_version != 0 || value.revision == 0 { return Err(SettingsError::CorruptDocument); } - Ok(Self { + let settings = Self { revision: value.revision, identity: value.identity.try_into()?, relays: value.relays.try_into()?, @@ -1161,7 +1174,9 @@ impl TryFrom<StoredSettingsV0> for MobileSettings { value.media_cache_bytes, value.media_cache_artifacts, )?, - }) + }; + settings.validate()?; + Ok(settings) } } @@ -1503,6 +1518,15 @@ mod tests { assert_eq!(conflict, SettingsError::RevisionConflict); } + #[test] + fn settings_reject_mixed_network_environments() { + let settings = MobileSettings::default().with_blossom(BlossomPreferences::simulator_default()); + assert_eq!( + ReplaceMobileSettings::new(settings.revision(), settings).unwrap_err(), + SettingsError::NetworkEnvironmentMismatch + ); + } + #[tokio::test] async fn atomic_identity_commands_persist_public_state_but_keep_unlock_process_local() { let runtime = RadrootsRuntime::test_memory().unwrap(); diff --git a/crates/mobile_core/src/runtime/sdk.rs b/crates/mobile_core/src/runtime/sdk.rs @@ -1,6 +1,7 @@ use radroots_sdk::capability::{Availability, Maturity}; use super::RadrootsRuntime; +use super::product_surface::{BlossomPreferences, RelayPreferences}; use crate::RadrootsAppError; #[derive(Clone, Debug)] @@ -155,6 +156,19 @@ impl RadrootsRuntime { .map_err(RadrootsAppError::from_sdk) } + /// Installs the exact validated relay preferences persisted by the mobile product. + #[cfg(feature = "mobile-social")] + pub fn configure_relay_preferences( + &self, + preferences: &RelayPreferences, + ) -> Result<(), RadrootsAppError> { + self.configure_relay_profile( + preferences + .sdk_profile() + .map_err(|error| RadrootsAppError::runtime(error.code()))?, + ) + } + /// Installs one canonical inert Blossom configuration without probing it. #[cfg(feature = "mobile-social")] pub fn configure_blossom( @@ -187,6 +201,19 @@ impl RadrootsRuntime { .map_err(RadrootsAppError::from_sdk) } + /// Installs the exact validated Blossom preferences persisted by the mobile product. + #[cfg(feature = "mobile-social")] + pub fn configure_blossom_preferences( + &self, + preferences: &BlossomPreferences, + ) -> Result<(), RadrootsAppError> { + self.configure_blossom_profile( + preferences + .sdk_profile() + .map_err(|error| RadrootsAppError::runtime(error.code()))?, + ) + } + /// Returns the configured adapter slot for Rust-owned media binding. #[cfg(feature = "mobile-social")] pub fn sdk_blossom_slot( diff --git a/crates/mobile_ffi/src/runtime.rs b/crates/mobile_ffi/src/runtime.rs @@ -843,6 +843,21 @@ impl RadrootsRuntime { .map_err(Into::into) } + pub async fn phase1_apply_settings_to_runtime( + &self, + ) -> Result<FfiMobileSettingsRecord, RadrootsAppError> { + let settings = self.inner.phase1_settings().await?; + self.inner + .configure_relay_preferences(settings.relays()) + .map_err(RadrootsAppError::from)?; + self.inner + .configure_blossom_preferences(settings.blossom()) + .map_err(RadrootsAppError::from)?; + self.subscriptions + .notify(FfiRuntimeChangeKind::Settings, None); + Ok((&settings).into()) + } + pub async fn phase1_replace_settings( &self, input: FfiReplaceSettingsRecord, diff --git a/crates/mobile_ffi/tests/runtime_delegation.rs b/crates/mobile_ffi/tests/runtime_delegation.rs @@ -1,11 +1,14 @@ use radroots_mobile_ffi::{ - FfiAddCommandType, FfiAddDraftInput, FfiBlossomEndpointAuthority, FfiBlossomHostKind, + FfiAddCommandType, FfiAddDraftInput, FfiBlossomAuthorityPreference, + FfiBlossomEndpointAuthority, FfiBlossomHostKind, FfiBlossomPreferencesRecord, FfiBlossomUploadIntent, FfiCancellationPolicy, FfiDraftKind, FfiIdentityCommandKind, - FfiIdentityCommandRecord, FfiIdentityLockState, FfiLocalNetworkRecord, FfiOutboxState, - FfiPreparedMediaInput, FfiProfileMetadataInputRecord, FfiQueuePolicyRecord, - FfiRelayAccessRecord, FfiRelaySatisfaction, FfiRetractionDraftInput, FfiRevisionInputRecord, - FfiRevisionPhase, FfiTodayCardType, FfiTodayProjectionUpdate, MOBILE_FFI_SCHEMA_VERSION, - RadrootsAppError, + FfiIdentityCommandRecord, FfiIdentityLockState, FfiLocalNetworkRecord, + FfiLocalStoragePolicyRecord, FfiMediaNetworkPolicyRecord, FfiMobileNetworkEnvironment, + FfiOutboxState, FfiPreparedMediaInput, FfiProfileMetadataInputRecord, FfiQueuePolicyRecord, + FfiRelayAccessPreference, FfiRelayAccessRecord, FfiRelayPreferenceRecord, + FfiRelayPreferencesRecord, FfiRelaySatisfaction, FfiReplaceSettingsRecord, + FfiRetractionDraftInput, FfiRevisionInputRecord, FfiRevisionPhase, FfiTodayCardType, + FfiTodayProjectionUpdate, MOBILE_FFI_SCHEMA_VERSION, RadrootsAppError, }; mod support; @@ -60,6 +63,60 @@ async fn native_boundary_delegates_the_complete_core_surface() { .is_err() ); + let settings = runtime.phase1_settings().await.expect("settings"); + runtime + .phase1_replace_settings(FfiReplaceSettingsRecord { + schema_version: MOBILE_FFI_SCHEMA_VERSION, + expected_revision: settings.revision, + relays: FfiRelayPreferencesRecord { + schema_version: MOBILE_FFI_SCHEMA_VERSION, + environment: FfiMobileNetworkEnvironment::Public, + endpoints: vec![ + FfiRelayPreferenceRecord { + schema_version: MOBILE_FFI_SCHEMA_VERSION, + url: "wss://read.example".to_owned(), + access: FfiRelayAccessPreference::ReadOnly, + }, + FfiRelayPreferenceRecord { + schema_version: MOBILE_FFI_SCHEMA_VERSION, + url: "wss://write.example".to_owned(), + access: FfiRelayAccessPreference::ReadWrite, + }, + ], + }, + blossom: FfiBlossomPreferencesRecord { + schema_version: MOBILE_FFI_SCHEMA_VERSION, + environment: FfiMobileNetworkEnvironment::Public, + authority: FfiBlossomAuthorityPreference::PublicWebPki, + primary_origin: "https://media.example".to_owned(), + fallback_origins: vec![], + }, + media_network: FfiMediaNetworkPolicyRecord { + schema_version: MOBILE_FFI_SCHEMA_VERSION, + allow_cellular_downloads: true, + allow_cellular_uploads: true, + allow_background_transfers: true, + }, + local_storage: FfiLocalStoragePolicyRecord { + schema_version: MOBILE_FFI_SCHEMA_VERSION, + media_cache_bytes: 256 * 1024 * 1024, + media_cache_artifacts: 1024, + }, + }) + .await + .expect("replace settings"); + runtime + .phase1_apply_settings_to_runtime() + .await + .expect("apply settings"); + let applied = runtime + .sdk_relay_status() + .expect("relay status") + .expect("applied settings profile"); + assert_eq!(applied.relays.len(), 2); + assert_eq!(applied.relays[0].access, FfiRelayAccessRecord::ReadOnly); + assert_eq!(applied.relays[1].access, FfiRelayAccessRecord::ReadWrite); + runtime .configure_simulator_relays(vec!["ws://127.0.0.1:7447".to_owned()]) .expect("simulator relays");