sdk

Radroots SDK and bindings
git clone https://radroots.dev/git/sdk.git
Log | Files | Refs | README

commit a752e21babfaf382383b5e1f3b7005d7890d64a0
parent 8983bedf79f8b558dee05e643eff61fa54746271
Author: triesap <tyson@radroots.org>
Date:   Mon,  3 Aug 2026 13:36:36 +0000

ffi: wrap shared sdk engine

- Add a private UniFFI crate that directly owns the shared SDK client.
- Expose versioned capability, error, and explicit lifecycle DTOs for mobile hosts.
- Keep keychain, background execution, scheduling, and presentation host-owned.
- Verify locked metadata, all targets, lifecycle tests, Clippy, and architecture policy.

Diffstat:
MCargo.lock | 333++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
MCargo.toml | 4++++
Mcontracts/releases/publication.toml | 2+-
Acrates/ffi/Cargo.toml | 26++++++++++++++++++++++++++
Acrates/ffi/README.md | 10++++++++++
Acrates/ffi/src/lib.rs | 173+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 545 insertions(+), 3 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -57,6 +57,48 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] +name = "askama" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4744ed2eef2645831b441d8f5459689ade2ab27c854488fbab1fbe94fce1a7" +dependencies = [ + "askama_derive", + "itoa", + "percent-encoding", + "serde", + "serde_json", +] + +[[package]] +name = "askama_derive" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d661e0f57be36a5c14c48f78d09011e67e0cb618f269cca9f2fd8d15b68c46ac" +dependencies = [ + "askama_parser", + "basic-toml", + "memchr", + "proc-macro2", + "quote", + "rustc-hash", + "serde", + "serde_derive", + "syn", +] + +[[package]] +name = "askama_parser" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf315ce6524c857bb129ff794935cf6d42c82a6cff60526fe2a63593de4d0d4f" +dependencies = [ + "memchr", + "serde", + "serde_derive", + "winnow", +] + +[[package]] name = "async-trait" version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -144,6 +186,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" [[package]] +name = "basic-toml" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a" +dependencies = [ + "serde", +] + +[[package]] name = "bech32" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -214,6 +265,38 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] +name = "camino" +version = "1.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb1307f12aa967b5a58416e87b3653360e0fd614a016b6e970db08fecbb1b80d" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] name = "cast" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -477,7 +560,7 @@ dependencies = [ "serde_json", "sha2", "syn", - "toml", + "toml 0.8.23", ] [[package]] @@ -631,6 +714,15 @@ dependencies = [ ] [[package]] +name = "fs-err" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41" +dependencies = [ + "autocfg", +] + +[[package]] name = "fs2" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -778,6 +870,12 @@ dependencies = [ ] [[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + +[[package]] name = "gloo-timers" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -790,6 +888,17 @@ dependencies = [ ] [[package]] +name = "goblin" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b363a30c165f666402fe6a3024d3bec7ebc898f96a4a23bd1c99f8dbf3f4f47" +dependencies = [ + "log", + "plain", + "scroll", +] + +[[package]] name = "group" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1270,6 +1379,12 @@ dependencies = [ ] [[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] name = "miniz_oxide" version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1297,6 +1412,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0efe882e02d206d8d279c20eb40e03baf7cb5136a1476dc084a324fbc3ec42d" [[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] name = "nostr" version = "0.44.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1481,6 +1606,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] name = "poly1305" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1902,6 +2033,16 @@ dependencies = [ ] [[package]] +name = "radroots_sdk_ffi" +version = "0.1.0-alpha" +dependencies = [ + "radroots_sdk", + "thiserror 2.0.18", + "tokio", + "uniffi", +] + +[[package]] name = "radroots_sdk_sql_wasm_runtime" version = "0.1.0-alpha" dependencies = [ @@ -1932,7 +2073,7 @@ dependencies = [ "syn", "tar", "tempfile", - "toml", + "toml 0.8.23", ] [[package]] @@ -2291,6 +2432,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] +name = "scroll" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ab8598aa408498679922eff7fa985c25d58a90771bd6be794434c5277eab1a6" +dependencies = [ + "scroll_derive", +] + +[[package]] +name = "scroll_derive" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1783eabc414609e28a5ba76aee5ddd52199f7107a0b24c2e9746a1ecc34a683d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] name = "scrypt" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2340,6 +2501,10 @@ name = "semver" version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] [[package]] name = "serde" @@ -2451,6 +2616,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" [[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] name = "slab" version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2463,6 +2634,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] +name = "smawk" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8e2fb0f499abb4d162f2bedad68f5ef91a1682b5a03596ddb67efd37768d100" + +[[package]] name = "socket2" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2604,6 +2781,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] name = "subtle" version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2665,6 +2848,15 @@ dependencies = [ ] [[package]] +name = "textwrap" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" +dependencies = [ + "smawk", +] + +[[package]] name = "thiserror" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2806,6 +2998,15 @@ dependencies = [ [[package]] name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "toml" version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" @@ -2987,6 +3188,125 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] +name = "uniffi" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3291800a6b06569f7d3e15bdb6dc235e0f0c8bd3eb07177f430057feb076415f" +dependencies = [ + "anyhow", + "cargo_metadata", + "uniffi_bindgen", + "uniffi_core", + "uniffi_macros", + "uniffi_pipeline", +] + +[[package]] +name = "uniffi_bindgen" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a04b99fa7796eaaa7b87976a0dbdd1178dc1ee702ea00aca2642003aef9b669e" +dependencies = [ + "anyhow", + "askama", + "camino", + "cargo_metadata", + "fs-err", + "glob", + "goblin", + "heck", + "indexmap", + "once_cell", + "serde", + "tempfile", + "textwrap", + "toml 0.5.11", + "uniffi_internal_macros", + "uniffi_meta", + "uniffi_pipeline", + "uniffi_udl", +] + +[[package]] +name = "uniffi_core" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38a9a27529ccff732f8efddb831b65b1e07f7dea3fd4cacd4a35a8c4b253b98" +dependencies = [ + "anyhow", + "bytes", + "once_cell", + "static_assertions", +] + +[[package]] +name = "uniffi_internal_macros" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09acd2ce09c777dd65ee97c251d33c8a972afc04873f1e3b21eb3492ade16933" +dependencies = [ + "anyhow", + "indexmap", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "uniffi_macros" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5596f178c4f7aafa1a501c4e0b96236a96bc2ef92bdb453d83e609dad0040152" +dependencies = [ + "camino", + "fs-err", + "once_cell", + "proc-macro2", + "quote", + "serde", + "syn", + "toml 0.5.11", + "uniffi_meta", +] + +[[package]] +name = "uniffi_meta" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "beadc1f460eb2e209263c49c4f5b19e9a02e00a3b2b393f78ad10d766346ecff" +dependencies = [ + "anyhow", + "siphasher", + "uniffi_internal_macros", + "uniffi_pipeline", +] + +[[package]] +name = "uniffi_pipeline" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd76b3ac8a2d964ca9fce7df21c755afb4c77b054a85ad7a029ad179cc5abb8a" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "tempfile", + "uniffi_internal_macros", +] + +[[package]] +name = "uniffi_udl" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4319cf905911d70d5b97ce0f46f101619a22e9a189c8c46d797a9955e9233716" +dependencies = [ + "anyhow", + "textwrap", + "uniffi_meta", + "weedle2", +] + +[[package]] name = "universal-hash" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3273,6 +3593,15 @@ dependencies = [ ] [[package]] +name = "weedle2" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "998d2c24ec099a87daf9467808859f9d82b61f1d9c9701251aea037f514eae0e" +dependencies = [ + "nom", +] + +[[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/Cargo.toml b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "crates/event_codec_wasm", "crates/event_bindings", "crates/event_index_bindings", + "crates/ffi", "crates/identity_bindings", "crates/replica_store_wasm", "crates/replica_schema_bindings", @@ -54,6 +55,7 @@ radroots_event_store = { version = "=0.1.0-alpha", default-features = false } radroots_event = { package = "radroots_event", version = "=0.1.0-alpha", default-features = false } radroots_event_codec = { package = "radroots_event_codec", version = "=0.1.0-alpha", default-features = false } radroots_event_index = { version = "=0.1.0-alpha", default-features = false } +radroots_sdk_ffi = { path = "crates/ffi", version = "=0.1.0-alpha" } radroots_geocoder = { version = "=0.1.0-alpha" } radroots_geonames = { package = "radroots_geonames", version = "=0.1.0-alpha", default-features = false } radroots_identity = { package = "radroots_identity", version = "=0.1.0-alpha", default-features = false, features = [ @@ -106,8 +108,10 @@ serde-wasm-bindgen = { version = "0.6" } sha2 = { version = "0.10", default-features = false } sqlx = { version = "0.9.0", default-features = false } tempfile = { version = "3" } +thiserror = { version = "2" } tokio = { version = "1" } tokio-tungstenite = "0.26.2" uuid = { version = "1.22.0", features = ["v4", "v7"] } +uniffi = { version = "0.29.4" } wasm-bindgen = { version = "0.2" } wasm-bindgen-test = { version = "0.3" } diff --git a/contracts/releases/publication.toml b/contracts/releases/publication.toml @@ -46,7 +46,7 @@ external_packages = [ ] [workspace_classification] -private = [] +private = ["radroots_sdk_ffi"] build_codegen = [ "radroots_core_bindings", "radroots_event_bindings", diff --git a/crates/ffi/Cargo.toml b/crates/ffi/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "radroots_sdk_ffi" +publish = false +version.workspace = true +edition.workspace = true +authors.workspace = true +rust-version.workspace = true +license.workspace = true +description = "Private UniFFI wrapper over the shared Radroots SDK engine" +repository.workspace = true +homepage.workspace = true +readme = "README.md" + +[lib] +crate-type = ["lib", "staticlib", "cdylib"] + +[dependencies] +radroots_sdk = { workspace = true, features = ["memory"] } +thiserror = { workspace = true } +uniffi = { workspace = true } + +[dev-dependencies] +tokio = { workspace = true, features = ["macros", "rt"] } + +[lints] +workspace = true diff --git a/crates/ffi/README.md b/crates/ffi/README.md @@ -0,0 +1,10 @@ +# radroots_sdk_ffi + +Private UniFFI bindings over the host-neutral `radroots_sdk` engine. The V1 +surface exposes deterministic memory-client construction, versioned capability +DTOs, secret-safe errors, and explicit asynchronous close. It does not own +keychain prompts, background execution, application scheduling, or presentation +DTOs; those remain mobile-host responsibilities. + +This Rust build crate is never published. Generated Swift and Kotlin artifacts +are qualified independently from the public 19-crate Rust inventory. diff --git a/crates/ffi/src/lib.rs b/crates/ffi/src/lib.rs @@ -0,0 +1,173 @@ +//! Private mobile FFI over the shared SDK engine. + +#![forbid(unsafe_code)] +#![doc = include_str!("../README.md")] + +use std::sync::Arc; + +use radroots_sdk::{Client, ClientBuilder}; + +uniffi::setup_scaffolding!("radroots_sdk"); + +/// Generation-1 mobile DTOs. +pub mod v1 { + use super::*; + + /// Stable capability maturity independent of runtime availability. + #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)] + pub enum CapabilityMaturity { + Stable, + Preview, + Experimental, + } + + /// Current side-effect-free capability availability. + #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)] + pub enum CapabilityAvailability { + Available, + Degraded, + Unavailable, + Unsupported, + } + + /// Versioned, presentation-independent capability observation. + #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)] + pub struct CapabilityStatus { + pub id: String, + pub compiled: bool, + pub configured: bool, + pub availability: CapabilityAvailability, + pub maturity: CapabilityMaturity, + } + + /// Secret-safe SDK failure at the language boundary. + #[derive(Debug, thiserror::Error, uniffi::Error)] + pub enum Error { + #[error("SDK {code}: {message}")] + Sdk { + code: String, + message: String, + retryable: bool, + }, + } + + impl From<radroots_sdk::Error> for Error { + fn from(error: radroots_sdk::Error) -> Self { + let descriptor = error.descriptor(); + Self::Sdk { + code: descriptor.code().as_str().to_owned(), + message: descriptor.message().to_owned(), + retryable: descriptor.retryable(), + } + } + } + + /// Thread-safe mobile handle delegating lifecycle to [`radroots_sdk::Client`]. + #[derive(uniffi::Object)] + pub struct MobileClient { + client: Client, + } + + #[uniffi::export] + impl MobileClient { + /// Creates a deterministic, local-only memory client without I/O. + #[uniffi::constructor] + pub fn memory() -> Result<Arc<Self>, Error> { + let client = ClientBuilder::memory_default().build()?; + Ok(Arc::new(Self { client })) + } + + /// Returns stable capability DTOs without probing host resources. + pub fn capabilities(&self) -> Vec<CapabilityStatus> { + self.client + .capabilities() + .iter() + .copied() + .map(CapabilityStatus::from) + .collect() + } + + /// Returns whether explicit SDK close completed across all clones. + pub fn is_closed(&self) -> bool { + self.client.is_closed() + } + + /// Explicitly closes SDK-owned resources without installing a runtime. + pub async fn close(&self) -> Result<(), Error> { + self.client.close().await.map_err(Error::from) + } + } + + impl From<radroots_sdk::capability::CapabilityStatus> for CapabilityStatus { + fn from(status: radroots_sdk::capability::CapabilityStatus) -> Self { + Self { + id: status.id().as_str().to_owned(), + compiled: status.is_compiled(), + configured: status.is_configured(), + availability: match status.availability() { + radroots_sdk::capability::Availability::Available => { + CapabilityAvailability::Available + } + radroots_sdk::capability::Availability::Degraded => { + CapabilityAvailability::Degraded + } + radroots_sdk::capability::Availability::Unavailable => { + CapabilityAvailability::Unavailable + } + radroots_sdk::capability::Availability::Unsupported => { + CapabilityAvailability::Unsupported + } + }, + maturity: match status.maturity() { + radroots_sdk::capability::Maturity::Stable => CapabilityMaturity::Stable, + radroots_sdk::capability::Maturity::Preview => CapabilityMaturity::Preview, + radroots_sdk::capability::Maturity::Experimental => { + CapabilityMaturity::Experimental + } + }, + } + } + } +} + +#[cfg(test)] +mod tests { + use super::v1::{CapabilityAvailability, Error, MobileClient}; + + fn assert_send_sync<T: Send + Sync>() {} + + #[tokio::test(flavor = "current_thread")] + async fn memory_client_delegates_capabilities_and_lifecycle() { + assert_send_sync::<MobileClient>(); + let client = MobileClient::memory().expect("memory client"); + let capabilities = client.capabilities(); + let storage = capabilities + .iter() + .find(|status| status.id == "storage.canonical") + .expect("canonical storage"); + assert!(storage.compiled); + assert!(storage.configured); + assert_eq!(storage.availability, CapabilityAvailability::Available); + assert!(!client.is_closed()); + + client.close().await.expect("close"); + assert!(client.is_closed()); + client.close().await.expect("idempotent close"); + } + + #[test] + fn sdk_error_mapping_is_versioned_and_secret_safe() { + let native = radroots_sdk::ClientBuilder::new() + .build() + .expect_err("missing storage"); + let error = Error::from(native); + let Error::Sdk { + code, + message, + retryable, + } = error; + assert_eq!(code, "missing_storage"); + assert_eq!(message, "SDK storage capability is not configured"); + assert!(!retryable); + } +}