field_ios

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

commit 8c8c37ddcb705f0b7b1c5408a2c205d42e4b924e
parent fce2a6d110daa98b7a3e5501f3005c71530ee82c
Author: triesap <tyson@radroots.org>
Date:   Sun, 16 Aug 2026 20:33:19 +0000

transport-nostr: require explicit relay endpoints

- Intent: Require every relay URL, destination policy, and access mode to be supplied explicitly.
- Why: Implicit profiles could inject or infer network authority outside the caller-owned configuration boundary.
- Verification: Transport, SDK, and mobile tests, warnings-denied Clippy and Rustdoc, full workspace check, and exact API regeneration passed through extbuild.
- Risk: This is an intentional breaking constructor change; active Rust consumers are migrated while terminal generated consumers remain governed by the later RCLD checkpoint.

Diffstat:
Mcore/crates/tera_core/src/runtime/builder.rs | 12++++++++++--
Mcore/crates/tera_core/src/runtime/product_surface/outbox.rs | 30++++++++++++++++++++----------
Mcore/crates/tera_core/src/runtime/product_surface/settings.rs | 26++++++++++++++++++--------
Mcore/crates/tera_core/src/runtime/sdk.rs | 44+++++++++++++++++++++++++++++++++++---------
4 files changed, 83 insertions(+), 29 deletions(-)

diff --git a/core/crates/tera_core/src/runtime/builder.rs b/core/crates/tera_core/src/runtime/builder.rs @@ -20,8 +20,16 @@ impl RuntimeBuilder { #[cfg(feature = "mobile-social")] signer: None, #[cfg(feature = "mobile-social")] - relay_profile: radroots_sdk::transport::RelayProfile::public(Vec::<String>::new()) - .expect("bundled public relay profile is valid"), + relay_profile: radroots_sdk::transport::RelayProfile::explicit( + radroots_sdk::transport::RelayProfileKind::Public, + [radroots_sdk::transport::RelayEndpoint::new( + "wss://radroots.org", + radroots_sdk::transport::RelayUrlPolicy::Public, + radroots_sdk::transport::RelayAccess::ReadWrite, + ) + .expect("bundled public relay endpoint is valid")], + ) + .expect("bundled public relay profile is valid"), #[cfg(feature = "mobile-social")] blossom_config: None, } diff --git a/core/crates/tera_core/src/runtime/product_surface/outbox.rs b/core/crates/tera_core/src/runtime/product_surface/outbox.rs @@ -3420,14 +3420,18 @@ mod tests { let profile = radroots_sdk::transport::RelayProfile::explicit( radroots_sdk::transport::RelayProfileKind::Public, [ - ( + radroots_sdk::transport::RelayEndpoint::new( "wss://read.example", + radroots_sdk::transport::RelayUrlPolicy::Public, radroots_sdk::transport::RelayAccess::ReadOnly, - ), - ( + ) + .unwrap(), + radroots_sdk::transport::RelayEndpoint::new( "wss://write.example", + radroots_sdk::transport::RelayUrlPolicy::Public, radroots_sdk::transport::RelayAccess::ReadWrite, - ), + ) + .unwrap(), ], ) .unwrap(); @@ -3475,10 +3479,12 @@ mod tests { async fn profile_metadata_uses_the_durable_outbox_with_stable_operation_identity() { let profile = radroots_sdk::transport::RelayProfile::explicit( radroots_sdk::transport::RelayProfileKind::Public, - [( + [radroots_sdk::transport::RelayEndpoint::new( "wss://write.example", + radroots_sdk::transport::RelayUrlPolicy::Public, radroots_sdk::transport::RelayAccess::ReadWrite, - )], + ) + .unwrap()], ) .unwrap(); let runtime = profiled_runtime(profile); @@ -3531,10 +3537,12 @@ mod tests { async fn add_queue_intent_fails_closed_without_a_writable_relay() { let profile = radroots_sdk::transport::RelayProfile::explicit( radroots_sdk::transport::RelayProfileKind::Public, - [( + [radroots_sdk::transport::RelayEndpoint::new( "wss://read.example", + radroots_sdk::transport::RelayUrlPolicy::Public, radroots_sdk::transport::RelayAccess::ReadOnly, - )], + ) + .unwrap()], ) .unwrap(); let runtime = profiled_runtime(profile); @@ -3733,10 +3741,12 @@ mod tests { async fn kind_one_revision_never_creates_retraction_before_replacement_acceptance() { let profile = radroots_sdk::transport::RelayProfile::explicit( radroots_sdk::transport::RelayProfileKind::Public, - [( + [radroots_sdk::transport::RelayEndpoint::new( "wss://offline.example", + radroots_sdk::transport::RelayUrlPolicy::Public, radroots_sdk::transport::RelayAccess::ReadWrite, - )], + ) + .unwrap()], ) .unwrap(); let signer = radroots_nostr::signing::LocalSigner::new( diff --git a/core/crates/tera_core/src/runtime/product_surface/settings.rs b/core/crates/tera_core/src/runtime/product_surface/settings.rs @@ -15,7 +15,7 @@ use sha2::{Digest, Sha256}; use super::super::RadrootsRuntime; -pub use radroots_sdk::transport::DEFAULT_PUBLIC_RELAY; +pub const DEFAULT_PUBLIC_RELAY: &str = "wss://radroots.org"; pub const MOBILE_SETTINGS_SCHEMA_VERSION: u16 = 1; pub const DEFAULT_PUBLIC_BLOSSOM_ORIGIN: &str = "https://blossom.radroots.org"; @@ -437,9 +437,17 @@ impl RelayPreferences { radroots_sdk::transport::RelayProfileKind::Device } }; - radroots_sdk::transport::RelayProfile::explicit( - kind, - self.endpoints.iter().map(|endpoint| { + let policy = match self.environment { + MobileNetworkEnvironment::Public => radroots_sdk::transport::RelayUrlPolicy::Public, + MobileNetworkEnvironment::Simulator => radroots_sdk::transport::RelayUrlPolicy::Local, + MobileNetworkEnvironment::PhysicalDevice => { + radroots_sdk::transport::RelayUrlPolicy::PrivateNetwork + } + }; + let endpoints = self + .endpoints + .iter() + .map(|endpoint| { let access = match endpoint.access { RelayAccessPreference::ReadOnly => { radroots_sdk::transport::RelayAccess::ReadOnly @@ -448,10 +456,12 @@ impl RelayPreferences { radroots_sdk::transport::RelayAccess::ReadWrite } }; - (endpoint.url.as_str(), access) - }), - ) - .map_err(|_| SettingsError::InvalidRelayEndpoint) + radroots_sdk::transport::RelayEndpoint::new(endpoint.url.as_str(), policy, access) + }) + .collect::<Result<Vec<_>, _>>() + .map_err(|_| SettingsError::InvalidRelayEndpoint)?; + radroots_sdk::transport::RelayProfile::explicit(kind, endpoints) + .map_err(|_| SettingsError::InvalidRelayEndpoint) } } diff --git a/core/crates/tera_core/src/runtime/sdk.rs b/core/crates/tera_core/src/runtime/sdk.rs @@ -117,9 +117,10 @@ impl RadrootsRuntime { &self, writable_relays: Vec<String>, ) -> Result<(), RadrootsAppError> { - self.configure_relay_profile( - radroots_sdk::transport::RelayProfile::public(writable_relays) - .map_err(|error| RadrootsAppError::runtime(error.to_string()))?, + self.configure_relay_endpoints( + radroots_sdk::transport::RelayProfileKind::Public, + radroots_sdk::transport::RelayUrlPolicy::Public, + writable_relays, ) } @@ -129,9 +130,10 @@ impl RadrootsRuntime { &self, loopback_relays: Vec<String>, ) -> Result<(), RadrootsAppError> { - self.configure_relay_profile( - radroots_sdk::transport::RelayProfile::simulator(loopback_relays) - .map_err(|error| RadrootsAppError::runtime(error.to_string()))?, + self.configure_relay_endpoints( + radroots_sdk::transport::RelayProfileKind::Simulator, + radroots_sdk::transport::RelayUrlPolicy::Local, + loopback_relays, ) } @@ -141,13 +143,37 @@ impl RadrootsRuntime { &self, writable_relays: Vec<String>, ) -> Result<(), RadrootsAppError> { - self.configure_relay_profile( - radroots_sdk::transport::RelayProfile::device(writable_relays) - .map_err(|error| RadrootsAppError::runtime(error.to_string()))?, + self.configure_relay_endpoints( + radroots_sdk::transport::RelayProfileKind::Device, + radroots_sdk::transport::RelayUrlPolicy::PrivateNetwork, + writable_relays, ) } #[cfg(feature = "mobile-social")] + fn configure_relay_endpoints( + &self, + kind: radroots_sdk::transport::RelayProfileKind, + policy: radroots_sdk::transport::RelayUrlPolicy, + relays: Vec<String>, + ) -> Result<(), RadrootsAppError> { + let endpoints = relays + .into_iter() + .map(|relay| { + radroots_sdk::transport::RelayEndpoint::new( + relay, + policy, + radroots_sdk::transport::RelayAccess::ReadWrite, + ) + }) + .collect::<Result<Vec<_>, _>>() + .map_err(|error| RadrootsAppError::runtime(error.to_string()))?; + let profile = radroots_sdk::transport::RelayProfile::explicit(kind, endpoints) + .map_err(|error| RadrootsAppError::runtime(error.to_string()))?; + self.configure_relay_profile(profile) + } + + #[cfg(feature = "mobile-social")] fn configure_relay_profile( &self, profile: radroots_sdk::transport::RelayProfile,