From 9c762edbd20e29fc475db23cfced69a3035547df Mon Sep 17 00:00:00 2001 From: Laan Tungir Date: Thu, 20 Aug 2026 18:34:14 -0400 Subject: [PATCH] v0.0.18 - Fix sign-event JSON escaping and variable-path role key caching (per-path re-derivation) --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/dispatcher.rs | 20 ++++++++++++++++---- src/key_store.rs | 2 ++ src/lib.rs | 2 +- src/role_table.rs | 3 +++ 6 files changed, 24 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 883eb51..7b053f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2207,7 +2207,7 @@ dependencies = [ [[package]] name = "signer" -version = "0.0.17" +version = "0.0.18" dependencies = [ "base64", "chacha20poly1305", diff --git a/Cargo.toml b/Cargo.toml index e1f7ea0..e0d9f56 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "signer" -version = "0.0.17" +version = "0.0.18" edition = "2021" license = "MIT" description = "Attended Nostr signing daemon — Rust port of n_signer" diff --git a/src/dispatcher.rs b/src/dispatcher.rs index 6a2a766..23e4376 100644 --- a/src/dispatcher.rs +++ b/src/dispatcher.rs @@ -359,9 +359,16 @@ fn handle_nostr_verb( // Ensure key is derived. For variable-path roles, use the concrete // path supplied by the client; for fixed-path roles, use the stored - // template. - if !role.derived { - let has_variable = role.has_variable_path(); + // template. Variable-path roles are re-derived whenever the requested + // path differs from the currently-derived one (the cache is per-path, + // not per-role). + let has_variable = role.has_variable_path(); + let needs_derive = if has_variable && sel.has_role_path { + role.derived_path.as_deref() != Some(sel.role_path.as_str()) + } else { + !role.derived + }; + if needs_derive { let result = if has_variable && sel.has_role_path { ctx.key_store .derive_one_with_path(ctx.role_table, ctx.mnemonic, role_index, &sel.role_path) @@ -414,7 +421,12 @@ fn handle_nostr_verb( None => return make_error_response(id, RpcError::INVALID_PARAMS), }; match ctx.key_store.sign_event(role_index, event_json) { - Ok(signed) => make_success_response(id, &format!("\"{}\"", signed)), + // The signed event is itself JSON; serialize it as a proper + // JSON string value so embedded quotes are escaped. + Ok(signed) => { + let wrapped = serde_json::to_string(&signed).unwrap_or_else(|_| "\"\"".into()); + make_success_response(id, &wrapped) + } Err(_) => make_error_response(id, RpcError::INVALID_PARAMS), } } diff --git a/src/key_store.rs b/src/key_store.rs index 6f57546..31449fd 100644 --- a/src/key_store.rs +++ b/src/key_store.rs @@ -133,10 +133,12 @@ impl KeyStore { role.derived = false; role.pubkey_hex.clear(); + role.derived_path = None; let dk = derive_for_role(concrete_path, role, phrase)?; role.pubkey_hex = dk.pubkey_hex.clone(); role.derived = true; + role.derived_path = Some(concrete_path.to_string()); if self.keys.len() <= role_index { self.keys.resize_with(role_index + 1, || None); diff --git a/src/lib.rs b/src/lib.rs index 9daf21e..d8d2622 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -31,4 +31,4 @@ pub mod error; pub use error::SignerError; /// Version string (matches C NSIGNER_VERSION). -pub const VERSION: &str = "v0.0.17"; +pub const VERSION: &str = "v0.0.18"; diff --git a/src/role_table.rs b/src/role_table.rs index 9d988d3..9f55460 100644 --- a/src/role_table.rs +++ b/src/role_table.rs @@ -137,6 +137,8 @@ pub struct RoleEntry { pub pubkey_hex: String, /// 1 if pubkey_hex has been populated. pub derived: bool, + /// The concrete path the key was last derived for (variable-path roles). + pub derived_path: Option, /// Inclusive lower bound for %d; -1 = fixed path (no variable). pub path_range_lo: i32, /// Inclusive upper bound; == path_range_lo for single. @@ -162,6 +164,7 @@ impl Default for RoleEntry { role_path: String::new(), pubkey_hex: String::new(), derived: false, + derived_path: None, path_range_lo: -1, path_range_hi: -1, path_default_index: -1,