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