commit 14ff72b26eeba3d89a1cf7a161188316034caee3
parent 6d2bdc5fb5172de8e369a4658399d1eaabffbab3
Author: triesap <tyson@radroots.org>
Date: Thu, 26 Mar 2026 16:12:31 +0000
persistence: wire sqlite signer backend
Diffstat:
5 files changed, 138 insertions(+), 10 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -1828,6 +1828,7 @@ dependencies = [
"radroots-identity",
"radroots-nostr-connect",
"radroots-runtime",
+ "radroots-sql-core",
"serde",
"serde_json",
"sha2",
diff --git a/Cargo.toml b/Cargo.toml
@@ -22,7 +22,7 @@ radroots-log = { path = "../lib/crates/log" }
radroots-nostr-accounts = { path = "../lib/crates/nostr-accounts", default-features = false, features = ["std", "memory-vault", "os-keyring"] }
radroots-nostr = { path = "../lib/crates/nostr", features = ["client", "events"] }
radroots-nostr-connect = { path = "../lib/crates/nostr-connect" }
-radroots-nostr-signer = { path = "../lib/crates/nostr-signer" }
+radroots-nostr-signer = { path = "../lib/crates/nostr-signer", features = ["native"] }
radroots-sql-core = { path = "../lib/crates/sql-core", features = ["native"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
diff --git a/src/app/mod.rs b/src/app/mod.rs
@@ -43,7 +43,7 @@ mod tests {
use radroots_identity::RadrootsIdentity;
- use crate::config::MycConfig;
+ use crate::config::{MycConfig, MycSignerStateBackend};
use super::MycApp;
@@ -89,4 +89,27 @@ mod tests {
assert!(!snapshot.user_public_key_hex.is_empty());
assert!(!snapshot.transport.enabled);
}
+
+ #[test]
+ fn app_bootstrap_uses_backend_aware_signer_state_path() {
+ let temp = tempfile::tempdir().expect("tempdir");
+ let mut config = MycConfig::default();
+ config.paths.state_dir = PathBuf::from(temp.path()).join("state");
+ config.paths.signer_identity_path = temp.path().join("identity.json");
+ config.paths.user_identity_path = temp.path().join("user.json");
+ config.persistence.signer_state_backend = MycSignerStateBackend::Sqlite;
+ write_test_identity(
+ &config.paths.signer_identity_path,
+ "1111111111111111111111111111111111111111111111111111111111111111",
+ );
+ write_test_identity(
+ &config.paths.user_identity_path,
+ "2222222222222222222222222222222222222222222222222222222222222222",
+ );
+
+ let app = MycApp::bootstrap(config).expect("bootstrap");
+ let snapshot = app.snapshot();
+
+ assert!(snapshot.signer_state_path.ends_with("signer-state.sqlite"));
+ }
}
diff --git a/src/app/runtime.rs b/src/app/runtime.rs
@@ -18,7 +18,7 @@ use crate::transport::{MycNip46Service, MycNostrTransport, MycTransportSnapshot}
use radroots_identity::{RadrootsIdentity, RadrootsIdentityPublic};
use radroots_nostr_signer::prelude::{
RadrootsNostrFileSignerStore, RadrootsNostrSignerApprovalRequirement,
- RadrootsNostrSignerManager, RadrootsNostrSignerStore,
+ RadrootsNostrSignerManager, RadrootsNostrSignerStore, RadrootsNostrSqliteSignerStore,
};
use serde::Serialize;
@@ -293,7 +293,10 @@ impl MycRuntimePaths {
Self {
signer_identity_path: config.paths.signer_identity_path.clone(),
user_identity_path: config.paths.user_identity_path.clone(),
- signer_state_path: state_dir.join("signer-state.json"),
+ signer_state_path: state_dir.join(match config.persistence.signer_state_backend {
+ MycSignerStateBackend::JsonFile => "signer-state.json",
+ MycSignerStateBackend::Sqlite => "signer-state.sqlite",
+ }),
audit_dir: state_dir.join("audit"),
state_dir,
}
@@ -385,7 +388,7 @@ impl MycSignerContext {
MycIdentityProvider::from_source("user", user_identity_source)?;
let signer_identity = signer_identity_provider.load_identity()?;
let user_identity = user_identity_provider.load_identity()?;
- let signer_store = Self::build_signer_store(persistence, &paths.signer_state_path);
+ let signer_store = Self::build_signer_store(persistence, &paths.signer_state_path)?;
let operation_audit_store =
Self::build_operation_audit_store(persistence, &paths.audit_dir, audit_config)?;
let manager = Self::load_signer_manager_from_store(signer_store.clone())?;
@@ -419,9 +422,14 @@ impl MycSignerContext {
fn build_signer_store(
persistence: &MycPersistenceConfig,
path: &Path,
- ) -> Arc<dyn RadrootsNostrSignerStore> {
+ ) -> Result<Arc<dyn RadrootsNostrSignerStore>, MycError> {
match persistence.signer_state_backend {
- MycSignerStateBackend::JsonFile => Arc::new(RadrootsNostrFileSignerStore::new(path)),
+ MycSignerStateBackend::JsonFile => {
+ Ok(Arc::new(RadrootsNostrFileSignerStore::new(path)))
+ }
+ MycSignerStateBackend::Sqlite => {
+ Ok(Arc::new(RadrootsNostrSqliteSignerStore::open(path)?))
+ }
}
}
@@ -498,11 +506,11 @@ mod tests {
use radroots_identity::RadrootsIdentity;
use radroots_nostr_signer::prelude::{
- RadrootsNostrFileSignerStore, RadrootsNostrSignerManager,
+ RadrootsNostrFileSignerStore, RadrootsNostrSignerManager, RadrootsNostrSqliteSignerStore,
};
use crate::audit::{MycOperationAuditKind, MycOperationAuditOutcome, MycOperationAuditRecord};
- use crate::config::{MycConfig, MycRuntimeAuditBackend};
+ use crate::config::{MycConfig, MycRuntimeAuditBackend, MycSignerStateBackend};
use crate::error::MycError;
use super::MycRuntime;
@@ -672,6 +680,76 @@ mod tests {
}
#[test]
+ fn bootstrap_supports_sqlite_signer_state_backend() {
+ let temp = tempfile::tempdir().expect("tempdir");
+ let mut config = MycConfig::default();
+ config.paths.state_dir = temp.path().join("state");
+ config.paths.signer_identity_path = temp.path().join("signer.json");
+ config.paths.user_identity_path = temp.path().join("user.json");
+ config.persistence.signer_state_backend = MycSignerStateBackend::Sqlite;
+ write_test_identity(
+ &config.paths.signer_identity_path,
+ "1111111111111111111111111111111111111111111111111111111111111111",
+ );
+ write_test_identity(
+ &config.paths.user_identity_path,
+ "2222222222222222222222222222222222222222222222222222222222222222",
+ );
+
+ let runtime = MycRuntime::bootstrap(config).expect("runtime");
+
+ assert!(
+ runtime
+ .paths()
+ .signer_state_path
+ .ends_with("signer-state.sqlite")
+ );
+ assert!(runtime.paths().signer_state_path.is_file());
+ }
+
+ #[test]
+ fn bootstrap_rejects_mismatched_persisted_sqlite_signer_identity() {
+ let temp = tempfile::tempdir().expect("tempdir");
+ let identity_path = temp.path().join("identity.json");
+ let user_path = temp.path().join("user.json");
+ write_test_identity(
+ &identity_path,
+ "1111111111111111111111111111111111111111111111111111111111111111",
+ );
+ write_test_identity(
+ &user_path,
+ "3333333333333333333333333333333333333333333333333333333333333333",
+ );
+
+ let store_identity = RadrootsIdentity::from_secret_key_str(
+ "2222222222222222222222222222222222222222222222222222222222222222",
+ )
+ .expect("second identity");
+ let store = Arc::new(
+ RadrootsNostrSqliteSignerStore::open(
+ temp.path().join("state").join("signer-state.sqlite"),
+ )
+ .expect("open sqlite store"),
+ );
+ let manager = RadrootsNostrSignerManager::new(store).expect("manager");
+ manager
+ .set_signer_identity(store_identity.to_public())
+ .expect("persist signer");
+
+ let mut config = MycConfig::default();
+ config.paths.state_dir = temp.path().join("state");
+ config.paths.signer_identity_path = identity_path;
+ config.paths.user_identity_path = user_path;
+ config.persistence.signer_state_backend = MycSignerStateBackend::Sqlite;
+
+ let err = match MycRuntime::bootstrap(config) {
+ Ok(_) => panic!("expected identity mismatch"),
+ Err(err) => err,
+ };
+ assert!(matches!(err, MycError::SignerIdentityMismatch { .. }));
+ }
+
+ #[test]
fn bootstrap_supports_sqlite_operation_audit_backend() {
let temp = tempfile::tempdir().expect("tempdir");
let mut config = MycConfig::default();
diff --git a/src/config.rs b/src/config.rs
@@ -141,6 +141,7 @@ pub enum MycIdentityBackend {
#[serde(rename_all = "snake_case")]
pub enum MycSignerStateBackend {
JsonFile,
+ Sqlite,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
@@ -368,6 +369,7 @@ impl MycSignerStateBackend {
pub fn as_str(self) -> &'static str {
match self {
Self::JsonFile => "json_file",
+ Self::Sqlite => "sqlite",
}
}
}
@@ -1352,10 +1354,11 @@ fn parse_signer_state_backend_env(
) -> Result<MycSignerStateBackend, MycError> {
match value {
"json_file" => Ok(MycSignerStateBackend::JsonFile),
+ "sqlite" => Ok(MycSignerStateBackend::Sqlite),
_ => Err(config_parse_error(
path,
line_number,
- format!("{key} must be `json_file`"),
+ format!("{key} must be `json_file` or `sqlite`"),
)),
}
}
@@ -2314,4 +2317,27 @@ MYC_PERSISTENCE_RUNTIME_AUDIT_BACKEND=sqlite
.contains("MYC_PERSISTENCE_RUNTIME_AUDIT_BACKEND=sqlite")
);
}
+
+ #[test]
+ fn parse_signer_state_backend_supports_sqlite() {
+ let config = MycConfig::from_env_str(
+ r#"
+MYC_PATHS_SIGNER_IDENTITY_PATH=/tmp/signer.json
+MYC_PATHS_USER_IDENTITY_PATH=/tmp/user.json
+MYC_PERSISTENCE_SIGNER_STATE_BACKEND=sqlite
+ "#,
+ )
+ .expect("config");
+
+ assert_eq!(
+ config.persistence.signer_state_backend,
+ MycSignerStateBackend::Sqlite
+ );
+ assert!(
+ config
+ .to_env_string()
+ .expect("render env")
+ .contains("MYC_PERSISTENCE_SIGNER_STATE_BACKEND=sqlite")
+ );
+ }
}