app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

commit e350c249f51d4834e1a0c356a861f98a84672f7d
parent bae6a210bc6b05849fa0517be911851a6a96bc90
Author: triesap <triesap@radroots.dev>
Date:   Mon, 19 Jan 2026 19:18:08 +0000

app: add keystore bootstrap helpers

- add helpers for keystore key lookup and generation
- map keystore errors into app keystore results
- add keystore helper tests with async stub
- export keystore helpers from app module

Diffstat:
Aapp/src/keystore.rs | 163+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mapp/src/lib.rs | 8++++++++
2 files changed, 171 insertions(+), 0 deletions(-)

diff --git a/app/src/keystore.rs b/app/src/keystore.rs @@ -0,0 +1,163 @@ +#![forbid(unsafe_code)] + +use radroots_app_core::keystore::{RadrootsClientKeystoreError, RadrootsClientKeystoreNostr}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AppKeystoreError { + Keystore(RadrootsClientKeystoreError), +} + +pub type AppKeystoreResult<T> = Result<T, AppKeystoreError>; + +impl AppKeystoreError { + pub const fn message(&self) -> &'static str { + match self { + AppKeystoreError::Keystore(err) => err.message(), + } + } +} + +impl std::fmt::Display for AppKeystoreError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.message()) + } +} + +impl std::error::Error for AppKeystoreError {} + +impl From<RadrootsClientKeystoreError> for AppKeystoreError { + fn from(err: RadrootsClientKeystoreError) -> Self { + AppKeystoreError::Keystore(err) + } +} + +pub async fn app_keystore_nostr_keys<T: RadrootsClientKeystoreNostr>( + keystore: &T, +) -> AppKeystoreResult<Vec<String>> { + keystore.keys().await.map_err(AppKeystoreError::from) +} + +pub async fn app_keystore_nostr_public_key<T: RadrootsClientKeystoreNostr>( + keystore: &T, +) -> AppKeystoreResult<Option<String>> { + match keystore.keys().await { + Ok(mut keys) => Ok(keys.pop()), + Err(RadrootsClientKeystoreError::NostrNoResults) => Ok(None), + Err(err) => Err(AppKeystoreError::from(err)), + } +} + +pub async fn app_keystore_nostr_ensure_key<T: RadrootsClientKeystoreNostr>( + keystore: &T, +) -> AppKeystoreResult<String> { + match app_keystore_nostr_public_key(keystore).await? { + Some(key) => Ok(key), + None => keystore.generate().await.map_err(AppKeystoreError::from), + } +} + +#[cfg(test)] +mod tests { + use super::{ + app_keystore_nostr_ensure_key, + app_keystore_nostr_public_key, + app_keystore_nostr_keys, + AppKeystoreError, + }; + use async_trait::async_trait; + use radroots_app_core::keystore::{ + RadrootsClientKeystoreError, + RadrootsClientKeystoreNostr, + RadrootsClientKeystoreResult, + }; + + struct TestKeystore { + keys_result: RadrootsClientKeystoreResult<Vec<String>>, + generate_result: RadrootsClientKeystoreResult<String>, + } + + #[async_trait(?Send)] + impl RadrootsClientKeystoreNostr for TestKeystore { + async fn generate(&self) -> RadrootsClientKeystoreResult<String> { + self.generate_result.clone() + } + + async fn add(&self, _secret_key: &str) -> RadrootsClientKeystoreResult<String> { + Err(RadrootsClientKeystoreError::IdbUndefined) + } + + async fn read(&self, _public_key: &str) -> RadrootsClientKeystoreResult<String> { + Err(RadrootsClientKeystoreError::IdbUndefined) + } + + async fn keys(&self) -> RadrootsClientKeystoreResult<Vec<String>> { + self.keys_result.clone() + } + + async fn remove(&self, _public_key: &str) -> RadrootsClientKeystoreResult<String> { + Err(RadrootsClientKeystoreError::IdbUndefined) + } + + async fn reset(&self) -> RadrootsClientKeystoreResult<()> { + Err(RadrootsClientKeystoreError::IdbUndefined) + } + } + + #[test] + fn keystore_public_key_maps_empty_to_none() { + let keystore = TestKeystore { + keys_result: Err(RadrootsClientKeystoreError::NostrNoResults), + generate_result: Ok("generated".to_string()), + }; + let result = futures::executor::block_on(app_keystore_nostr_public_key(&keystore)) + .expect("nostr key"); + assert!(result.is_none()); + } + + #[test] + fn keystore_public_key_returns_existing() { + let keystore = TestKeystore { + keys_result: Ok(vec!["a".to_string(), "b".to_string()]), + generate_result: Ok("generated".to_string()), + }; + let result = futures::executor::block_on(app_keystore_nostr_public_key(&keystore)) + .expect("nostr key"); + assert_eq!(result.as_deref(), Some("b")); + } + + #[test] + fn keystore_keys_maps_errors() { + let keystore = TestKeystore { + keys_result: Err(RadrootsClientKeystoreError::IdbUndefined), + generate_result: Ok("generated".to_string()), + }; + let err = futures::executor::block_on(app_keystore_nostr_keys(&keystore)) + .expect_err("nostr key"); + assert_eq!( + err, + AppKeystoreError::Keystore(RadrootsClientKeystoreError::IdbUndefined) + ); + } + + #[test] + fn keystore_ensure_generates_when_empty() { + let keystore = TestKeystore { + keys_result: Err(RadrootsClientKeystoreError::NostrNoResults), + generate_result: Ok("generated".to_string()), + }; + let result = futures::executor::block_on(app_keystore_nostr_ensure_key(&keystore)) + .expect("nostr key"); + assert_eq!(result, "generated"); + } + + #[test] + fn keystore_ensure_returns_existing() { + let keystore = TestKeystore { + keys_result: Ok(vec!["a".to_string()]), + generate_result: Ok("generated".to_string()), + }; + let result = futures::executor::block_on(app_keystore_nostr_ensure_key(&keystore)) + .expect("nostr key"); + assert_eq!(result, "a"); + } +} diff --git a/app/src/lib.rs b/app/src/lib.rs @@ -7,6 +7,7 @@ mod config; mod data; mod health; mod init; +mod keystore; mod entry; pub use app::App; @@ -30,6 +31,13 @@ pub use health::{ AppHealthCheckStatus, AppHealthReport, }; +pub use keystore::{ + app_keystore_nostr_ensure_key, + app_keystore_nostr_keys, + app_keystore_nostr_public_key, + AppKeystoreError, + AppKeystoreResult, +}; pub use config::{ app_config_default, app_config_from_env,