field_lib

Cross-platform Rust runtime for Radroots iOS and Android apps
git clone https://radroots.dev/git/field_lib.git
Log | Files | Refs | README | LICENSE

commit 383d56b6bf0887610c878f5e000d2b6323b87fe8
parent 9888b891ad9901962f83678c8d8aa9261d0d8a13
Author: triesap <tyson@radroots.org>
Date:   Thu, 11 Jun 2026 23:47:05 -0700

field: replace auth session ffi with nostr identity

- remove HTTP auth session state and client APIs from the runtime
- expose local Nostr identity records and snapshots through UniFFI
- rename exported identity management methods away from accounts terminology
- drop the unused reqwest dependency tree

Diffstat:
MCargo.lock | 302------------------------------------------------------------------------------
MCargo.toml | 1-
Mcrates/field_core/Cargo.toml | 1-
Dcrates/field_core/src/runtime/auth_session.rs | 1044-------------------------------------------------------------------------------
Mcrates/field_core/src/runtime/key_management.rs | 125++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
Mcrates/field_core/src/runtime/mod.rs | 6------
Mcrates/field_core/tests/no_nostr_runtime.rs | 20+++++++++++---------
7 files changed, 122 insertions(+), 1377 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -174,12 +174,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef49f5882e4b6afaac09ad239a4f8c70a24b8f2b0897edb1f706008efd109cf4" [[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - -[[package]] name = "autocfg" version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -343,12 +337,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - -[[package]] name = "chacha20" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -782,11 +770,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", - "js-sys", "libc", "r-efi 5.3.0", "wasip2", - "wasm-bindgen", ] [[package]] @@ -906,94 +892,12 @@ dependencies = [ ] [[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http", -] - -[[package]] -name = "http-body-util" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" -dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", - "pin-project-lite", -] - -[[package]] name = "httparse" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] -name = "hyper" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" -dependencies = [ - "atomic-waker", - "bytes", - "futures-channel", - "futures-core", - "http", - "http-body", - "httparse", - "itoa", - "pin-project-lite", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-rustls" -version = "0.27.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" -dependencies = [ - "http", - "hyper", - "hyper-util", - "rustls", - "tokio", - "tokio-rustls", - "tower-service", - "webpki-roots 1.0.7", -] - -[[package]] -name = "hyper-util" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-channel", - "futures-util", - "http", - "http-body", - "hyper", - "ipnet", - "libc", - "percent-encoding", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", -] - -[[package]] name = "iana-time-zone" version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1161,12 +1065,6 @@ dependencies = [ ] [[package]] -name = "ipnet" -version = "2.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" - -[[package]] name = "itoa" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1246,12 +1144,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" [[package]] -name = "lru-slab" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" - -[[package]] name = "matchers" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1572,61 +1464,6 @@ dependencies = [ ] [[package]] -name = "quinn" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" -dependencies = [ - "bytes", - "cfg_aliases", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash", - "rustls", - "socket2", - "thiserror 2.0.18", - "tokio", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-proto" -version = "0.11.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" -dependencies = [ - "bytes", - "getrandom 0.3.4", - "lru-slab", - "rand 0.9.4", - "ring", - "rustc-hash", - "rustls", - "rustls-pki-types", - "slab", - "thiserror 2.0.18", - "tinyvec", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-udp" -version = "0.5.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" -dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2", - "tracing", - "windows-sys 0.52.0", -] - -[[package]] name = "quote" version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1688,7 +1525,6 @@ dependencies = [ "radroots_net", "radroots_nostr", "radroots_trade", - "reqwest", "serde", "serde_json", "thiserror 1.0.69", @@ -1972,46 +1808,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] -name = "reqwest" -version = "0.12.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-util", - "js-sys", - "log", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls", - "rustls-pki-types", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tokio-rustls", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "webpki-roots 1.0.7", -] - -[[package]] name = "ring" version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2108,7 +1904,6 @@ version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ - "web-time", "zeroize", ] @@ -2130,12 +1925,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] -name = "ryu" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" - -[[package]] name = "salsa20" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2268,18 +2057,6 @@ dependencies = [ ] [[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - -[[package]] name = "sha1" version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2402,15 +2179,6 @@ dependencies = [ ] [[package]] -name = "sync_wrapper" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] - -[[package]] name = "synstructure" version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2673,51 +2441,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] -name = "tower" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper", - "tokio", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-http" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" -dependencies = [ - "bitflags", - "bytes", - "futures-util", - "http", - "http-body", - "pin-project-lite", - "tower", - "tower-layer", - "tower-service", - "url", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - -[[package]] name = "tracing" version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2792,12 +2515,6 @@ dependencies = [ ] [[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - -[[package]] name = "tungstenite" version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3052,15 +2769,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - -[[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3185,16 +2893,6 @@ dependencies = [ ] [[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] name = "webpki-roots" version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/Cargo.toml b/Cargo.toml @@ -37,5 +37,4 @@ tracing = { version = "0.1" } tracing-subscriber = { version = "0.3" } uniffi = { version = "0.29.4" } uniffi_build = { version = "0.29.4" } -reqwest = { version = "0.12", default-features = false } wasm-bindgen = { version = "0.2" } diff --git a/crates/field_core/Cargo.toml b/crates/field_core/Cargo.toml @@ -47,7 +47,6 @@ thiserror = { workspace = true } tracing = { workspace = true } uniffi = { workspace = true } tokio = { workspace = true } -reqwest = { workspace = true, features = ["blocking", "json", "rustls-tls"] } [dev-dependencies] tracing-subscriber = { workspace = true } diff --git a/crates/field_core/src/runtime/auth_session.rs b/crates/field_core/src/runtime/auth_session.rs @@ -1,1044 +0,0 @@ -use std::time::Duration; - -use reqwest::{Url, blocking::Client}; -use serde::{Deserialize, Serialize, de::DeserializeOwned}; - -use super::{ - RadrootsRuntime, - nostr::{NostrConnectionStatus, NostrLight}, -}; -use crate::RadrootsAppError; - -const AUTH_HTTP_TIMEOUT_SECONDS: u64 = 15; - -#[derive(uniffi::Enum, Debug, Clone, Copy, PartialEq, Eq)] -pub enum FieldSessionPhase { - SignedOut, - ChallengePending, - Authenticated, - Expired, - Revoked, -} - -impl Default for FieldSessionPhase { - fn default() -> Self { - Self::SignedOut - } -} - -#[derive(uniffi::Record, Debug, Clone, PartialEq, Eq, Default)] -pub struct FieldAuthConfig { - pub auth_api_base_url: Option<String>, - pub accounts_api_base_url: Option<String>, -} - -impl FieldAuthConfig { - fn from_inputs( - auth_api_base_url: Option<String>, - accounts_api_base_url: Option<String>, - ) -> Result<Self, RadrootsAppError> { - Ok(Self { - auth_api_base_url: normalize_http_base_url(auth_api_base_url, "auth_api_base_url")?, - accounts_api_base_url: normalize_http_base_url( - accounts_api_base_url, - "accounts_api_base_url", - )?, - }) - } -} - -#[derive(uniffi::Record, Debug, Clone, PartialEq, Eq)] -pub struct FieldLoginChallenge { - pub id: String, - pub challenge_kind: String, - pub login_username: Option<String>, - pub masked_email: String, - pub delivery_state: String, - pub max_attempts: i32, - pub attempt_count: i32, - pub expires_at_unix_seconds: i64, - pub delivered_at_unix_seconds: Option<i64>, -} - -#[derive(uniffi::Record, Debug, Clone, PartialEq, Eq)] -pub struct FieldSessionAccount { - pub id: String, - pub username: String, - pub display_name: String, - pub status: String, -} - -#[derive(uniffi::Record, Debug, Clone, PartialEq, Eq)] -pub struct FieldSessionProfile { - pub id: String, - pub account_id: String, - pub display_name: String, - pub email: Option<String>, - pub status: String, -} - -#[derive(uniffi::Record, Debug, Clone, PartialEq, Eq)] -pub struct FieldSessionCredential { - pub id: String, - pub account_id: String, - pub profile_id: String, - pub email: String, - pub status: String, - pub is_primary: bool, -} - -#[derive(uniffi::Record, Debug, Clone, PartialEq, Eq)] -pub struct FieldSession { - pub id: String, - pub account_id: String, - pub profile_id: String, - pub credential_id: String, - pub session_id: String, - pub status: String, - pub expires_at_unix_seconds: i64, - pub revoked_at_unix_seconds: Option<i64>, -} - -#[derive(uniffi::Record, Debug, Clone, PartialEq, Eq)] -pub struct FieldSessionSnapshot { - pub phase: FieldSessionPhase, - pub pending_challenge: Option<FieldLoginChallenge>, - pub account: Option<FieldSessionAccount>, - pub profile: Option<FieldSessionProfile>, - pub credential: Option<FieldSessionCredential>, - pub session: Option<FieldSession>, - pub access_token_present: bool, - pub refresh_token_present: bool, - pub selected_npub: Option<String>, - pub nostr_light: NostrLight, - pub nostr_connected: u32, - pub nostr_connecting: u32, - pub nostr_last_error: Option<String>, -} - -#[derive(uniffi::Record, Debug, Clone, PartialEq, Eq)] -pub struct FieldSessionTokenBundle { - pub access_token: String, - pub refresh_token: String, -} - -#[derive(Debug, Clone, Default)] -pub(crate) struct FieldSessionState { - phase: FieldSessionPhase, - pending_challenge: Option<FieldLoginChallenge>, - account: Option<FieldSessionAccount>, - profile: Option<FieldSessionProfile>, - credential: Option<FieldSessionCredential>, - session: Option<FieldSession>, - access_token: Option<String>, - refresh_token: Option<String>, -} - -impl FieldSessionState { - fn challenge_pending(challenge: FieldLoginChallenge) -> Self { - Self { - phase: FieldSessionPhase::ChallengePending, - pending_challenge: Some(challenge), - ..Self::default() - } - } - - fn authenticated(bundle: SessionBundleResponseDto) -> Self { - Self { - phase: FieldSessionPhase::Authenticated, - pending_challenge: None, - account: Some(bundle.account.into()), - profile: Some(bundle.profile.into()), - credential: Some(bundle.credential.into()), - session: Some(bundle.session.into()), - access_token: Some(bundle.access_token), - refresh_token: Some(bundle.refresh_token), - } - } - - fn restored( - view: SessionResponseDto, - access_token: String, - refresh_token: Option<String>, - ) -> Self { - Self { - phase: FieldSessionPhase::Authenticated, - pending_challenge: None, - account: Some(view.account.into()), - profile: Some(view.profile.into()), - credential: Some(view.credential.into()), - session: Some(view.session.into()), - access_token: Some(access_token), - refresh_token, - } - } - - fn revoked(view: SessionResponseDto) -> Self { - Self { - phase: FieldSessionPhase::Revoked, - pending_challenge: None, - account: Some(view.account.into()), - profile: Some(view.profile.into()), - credential: Some(view.credential.into()), - session: Some(view.session.into()), - access_token: None, - refresh_token: None, - } - } - - fn snapshot( - &self, - selected_npub: Option<String>, - nostr: NostrConnectionStatus, - ) -> FieldSessionSnapshot { - FieldSessionSnapshot { - phase: self.phase, - pending_challenge: self.pending_challenge.clone(), - account: self.account.clone(), - profile: self.profile.clone(), - credential: self.credential.clone(), - session: self.session.clone(), - access_token_present: self.access_token.is_some(), - refresh_token_present: self.refresh_token.is_some(), - selected_npub, - nostr_light: nostr.light, - nostr_connected: nostr.connected, - nostr_connecting: nostr.connecting, - nostr_last_error: nostr.last_error, - } - } - - fn token_bundle(&self) -> Option<FieldSessionTokenBundle> { - Some(FieldSessionTokenBundle { - access_token: self.access_token.clone()?, - refresh_token: self.refresh_token.clone()?, - }) - } -} - -#[cfg_attr(not(coverage_nightly), uniffi::export)] -impl RadrootsRuntime { - pub fn field_configure_auth( - &self, - auth_api_base_url: Option<String>, - accounts_api_base_url: Option<String>, - ) -> Result<(), RadrootsAppError> { - let config = FieldAuthConfig::from_inputs(auth_api_base_url, accounts_api_base_url)?; - let mut guard = self - .auth_config - .write() - .map_err(|err| RadrootsAppError::Msg(format!("{err}")))?; - *guard = config; - Ok(()) - } - - pub fn field_auth_config(&self) -> FieldAuthConfig { - self.auth_config - .read() - .map(|guard| guard.clone()) - .unwrap_or_default() - } - - pub fn field_session_snapshot(&self) -> FieldSessionSnapshot { - self.snapshot_from_state() - } - - pub fn field_session_token_bundle( - &self, - ) -> Result<Option<FieldSessionTokenBundle>, RadrootsAppError> { - let guard = self - .session - .read() - .map_err(|err| RadrootsAppError::Msg(format!("{err}")))?; - Ok(guard.token_bundle()) - } - - pub fn field_start_login( - &self, - username: String, - ) -> Result<FieldLoginChallenge, RadrootsAppError> { - let username = non_empty(username, "username")?; - let response: ChallengeResponseDto = - self.auth_post_public("/v1/auth/login", &StartLoginRequest { username })?; - let challenge: FieldLoginChallenge = response.challenge.into(); - self.replace_session(FieldSessionState::challenge_pending(challenge.clone()))?; - Ok(challenge) - } - - pub fn field_get_login_challenge( - &self, - challenge_id: String, - ) -> Result<FieldLoginChallenge, RadrootsAppError> { - let challenge_id = path_id(challenge_id, "challenge_id")?; - let response: ChallengeResponseDto = - self.auth_get_public(format!("/v1/auth/challenges/{challenge_id}").as_str())?; - let challenge: FieldLoginChallenge = response.challenge.into(); - self.replace_session(FieldSessionState::challenge_pending(challenge.clone()))?; - Ok(challenge) - } - - pub fn field_resend_login_challenge( - &self, - challenge_id: String, - ) -> Result<FieldLoginChallenge, RadrootsAppError> { - let challenge_id = path_id(challenge_id, "challenge_id")?; - let response: ChallengeResponseDto = self.auth_post_public( - format!("/v1/auth/challenges/{challenge_id}/resend").as_str(), - &EmptyRequest {}, - )?; - let challenge: FieldLoginChallenge = response.challenge.into(); - self.replace_session(FieldSessionState::challenge_pending(challenge.clone()))?; - Ok(challenge) - } - - pub fn field_verify_login_challenge( - &self, - challenge_id: String, - code: String, - ) -> Result<FieldSessionSnapshot, RadrootsAppError> { - let challenge_id = path_id(challenge_id, "challenge_id")?; - let code = non_empty(code, "code")?; - let response: SessionBundleResponseDto = self.auth_post_public( - format!("/v1/auth/challenges/{challenge_id}/verify").as_str(), - &VerifyChallengeRequest { code }, - )?; - self.replace_session(FieldSessionState::authenticated(response))?; - Ok(self.snapshot_from_state()) - } - - pub fn field_refresh_session( - &self, - request_id: String, - ) -> Result<FieldSessionSnapshot, RadrootsAppError> { - let request_id = non_empty(request_id, "request_id")?; - let refresh_token = self.require_refresh_token()?; - let response: SessionBundleResponseDto = self.auth_post_public( - "/v1/auth/session/refresh", - &RefreshSessionRequest { - request_id, - refresh_token, - }, - )?; - self.replace_session(FieldSessionState::authenticated(response))?; - Ok(self.snapshot_from_state()) - } - - pub fn field_restore_session( - &self, - access_token: String, - refresh_token: Option<String>, - ) -> Result<FieldSessionSnapshot, RadrootsAppError> { - let access_token = non_empty(access_token, "access_token")?; - let refresh_token = optional_non_empty(refresh_token, "refresh_token")?; - let response: SessionResponseDto = - self.auth_get_bearer("/v1/auth/session", &access_token)?; - self.replace_session(FieldSessionState::restored( - response, - access_token, - refresh_token, - ))?; - Ok(self.snapshot_from_state()) - } - - pub fn field_current_session(&self) -> Result<FieldSessionSnapshot, RadrootsAppError> { - let access_token = self.require_access_token()?; - let response: SessionResponseDto = - self.auth_get_bearer("/v1/auth/session", &access_token)?; - let refresh_token = self.optional_refresh_token()?; - self.replace_session(FieldSessionState::restored( - response, - access_token, - refresh_token, - ))?; - Ok(self.snapshot_from_state()) - } - - pub fn field_revoke_session(&self) -> Result<FieldSessionSnapshot, RadrootsAppError> { - let access_token = self.require_access_token()?; - let session_id = self.require_session_id()?; - let response: SessionResponseDto = self.auth_post_bearer( - "/v1/auth/session/revoke", - &access_token, - &RevokeSessionRequest { session_id }, - )?; - self.replace_session(FieldSessionState::revoked(response))?; - Ok(self.snapshot_from_state()) - } - - pub fn field_clear_session(&self) -> FieldSessionSnapshot { - let _ = self.replace_session(FieldSessionState::default()); - self.snapshot_from_state() - } - - pub fn field_prepare_authenticated_nostr( - &self, - relays: Vec<String>, - ) -> Result<FieldSessionSnapshot, RadrootsAppError> { - self.require_authenticated_session()?; - if relays.is_empty() { - return Err(RadrootsAppError::Msg( - "at least one relay is required".into(), - )); - } - if !self.accounts_has_selected_signing_identity() { - let label = self - .session - .read() - .ok() - .and_then(|guard| { - guard - .account - .as_ref() - .map(|account| account.username.clone()) - }) - .unwrap_or_else(|| "Radroots Field".to_owned()); - self.accounts_generate(Some(label), true)?; - } - self.nostr_set_default_relays(relays)?; - self.nostr_connect_if_key_present()?; - Ok(self.snapshot_from_state()) - } -} - -impl RadrootsRuntime { - fn snapshot_from_state(&self) -> FieldSessionSnapshot { - let selected_npub = self.accounts_selected_npub(); - let nostr = self.nostr_connection_status(); - self.session - .read() - .map(|guard| guard.snapshot(selected_npub, nostr)) - .unwrap_or_else(|_| { - FieldSessionState::default().snapshot( - self.accounts_selected_npub(), - NostrConnectionStatus { - light: NostrLight::Red, - connected: 0, - connecting: 0, - last_error: None, - }, - ) - }) - } - - fn replace_session(&self, session: FieldSessionState) -> Result<(), RadrootsAppError> { - let mut guard = self - .session - .write() - .map_err(|err| RadrootsAppError::Msg(format!("{err}")))?; - *guard = session; - Ok(()) - } - - fn require_authenticated_session(&self) -> Result<(), RadrootsAppError> { - let guard = self - .session - .read() - .map_err(|err| RadrootsAppError::Msg(format!("{err}")))?; - if guard.phase == FieldSessionPhase::Authenticated && guard.access_token.is_some() { - Ok(()) - } else { - Err(RadrootsAppError::Msg( - "authenticated field session is required".into(), - )) - } - } - - fn require_access_token(&self) -> Result<String, RadrootsAppError> { - let guard = self - .session - .read() - .map_err(|err| RadrootsAppError::Msg(format!("{err}")))?; - guard - .access_token - .clone() - .ok_or_else(|| RadrootsAppError::Msg("access token is not configured".into())) - } - - fn optional_refresh_token(&self) -> Result<Option<String>, RadrootsAppError> { - let guard = self - .session - .read() - .map_err(|err| RadrootsAppError::Msg(format!("{err}")))?; - Ok(guard.refresh_token.clone()) - } - - fn require_refresh_token(&self) -> Result<String, RadrootsAppError> { - let guard = self - .session - .read() - .map_err(|err| RadrootsAppError::Msg(format!("{err}")))?; - guard - .refresh_token - .clone() - .ok_or_else(|| RadrootsAppError::Msg("refresh token is not configured".into())) - } - - fn require_session_id(&self) -> Result<String, RadrootsAppError> { - let guard = self - .session - .read() - .map_err(|err| RadrootsAppError::Msg(format!("{err}")))?; - guard - .session - .as_ref() - .map(|session| session.session_id.clone()) - .ok_or_else(|| RadrootsAppError::Msg("session id is not configured".into())) - } - - fn auth_base_url(&self) -> Result<String, RadrootsAppError> { - let guard = self - .auth_config - .read() - .map_err(|err| RadrootsAppError::Msg(format!("{err}")))?; - guard - .auth_api_base_url - .clone() - .ok_or_else(|| RadrootsAppError::Msg("auth API base URL is not configured".into())) - } - - fn auth_url(&self, path: &str) -> Result<Url, RadrootsAppError> { - if !path.starts_with('/') { - return Err(RadrootsAppError::Msg( - "auth API path must start with /".into(), - )); - } - Url::parse(format!("{}{}", self.auth_base_url()?, path).as_str()) - .map_err(|err| RadrootsAppError::Msg(format!("auth API URL is invalid: {err}"))) - } - - fn auth_get_public<T>(&self, path: &str) -> Result<T, RadrootsAppError> - where - T: DeserializeOwned, - { - self.auth_get(path, None) - } - - fn auth_get_bearer<T>(&self, path: &str, access_token: &str) -> Result<T, RadrootsAppError> - where - T: DeserializeOwned, - { - self.auth_get(path, Some(access_token)) - } - - fn auth_get<T>(&self, path: &str, access_token: Option<&str>) -> Result<T, RadrootsAppError> - where - T: DeserializeOwned, - { - let client = auth_client()?; - let mut request = client.get(self.auth_url(path)?); - if let Some(token) = access_token { - request = request.bearer_auth(token); - } - decode_response("GET", path, request.send()) - } - - fn auth_post_public<B, T>(&self, path: &str, body: &B) -> Result<T, RadrootsAppError> - where - B: Serialize, - T: DeserializeOwned, - { - self.auth_post(path, None, body) - } - - fn auth_post_bearer<B, T>( - &self, - path: &str, - access_token: &str, - body: &B, - ) -> Result<T, RadrootsAppError> - where - B: Serialize, - T: DeserializeOwned, - { - self.auth_post(path, Some(access_token), body) - } - - fn auth_post<B, T>( - &self, - path: &str, - access_token: Option<&str>, - body: &B, - ) -> Result<T, RadrootsAppError> - where - B: Serialize, - T: DeserializeOwned, - { - let client = auth_client()?; - let mut request = client.post(self.auth_url(path)?).json(body); - if let Some(token) = access_token { - request = request.bearer_auth(token); - } - decode_response("POST", path, request.send()) - } -} - -#[derive(Debug, Serialize)] -struct EmptyRequest {} - -#[derive(Debug, Serialize)] -struct StartLoginRequest { - username: String, -} - -#[derive(Debug, Serialize)] -struct VerifyChallengeRequest { - code: String, -} - -#[derive(Debug, Serialize)] -struct RefreshSessionRequest { - request_id: String, - refresh_token: String, -} - -#[derive(Debug, Serialize)] -struct RevokeSessionRequest { - session_id: String, -} - -#[derive(Debug, Deserialize)] -struct ChallengeResponseDto { - challenge: ChallengeBodyDto, -} - -#[derive(Debug, Deserialize)] -struct ChallengeBodyDto { - id: String, - challenge_kind: String, - login_username: Option<String>, - masked_email: String, - delivery_state: String, - max_attempts: i32, - attempt_count: i32, - expires_at_unix_seconds: i64, - delivered_at_unix_seconds: Option<i64>, -} - -impl From<ChallengeBodyDto> for FieldLoginChallenge { - fn from(value: ChallengeBodyDto) -> Self { - Self { - id: value.id, - challenge_kind: value.challenge_kind, - login_username: value.login_username, - masked_email: value.masked_email, - delivery_state: value.delivery_state, - max_attempts: value.max_attempts, - attempt_count: value.attempt_count, - expires_at_unix_seconds: value.expires_at_unix_seconds, - delivered_at_unix_seconds: value.delivered_at_unix_seconds, - } - } -} - -#[derive(Debug, Deserialize)] -struct SessionBundleResponseDto { - account: AccountBodyDto, - profile: ProfileBodyDto, - credential: CredentialBodyDto, - session: SessionBodyDto, - access_token: String, - refresh_token: String, -} - -#[derive(Debug, Deserialize)] -struct SessionResponseDto { - account: AccountBodyDto, - profile: ProfileBodyDto, - credential: CredentialBodyDto, - session: SessionBodyDto, -} - -#[derive(Debug, Deserialize)] -struct AccountBodyDto { - id: String, - username: String, - display_name: String, - status: String, -} - -impl From<AccountBodyDto> for FieldSessionAccount { - fn from(value: AccountBodyDto) -> Self { - Self { - id: value.id, - username: value.username, - display_name: value.display_name, - status: value.status, - } - } -} - -#[derive(Debug, Deserialize)] -struct ProfileBodyDto { - id: String, - account_id: String, - display_name: String, - email: Option<String>, - status: String, -} - -impl From<ProfileBodyDto> for FieldSessionProfile { - fn from(value: ProfileBodyDto) -> Self { - Self { - id: value.id, - account_id: value.account_id, - display_name: value.display_name, - email: value.email, - status: value.status, - } - } -} - -#[derive(Debug, Deserialize)] -struct CredentialBodyDto { - id: String, - account_id: String, - profile_id: String, - email: String, - status: String, - is_primary: bool, -} - -impl From<CredentialBodyDto> for FieldSessionCredential { - fn from(value: CredentialBodyDto) -> Self { - Self { - id: value.id, - account_id: value.account_id, - profile_id: value.profile_id, - email: value.email, - status: value.status, - is_primary: value.is_primary, - } - } -} - -#[derive(Debug, Deserialize)] -struct SessionBodyDto { - id: String, - account_id: String, - profile_id: String, - credential_id: String, - session_id: String, - status: String, - expires_at_unix_seconds: i64, - revoked_at_unix_seconds: Option<i64>, -} - -impl From<SessionBodyDto> for FieldSession { - fn from(value: SessionBodyDto) -> Self { - Self { - id: value.id, - account_id: value.account_id, - profile_id: value.profile_id, - credential_id: value.credential_id, - session_id: value.session_id, - status: value.status, - expires_at_unix_seconds: value.expires_at_unix_seconds, - revoked_at_unix_seconds: value.revoked_at_unix_seconds, - } - } -} - -#[derive(Debug, Deserialize)] -struct ApiErrorBodyDto { - code: String, - message: String, -} - -fn auth_client() -> Result<Client, RadrootsAppError> { - Client::builder() - .timeout(Duration::from_secs(AUTH_HTTP_TIMEOUT_SECONDS)) - .build() - .map_err(|err| RadrootsAppError::Msg(format!("auth API client build failed: {err}"))) -} - -fn decode_response<T>( - method: &str, - path: &str, - response: Result<reqwest::blocking::Response, reqwest::Error>, -) -> Result<T, RadrootsAppError> -where - T: DeserializeOwned, -{ - let response = - response.map_err(|err| RadrootsAppError::Msg(format!("{method} {path} failed: {err}")))?; - let status = response.status(); - if status.is_success() { - return response.json::<T>().map_err(|err| { - RadrootsAppError::Msg(format!("{method} {path} response decode failed: {err}")) - }); - } - let body = response - .text() - .unwrap_or_else(|err| format!("failed to read error response: {err}")); - let message = serde_json::from_str::<ApiErrorBodyDto>(body.as_str()) - .map(|body| format!("{} {}", body.code, body.message)) - .unwrap_or(body); - Err(RadrootsAppError::Msg(format!( - "{method} {path} failed with HTTP {status}: {message}" - ))) -} - -fn normalize_http_base_url( - value: Option<String>, - field: &str, -) -> Result<Option<String>, RadrootsAppError> { - let Some(value) = value else { - return Ok(None); - }; - let value = value.trim().trim_end_matches('/').to_owned(); - if value.is_empty() { - return Ok(None); - } - let parsed = Url::parse(value.as_str()) - .map_err(|err| RadrootsAppError::Msg(format!("{field} is invalid: {err}")))?; - match parsed.scheme() { - "http" | "https" => Ok(Some(value)), - _ => Err(RadrootsAppError::Msg(format!( - "{field} must use http or https" - ))), - } -} - -fn optional_non_empty( - value: Option<String>, - field: &str, -) -> Result<Option<String>, RadrootsAppError> { - let Some(value) = value else { - return Ok(None); - }; - Ok(Some(non_empty(value, field)?)) -} - -fn non_empty(value: String, field: &str) -> Result<String, RadrootsAppError> { - let value = value.trim().to_owned(); - if value.is_empty() { - return Err(RadrootsAppError::Msg(format!("{field} is required"))); - } - Ok(value) -} - -fn path_id(value: String, field: &str) -> Result<String, RadrootsAppError> { - let value = non_empty(value, field)?; - if value.contains('/') || value.contains('?') || value.contains('#') { - return Err(RadrootsAppError::Msg(format!( - "{field} contains invalid path characters" - ))); - } - Ok(value) -} - -#[cfg(test)] -mod tests { - use std::{ - io::{Read, Write}, - net::TcpListener, - thread, - }; - - use super::{FieldAuthConfig, FieldSessionPhase}; - use crate::runtime::RadrootsRuntime; - - #[test] - fn auth_config_normalizes_and_validates_http_urls() { - let config = FieldAuthConfig::from_inputs( - Some(" http://127.0.0.1:8081/ ".to_owned()), - Some("https://accounts.example.test/api/".to_owned()), - ) - .expect("config"); - - assert_eq!( - config.auth_api_base_url.as_deref(), - Some("http://127.0.0.1:8081") - ); - assert_eq!( - config.accounts_api_base_url.as_deref(), - Some("https://accounts.example.test/api") - ); - let error = FieldAuthConfig::from_inputs(Some("wss://relay.example.test".to_owned()), None) - .expect_err("reject non-http URL"); - assert!(error.to_string().contains("auth_api_base_url")); - } - - #[test] - fn session_snapshot_redacts_tokens() { - let runtime = RadrootsRuntime::new().expect("runtime"); - let snapshot = runtime - .field_restore_session("access-token".to_owned(), Some("refresh-token".to_owned())) - .expect_err("auth URL missing"); - assert!(snapshot.to_string().contains("auth API base URL")); - - runtime - .replace_session(super::FieldSessionState { - phase: FieldSessionPhase::Authenticated, - access_token: Some("access-token".to_owned()), - refresh_token: Some("refresh-token".to_owned()), - ..Default::default() - }) - .expect("session"); - - let snapshot = runtime.field_session_snapshot(); - assert!(snapshot.access_token_present); - assert!(snapshot.refresh_token_present); - assert!(!format!("{snapshot:?}").contains("access-token")); - assert!(!format!("{snapshot:?}").contains("refresh-token")); - let tokens = runtime - .field_session_token_bundle() - .expect("token bundle") - .expect("tokens"); - assert_eq!(tokens.access_token, "access-token"); - runtime.stop(); - } - - #[test] - fn start_login_posts_to_auth_api_and_stores_pending_challenge() { - let (base_url, handle) = spawn_response( - "POST /v1/auth/login HTTP/1.1", - Some("\"username\":\"field@radroots.test\""), - r#"{"challenge":{"id":"challenge-1","challenge_kind":"login","login_username":"field@radroots.test","masked_email":"f***@radroots.test","delivery_state":"sent","max_attempts":6,"attempt_count":0,"expires_at_unix_seconds":1893456000,"delivered_at_unix_seconds":1893455900}}"#, - ); - let runtime = RadrootsRuntime::new().expect("runtime"); - runtime - .field_configure_auth(Some(base_url), None) - .expect("configure"); - - let challenge = runtime - .field_start_login("field@radroots.test".to_owned()) - .expect("login"); - assert_eq!(challenge.id, "challenge-1"); - assert_eq!( - runtime.field_session_snapshot().phase, - FieldSessionPhase::ChallengePending - ); - handle.join().expect("server"); - runtime.stop(); - } - - #[test] - fn verify_login_stores_authenticated_session_and_redacted_snapshot() { - let body = sample_session_bundle_json("access-token", "refresh-token"); - let (base_url, handle) = spawn_response( - "POST /v1/auth/challenges/challenge-1/verify HTTP/1.1", - Some("\"code\":\"123456\""), - body.as_str(), - ); - let runtime = RadrootsRuntime::new().expect("runtime"); - runtime - .field_configure_auth(Some(base_url), None) - .expect("configure"); - - let snapshot = runtime - .field_verify_login_challenge("challenge-1".to_owned(), "123456".to_owned()) - .expect("verify"); - assert_eq!(snapshot.phase, FieldSessionPhase::Authenticated); - assert_eq!( - snapshot - .account - .as_ref() - .map(|account| account.username.as_str()), - Some("field") - ); - assert!(snapshot.access_token_present); - assert!(snapshot.refresh_token_present); - assert!(!format!("{snapshot:?}").contains("access-token")); - let tokens = runtime - .field_session_token_bundle() - .expect("tokens") - .expect("token bundle"); - assert_eq!(tokens.refresh_token, "refresh-token"); - handle.join().expect("server"); - runtime.stop(); - } - - #[test] - fn restore_session_fetches_current_session_with_bearer_token() { - let body = sample_session_json(); - let (base_url, handle) = spawn_response( - "GET /v1/auth/session HTTP/1.1", - Some("authorization: Bearer access-token"), - body.as_str(), - ); - let runtime = RadrootsRuntime::new().expect("runtime"); - runtime - .field_configure_auth(Some(base_url), None) - .expect("configure"); - - let snapshot = runtime - .field_restore_session("access-token".to_owned(), Some("refresh-token".to_owned())) - .expect("restore"); - assert_eq!(snapshot.phase, FieldSessionPhase::Authenticated); - assert_eq!( - snapshot - .session - .as_ref() - .map(|session| session.session_id.as_str()), - Some("session-public-id") - ); - handle.join().expect("server"); - runtime.stop(); - } - - fn spawn_response( - expected_start: &'static str, - expected_contains: Option<&'static str>, - body: &str, - ) -> (String, thread::JoinHandle<()>) { - let listener = TcpListener::bind("127.0.0.1:0").expect("bind"); - let addr = listener.local_addr().expect("addr"); - let body = body.to_owned(); - let handle = thread::spawn(move || { - let (mut stream, _) = listener.accept().expect("accept"); - let mut request = vec![0; 8192]; - let read = stream.read(&mut request).expect("read"); - let request = String::from_utf8_lossy(&request[..read]); - assert!(request.starts_with(expected_start), "{request}"); - if let Some(expected) = expected_contains { - assert!(request.contains(expected), "{request}"); - } - let response = format!( - "HTTP/1.1 200 OK\r\ncontent-type: application/json\r\ncontent-length: {}\r\n\r\n{}", - body.len(), - body - ); - stream.write_all(response.as_bytes()).expect("write"); - }); - (format!("http://{addr}"), handle) - } - - fn sample_session_bundle_json(access_token: &str, refresh_token: &str) -> String { - format!( - r#"{{"account":{},"profile":{},"credential":{},"session":{},"access_token":"{}","refresh_token":"{}"}}"#, - sample_account_json(), - sample_profile_json(), - sample_credential_json(), - sample_session_body_json(), - access_token, - refresh_token - ) - } - - fn sample_session_json() -> String { - format!( - r#"{{"account":{},"profile":{},"credential":{},"session":{}}}"#, - sample_account_json(), - sample_profile_json(), - sample_credential_json(), - sample_session_body_json() - ) - } - - fn sample_account_json() -> &'static str { - r#"{"id":"account-1","username":"field","display_name":"Field Operator","status":"active"}"# - } - - fn sample_profile_json() -> &'static str { - r#"{"id":"profile-1","account_id":"account-1","display_name":"Field Operator","email":"field@radroots.test","status":"active"}"# - } - - fn sample_credential_json() -> &'static str { - r#"{"id":"credential-1","account_id":"account-1","profile_id":"profile-1","email":"field@radroots.test","status":"active","is_primary":true}"# - } - - fn sample_session_body_json() -> &'static str { - r#"{"id":"session-row-1","account_id":"account-1","profile_id":"profile-1","credential_id":"credential-1","session_id":"session-public-id","status":"active","expires_at_unix_seconds":1893456000,"revoked_at_unix_seconds":null}"# - } -} diff --git a/crates/field_core/src/runtime/key_management.rs b/crates/field_core/src/runtime/key_management.rs @@ -5,9 +5,26 @@ use radroots_identity::{RadrootsIdentity, RadrootsIdentityId}; #[cfg(feature = "nostr-client")] use std::path::PathBuf; +#[derive(uniffi::Record, Debug, Clone)] +pub struct NostrIdentityRecord { + pub id: String, + pub public_key_hex: String, + pub public_key_npub: String, + pub label: Option<String>, + pub is_selected: bool, +} + +#[derive(uniffi::Record, Debug, Clone)] +pub struct NostrIdentitySnapshot { + pub has_selected_signing_identity: bool, + pub selected_identity_id: Option<String>, + pub selected_npub: Option<String>, + pub identities: Vec<NostrIdentityRecord>, +} + #[cfg_attr(not(coverage_nightly), uniffi::export)] impl RadrootsRuntime { - pub fn accounts_has_selected_signing_identity(&self) -> bool { + pub fn nostr_identity_has_selected_signing_identity(&self) -> bool { #[cfg(feature = "nostr-client")] { if let Ok(guard) = self.net.lock() { @@ -29,7 +46,7 @@ impl RadrootsRuntime { false } - pub fn accounts_selected_npub(&self) -> Option<String> { + pub fn nostr_identity_selected_npub(&self) -> Option<String> { #[cfg(feature = "nostr-client")] { if let Ok(guard) = self.net.lock() { @@ -51,20 +68,36 @@ impl RadrootsRuntime { None } - pub fn accounts_list_ids(&self) -> Result<Vec<String>, RadrootsAppError> { + pub fn nostr_identity_list(&self) -> Result<Vec<NostrIdentityRecord>, RadrootsAppError> { #[cfg(feature = "nostr-client")] { let guard = match self.net.lock() { Ok(guard) => guard, Err(err) => return Err(RadrootsAppError::Msg(format!("{err}"))), }; + let selected_identity_id = guard + .accounts + .default_account_id() + .map_err(|e| RadrootsAppError::Msg(format!("{e}")))?; let accounts = guard .accounts .list_accounts() .map_err(|e| RadrootsAppError::Msg(format!("{e}")))?; return Ok(accounts .into_iter() - .map(|account| account.account_id.to_string()) + .map(|account| { + let is_selected = selected_identity_id + .as_ref() + .map(|selected| selected == &account.account_id) + .unwrap_or(false); + NostrIdentityRecord { + id: account.account_id.to_string(), + public_key_hex: account.public_identity.public_key_hex, + public_key_npub: account.public_identity.public_key_npub, + label: account.label, + is_selected, + } + }) .collect()); } #[cfg(not(feature = "nostr-client"))] @@ -73,7 +106,69 @@ impl RadrootsRuntime { } } - pub fn accounts_generate( + pub fn nostr_identity_list_ids(&self) -> Result<Vec<String>, RadrootsAppError> { + Ok(self + .nostr_identity_list()? + .into_iter() + .map(|identity| identity.id) + .collect()) + } + + pub fn nostr_identity_snapshot(&self) -> Result<NostrIdentitySnapshot, RadrootsAppError> { + #[cfg(feature = "nostr-client")] + { + let guard = match self.net.lock() { + Ok(guard) => guard, + Err(err) => return Err(RadrootsAppError::Msg(format!("{err}"))), + }; + let selected_identity_id = guard + .accounts + .default_account_id() + .map_err(|e| RadrootsAppError::Msg(format!("{e}")))?; + let selected_npub = guard + .accounts + .default_public_identity() + .map_err(|e| RadrootsAppError::Msg(format!("{e}")))? + .map(|identity| identity.public_key_npub); + let has_selected_signing_identity = guard + .accounts + .default_signing_identity() + .ok() + .flatten() + .is_some(); + let identities = guard + .accounts + .list_accounts() + .map_err(|e| RadrootsAppError::Msg(format!("{e}")))? + .into_iter() + .map(|account| { + let is_selected = selected_identity_id + .as_ref() + .map(|selected| selected == &account.account_id) + .unwrap_or(false); + NostrIdentityRecord { + id: account.account_id.to_string(), + public_key_hex: account.public_identity.public_key_hex, + public_key_npub: account.public_identity.public_key_npub, + label: account.label, + is_selected, + } + }) + .collect(); + return Ok(NostrIdentitySnapshot { + has_selected_signing_identity, + selected_identity_id: selected_identity_id.map(|id| id.to_string()), + selected_npub, + identities, + }); + } + #[cfg(not(feature = "nostr-client"))] + { + Err(RadrootsAppError::Msg("nostr disabled".into())) + } + } + + pub fn nostr_identity_generate( &self, label: Option<String>, make_selected: bool, @@ -98,7 +193,7 @@ impl RadrootsRuntime { } } - pub fn accounts_import_secret( + pub fn nostr_identity_import_secret( &self, secret_key: String, label: Option<String>, @@ -126,7 +221,7 @@ impl RadrootsRuntime { } } - pub fn accounts_import_from_path( + pub fn nostr_identity_import_from_path( &self, path: String, label: Option<String>, @@ -152,7 +247,9 @@ impl RadrootsRuntime { } } - pub fn accounts_export_selected_secret_hex(&self) -> Result<Option<String>, RadrootsAppError> { + pub fn nostr_identity_export_selected_secret_hex( + &self, + ) -> Result<Option<String>, RadrootsAppError> { #[cfg(feature = "nostr-client")] { let guard = match self.net.lock() { @@ -177,14 +274,14 @@ impl RadrootsRuntime { } } - pub fn accounts_select(&self, account_id: String) -> Result<(), RadrootsAppError> { + pub fn nostr_identity_select(&self, identity_id: String) -> Result<(), RadrootsAppError> { #[cfg(feature = "nostr-client")] { let mut guard = match self.net.lock() { Ok(guard) => guard, Err(err) => return Err(RadrootsAppError::Msg(format!("{err}"))), }; - let account_id = RadrootsIdentityId::parse(account_id.as_str()) + let account_id = RadrootsIdentityId::parse(identity_id.as_str()) .map_err(|e| RadrootsAppError::Msg(format!("{e}")))?; guard .accounts @@ -195,19 +292,19 @@ impl RadrootsRuntime { } #[cfg(not(feature = "nostr-client"))] { - let _ = account_id; + let _ = identity_id; Err(RadrootsAppError::Msg("nostr disabled".into())) } } - pub fn accounts_remove(&self, account_id: String) -> Result<(), RadrootsAppError> { + pub fn nostr_identity_remove(&self, identity_id: String) -> Result<(), RadrootsAppError> { #[cfg(feature = "nostr-client")] { let mut guard = match self.net.lock() { Ok(guard) => guard, Err(err) => return Err(RadrootsAppError::Msg(format!("{err}"))), }; - let account_id = RadrootsIdentityId::parse(account_id.as_str()) + let account_id = RadrootsIdentityId::parse(identity_id.as_str()) .map_err(|e| RadrootsAppError::Msg(format!("{e}")))?; guard .accounts @@ -218,7 +315,7 @@ impl RadrootsRuntime { } #[cfg(not(feature = "nostr-client"))] { - let _ = account_id; + let _ = identity_id; Err(RadrootsAppError::Msg("nostr disabled".into())) } } diff --git a/crates/field_core/src/runtime/mod.rs b/crates/field_core/src/runtime/mod.rs @@ -1,5 +1,4 @@ pub mod app_info; -pub mod auth_session; pub mod builder; pub mod info; pub mod key_management; @@ -20,7 +19,6 @@ use tokio::sync::broadcast::Receiver; use self::{ app_info::AppInfoPlatform, - auth_session::{FieldAuthConfig, FieldSessionState}, info::{RuntimeInfo, gather_runtime_info}, }; use crate::RadrootsAppError; @@ -31,8 +29,6 @@ pub struct RadrootsRuntime { pub(crate) started_unix_ms: i64, pub(crate) shutting_down: AtomicBool, pub(crate) platform_app: RwLock<Option<AppInfoPlatform>>, - pub(crate) auth_config: RwLock<FieldAuthConfig>, - pub(crate) session: RwLock<FieldSessionState>, #[cfg(feature = "nostr-client")] pub(crate) post_events_rx: Mutex< Option< @@ -67,8 +63,6 @@ impl RadrootsRuntime { started_unix_ms: Utc::now().timestamp_millis(), shutting_down: AtomicBool::new(false), platform_app: RwLock::new(None), - auth_config: RwLock::new(FieldAuthConfig::default()), - session: RwLock::new(FieldSessionState::default()), #[cfg(feature = "nostr-client")] post_events_rx: Mutex::new(None), }) diff --git a/crates/field_core/tests/no_nostr_runtime.rs b/crates/field_core/tests/no_nostr_runtime.rs @@ -29,23 +29,25 @@ fn runtime_info_and_platform_paths_are_exercised() { fn key_management_disabled_paths_are_exercised() { let runtime = RadrootsRuntime::new().expect("runtime"); - assert!(!runtime.accounts_has_selected_signing_identity()); - assert_eq!(runtime.accounts_selected_npub(), None); - expect_disabled(runtime.accounts_list_ids()); - expect_disabled(runtime.accounts_generate(Some("alpha".to_string()), true)); - expect_disabled(runtime.accounts_import_secret( + assert!(!runtime.nostr_identity_has_selected_signing_identity()); + assert_eq!(runtime.nostr_identity_selected_npub(), None); + expect_disabled(runtime.nostr_identity_list()); + expect_disabled(runtime.nostr_identity_list_ids()); + expect_disabled(runtime.nostr_identity_snapshot()); + expect_disabled(runtime.nostr_identity_generate(Some("alpha".to_string()), true)); + expect_disabled(runtime.nostr_identity_import_secret( "deadbeef".to_string(), Some("alpha".to_string()), true, )); - expect_disabled(runtime.accounts_import_from_path( + expect_disabled(runtime.nostr_identity_import_from_path( "/tmp/nostr.json".to_string(), Some("alpha".to_string()), true, )); - expect_disabled(runtime.accounts_export_selected_secret_hex()); - expect_disabled(runtime.accounts_select("account-1".to_string())); - expect_disabled(runtime.accounts_remove("account-1".to_string())); + expect_disabled(runtime.nostr_identity_export_selected_secret_hex()); + expect_disabled(runtime.nostr_identity_select("account-1".to_string())); + expect_disabled(runtime.nostr_identity_remove("account-1".to_string())); } #[test]