commit 9c2a27ec147ebcd5a37bd53411e5893257788c9c
parent c1b9477907e83decd889ddf59b3dd0c3fa003faa
Author: triesap <tyson@radroots.org>
Date: Fri, 27 Mar 2026 22:29:07 +0000
service: add runtime-backed signer backend
- add a myc signer backend adapter over the shared rr-rs signer contract
- route signer-facing cli and control flows through the runtime-backed backend
- expose signer backend capabilities and workflow counts in operability status
- cover the backend-first surface with tests and update the runtime docs
Diffstat:
7 files changed, 588 insertions(+), 60 deletions(-)
diff --git a/src/app/backend.rs b/src/app/backend.rs
@@ -0,0 +1,426 @@
+use nostr::{PublicKey, RelayUrl, UnsignedEvent};
+use radroots_identity::RadrootsIdentityPublic;
+use radroots_nostr_connect::prelude::{
+ RadrootsNostrConnectMethod, RadrootsNostrConnectPermissions, RadrootsNostrConnectRequest,
+ RadrootsNostrConnectRequestMessage,
+};
+use radroots_nostr_signer::prelude::{
+ RadrootsNostrLocalSignerAvailability, RadrootsNostrLocalSignerCapability,
+ RadrootsNostrRemoteSessionSignerCapability, RadrootsNostrSignerAuthorizationOutcome,
+ RadrootsNostrSignerBackend, RadrootsNostrSignerBackendCapabilities,
+ RadrootsNostrSignerCapability, RadrootsNostrSignerConnectEvaluation,
+ RadrootsNostrSignerConnectionDraft, RadrootsNostrSignerConnectionId,
+ RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerConnectionStatus,
+ RadrootsNostrSignerError, RadrootsNostrSignerManager, RadrootsNostrSignerPendingRequest,
+ RadrootsNostrSignerPublishTransition, RadrootsNostrSignerPublishWorkflowRecord,
+ RadrootsNostrSignerRequestAuditRecord, RadrootsNostrSignerRequestDecision,
+ RadrootsNostrSignerRequestEvaluation, RadrootsNostrSignerSessionLookup,
+ RadrootsNostrSignerSignOutput, RadrootsNostrSignerWorkflowId,
+};
+
+use crate::app::MycSignerContext;
+use crate::error::MycError;
+
+#[derive(Clone)]
+pub struct MycSignerBackend {
+ signer: MycSignerContext,
+}
+
+impl MycSignerBackend {
+ pub fn new(signer: MycSignerContext) -> Self {
+ Self { signer }
+ }
+
+ fn manager(&self) -> Result<RadrootsNostrSignerManager, RadrootsNostrSignerError> {
+ self.signer
+ .load_signer_manager()
+ .map_err(convert_runtime_signer_error)
+ }
+
+ fn configured_signer_identity(&self) -> RadrootsIdentityPublic {
+ self.signer.signer_public_identity()
+ }
+
+ fn local_signer_capability(&self) -> RadrootsNostrLocalSignerCapability {
+ let public_identity = self.configured_signer_identity();
+ RadrootsNostrLocalSignerCapability::new(
+ public_identity.id.clone(),
+ public_identity,
+ RadrootsNostrLocalSignerAvailability::SecretBacked,
+ )
+ }
+}
+
+impl RadrootsNostrSignerBackend for MycSignerBackend {
+ fn signer_identity(&self) -> Result<Option<RadrootsIdentityPublic>, RadrootsNostrSignerError> {
+ Ok(Some(self.configured_signer_identity()))
+ }
+
+ fn set_signer_identity(
+ &self,
+ signer_identity: RadrootsIdentityPublic,
+ ) -> Result<(), RadrootsNostrSignerError> {
+ let configured = self.configured_signer_identity();
+ if configured.id != signer_identity.id
+ || configured.public_key_hex != signer_identity.public_key_hex
+ || configured.public_key_npub != signer_identity.public_key_npub
+ {
+ return Err(RadrootsNostrSignerError::InvalidState(format!(
+ "runtime-backed myc signer backend cannot switch signer identity from `{}` to `{}`",
+ configured.id, signer_identity.id
+ )));
+ }
+ self.manager()?.set_signer_identity(signer_identity)
+ }
+
+ fn capabilities(
+ &self,
+ ) -> Result<RadrootsNostrSignerBackendCapabilities, RadrootsNostrSignerError> {
+ let remote_sessions = self
+ .manager()?
+ .list_connections()?
+ .into_iter()
+ .filter(|record| record.status == RadrootsNostrSignerConnectionStatus::Active)
+ .map(|record| RadrootsNostrRemoteSessionSignerCapability::from(&record))
+ .collect();
+ Ok(RadrootsNostrSignerBackendCapabilities::new(
+ Some(self.local_signer_capability()),
+ remote_sessions,
+ ))
+ }
+
+ fn list_connections(
+ &self,
+ ) -> Result<Vec<RadrootsNostrSignerConnectionRecord>, RadrootsNostrSignerError> {
+ self.manager()?.list_connections()
+ }
+
+ fn get_connection(
+ &self,
+ connection_id: &RadrootsNostrSignerConnectionId,
+ ) -> Result<Option<RadrootsNostrSignerConnectionRecord>, RadrootsNostrSignerError> {
+ self.manager()?.get_connection(connection_id)
+ }
+
+ fn list_publish_workflows(
+ &self,
+ ) -> Result<Vec<RadrootsNostrSignerPublishWorkflowRecord>, RadrootsNostrSignerError> {
+ self.manager()?.list_publish_workflows()
+ }
+
+ fn get_publish_workflow(
+ &self,
+ workflow_id: &RadrootsNostrSignerWorkflowId,
+ ) -> Result<Option<RadrootsNostrSignerPublishWorkflowRecord>, RadrootsNostrSignerError> {
+ self.manager()?.get_publish_workflow(workflow_id)
+ }
+
+ fn find_connections_by_client_public_key(
+ &self,
+ client_public_key: &PublicKey,
+ ) -> Result<Vec<RadrootsNostrSignerConnectionRecord>, RadrootsNostrSignerError> {
+ self.manager()?
+ .find_connections_by_client_public_key(client_public_key)
+ }
+
+ fn find_connection_by_connect_secret(
+ &self,
+ connect_secret: &str,
+ ) -> Result<Option<RadrootsNostrSignerConnectionRecord>, RadrootsNostrSignerError> {
+ self.manager()?
+ .find_connection_by_connect_secret(connect_secret)
+ }
+
+ fn lookup_session(
+ &self,
+ client_public_key: &PublicKey,
+ connect_secret: Option<&str>,
+ ) -> Result<RadrootsNostrSignerSessionLookup, RadrootsNostrSignerError> {
+ self.manager()?
+ .lookup_session(client_public_key, connect_secret)
+ }
+
+ fn evaluate_connect_request(
+ &self,
+ client_public_key: PublicKey,
+ request: RadrootsNostrConnectRequest,
+ ) -> Result<RadrootsNostrSignerConnectEvaluation, RadrootsNostrSignerError> {
+ self.manager()?
+ .evaluate_connect_request(client_public_key, request)
+ }
+
+ fn register_connection(
+ &self,
+ draft: RadrootsNostrSignerConnectionDraft,
+ ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> {
+ self.manager()?.register_connection(draft)
+ }
+
+ fn set_granted_permissions(
+ &self,
+ connection_id: &RadrootsNostrSignerConnectionId,
+ granted_permissions: RadrootsNostrConnectPermissions,
+ ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> {
+ self.manager()?
+ .set_granted_permissions(connection_id, granted_permissions)
+ }
+
+ fn approve_connection(
+ &self,
+ connection_id: &RadrootsNostrSignerConnectionId,
+ granted_permissions: RadrootsNostrConnectPermissions,
+ ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> {
+ self.manager()?
+ .approve_connection(connection_id, granted_permissions)
+ }
+
+ fn reject_connection(
+ &self,
+ connection_id: &RadrootsNostrSignerConnectionId,
+ reason: Option<String>,
+ ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> {
+ self.manager()?.reject_connection(connection_id, reason)
+ }
+
+ fn revoke_connection(
+ &self,
+ connection_id: &RadrootsNostrSignerConnectionId,
+ reason: Option<String>,
+ ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> {
+ self.manager()?.revoke_connection(connection_id, reason)
+ }
+
+ fn update_relays(
+ &self,
+ connection_id: &RadrootsNostrSignerConnectionId,
+ relays: Vec<RelayUrl>,
+ ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> {
+ self.manager()?.update_relays(connection_id, relays)
+ }
+
+ fn require_auth_challenge(
+ &self,
+ connection_id: &RadrootsNostrSignerConnectionId,
+ auth_url: &str,
+ ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> {
+ self.manager()?
+ .require_auth_challenge(connection_id, auth_url)
+ }
+
+ fn set_pending_request(
+ &self,
+ connection_id: &RadrootsNostrSignerConnectionId,
+ request_message: RadrootsNostrConnectRequestMessage,
+ ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> {
+ self.manager()?
+ .set_pending_request(connection_id, request_message)
+ }
+
+ fn authorize_auth_challenge(
+ &self,
+ connection_id: &RadrootsNostrSignerConnectionId,
+ ) -> Result<RadrootsNostrSignerAuthorizationOutcome, RadrootsNostrSignerError> {
+ self.manager()?.authorize_auth_challenge(connection_id)
+ }
+
+ fn restore_pending_auth_challenge(
+ &self,
+ connection_id: &RadrootsNostrSignerConnectionId,
+ pending_request: RadrootsNostrSignerPendingRequest,
+ ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> {
+ self.manager()?
+ .restore_pending_auth_challenge(connection_id, pending_request)
+ }
+
+ fn begin_connect_secret_publish_finalization(
+ &self,
+ connection_id: &RadrootsNostrSignerConnectionId,
+ ) -> Result<RadrootsNostrSignerPublishTransition, RadrootsNostrSignerError> {
+ self.manager()?
+ .begin_connect_secret_publish_finalization(connection_id)
+ .map(RadrootsNostrSignerPublishTransition::begun)
+ }
+
+ fn begin_auth_replay_publish_finalization(
+ &self,
+ connection_id: &RadrootsNostrSignerConnectionId,
+ ) -> Result<RadrootsNostrSignerPublishTransition, RadrootsNostrSignerError> {
+ self.manager()?
+ .begin_auth_replay_publish_finalization(connection_id)
+ .map(RadrootsNostrSignerPublishTransition::begun)
+ }
+
+ fn mark_publish_workflow_published(
+ &self,
+ workflow_id: &RadrootsNostrSignerWorkflowId,
+ ) -> Result<RadrootsNostrSignerPublishTransition, RadrootsNostrSignerError> {
+ self.manager()?
+ .mark_publish_workflow_published(workflow_id)
+ .map(RadrootsNostrSignerPublishTransition::marked_published)
+ }
+
+ fn finalize_publish_workflow(
+ &self,
+ workflow_id: &RadrootsNostrSignerWorkflowId,
+ ) -> Result<RadrootsNostrSignerPublishTransition, RadrootsNostrSignerError> {
+ let connection = self.manager()?.finalize_publish_workflow(workflow_id)?;
+ Ok(RadrootsNostrSignerPublishTransition::finalized(
+ workflow_id.clone(),
+ connection,
+ ))
+ }
+
+ fn cancel_publish_workflow(
+ &self,
+ workflow_id: &RadrootsNostrSignerWorkflowId,
+ ) -> Result<RadrootsNostrSignerPublishTransition, RadrootsNostrSignerError> {
+ self.manager()?
+ .cancel_publish_workflow(workflow_id)
+ .map(RadrootsNostrSignerPublishTransition::cancelled)
+ }
+
+ fn mark_authenticated(
+ &self,
+ connection_id: &RadrootsNostrSignerConnectionId,
+ ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> {
+ self.manager()?.mark_authenticated(connection_id)
+ }
+
+ fn mark_connect_secret_consumed(
+ &self,
+ connection_id: &RadrootsNostrSignerConnectionId,
+ ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> {
+ self.manager()?.mark_connect_secret_consumed(connection_id)
+ }
+
+ fn evaluate_request(
+ &self,
+ connection_id: &RadrootsNostrSignerConnectionId,
+ request_message: RadrootsNostrConnectRequestMessage,
+ ) -> Result<RadrootsNostrSignerRequestEvaluation, RadrootsNostrSignerError> {
+ self.manager()?
+ .evaluate_request(connection_id, request_message)
+ }
+
+ fn evaluate_auth_replay_publish_workflow(
+ &self,
+ workflow_id: &RadrootsNostrSignerWorkflowId,
+ ) -> Result<RadrootsNostrSignerRequestEvaluation, RadrootsNostrSignerError> {
+ self.manager()?
+ .evaluate_auth_replay_publish_workflow(workflow_id)
+ }
+
+ fn record_request(
+ &self,
+ connection_id: &RadrootsNostrSignerConnectionId,
+ request_id: &str,
+ method: RadrootsNostrConnectMethod,
+ decision: RadrootsNostrSignerRequestDecision,
+ message: Option<String>,
+ ) -> Result<RadrootsNostrSignerRequestAuditRecord, RadrootsNostrSignerError> {
+ self.manager()?
+ .record_request(connection_id, request_id, method, decision, message)
+ }
+
+ fn sign_unsigned_event(
+ &self,
+ unsigned_event: UnsignedEvent,
+ ) -> Result<RadrootsNostrSignerSignOutput, RadrootsNostrSignerError> {
+ let event = self
+ .signer
+ .signer_identity()
+ .sign_unsigned_event(unsigned_event, "myc signer backend event")
+ .map_err(|error| RadrootsNostrSignerError::Sign(error.to_string()))?;
+ Ok(RadrootsNostrSignerSignOutput::new(
+ RadrootsNostrSignerCapability::LocalAccount(self.local_signer_capability()),
+ event,
+ ))
+ }
+}
+
+fn convert_runtime_signer_error(error: MycError) -> RadrootsNostrSignerError {
+ match error {
+ MycError::SignerState(source) => source,
+ other => RadrootsNostrSignerError::InvalidState(other.to_string()),
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use std::path::PathBuf;
+
+ use nostr::Keys;
+ use radroots_identity::RadrootsIdentity;
+ use radroots_nostr_signer::prelude::{
+ RadrootsNostrSignerBackend, RadrootsNostrSignerConnectionDraft,
+ };
+
+ use crate::app::MycRuntime;
+ use crate::config::MycConfig;
+
+ fn write_identity(path: &std::path::Path, secret_key: &str) {
+ RadrootsIdentity::from_secret_key_str(secret_key)
+ .expect("identity")
+ .save_json(path)
+ .expect("save identity");
+ }
+
+ fn test_runtime() -> MycRuntime {
+ let temp = tempfile::tempdir().expect("tempdir").keep();
+ let mut config = MycConfig::default();
+ config.paths.state_dir = PathBuf::from(&temp).join("state");
+ config.paths.signer_identity_path = PathBuf::from(&temp).join("signer.json");
+ config.paths.user_identity_path = PathBuf::from(&temp).join("user.json");
+ write_identity(
+ &config.paths.signer_identity_path,
+ "1111111111111111111111111111111111111111111111111111111111111111",
+ );
+ write_identity(
+ &config.paths.user_identity_path,
+ "2222222222222222222222222222222222222222222222222222222222222222",
+ );
+ MycRuntime::bootstrap(config).expect("runtime")
+ }
+
+ #[test]
+ fn runtime_backed_backend_projects_local_and_remote_capabilities() {
+ let runtime = test_runtime();
+ let backend = runtime.signer_backend();
+
+ let initial = backend.capabilities().expect("capabilities");
+ assert!(
+ initial
+ .local_signer
+ .expect("local signer capability")
+ .is_secret_backed()
+ );
+ assert!(initial.remote_sessions.is_empty());
+
+ let connection = backend
+ .register_connection(RadrootsNostrSignerConnectionDraft::new(
+ Keys::generate().public_key(),
+ runtime.user_public_identity(),
+ ))
+ .expect("register connection");
+
+ let capabilities = backend.capabilities().expect("capabilities after approval");
+ assert_eq!(capabilities.remote_sessions.len(), 1);
+ assert_eq!(
+ capabilities.remote_sessions[0].connection_id,
+ connection.connection_id
+ );
+ }
+
+ #[test]
+ fn runtime_backed_backend_rejects_signer_identity_drift() {
+ let runtime = test_runtime();
+ let backend = runtime.signer_backend();
+ let other_identity = RadrootsIdentity::generate().to_public();
+
+ let error = backend
+ .set_signer_identity(other_identity)
+ .expect_err("identity drift should be rejected");
+
+ assert!(error.to_string().contains("cannot switch signer identity"));
+ }
+}
diff --git a/src/app/mod.rs b/src/app/mod.rs
@@ -1,8 +1,10 @@
+pub mod backend;
pub mod runtime;
use crate::config::MycConfig;
use crate::error::MycError;
+pub use backend::MycSignerBackend;
pub use runtime::{MycRuntime, MycRuntimePaths, MycSignerContext, MycStartupSnapshot};
#[derive(Clone)]
diff --git a/src/app/runtime.rs b/src/app/runtime.rs
@@ -4,6 +4,7 @@ use std::net::SocketAddr;
use std::path::{Path, PathBuf};
use std::sync::Arc;
+use super::backend::MycSignerBackend;
use crate::audit::{
MycJsonlOperationAuditStore, MycOperationAuditKind, MycOperationAuditOutcome,
MycOperationAuditRecord, MycOperationAuditStore,
@@ -162,6 +163,10 @@ impl MycRuntime {
self.signer.load_signer_manager()
}
+ pub fn signer_backend(&self) -> MycSignerBackend {
+ MycSignerBackend::new(self.signer.clone())
+ }
+
pub fn transport(&self) -> Option<&MycNostrTransport> {
self.transport.as_ref()
}
diff --git a/src/cli.rs b/src/cli.rs
@@ -4,8 +4,8 @@ use std::path::{Path, PathBuf};
use clap::{Args, Parser, Subcommand, ValueEnum};
use radroots_nostr_connect::prelude::RadrootsNostrConnectPermissions;
use radroots_nostr_signer::prelude::{
- RadrootsNostrSignerConnectionId, RadrootsNostrSignerConnectionRecord,
- RadrootsNostrSignerRequestAuditRecord,
+ RadrootsNostrSignerBackend, RadrootsNostrSignerConnectionId,
+ RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerRequestAuditRecord,
};
use serde::Serialize;
@@ -432,34 +432,29 @@ pub async fn run_from_env() -> Result<(), MycError> {
}
MycCommand::Connections { command } => {
let runtime = MycRuntime::bootstrap(config)?;
+ let backend = runtime.signer_backend();
match command {
- MycConnectionsCommand::List => {
- let manager = runtime.signer_manager()?;
- print_json(&manager.list_connections()?)
- }
+ MycConnectionsCommand::List => print_json(&backend.list_connections()?),
MycConnectionsCommand::Approve(args) => {
let connection_id = parse_connection_id(&args.connection_id)?;
- let manager = runtime.signer_manager()?;
let granted_permissions = granted_permissions_for_approval(
runtime.signer_context().policy(),
- &manager.list_connections()?,
+ &backend.list_connections()?,
&connection_id,
&args.grants,
)?;
let connection =
- manager.approve_connection(&connection_id, granted_permissions)?;
+ backend.approve_connection(&connection_id, granted_permissions)?;
print_json(&connection)
}
MycConnectionsCommand::Reject(args) => {
let connection_id = parse_connection_id(&args.connection_id)?;
- let manager = runtime.signer_manager()?;
- let connection = manager.reject_connection(&connection_id, args.reason)?;
+ let connection = backend.reject_connection(&connection_id, args.reason)?;
print_json(&connection)
}
MycConnectionsCommand::Revoke(args) => {
let connection_id = parse_connection_id(&args.connection_id)?;
- let manager = runtime.signer_manager()?;
- let connection = manager.revoke_connection(&connection_id, args.reason)?;
+ let connection = backend.revoke_connection(&connection_id, args.reason)?;
print_json(&connection)
}
}
@@ -513,11 +508,11 @@ pub async fn run_from_env() -> Result<(), MycError> {
}
MycCommand::Auth { command } => {
let runtime = MycRuntime::bootstrap(config)?;
+ let backend = runtime.signer_backend();
match command {
MycAuthCommand::Require { connection_id, url } => {
let connection_id = parse_connection_id(&connection_id)?;
- let manager = runtime.signer_manager()?;
- let connection = manager.require_auth_challenge(&connection_id, url)?;
+ let connection = backend.require_auth_challenge(&connection_id, &url)?;
print_json(&connection)
}
MycAuthCommand::Authorize { connection_id } => {
diff --git a/src/control.rs b/src/control.rs
@@ -5,9 +5,10 @@ use radroots_nostr_connect::prelude::{
RadrootsNostrConnectResponse, RadrootsNostrConnectUri,
};
use radroots_nostr_signer::prelude::{
- RadrootsNostrSignerApprovalRequirement, RadrootsNostrSignerConnectionId,
- RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerRequestId,
- RadrootsNostrSignerWorkflowId,
+ RadrootsNostrSignerApprovalRequirement, RadrootsNostrSignerBackend,
+ RadrootsNostrSignerConnectionId, RadrootsNostrSignerConnectionRecord,
+ RadrootsNostrSignerPublishTransition, RadrootsNostrSignerPublishWorkflowRecord,
+ RadrootsNostrSignerRequestId, RadrootsNostrSignerWorkflowId,
};
use serde::Serialize;
@@ -34,20 +35,23 @@ pub async fn authorize_auth_challenge(
runtime: &MycRuntime,
connection_id: &RadrootsNostrSignerConnectionId,
) -> Result<MycAuthorizedReplayOutput, MycError> {
- let manager = runtime.signer_manager()?;
- let connection = manager.get_connection(connection_id)?.ok_or_else(|| {
+ let backend = runtime.signer_backend();
+ let connection = backend.get_connection(connection_id)?.ok_or_else(|| {
MycError::InvalidOperation(format!("connection `{connection_id}` was not found"))
})?;
runtime
.signer_context()
.policy()
.ensure_authorize_auth_challenge_allowed(&connection)?;
- let workflow = manager.begin_auth_replay_publish_finalization(connection_id)?;
+ let workflow = workflow_from_transition(
+ backend.begin_auth_replay_publish_finalization(connection_id)?,
+ "auth replay",
+ )?;
let replayed_request_id =
replay_authorized_request(runtime, &connection.connection_id, &workflow.workflow_id)
.await?;
let connection = runtime
- .signer_manager()?
+ .signer_backend()
.get_connection(connection_id)?
.ok_or_else(|| {
MycError::InvalidOperation(format!("connection `{connection_id}` was not found"))
@@ -88,7 +92,7 @@ pub async fn accept_client_uri(
secret: Some(client_uri.secret.clone()),
requested_permissions: client_uri.metadata.requested_permissions.clone(),
};
- let manager = runtime.signer_manager()?;
+ let backend = runtime.signer_backend();
let Some(approval_requirement) = runtime
.signer_context()
.policy()
@@ -98,7 +102,7 @@ pub async fn accept_client_uri(
"client public key denied by policy".to_owned(),
));
};
- let connection = match manager.evaluate_connect_request(client_uri.client_public_key, request)? {
+ let connection = match backend.evaluate_connect_request(client_uri.client_public_key, request)? {
radroots_nostr_signer::prelude::RadrootsNostrSignerConnectEvaluation::ExistingConnection(
connection,
) => {
@@ -132,7 +136,7 @@ pub async fn accept_client_uri(
.with_requested_permissions(requested_permissions)
.with_relays(preferred_relays.clone())
.with_approval_requirement(approval_requirement);
- let connection = manager.register_connection(draft)?;
+ let connection = backend.register_connection(draft)?;
if approval_requirement
== RadrootsNostrSignerApprovalRequirement::NotRequired
{
@@ -140,7 +144,7 @@ pub async fn accept_client_uri(
.signer_context()
.policy()
.auto_granted_permissions(&connection.requested_permissions);
- let _ = manager.set_granted_permissions(
+ let _ = backend.set_granted_permissions(
&connection.connection_id,
granted_permissions,
)?;
@@ -157,7 +161,10 @@ pub async fn accept_client_uri(
RadrootsNostrConnectResponse::ConnectSecretEcho(client_uri.secret),
)?;
let response_relays = merge_relays(&client_uri.relays, &preferred_relays);
- let workflow = manager.begin_connect_secret_publish_finalization(&connection.connection_id)?;
+ let workflow = workflow_from_transition(
+ backend.begin_connect_secret_publish_finalization(&connection.connection_id)?,
+ "connect accept",
+ )?;
let event = match runtime
.signer_identity()
.sign_event_builder(event, "connect accept response")
@@ -223,7 +230,7 @@ pub async fn accept_client_uri(
));
}
};
- if let Err(error) = manager.mark_publish_workflow_published(&workflow.workflow_id) {
+ if let Err(error) = backend.mark_publish_workflow_published(&workflow.workflow_id) {
record_post_publish_failure(
runtime,
MycOperationAuditKind::ConnectAcceptPublish,
@@ -248,7 +255,7 @@ pub async fn accept_client_uri(
);
return Err(error);
}
- if let Err(error) = manager.finalize_publish_workflow(&workflow.workflow_id) {
+ if let Err(error) = backend.finalize_publish_workflow(&workflow.workflow_id) {
record_post_publish_failure(
runtime,
MycOperationAuditKind::ConnectAcceptPublish,
@@ -283,11 +290,8 @@ pub async fn accept_client_uri(
);
Ok(MycAcceptedConnectionOutput {
- connection: runtime
- .signer_manager()?
- .list_connections()?
- .into_iter()
- .find(|record| record.connection_id == connection.connection_id)
+ connection: backend
+ .get_connection(&connection.connection_id)?
.ok_or_else(|| {
MycError::InvalidOperation("accepted connection was not persisted".to_owned())
})?,
@@ -319,8 +323,8 @@ async fn replay_authorized_request(
connection_id: &RadrootsNostrSignerConnectionId,
workflow_id: &RadrootsNostrSignerWorkflowId,
) -> Result<Option<String>, MycError> {
- let manager = runtime.signer_manager()?;
- let workflow = manager.get_publish_workflow(workflow_id)?.ok_or_else(|| {
+ let backend = runtime.signer_backend();
+ let workflow = backend.get_publish_workflow(workflow_id)?.ok_or_else(|| {
MycError::InvalidOperation(format!("publish workflow `{workflow_id}` was not found"))
})?;
let Some(pending_request) = workflow.pending_request.clone() else {
@@ -342,7 +346,7 @@ async fn replay_authorized_request(
}
};
let handler = MycNip46Handler::new(runtime.signer_context(), transport.relays().to_vec());
- let evaluation = match manager.evaluate_auth_replay_publish_workflow(workflow_id) {
+ let evaluation = match backend.evaluate_auth_replay_publish_workflow(workflow_id) {
Ok(evaluation) => evaluation,
Err(error) => {
return Err(cancel_auth_replay_workflow_on_error(
@@ -396,7 +400,7 @@ async fn replay_authorized_request(
));
}
let event = match handler.build_response_event(
- manager
+ backend
.get_connection(connection_id)?
.ok_or_else(|| {
MycError::InvalidOperation(format!("connection `{connection_id}` was not found"))
@@ -416,7 +420,7 @@ async fn replay_authorized_request(
));
}
};
- let connection = manager.get_connection(connection_id)?.ok_or_else(|| {
+ let connection = backend.get_connection(connection_id)?.ok_or_else(|| {
MycError::InvalidOperation(format!("connection `{connection_id}` was not found"))
})?;
let event = match runtime
@@ -497,7 +501,7 @@ async fn replay_authorized_request(
));
}
};
- if let Err(error) = manager.mark_publish_workflow_published(workflow_id) {
+ if let Err(error) = backend.mark_publish_workflow_published(workflow_id) {
record_post_publish_failure(
runtime,
MycOperationAuditKind::AuthReplayPublish,
@@ -522,7 +526,7 @@ async fn replay_authorized_request(
);
return Err(error);
}
- if let Err(error) = manager.finalize_publish_workflow(workflow_id) {
+ if let Err(error) = backend.finalize_publish_workflow(workflow_id) {
record_post_publish_failure(
runtime,
MycOperationAuditKind::AuthReplayPublish,
@@ -566,11 +570,11 @@ fn cancel_auth_replay_workflow_on_error(
error: MycError,
) -> MycError {
let summary = publish_failure_summary(&error);
- match runtime.signer_manager().and_then(|manager| {
- manager
- .cancel_publish_workflow(workflow_id)
- .map_err(Into::into)
- }) {
+ match runtime
+ .signer_backend()
+ .cancel_publish_workflow(workflow_id)
+ .map_err(MycError::from)
+ {
Ok(_) => {
let mut record = MycOperationAuditRecord::new(
MycOperationAuditKind::AuthReplayRestore,
@@ -616,12 +620,12 @@ fn cancel_connect_accept_workflow_on_error(
workflow_id: &RadrootsNostrSignerWorkflowId,
error: MycError,
) -> MycError {
- match runtime.signer_manager().and_then(|manager| {
- manager
- .cancel_publish_workflow(workflow_id)
- .map(|_| ())
- .map_err(Into::into)
- }) {
+ match runtime
+ .signer_backend()
+ .cancel_publish_workflow(workflow_id)
+ .map(|_| ())
+ .map_err(MycError::from)
+ {
Ok(()) => error,
Err(cancel_error) => MycError::InvalidOperation(format!(
"{error}; additionally failed to cancel connect-accept publish workflow: {cancel_error}"
@@ -629,6 +633,17 @@ fn cancel_connect_accept_workflow_on_error(
}
}
+fn workflow_from_transition(
+ transition: RadrootsNostrSignerPublishTransition,
+ operation: &str,
+) -> Result<RadrootsNostrSignerPublishWorkflowRecord, MycError> {
+ transition.workflow().cloned().ok_or_else(|| {
+ MycError::InvalidOperation(format!(
+ "{operation} publish workflow did not return a workflow record"
+ ))
+ })
+}
+
fn build_control_outbox_record(
kind: MycDeliveryOutboxKind,
event: radroots_nostr::prelude::RadrootsNostrEvent,
diff --git a/src/lib.rs b/src/lib.rs
@@ -17,7 +17,9 @@ pub mod persistence;
pub mod policy;
pub mod transport;
-pub use app::{MycApp, MycRuntime, MycRuntimePaths, MycSignerContext, MycStartupSnapshot};
+pub use app::{
+ MycApp, MycRuntime, MycRuntimePaths, MycSignerBackend, MycSignerContext, MycStartupSnapshot,
+};
pub use audit::{
MycJsonlOperationAuditStore, MycOperationAuditKind, MycOperationAuditOutcome,
MycOperationAuditRecord, MycOperationAuditStore,
@@ -52,9 +54,10 @@ pub use operability::{
MycDeliveryRecoveryStatusOutput, MycDiscoveryStatusOutput, MycMetricsSnapshot,
MycOperationOutcomeCounts, MycPersistenceStatusOutput, MycRelayProbe,
MycRelayProbeAvailability, MycRuntimeAuditPersistenceStatusOutput, MycRuntimeStatus,
- MycSignerStatePersistenceStatusOutput, MycSqliteSchemaStatusOutput, MycStatusFullOutput,
- MycStatusSummaryOutput, MycTransportStatusOutput, collect_metrics, collect_status_full,
- collect_status_summary, render_metrics_text,
+ MycSignerBackendStatusOutput, MycSignerStatePersistenceStatusOutput,
+ MycSqliteSchemaStatusOutput, MycStatusFullOutput, MycStatusSummaryOutput,
+ MycTransportStatusOutput, collect_metrics, collect_status_full, collect_status_summary,
+ render_metrics_text,
};
pub use outbox::{
MycDeliveryOutboxJobId, MycDeliveryOutboxKind, MycDeliveryOutboxRecord,
diff --git a/src/operability/mod.rs b/src/operability/mod.rs
@@ -7,8 +7,10 @@ use std::time::Duration;
use radroots_nostr::prelude::{RadrootsNostrRelayStatus, RadrootsNostrRelayUrl};
use radroots_nostr_signer::prelude::{
- RadrootsNostrSignerPublishWorkflowRecord, RadrootsNostrSignerPublishWorkflowState,
- RadrootsNostrSignerRequestAuditRecord, RadrootsNostrSignerRequestDecision,
+ RadrootsNostrLocalSignerCapability, RadrootsNostrRemoteSessionSignerCapability,
+ RadrootsNostrSignerBackend, RadrootsNostrSignerPublishWorkflowRecord,
+ RadrootsNostrSignerPublishWorkflowState, RadrootsNostrSignerRequestAuditRecord,
+ RadrootsNostrSignerRequestDecision,
};
use radroots_sql_core::{SqlExecutor, SqliteExecutor};
use serde::{Deserialize, Serialize};
@@ -103,6 +105,16 @@ pub struct MycPersistenceStatusOutput {
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
+pub struct MycSignerBackendStatusOutput {
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub local_signer: Option<RadrootsNostrLocalSignerCapability>,
+ pub remote_session_count: usize,
+ #[serde(skip_serializing_if = "Vec::is_empty", default)]
+ pub remote_sessions: Vec<RadrootsNostrRemoteSessionSignerCapability>,
+ pub publish_workflow_count: usize,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct MycDeliveryRecoveryStatusOutput {
pub recorded_at_unix: u64,
pub outcome: MycOperationAuditOutcome,
@@ -172,6 +184,7 @@ pub struct MycStatusFullOutput {
pub ready: bool,
pub reasons: Vec<String>,
pub startup: crate::app::MycStartupSnapshot,
+ pub signer_backend: MycSignerBackendStatusOutput,
pub custody: MycCustodyStatusOutput,
pub persistence: MycPersistenceStatusOutput,
pub delivery_outbox: MycDeliveryOutboxStatusOutput,
@@ -185,6 +198,7 @@ pub struct MycStatusSummaryOutput {
pub ready: bool,
pub reasons: Vec<String>,
pub instance_name: String,
+ pub signer_backend: MycSignerBackendStatusOutput,
pub custody: MycCustodyStatusOutput,
pub persistence: MycPersistenceStatusOutput,
pub delivery_outbox: MycDeliveryOutboxStatusOutput,
@@ -301,6 +315,7 @@ struct MycSqliteStoreVersionRow {
pub async fn collect_status_full(runtime: &MycRuntime) -> Result<MycStatusFullOutput, MycError> {
let snapshot = runtime.snapshot();
+ let signer_backend = collect_signer_backend_status(runtime)?;
let custody = collect_custody_status(runtime)?;
let persistence = collect_persistence_status(runtime);
let delivery_outbox = collect_delivery_outbox_status(runtime)?;
@@ -338,6 +353,7 @@ pub async fn collect_status_full(runtime: &MycRuntime) -> Result<MycStatusFullOu
ready,
reasons,
startup: snapshot,
+ signer_backend,
custody: custody.output,
persistence: persistence.output,
delivery_outbox: delivery_outbox.output,
@@ -355,6 +371,12 @@ pub async fn collect_status_summary(
ready: full.ready,
reasons: full.reasons,
instance_name: full.startup.instance_name,
+ signer_backend: MycSignerBackendStatusOutput {
+ local_signer: full.signer_backend.local_signer.clone(),
+ remote_session_count: full.signer_backend.remote_session_count,
+ remote_sessions: Vec::new(),
+ publish_workflow_count: full.signer_backend.publish_workflow_count,
+ },
custody: full.custody,
persistence: full.persistence,
delivery_outbox: full.delivery_outbox,
@@ -413,6 +435,20 @@ fn collect_custody_status(runtime: &MycRuntime) -> Result<MycCustodyStatusEvalua
})
}
+fn collect_signer_backend_status(
+ runtime: &MycRuntime,
+) -> Result<MycSignerBackendStatusOutput, MycError> {
+ let backend = runtime.signer_backend();
+ let capabilities = backend.capabilities()?;
+ let publish_workflow_count = backend.list_publish_workflows()?.len();
+ Ok(MycSignerBackendStatusOutput {
+ local_signer: capabilities.local_signer,
+ remote_session_count: capabilities.remote_sessions.len(),
+ remote_sessions: capabilities.remote_sessions,
+ publish_workflow_count,
+ })
+}
+
fn collect_persistence_status(runtime: &MycRuntime) -> MycPersistenceStatusEvaluation {
let signer_state_backend = runtime.config().persistence.signer_state_backend;
let runtime_audit_backend = runtime.config().persistence.runtime_audit_backend;
@@ -958,7 +994,7 @@ fn collect_delivery_outbox_status(
) -> Result<MycDeliveryOutboxStatusEvaluation, MycError> {
let outbox_records = runtime.delivery_outbox_store().list_all()?;
let workflow_by_id = runtime
- .signer_manager()?
+ .signer_backend()
.list_publish_workflows()?
.into_iter()
.map(|workflow| (workflow.workflow_id.to_string(), workflow))
@@ -1557,7 +1593,8 @@ mod tests {
use super::{
MycMetricsSnapshot, MycOperationOutcomeCounts, MycRuntimeStatus, collect_metrics,
- inspect_runtime_audit_sqlite_schema, render_metrics_text, worse_runtime_status,
+ collect_status_full, inspect_runtime_audit_sqlite_schema, render_metrics_text,
+ worse_runtime_status,
};
use crate::app::{MycRuntime, MycRuntimePaths};
use crate::audit::{MycOperationAuditKind, MycOperationAuditOutcome, MycOperationAuditRecord};
@@ -1716,4 +1753,49 @@ mod tests {
assert_eq!(metrics.runtime_operation_outcomes.succeeded, 1);
assert_eq!(metrics.delivery_recovery_success_count, 1);
}
+
+ #[tokio::test(flavor = "current_thread")]
+ async fn status_full_reports_signer_backend_capabilities() {
+ use radroots_nostr_signer::prelude::{
+ RadrootsNostrSignerBackend, RadrootsNostrSignerConnectionDraft,
+ };
+
+ 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");
+ 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");
+ let backend = runtime.signer_backend();
+ let connection = backend
+ .register_connection(RadrootsNostrSignerConnectionDraft::new(
+ nostr::Keys::generate().public_key(),
+ runtime.user_public_identity(),
+ ))
+ .expect("register connection");
+
+ let status = collect_status_full(&runtime).await.expect("status");
+ assert!(
+ status
+ .signer_backend
+ .local_signer
+ .expect("local signer")
+ .is_secret_backed()
+ );
+ assert_eq!(status.signer_backend.remote_session_count, 1);
+ assert_eq!(status.signer_backend.remote_sessions.len(), 1);
+ assert_eq!(
+ status.signer_backend.remote_sessions[0].connection_id,
+ connection.connection_id
+ );
+ }
}