myc

Self-custodial remote signer for Radroots apps
git clone https://radroots.dev/git/myc.git
Log | Files | Refs | README | LICENSE

commit c32ca9a6f5d9a1fde25f23f59337ff37618bc628
parent 580ec6f4d69a55c59b478f3c5ac6d1774419c715
Author: triesap <tyson@radroots.org>
Date:   Sun, 12 Apr 2026 19:20:43 +0000

myc: consume shared nip46 handler

Diffstat:
Msrc/audit_sqlite.rs | 1+
Msrc/control.rs | 13+++++++------
Msrc/outbox_sqlite.rs | 1+
Msrc/policy.rs | 128++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
Msrc/transport/nip46.rs | 419++++++++++++-------------------------------------------------------------------
5 files changed, 164 insertions(+), 398 deletions(-)

diff --git a/src/audit_sqlite.rs b/src/audit_sqlite.rs @@ -24,6 +24,7 @@ static MYC_OPERATION_AUDIT_MIGRATIONS: &[Migration] = &[Migration { down_sql: include_str!("../migrations/0000_runtime_audit_init.down.sql"), }]; +/// Myc keeps its operational audit store local to the service boundary. pub struct MycSqliteOperationAuditStore { db: MycOperationAuditSqliteDb, config: MycAuditConfig, diff --git a/src/control.rs b/src/control.rs @@ -358,13 +358,10 @@ async fn replay_authorized_request( )); } }; - runtime - .signer_context() - .record_signer_request_audit(&evaluation.audit); - let handled_request = match handler + let handled_outcome = match handler .handle_authorized_request_evaluation(pending_request.request_message.clone(), evaluation) { - Ok(handled_request) => handled_request, + Ok(handled_outcome) => handled_outcome, Err(error) => { return Err(cancel_auth_replay_workflow_on_error( runtime, @@ -375,7 +372,11 @@ async fn replay_authorized_request( )); } }; - let Some((response, _, consume_connect_secret_for)) = handled_request.into_publish_parts() + if let Some(audit) = handled_outcome.audit.as_ref() { + runtime.signer_context().record_signer_request_audit(audit); + } + let Some((response, _, consume_connect_secret_for)) = + handled_outcome.handled_request.into_publish_parts() else { let error = MycError::InvalidOperation( "authorized auth replay did not produce a response".to_owned(), diff --git a/src/outbox_sqlite.rs b/src/outbox_sqlite.rs @@ -22,6 +22,7 @@ static MYC_DELIVERY_OUTBOX_MIGRATIONS: &[Migration] = &[Migration { down_sql: include_str!("../migrations/0000_delivery_outbox_init.down.sql"), }]; +/// Myc keeps its delivery outbox store local to the service boundary. pub struct MycSqliteDeliveryOutboxStore { db: MycDeliveryOutboxSqliteDb, } diff --git a/src/policy.rs b/src/policy.rs @@ -8,9 +8,9 @@ use radroots_nostr_connect::prelude::{ RadrootsNostrConnectRequest, RadrootsNostrConnectRequestMessage, }; use radroots_nostr_signer::prelude::{ - RadrootsNostrSignerApprovalRequirement, RadrootsNostrSignerConnectionRecord, - RadrootsNostrSignerManager, RadrootsNostrSignerRequestAuditRecord, - RadrootsNostrSignerRequestDecision, + RadrootsNostrSignerApprovalRequirement, RadrootsNostrSignerBackend, + RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerManager, + RadrootsNostrSignerNip46ConnectDecision, RadrootsNostrSignerNip46Policy, }; use crate::config::{MycConnectionApproval, MycPolicyConfig}; @@ -176,9 +176,9 @@ impl MycPolicyContext { } } - pub fn prepare_request( + pub fn prepare_request<B: RadrootsNostrSignerBackend>( &self, - manager: &RadrootsNostrSignerManager, + backend: &B, connection: &RadrootsNostrSignerConnectionRecord, request_message: &RadrootsNostrConnectRequestMessage, ) -> Result<Option<String>, MycError> { @@ -196,7 +196,7 @@ impl MycPolicyContext { { if self.request_uses_automatic_auth(connection, &request_message.request) { if let Some(reason) = - self.require_auth_challenge_with_guardrails(manager, connection)? + self.require_auth_challenge_with_guardrails(backend, connection)? { return Ok(Some(reason)); } @@ -207,7 +207,7 @@ impl MycPolicyContext { } } else if self.should_require_fresh_auth(connection, &request_message.request) { if let Some(reason) = - self.require_auth_challenge_with_guardrails(manager, connection)? + self.require_auth_challenge_with_guardrails(backend, connection)? { return Ok(Some(reason)); } @@ -240,29 +240,12 @@ impl MycPolicyContext { if !self.stale_session_requires_cleanup(&connection) { continue; } - self.require_auth_challenge(manager, &connection)?; + self.require_auth_challenge_with_manager(manager, &connection)?; cleaned += 1; } Ok(cleaned) } - pub fn record_policy_denied_request( - &self, - manager: &RadrootsNostrSignerManager, - connection: &RadrootsNostrSignerConnectionRecord, - request_message: &RadrootsNostrConnectRequestMessage, - reason: impl Into<String>, - ) -> Result<RadrootsNostrSignerRequestAuditRecord, MycError> { - let reason = reason.into(); - Ok(manager.record_request( - &connection.connection_id, - &request_message.id, - request_message.request.method(), - RadrootsNostrSignerRequestDecision::Denied, - Some(reason.clone()), - )?) - } - fn client_is_denied(&self, client_public_key: &PublicKey) -> bool { self.denied_client_pubkeys .contains(&client_public_key.to_hex()) @@ -386,9 +369,9 @@ impl MycPolicyContext { self.auth_url.is_some() && self.client_is_trusted(&connection.client_public_key) } - fn require_auth_challenge_with_guardrails( + fn require_auth_challenge_with_guardrails<B: RadrootsNostrSignerBackend>( &self, - manager: &RadrootsNostrSignerManager, + backend: &B, connection: &RadrootsNostrSignerConnectionRecord, ) -> Result<Option<String>, MycError> { if let Some(retry_after_secs) = self @@ -401,11 +384,20 @@ impl MycPolicyContext { retry_after_secs, ))); } - self.require_auth_challenge(manager, connection)?; + self.require_auth_challenge_with_backend(backend, connection)?; Ok(None) } - fn require_auth_challenge( + fn require_auth_challenge_with_backend<B: RadrootsNostrSignerBackend>( + &self, + backend: &B, + connection: &RadrootsNostrSignerConnectionRecord, + ) -> Result<(), MycError> { + backend.require_auth_challenge(&connection.connection_id, self.auth_url()?)?; + Ok(()) + } + + fn require_auth_challenge_with_manager( &self, manager: &RadrootsNostrSignerManager, connection: &RadrootsNostrSignerConnectionRecord, @@ -448,6 +440,56 @@ impl MycPolicyContext { } } +impl<B: RadrootsNostrSignerBackend> RadrootsNostrSignerNip46Policy<B> for MycPolicyContext { + fn connect_decision( + &self, + client_public_key: &PublicKey, + ) -> RadrootsNostrSignerNip46ConnectDecision { + match self.connect_decision(client_public_key) { + MycConnectDecision::Allow => RadrootsNostrSignerNip46ConnectDecision::Allow, + MycConnectDecision::RequireApproval => { + RadrootsNostrSignerNip46ConnectDecision::RequireApproval + } + MycConnectDecision::Deny => RadrootsNostrSignerNip46ConnectDecision::Deny, + } + } + + fn connect_rate_limit_denied_reason(&self, client_public_key: &PublicKey) -> Option<String> { + self.connect_rate_limit_denied_reason(client_public_key) + } + + fn approval_requirement_for_client( + &self, + client_public_key: &PublicKey, + ) -> Option<RadrootsNostrSignerApprovalRequirement> { + self.approval_requirement_for_client(client_public_key) + } + + fn filtered_requested_permissions( + &self, + requested_permissions: &RadrootsNostrConnectPermissions, + ) -> RadrootsNostrConnectPermissions { + self.filtered_requested_permissions(requested_permissions) + } + + fn auto_granted_permissions( + &self, + requested_permissions: &RadrootsNostrConnectPermissions, + ) -> RadrootsNostrConnectPermissions { + self.auto_granted_permissions(requested_permissions) + } + + fn prepare_request( + &self, + backend: &B, + connection: &RadrootsNostrSignerConnectionRecord, + request_message: &RadrootsNostrConnectRequestMessage, + ) -> Result<Option<String>, radroots_nostr_signer::prelude::RadrootsNostrSignerError> { + self.prepare_request(backend, connection, request_message) + .map_err(myc_policy_signer_error) + } +} + impl MycPolicyRateLimiter { fn check_and_record(&self, key: &str) -> Option<u64> { let now_unix = now_unix_secs(); @@ -625,6 +667,12 @@ fn now_unix_secs() -> u64 { .unwrap_or_default() } +fn myc_policy_signer_error( + error: MycError, +) -> radroots_nostr_signer::prelude::RadrootsNostrSignerError { + radroots_nostr_signer::prelude::RadrootsNostrSignerError::InvalidState(error.to_string()) +} + #[cfg(test)] mod tests { use super::{MycConnectDecision, MycPolicyContext}; @@ -637,8 +685,9 @@ mod tests { RadrootsNostrConnectRequestMessage, }; use radroots_nostr_signer::prelude::{ - RadrootsNostrSignerApprovalRequirement, RadrootsNostrSignerAuthState, - RadrootsNostrSignerConnectionDraft, RadrootsNostrSignerManager, + RadrootsNostrEmbeddedSignerBackend, RadrootsNostrSignerApprovalRequirement, + RadrootsNostrSignerAuthState, RadrootsNostrSignerConnectionDraft, + RadrootsNostrSignerManager, }; use serde_json::json; use std::thread; @@ -663,6 +712,14 @@ mod tests { manager } + fn backend_for(manager: &RadrootsNostrSignerManager) -> RadrootsNostrEmbeddedSignerBackend { + RadrootsNostrEmbeddedSignerBackend::new( + manager.clone(), + identity("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), + ) + .expect("backend") + } + fn register_connection( manager: &RadrootsNostrSignerManager, client_public_key: PublicKey, @@ -761,6 +818,7 @@ mod tests { config.allowed_sign_event_kinds = vec![1]; let policy = MycPolicyContext::from_config(&config).expect("policy"); let manager = in_memory_manager(); + let backend = backend_for(&manager); let connection = register_connection( &manager, public_key("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"), @@ -768,7 +826,7 @@ mod tests { let denied = policy .prepare_request( - &manager, + &backend, &connection, &RadrootsNostrConnectRequestMessage::new( "request-1", @@ -817,6 +875,7 @@ mod tests { config.auth_authorized_ttl_secs = Some(1); let policy = MycPolicyContext::from_config(&config).expect("policy"); let manager = in_memory_manager(); + let backend = backend_for(&manager); let connection = register_connection(&manager, client_public_key); manager @@ -833,7 +892,7 @@ mod tests { .expect("connection"); let denied = policy .prepare_request( - &manager, + &backend, &connection, &RadrootsNostrConnectRequestMessage::new( "request-1", @@ -870,6 +929,7 @@ mod tests { config.reauth_after_inactivity_secs = Some(1); let policy = MycPolicyContext::from_config(&config).expect("policy"); let manager = in_memory_manager(); + let backend = backend_for(&manager); let connection = register_connection(&manager, client_public_key); manager @@ -895,7 +955,7 @@ mod tests { .expect("connection"); let denied = policy .prepare_request( - &manager, + &backend, &connection, &RadrootsNostrConnectRequestMessage::new( "request-1", diff --git a/src/transport/nip46.rs b/src/transport/nip46.rs @@ -6,30 +6,36 @@ use radroots_nostr::prelude::{ RadrootsNostrRelayPoolNotification, RadrootsNostrRelayUrl, }; use radroots_nostr_connect::prelude::{ - RADROOTS_NOSTR_CONNECT_RPC_KIND, RadrootsNostrConnectRequest, - RadrootsNostrConnectRequestMessage, RadrootsNostrConnectResponse, + RADROOTS_NOSTR_CONNECT_RPC_KIND, RadrootsNostrConnectRequestMessage, + RadrootsNostrConnectResponse, }; use radroots_nostr_signer::prelude::{ - RadrootsNostrSignerConnectEvaluation, RadrootsNostrSignerConnectionId, - RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerHandledRequest, - RadrootsNostrSignerNip46Codec, RadrootsNostrSignerNip46Signer, - RadrootsNostrSignerRequestAction, RadrootsNostrSignerRequestEvaluation, - RadrootsNostrSignerSessionLookup, RadrootsNostrSignerWorkflowId, connect_response_outcome, - handled_request_for_action, response_from_hint, + RadrootsNostrSignerConnectionId, RadrootsNostrSignerHandledRequestOutcome, + RadrootsNostrSignerNip46Handler, RadrootsNostrSignerNip46Signer, + RadrootsNostrSignerRequestEvaluation, RadrootsNostrSignerWorkflowId, }; use tokio::sync::broadcast; +#[cfg(test)] +use radroots_nostr_signer::prelude::RadrootsNostrSignerHandledRequest; + use crate::app::MycSignerContext; +use crate::app::backend::MycSignerBackend; use crate::audit::{MycOperationAuditKind, MycOperationAuditOutcome, MycOperationAuditRecord}; use crate::error::MycError; use crate::outbox::{MycDeliveryOutboxKind, MycDeliveryOutboxRecord, MycDeliveryOutboxStore}; use crate::transport::MycNostrTransport; +type MycNip46CoreHandler = RadrootsNostrSignerNip46Handler< + MycSignerBackend, + crate::policy::MycPolicyContext, + MycNip46Signer, +>; + #[derive(Clone)] pub struct MycNip46Handler { signer: MycSignerContext, - relays: Vec<RadrootsNostrRelayUrl>, - codec: RadrootsNostrSignerNip46Codec<MycNip46Signer>, + handler: MycNip46CoreHandler, } pub struct MycNip46Service { @@ -38,12 +44,7 @@ pub struct MycNip46Service { delivery_outbox_store: Arc<dyn MycDeliveryOutboxStore>, } -enum MycPreparedRequestEvaluation { - Denied(String), - Evaluation(RadrootsNostrSignerRequestEvaluation), -} - -type MycNip46HandledRequest = RadrootsNostrSignerHandledRequest; +type MycNip46HandledOutcome = RadrootsNostrSignerHandledRequestOutcome; #[derive(Clone)] struct MycNip46Signer { @@ -81,8 +82,8 @@ impl RadrootsNostrSignerNip46Signer for MycNip46Signer { }) } - fn user_public_key(&self) -> RadrootsNostrPublicKey { - self.signer.user_identity().public_key() + fn user_identity(&self) -> radroots_identity::RadrootsIdentityPublic { + self.signer.user_public_identity() } fn sign_user_event( @@ -152,25 +153,26 @@ impl RadrootsNostrSignerNip46Signer for MycNip46Signer { impl MycNip46Handler { pub fn new(signer: MycSignerContext, relays: Vec<RadrootsNostrRelayUrl>) -> Self { - let codec = RadrootsNostrSignerNip46Codec::new(MycNip46Signer { - signer: signer.clone(), - }); - Self { - signer, + let handler = RadrootsNostrSignerNip46Handler::new( + MycSignerBackend::new(signer.clone()), + signer.policy().clone(), relays, - codec, - } + MycNip46Signer { + signer: signer.clone(), + }, + ); + Self { signer, handler } } pub fn filter(&self) -> Result<RadrootsNostrFilter, MycError> { - self.codec.filter().map_err(Into::into) + self.handler.filter().map_err(Into::into) } pub fn parse_request_event( &self, event: &RadrootsNostrEvent, ) -> Result<RadrootsNostrConnectRequestMessage, MycError> { - self.codec.parse_request_event(event).map_err(Into::into) + self.handler.parse_request_event(event).map_err(Into::into) } pub fn build_response_event( @@ -179,7 +181,7 @@ impl MycNip46Handler { request_id: impl Into<String>, response: RadrootsNostrConnectResponse, ) -> Result<radroots_nostr::prelude::RadrootsNostrEventBuilder, MycError> { - self.codec + self.handler .build_response_event(client_public_key, request_id, response) .map_err(Into::into) } @@ -188,36 +190,10 @@ impl MycNip46Handler { &self, client_public_key: RadrootsNostrPublicKey, request_message: RadrootsNostrConnectRequestMessage, - ) -> Result<MycNip46HandledRequest, MycError> { - match request_message.request.clone() { - RadrootsNostrConnectRequest::Connect { secret, .. } => { - self.handle_connect_request(client_public_key, request_message.request, secret) - } - RadrootsNostrConnectRequest::SignEvent(unsigned_event) => { - self.handle_sign_event_request(client_public_key, request_message, unsigned_event) - } - RadrootsNostrConnectRequest::Nip04Encrypt { .. } - | RadrootsNostrConnectRequest::Nip04Decrypt { .. } - | RadrootsNostrConnectRequest::Nip44Encrypt { .. } - | RadrootsNostrConnectRequest::Nip44Decrypt { .. } => { - self.handle_crypto_request(client_public_key, request_message) - } - RadrootsNostrConnectRequest::GetPublicKey - | RadrootsNostrConnectRequest::GetSessionCapability - | RadrootsNostrConnectRequest::Ping - | RadrootsNostrConnectRequest::SwitchRelays => { - self.handle_base_request(client_public_key, request_message) - } - _ => Ok(MycNip46HandledRequest::respond( - RadrootsNostrConnectResponse::Error { - result: None, - error: format!( - "method `{}` is not implemented yet", - request_message.request.method() - ), - }, - )), - } + ) -> Result<MycNip46HandledOutcome, MycError> { + self.handler + .handle_request(client_public_key, request_message) + .map_err(Into::into) } #[cfg(test)] @@ -227,308 +203,27 @@ impl MycNip46Handler { request_message: RadrootsNostrConnectRequestMessage, ) -> Result<RadrootsNostrConnectResponse, MycError> { match self.handle_request(client_public_key, request_message)? { - MycNip46HandledRequest::Respond { response, .. } => Ok(response), - MycNip46HandledRequest::Ignore => Err(MycError::InvalidOperation( + MycNip46HandledOutcome { + handled_request: RadrootsNostrSignerHandledRequest::Respond { response, .. }, + .. + } => Ok(response), + MycNip46HandledOutcome { + handled_request: RadrootsNostrSignerHandledRequest::Ignore, + .. + } => Err(MycError::InvalidOperation( "request was ignored without a response".to_owned(), )), } } - fn handle_connect_request( - &self, - client_public_key: RadrootsNostrPublicKey, - request: RadrootsNostrConnectRequest, - secret: Option<String>, - ) -> Result<MycNip46HandledRequest, MycError> { - let manager = self.signer.load_signer_manager()?; - let connect_decision = self.signer.policy().connect_decision(&client_public_key); - if let Some(connect_secret) = secret.as_deref() { - if let Some(connection) = manager.find_connection_by_connect_secret(connect_secret)? { - if connection.connect_secret_is_consumed() { - tracing::debug!( - connection_id = %connection.connection_id, - "ignoring reused consumed NIP-46 connect secret" - ); - return Ok(MycNip46HandledRequest::Ignore); - } - } - } - if !matches!(connect_decision, crate::policy::MycConnectDecision::Deny) { - if let Some(reason) = self - .signer - .policy() - .connect_rate_limit_denied_reason(&client_public_key) - { - return Ok(MycNip46HandledRequest::respond( - RadrootsNostrConnectResponse::Error { - result: None, - error: reason, - }, - )); - } - } - let evaluation = manager.evaluate_connect_request(client_public_key, request)?; - - match evaluation { - RadrootsNostrSignerConnectEvaluation::ExistingConnection(connection) => { - if secret.is_some() && connection.connect_secret_is_consumed() { - tracing::debug!( - connection_id = %connection.connection_id, - "ignoring reused consumed NIP-46 connect secret" - ); - return Ok(MycNip46HandledRequest::Ignore); - } - if matches!(connect_decision, crate::policy::MycConnectDecision::Deny) { - return Ok(MycNip46HandledRequest::respond( - RadrootsNostrConnectResponse::Error { - result: None, - error: "client public key denied by policy".to_owned(), - }, - )); - } - Ok(connect_response_outcome(&connection, secret)) - } - RadrootsNostrSignerConnectEvaluation::RegistrationRequired(proposal) => { - let requested_permissions = self - .signer - .policy() - .filtered_requested_permissions(&proposal.requested_permissions); - let Some(approval_requirement) = self - .signer - .policy() - .approval_requirement_for_client(&client_public_key) - else { - return Ok(MycNip46HandledRequest::respond( - RadrootsNostrConnectResponse::Error { - result: None, - error: "client public key denied by policy".to_owned(), - }, - )); - }; - let draft = proposal - .into_connection_draft(self.signer.user_public_identity()) - .with_requested_permissions(requested_permissions) - .with_relays(self.relays.clone()) - .with_approval_requirement(approval_requirement); - let connection = manager.register_connection(draft)?; - if approval_requirement - == radroots_nostr_signer::prelude::RadrootsNostrSignerApprovalRequirement::NotRequired - { - let granted_permissions = self - .signer - .policy() - .auto_granted_permissions(&connection.requested_permissions); - let _ = manager.set_granted_permissions( - &connection.connection_id, - granted_permissions, - )?; - } - Ok(connect_response_outcome(&connection, secret)) - } - } - } - - fn handle_base_request( - &self, - client_public_key: RadrootsNostrPublicKey, - request_message: RadrootsNostrConnectRequestMessage, - ) -> Result<MycNip46HandledRequest, MycError> { - let connection = match self.lookup_connection(client_public_key)? { - Ok(connection) => connection, - Err(response) => return Ok(MycNip46HandledRequest::respond(response)), - }; - - match self.evaluate_request_with_policy(&connection, request_message)? { - MycPreparedRequestEvaluation::Denied(reason) => { - Ok(MycNip46HandledRequest::respond_for_connection( - Some(connection.connection_id.clone()), - RadrootsNostrConnectResponse::Error { - result: None, - error: reason, - }, - )) - } - MycPreparedRequestEvaluation::Evaluation(evaluation) => { - let response_hint = match &evaluation.action { - RadrootsNostrSignerRequestAction::Allowed { response_hint, .. } => { - Some(response_hint.clone()) - } - _ => None, - }; - Ok(handled_request_for_action( - &evaluation.connection, - evaluation.action, - || { - Ok(response_from_hint( - &evaluation.connection, - response_hint.expect("allowed action carries response hint"), - )) - }, - )?) - } - } - } - - fn handle_sign_event_request( - &self, - client_public_key: RadrootsNostrPublicKey, - request_message: RadrootsNostrConnectRequestMessage, - unsigned_event: nostr::UnsignedEvent, - ) -> Result<MycNip46HandledRequest, MycError> { - let connection = match self.lookup_connection(client_public_key)? { - Ok(connection) => connection, - Err(response) => return Ok(MycNip46HandledRequest::respond(response)), - }; - - match self.evaluate_request_with_policy(&connection, request_message)? { - MycPreparedRequestEvaluation::Denied(reason) => { - Ok(MycNip46HandledRequest::respond_for_connection( - Some(connection.connection_id.clone()), - RadrootsNostrConnectResponse::Error { - result: None, - error: reason, - }, - )) - } - MycPreparedRequestEvaluation::Evaluation(evaluation) => Ok(handled_request_for_action( - &evaluation.connection, - evaluation.action, - || self.codec.sign_event_response(unsigned_event), - )?), - } - } - - fn handle_crypto_request( - &self, - client_public_key: RadrootsNostrPublicKey, - request_message: RadrootsNostrConnectRequestMessage, - ) -> Result<MycNip46HandledRequest, MycError> { - let request = request_message.request.clone(); - let connection = match self.lookup_connection(client_public_key)? { - Ok(connection) => connection, - Err(response) => return Ok(MycNip46HandledRequest::respond(response)), - }; - - match self.evaluate_request_with_policy(&connection, request_message)? { - MycPreparedRequestEvaluation::Denied(reason) => { - Ok(MycNip46HandledRequest::respond_for_connection( - Some(connection.connection_id.clone()), - RadrootsNostrConnectResponse::Error { - result: None, - error: reason, - }, - )) - } - MycPreparedRequestEvaluation::Evaluation(evaluation) => Ok(handled_request_for_action( - &evaluation.connection, - evaluation.action, - || self.codec.crypto_response(request), - )?), - } - } - pub(crate) fn handle_authorized_request_evaluation( &self, request_message: RadrootsNostrConnectRequestMessage, evaluation: RadrootsNostrSignerRequestEvaluation, - ) -> Result<MycNip46HandledRequest, MycError> { - Ok(match request_message.request.clone() { - RadrootsNostrConnectRequest::SignEvent(unsigned_event) => { - handled_request_for_action(&evaluation.connection, evaluation.action, || { - self.codec.sign_event_response(unsigned_event) - })? - } - RadrootsNostrConnectRequest::Nip04Encrypt { .. } - | RadrootsNostrConnectRequest::Nip04Decrypt { .. } - | RadrootsNostrConnectRequest::Nip44Encrypt { .. } - | RadrootsNostrConnectRequest::Nip44Decrypt { .. } => { - handled_request_for_action(&evaluation.connection, evaluation.action, || { - self.codec.crypto_response(request_message.request) - })? - } - RadrootsNostrConnectRequest::GetPublicKey - | RadrootsNostrConnectRequest::GetSessionCapability - | RadrootsNostrConnectRequest::Ping - | RadrootsNostrConnectRequest::SwitchRelays => { - let response_hint = match &evaluation.action { - RadrootsNostrSignerRequestAction::Allowed { response_hint, .. } => { - Some(response_hint.clone()) - } - _ => None, - }; - handled_request_for_action(&evaluation.connection, evaluation.action, || { - Ok(response_from_hint( - &evaluation.connection, - response_hint.expect("allowed action carries response hint"), - )) - })? - } - other => MycNip46HandledRequest::respond_for_connection( - Some(evaluation.connection.connection_id.clone()), - RadrootsNostrConnectResponse::Error { - result: None, - error: format!("method `{}` is not implemented yet", other.method()), - }, - ), - }) - } - - fn evaluate_request_with_policy( - &self, - connection: &RadrootsNostrSignerConnectionRecord, - request_message: RadrootsNostrConnectRequestMessage, - ) -> Result<MycPreparedRequestEvaluation, MycError> { - let manager = self.signer.load_signer_manager()?; - if let Some(reason) = - self.signer - .policy() - .prepare_request(&manager, connection, &request_message)? - { - let audit = self.signer.policy().record_policy_denied_request( - &manager, - connection, - &request_message, - reason, - )?; - self.signer.record_signer_request_audit(&audit); - return Ok(MycPreparedRequestEvaluation::Denied( - audit - .message - .unwrap_or_else(|| "request denied by policy".to_owned()), - )); - } - - let evaluation = manager.evaluate_request(&connection.connection_id, request_message)?; - self.signer.record_signer_request_audit(&evaluation.audit); - Ok(MycPreparedRequestEvaluation::Evaluation(evaluation)) - } - - fn lookup_connection( - &self, - client_public_key: RadrootsNostrPublicKey, - ) -> Result<Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrConnectResponse>, MycError> - { - Ok( - match self - .signer - .load_signer_manager()? - .lookup_session(&client_public_key, None)? - { - RadrootsNostrSignerSessionLookup::Connection(connection) => Ok(connection), - RadrootsNostrSignerSessionLookup::None => { - Err(RadrootsNostrConnectResponse::Error { - result: None, - error: "unauthorized".to_owned(), - }) - } - RadrootsNostrSignerSessionLookup::Ambiguous(_) => { - Err(RadrootsNostrConnectResponse::Error { - result: None, - error: "ambiguous client sessions".to_owned(), - }) - } - }, - ) + ) -> Result<MycNip46HandledOutcome, MycError> { + self.handler + .handle_authorized_request_evaluation(request_message, evaluation) + .map_err(Into::into) } } @@ -596,18 +291,21 @@ impl MycNip46Service { }; let request_id = request_message.id.clone(); - let handled_request = match self.handler.handle_request(event.pubkey, request_message) { - Ok(handled_request) => handled_request, + let handled_outcome = match self.handler.handle_request(event.pubkey, request_message) { + Ok(handled_outcome) => handled_outcome, Err(error) => { tracing::warn!(error = %error, "failed to handle NIP-46 request"); - MycNip46HandledRequest::respond(RadrootsNostrConnectResponse::Error { + MycNip46HandledOutcome::respond(RadrootsNostrConnectResponse::Error { result: None, error: error.to_string(), }) } }; + if let Some(audit) = handled_outcome.audit.as_ref() { + self.handler.signer.record_signer_request_audit(audit); + } let Some((response, connection_id, consume_connect_secret_for)) = - handled_request.into_publish_parts() + handled_outcome.handled_request.into_publish_parts() else { tracing::debug!( request_id = %request_id, @@ -984,13 +682,15 @@ mod tests { RadrootsNostrConnectRequestMessage, RadrootsNostrConnectResponse, RadrootsNostrConnectResponseEnvelope, }; - use radroots_nostr_signer::prelude::RadrootsNostrSignerConnectionRecord; + use radroots_nostr_signer::prelude::{ + RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerHandledRequest, + }; use serde_json::json; use crate::app::MycRuntime; use crate::config::{MycConfig, MycConnectionApproval}; - use super::{MycNip46HandledRequest, MycNip46Handler}; + use super::MycNip46Handler; fn write_identity(path: &std::path::Path, secret_key: &str) { let identity = @@ -1336,7 +1036,10 @@ mod tests { ) .expect("ignored response"); - assert_eq!(ignored, MycNip46HandledRequest::Ignore); + assert_eq!( + ignored.handled_request, + RadrootsNostrSignerHandledRequest::Ignore + ); let connections = runtime .signer_manager() .expect("manager")