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:
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]