lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

backend.rs (69684B)


      1 use crate::capability::{
      2     RadrootsNostrLocalSignerAvailability, RadrootsNostrLocalSignerCapability,
      3     RadrootsNostrRemoteSessionSignerCapability, RadrootsNostrSignerCapability,
      4 };
      5 use crate::error::RadrootsNostrSignerError;
      6 use crate::evaluation::{
      7     RadrootsNostrSignerConnectEvaluation, RadrootsNostrSignerRequestEvaluation,
      8     RadrootsNostrSignerSessionLookup,
      9 };
     10 use crate::manager::RadrootsNostrSignerManager;
     11 use crate::model::{
     12     RadrootsNostrSignerAuthorizationOutcome, RadrootsNostrSignerConnectionDraft,
     13     RadrootsNostrSignerConnectionId, RadrootsNostrSignerConnectionRecord,
     14     RadrootsNostrSignerConnectionStatus, RadrootsNostrSignerPendingRequest,
     15     RadrootsNostrSignerPublishWorkflowRecord, RadrootsNostrSignerRequestAuditRecord,
     16     RadrootsNostrSignerRequestDecision, RadrootsNostrSignerWorkflowId,
     17 };
     18 use nostr::{Event, EventBuilder, PublicKey, RelayUrl, UnsignedEvent};
     19 use radroots_identity::{RadrootsIdentity, RadrootsIdentityPublic};
     20 use radroots_nostr_connect::prelude::{
     21     RadrootsNostrConnectMethod, RadrootsNostrConnectPermissions, RadrootsNostrConnectRequest,
     22     RadrootsNostrConnectRequestMessage,
     23 };
     24 use serde::{Deserialize, Serialize};
     25 
     26 #[derive(Debug, Clone, Serialize, Deserialize)]
     27 pub struct RadrootsNostrSignerBackendCapabilities {
     28     #[serde(default, skip_serializing_if = "Option::is_none")]
     29     pub local_signer: Option<RadrootsNostrLocalSignerCapability>,
     30     #[serde(default)]
     31     pub remote_sessions: Vec<RadrootsNostrRemoteSessionSignerCapability>,
     32 }
     33 
     34 #[derive(Debug, Clone, Serialize, Deserialize)]
     35 pub struct RadrootsNostrSignerSignOutput {
     36     pub signer: RadrootsNostrSignerCapability,
     37     pub event: Event,
     38 }
     39 
     40 #[derive(Debug, Clone, Serialize, Deserialize)]
     41 #[serde(rename_all = "snake_case", tag = "state", content = "value")]
     42 pub enum RadrootsNostrSignerPublishTransition {
     43     Begun(RadrootsNostrSignerPublishWorkflowRecord),
     44     MarkedPublished(RadrootsNostrSignerPublishWorkflowRecord),
     45     Finalized {
     46         workflow_id: RadrootsNostrSignerWorkflowId,
     47         connection: Box<RadrootsNostrSignerConnectionRecord>,
     48     },
     49     Cancelled(RadrootsNostrSignerPublishWorkflowRecord),
     50 }
     51 
     52 pub trait RadrootsNostrSignerBackend: Send + Sync {
     53     fn signer_identity(&self) -> Result<Option<RadrootsIdentityPublic>, RadrootsNostrSignerError>;
     54 
     55     fn set_signer_identity(
     56         &self,
     57         signer_identity: RadrootsIdentityPublic,
     58     ) -> Result<(), RadrootsNostrSignerError>;
     59 
     60     fn capabilities(
     61         &self,
     62     ) -> Result<RadrootsNostrSignerBackendCapabilities, RadrootsNostrSignerError>;
     63 
     64     fn list_connections(
     65         &self,
     66     ) -> Result<Vec<RadrootsNostrSignerConnectionRecord>, RadrootsNostrSignerError>;
     67 
     68     fn get_connection(
     69         &self,
     70         connection_id: &RadrootsNostrSignerConnectionId,
     71     ) -> Result<Option<RadrootsNostrSignerConnectionRecord>, RadrootsNostrSignerError>;
     72 
     73     fn list_publish_workflows(
     74         &self,
     75     ) -> Result<Vec<RadrootsNostrSignerPublishWorkflowRecord>, RadrootsNostrSignerError>;
     76 
     77     fn get_publish_workflow(
     78         &self,
     79         workflow_id: &RadrootsNostrSignerWorkflowId,
     80     ) -> Result<Option<RadrootsNostrSignerPublishWorkflowRecord>, RadrootsNostrSignerError>;
     81 
     82     fn find_connections_by_client_public_key(
     83         &self,
     84         client_public_key: &PublicKey,
     85     ) -> Result<Vec<RadrootsNostrSignerConnectionRecord>, RadrootsNostrSignerError>;
     86 
     87     fn find_connection_by_connect_secret(
     88         &self,
     89         connect_secret: &str,
     90     ) -> Result<Option<RadrootsNostrSignerConnectionRecord>, RadrootsNostrSignerError>;
     91 
     92     fn lookup_session(
     93         &self,
     94         client_public_key: &PublicKey,
     95         connect_secret: Option<&str>,
     96     ) -> Result<RadrootsNostrSignerSessionLookup, RadrootsNostrSignerError>;
     97 
     98     fn evaluate_connect_request(
     99         &self,
    100         client_public_key: PublicKey,
    101         request: RadrootsNostrConnectRequest,
    102     ) -> Result<RadrootsNostrSignerConnectEvaluation, RadrootsNostrSignerError>;
    103 
    104     fn register_connection(
    105         &self,
    106         draft: RadrootsNostrSignerConnectionDraft,
    107     ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError>;
    108 
    109     fn set_granted_permissions(
    110         &self,
    111         connection_id: &RadrootsNostrSignerConnectionId,
    112         granted_permissions: RadrootsNostrConnectPermissions,
    113     ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError>;
    114 
    115     fn approve_connection(
    116         &self,
    117         connection_id: &RadrootsNostrSignerConnectionId,
    118         granted_permissions: RadrootsNostrConnectPermissions,
    119     ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError>;
    120 
    121     fn reject_connection(
    122         &self,
    123         connection_id: &RadrootsNostrSignerConnectionId,
    124         reason: Option<String>,
    125     ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError>;
    126 
    127     fn revoke_connection(
    128         &self,
    129         connection_id: &RadrootsNostrSignerConnectionId,
    130         reason: Option<String>,
    131     ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError>;
    132 
    133     fn update_relays(
    134         &self,
    135         connection_id: &RadrootsNostrSignerConnectionId,
    136         relays: Vec<RelayUrl>,
    137     ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError>;
    138 
    139     fn require_auth_challenge(
    140         &self,
    141         connection_id: &RadrootsNostrSignerConnectionId,
    142         auth_url: &str,
    143     ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError>;
    144 
    145     fn set_pending_request(
    146         &self,
    147         connection_id: &RadrootsNostrSignerConnectionId,
    148         request_message: RadrootsNostrConnectRequestMessage,
    149     ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError>;
    150 
    151     fn authorize_auth_challenge(
    152         &self,
    153         connection_id: &RadrootsNostrSignerConnectionId,
    154     ) -> Result<RadrootsNostrSignerAuthorizationOutcome, RadrootsNostrSignerError>;
    155 
    156     fn restore_pending_auth_challenge(
    157         &self,
    158         connection_id: &RadrootsNostrSignerConnectionId,
    159         pending_request: RadrootsNostrSignerPendingRequest,
    160     ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError>;
    161 
    162     fn begin_connect_secret_publish_finalization(
    163         &self,
    164         connection_id: &RadrootsNostrSignerConnectionId,
    165     ) -> Result<RadrootsNostrSignerPublishTransition, RadrootsNostrSignerError>;
    166 
    167     fn begin_auth_replay_publish_finalization(
    168         &self,
    169         connection_id: &RadrootsNostrSignerConnectionId,
    170     ) -> Result<RadrootsNostrSignerPublishTransition, RadrootsNostrSignerError>;
    171 
    172     fn mark_publish_workflow_published(
    173         &self,
    174         workflow_id: &RadrootsNostrSignerWorkflowId,
    175     ) -> Result<RadrootsNostrSignerPublishTransition, RadrootsNostrSignerError>;
    176 
    177     fn finalize_publish_workflow(
    178         &self,
    179         workflow_id: &RadrootsNostrSignerWorkflowId,
    180     ) -> Result<RadrootsNostrSignerPublishTransition, RadrootsNostrSignerError>;
    181 
    182     fn cancel_publish_workflow(
    183         &self,
    184         workflow_id: &RadrootsNostrSignerWorkflowId,
    185     ) -> Result<RadrootsNostrSignerPublishTransition, RadrootsNostrSignerError>;
    186 
    187     fn mark_authenticated(
    188         &self,
    189         connection_id: &RadrootsNostrSignerConnectionId,
    190     ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError>;
    191 
    192     fn mark_connect_secret_consumed(
    193         &self,
    194         connection_id: &RadrootsNostrSignerConnectionId,
    195     ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError>;
    196 
    197     fn evaluate_request(
    198         &self,
    199         connection_id: &RadrootsNostrSignerConnectionId,
    200         request_message: RadrootsNostrConnectRequestMessage,
    201     ) -> Result<RadrootsNostrSignerRequestEvaluation, RadrootsNostrSignerError>;
    202 
    203     fn evaluate_auth_replay_publish_workflow(
    204         &self,
    205         workflow_id: &RadrootsNostrSignerWorkflowId,
    206     ) -> Result<RadrootsNostrSignerRequestEvaluation, RadrootsNostrSignerError>;
    207 
    208     fn record_request(
    209         &self,
    210         connection_id: &RadrootsNostrSignerConnectionId,
    211         request_id: &str,
    212         method: RadrootsNostrConnectMethod,
    213         decision: RadrootsNostrSignerRequestDecision,
    214         message: Option<String>,
    215     ) -> Result<RadrootsNostrSignerRequestAuditRecord, RadrootsNostrSignerError>;
    216 
    217     fn sign_unsigned_event(
    218         &self,
    219         unsigned_event: UnsignedEvent,
    220     ) -> Result<RadrootsNostrSignerSignOutput, RadrootsNostrSignerError>;
    221 
    222     fn sign_event_builder(
    223         &self,
    224         builder: EventBuilder,
    225     ) -> Result<RadrootsNostrSignerSignOutput, RadrootsNostrSignerError> {
    226         let signer_identity = self
    227             .signer_identity()?
    228             .ok_or(RadrootsNostrSignerError::MissingSignerIdentity)?;
    229         let public_key = parse_identity_public_key(&signer_identity)?;
    230         self.sign_unsigned_event(builder.build(public_key))
    231     }
    232 }
    233 
    234 #[derive(Clone)]
    235 pub struct RadrootsNostrEmbeddedSignerBackend {
    236     manager: RadrootsNostrSignerManager,
    237     signer_identity: RadrootsIdentity,
    238 }
    239 
    240 impl RadrootsNostrSignerBackendCapabilities {
    241     pub fn new(
    242         local_signer: Option<RadrootsNostrLocalSignerCapability>,
    243         remote_sessions: Vec<RadrootsNostrRemoteSessionSignerCapability>,
    244     ) -> Self {
    245         Self {
    246             local_signer,
    247             remote_sessions,
    248         }
    249     }
    250 
    251     pub fn all_signers(&self) -> Vec<RadrootsNostrSignerCapability> {
    252         let mut signers = Vec::new();
    253         if let Some(local_signer) = self.local_signer.clone() {
    254             signers.push(RadrootsNostrSignerCapability::LocalAccount(Box::new(
    255                 local_signer,
    256             )));
    257         }
    258         signers.extend(
    259             self.remote_sessions
    260                 .iter()
    261                 .cloned()
    262                 .map(Box::new)
    263                 .map(RadrootsNostrSignerCapability::RemoteSession),
    264         );
    265         signers
    266     }
    267 }
    268 
    269 impl RadrootsNostrSignerSignOutput {
    270     pub fn new(signer: RadrootsNostrSignerCapability, event: Event) -> Self {
    271         Self { signer, event }
    272     }
    273 }
    274 
    275 impl RadrootsNostrSignerPublishTransition {
    276     pub fn begun(workflow: RadrootsNostrSignerPublishWorkflowRecord) -> Self {
    277         Self::Begun(workflow)
    278     }
    279 
    280     pub fn marked_published(workflow: RadrootsNostrSignerPublishWorkflowRecord) -> Self {
    281         Self::MarkedPublished(workflow)
    282     }
    283 
    284     pub fn finalized(
    285         workflow_id: RadrootsNostrSignerWorkflowId,
    286         connection: RadrootsNostrSignerConnectionRecord,
    287     ) -> Self {
    288         Self::Finalized {
    289             workflow_id,
    290             connection: Box::new(connection),
    291         }
    292     }
    293 
    294     pub fn cancelled(workflow: RadrootsNostrSignerPublishWorkflowRecord) -> Self {
    295         Self::Cancelled(workflow)
    296     }
    297 
    298     pub fn workflow(&self) -> Option<&RadrootsNostrSignerPublishWorkflowRecord> {
    299         match self {
    300             Self::Begun(workflow) | Self::MarkedPublished(workflow) | Self::Cancelled(workflow) => {
    301                 Some(workflow)
    302             }
    303             Self::Finalized { .. } => None,
    304         }
    305     }
    306 
    307     pub fn finalized_connection(&self) -> Option<&RadrootsNostrSignerConnectionRecord> {
    308         match self {
    309             Self::Finalized { connection, .. } => Some(connection.as_ref()),
    310             _ => None,
    311         }
    312     }
    313 }
    314 
    315 impl RadrootsNostrEmbeddedSignerBackend {
    316     pub fn new(
    317         manager: RadrootsNostrSignerManager,
    318         signer_identity: RadrootsIdentity,
    319     ) -> Result<Self, RadrootsNostrSignerError> {
    320         let public_identity = signer_identity.to_public();
    321         let existing_identity = manager.signer_identity()?;
    322         if let Some(existing_identity) = existing_identity {
    323             if !same_public_identity_key(&existing_identity, &public_identity) {
    324                 return Err(RadrootsNostrSignerError::InvalidState(
    325                     "embedded signer identity does not match signer manager identity".into(),
    326                 ));
    327             }
    328         } else {
    329             manager.set_signer_identity(public_identity)?;
    330         }
    331 
    332         Ok(Self {
    333             manager,
    334             signer_identity,
    335         })
    336     }
    337 
    338     pub fn new_in_memory(
    339         signer_identity: RadrootsIdentity,
    340     ) -> Result<Self, RadrootsNostrSignerError> {
    341         Self::new(RadrootsNostrSignerManager::new_in_memory(), signer_identity)
    342     }
    343 
    344     pub fn manager(&self) -> &RadrootsNostrSignerManager {
    345         &self.manager
    346     }
    347 
    348     pub fn local_identity(&self) -> &RadrootsIdentity {
    349         &self.signer_identity
    350     }
    351 
    352     fn local_signer_capability(&self) -> RadrootsNostrLocalSignerCapability {
    353         let public_identity = self.signer_identity.to_public();
    354         RadrootsNostrLocalSignerCapability::new(
    355             public_identity.id.clone(),
    356             public_identity,
    357             RadrootsNostrLocalSignerAvailability::SecretBacked,
    358         )
    359     }
    360 }
    361 
    362 impl RadrootsNostrSignerBackend for RadrootsNostrEmbeddedSignerBackend {
    363     fn signer_identity(&self) -> Result<Option<RadrootsIdentityPublic>, RadrootsNostrSignerError> {
    364         self.manager.signer_identity()
    365     }
    366 
    367     fn set_signer_identity(
    368         &self,
    369         signer_identity: RadrootsIdentityPublic,
    370     ) -> Result<(), RadrootsNostrSignerError> {
    371         self.manager.set_signer_identity(signer_identity)
    372     }
    373 
    374     fn capabilities(
    375         &self,
    376     ) -> Result<RadrootsNostrSignerBackendCapabilities, RadrootsNostrSignerError> {
    377         let mut remote_sessions = Vec::new();
    378         for record in self.manager.list_connections()? {
    379             if record.status == RadrootsNostrSignerConnectionStatus::Active {
    380                 remote_sessions.push(RadrootsNostrRemoteSessionSignerCapability::from(&record));
    381             }
    382         }
    383         Ok(RadrootsNostrSignerBackendCapabilities::new(
    384             Some(self.local_signer_capability()),
    385             remote_sessions,
    386         ))
    387     }
    388 
    389     fn list_connections(
    390         &self,
    391     ) -> Result<Vec<RadrootsNostrSignerConnectionRecord>, RadrootsNostrSignerError> {
    392         self.manager.list_connections()
    393     }
    394 
    395     fn get_connection(
    396         &self,
    397         connection_id: &RadrootsNostrSignerConnectionId,
    398     ) -> Result<Option<RadrootsNostrSignerConnectionRecord>, RadrootsNostrSignerError> {
    399         self.manager.get_connection(connection_id)
    400     }
    401 
    402     fn list_publish_workflows(
    403         &self,
    404     ) -> Result<Vec<RadrootsNostrSignerPublishWorkflowRecord>, RadrootsNostrSignerError> {
    405         self.manager.list_publish_workflows()
    406     }
    407 
    408     fn get_publish_workflow(
    409         &self,
    410         workflow_id: &RadrootsNostrSignerWorkflowId,
    411     ) -> Result<Option<RadrootsNostrSignerPublishWorkflowRecord>, RadrootsNostrSignerError> {
    412         self.manager.get_publish_workflow(workflow_id)
    413     }
    414 
    415     fn find_connections_by_client_public_key(
    416         &self,
    417         client_public_key: &PublicKey,
    418     ) -> Result<Vec<RadrootsNostrSignerConnectionRecord>, RadrootsNostrSignerError> {
    419         self.manager
    420             .find_connections_by_client_public_key(client_public_key)
    421     }
    422 
    423     fn find_connection_by_connect_secret(
    424         &self,
    425         connect_secret: &str,
    426     ) -> Result<Option<RadrootsNostrSignerConnectionRecord>, RadrootsNostrSignerError> {
    427         self.manager
    428             .find_connection_by_connect_secret(connect_secret)
    429     }
    430 
    431     fn lookup_session(
    432         &self,
    433         client_public_key: &PublicKey,
    434         connect_secret: Option<&str>,
    435     ) -> Result<RadrootsNostrSignerSessionLookup, RadrootsNostrSignerError> {
    436         self.manager
    437             .lookup_session(client_public_key, connect_secret)
    438     }
    439 
    440     fn evaluate_connect_request(
    441         &self,
    442         client_public_key: PublicKey,
    443         request: RadrootsNostrConnectRequest,
    444     ) -> Result<RadrootsNostrSignerConnectEvaluation, RadrootsNostrSignerError> {
    445         self.manager
    446             .evaluate_connect_request(client_public_key, request)
    447     }
    448 
    449     fn register_connection(
    450         &self,
    451         draft: RadrootsNostrSignerConnectionDraft,
    452     ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> {
    453         self.manager.register_connection(draft)
    454     }
    455 
    456     fn set_granted_permissions(
    457         &self,
    458         connection_id: &RadrootsNostrSignerConnectionId,
    459         granted_permissions: RadrootsNostrConnectPermissions,
    460     ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> {
    461         self.manager
    462             .set_granted_permissions(connection_id, granted_permissions)
    463     }
    464 
    465     fn approve_connection(
    466         &self,
    467         connection_id: &RadrootsNostrSignerConnectionId,
    468         granted_permissions: RadrootsNostrConnectPermissions,
    469     ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> {
    470         self.manager
    471             .approve_connection(connection_id, granted_permissions)
    472     }
    473 
    474     fn reject_connection(
    475         &self,
    476         connection_id: &RadrootsNostrSignerConnectionId,
    477         reason: Option<String>,
    478     ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> {
    479         self.manager.reject_connection(connection_id, reason)
    480     }
    481 
    482     fn revoke_connection(
    483         &self,
    484         connection_id: &RadrootsNostrSignerConnectionId,
    485         reason: Option<String>,
    486     ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> {
    487         self.manager.revoke_connection(connection_id, reason)
    488     }
    489 
    490     fn update_relays(
    491         &self,
    492         connection_id: &RadrootsNostrSignerConnectionId,
    493         relays: Vec<RelayUrl>,
    494     ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> {
    495         self.manager.update_relays(connection_id, relays)
    496     }
    497 
    498     fn require_auth_challenge(
    499         &self,
    500         connection_id: &RadrootsNostrSignerConnectionId,
    501         auth_url: &str,
    502     ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> {
    503         self.manager.require_auth_challenge(connection_id, auth_url)
    504     }
    505 
    506     fn set_pending_request(
    507         &self,
    508         connection_id: &RadrootsNostrSignerConnectionId,
    509         request_message: RadrootsNostrConnectRequestMessage,
    510     ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> {
    511         self.manager
    512             .set_pending_request(connection_id, request_message)
    513     }
    514 
    515     fn authorize_auth_challenge(
    516         &self,
    517         connection_id: &RadrootsNostrSignerConnectionId,
    518     ) -> Result<RadrootsNostrSignerAuthorizationOutcome, RadrootsNostrSignerError> {
    519         self.manager.authorize_auth_challenge(connection_id)
    520     }
    521 
    522     fn restore_pending_auth_challenge(
    523         &self,
    524         connection_id: &RadrootsNostrSignerConnectionId,
    525         pending_request: RadrootsNostrSignerPendingRequest,
    526     ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> {
    527         self.manager
    528             .restore_pending_auth_challenge(connection_id, pending_request)
    529     }
    530 
    531     fn begin_connect_secret_publish_finalization(
    532         &self,
    533         connection_id: &RadrootsNostrSignerConnectionId,
    534     ) -> Result<RadrootsNostrSignerPublishTransition, RadrootsNostrSignerError> {
    535         let workflow = self
    536             .manager
    537             .begin_connect_secret_publish_finalization(connection_id)?;
    538         Ok(RadrootsNostrSignerPublishTransition::begun(workflow))
    539     }
    540 
    541     fn begin_auth_replay_publish_finalization(
    542         &self,
    543         connection_id: &RadrootsNostrSignerConnectionId,
    544     ) -> Result<RadrootsNostrSignerPublishTransition, RadrootsNostrSignerError> {
    545         let workflow = self
    546             .manager
    547             .begin_auth_replay_publish_finalization(connection_id)?;
    548         Ok(RadrootsNostrSignerPublishTransition::begun(workflow))
    549     }
    550 
    551     fn mark_publish_workflow_published(
    552         &self,
    553         workflow_id: &RadrootsNostrSignerWorkflowId,
    554     ) -> Result<RadrootsNostrSignerPublishTransition, RadrootsNostrSignerError> {
    555         let workflow = self.manager.mark_publish_workflow_published(workflow_id)?;
    556         Ok(RadrootsNostrSignerPublishTransition::marked_published(
    557             workflow,
    558         ))
    559     }
    560 
    561     fn finalize_publish_workflow(
    562         &self,
    563         workflow_id: &RadrootsNostrSignerWorkflowId,
    564     ) -> Result<RadrootsNostrSignerPublishTransition, RadrootsNostrSignerError> {
    565         let connection = self.manager.finalize_publish_workflow(workflow_id)?;
    566         Ok(RadrootsNostrSignerPublishTransition::finalized(
    567             workflow_id.clone(),
    568             connection,
    569         ))
    570     }
    571 
    572     fn cancel_publish_workflow(
    573         &self,
    574         workflow_id: &RadrootsNostrSignerWorkflowId,
    575     ) -> Result<RadrootsNostrSignerPublishTransition, RadrootsNostrSignerError> {
    576         let workflow = self.manager.cancel_publish_workflow(workflow_id)?;
    577         Ok(RadrootsNostrSignerPublishTransition::cancelled(workflow))
    578     }
    579 
    580     fn mark_authenticated(
    581         &self,
    582         connection_id: &RadrootsNostrSignerConnectionId,
    583     ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> {
    584         self.manager.mark_authenticated(connection_id)
    585     }
    586 
    587     fn mark_connect_secret_consumed(
    588         &self,
    589         connection_id: &RadrootsNostrSignerConnectionId,
    590     ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> {
    591         self.manager.mark_connect_secret_consumed(connection_id)
    592     }
    593 
    594     fn evaluate_request(
    595         &self,
    596         connection_id: &RadrootsNostrSignerConnectionId,
    597         request_message: RadrootsNostrConnectRequestMessage,
    598     ) -> Result<RadrootsNostrSignerRequestEvaluation, RadrootsNostrSignerError> {
    599         self.manager
    600             .evaluate_request(connection_id, request_message)
    601     }
    602 
    603     fn evaluate_auth_replay_publish_workflow(
    604         &self,
    605         workflow_id: &RadrootsNostrSignerWorkflowId,
    606     ) -> Result<RadrootsNostrSignerRequestEvaluation, RadrootsNostrSignerError> {
    607         self.manager
    608             .evaluate_auth_replay_publish_workflow(workflow_id)
    609     }
    610 
    611     fn record_request(
    612         &self,
    613         connection_id: &RadrootsNostrSignerConnectionId,
    614         request_id: &str,
    615         method: RadrootsNostrConnectMethod,
    616         decision: RadrootsNostrSignerRequestDecision,
    617         message: Option<String>,
    618     ) -> Result<RadrootsNostrSignerRequestAuditRecord, RadrootsNostrSignerError> {
    619         self.manager
    620             .record_request(connection_id, request_id, method, decision, message)
    621     }
    622 
    623     fn sign_unsigned_event(
    624         &self,
    625         unsigned_event: UnsignedEvent,
    626     ) -> Result<RadrootsNostrSignerSignOutput, RadrootsNostrSignerError> {
    627         let event = unsigned_event.sign_with_keys(self.signer_identity.keys())?;
    628         Ok(RadrootsNostrSignerSignOutput::new(
    629             RadrootsNostrSignerCapability::LocalAccount(Box::new(self.local_signer_capability())),
    630             event,
    631         ))
    632     }
    633 }
    634 
    635 fn same_public_identity_key(left: &RadrootsIdentityPublic, right: &RadrootsIdentityPublic) -> bool {
    636     left.id == right.id
    637         && left.public_key_hex == right.public_key_hex
    638         && left.public_key_npub == right.public_key_npub
    639 }
    640 
    641 fn parse_identity_public_key(
    642     identity: &RadrootsIdentityPublic,
    643 ) -> Result<PublicKey, RadrootsNostrSignerError> {
    644     PublicKey::parse(identity.public_key_hex.as_str())
    645         .or_else(|_| PublicKey::from_hex(identity.public_key_hex.as_str()))
    646         .map_err(|_| {
    647             RadrootsNostrSignerError::InvalidState("identity public key is invalid".into())
    648         })
    649 }
    650 
    651 #[cfg(test)]
    652 #[cfg_attr(coverage_nightly, coverage(off))]
    653 mod tests {
    654     use super::{
    655         RadrootsNostrEmbeddedSignerBackend, RadrootsNostrSignerBackend,
    656         RadrootsNostrSignerBackendCapabilities, RadrootsNostrSignerPublishTransition,
    657         parse_identity_public_key, same_public_identity_key,
    658     };
    659     use crate::error::RadrootsNostrSignerError;
    660     use crate::evaluation::{
    661         RadrootsNostrSignerConnectEvaluation, RadrootsNostrSignerConnectProposal,
    662         RadrootsNostrSignerRequestAction, RadrootsNostrSignerSessionLookup,
    663     };
    664     use crate::manager::RadrootsNostrSignerManager;
    665     use crate::model::{
    666         RadrootsNostrSignerApprovalRequirement, RadrootsNostrSignerConnectionDraft,
    667         RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerConnectionStatus,
    668         RadrootsNostrSignerPublishWorkflowRecord, RadrootsNostrSignerRequestDecision,
    669         RadrootsNostrSignerStoreState, RadrootsNostrSignerWorkflowId,
    670     };
    671     use crate::store::RadrootsNostrSignerStore;
    672     use crate::test_support::{
    673         fixture_bob_identity, primary_relay, secondary_relay, synthetic_public_identity,
    674         synthetic_public_key, synthetic_secret_hex,
    675     };
    676     use nostr::{EventBuilder, EventId, Kind};
    677     use radroots_identity::{RadrootsIdentity, RadrootsIdentityPublic};
    678     use radroots_nostr_connect::prelude::{
    679         RadrootsNostrConnectMethod, RadrootsNostrConnectPermission, RadrootsNostrConnectRequest,
    680         RadrootsNostrConnectRequestMessage,
    681     };
    682     use serde_json::json;
    683     use std::panic::{AssertUnwindSafe, catch_unwind};
    684     use std::sync::Arc;
    685     use std::sync::RwLock;
    686     use std::sync::atomic::{AtomicU8, Ordering};
    687 
    688     fn embedded_identity(index: u32) -> RadrootsIdentity {
    689         RadrootsIdentity::from_secret_key_str(synthetic_secret_hex(index).as_str())
    690             .expect("identity")
    691     }
    692 
    693     fn expect_registration_required(
    694         evaluation: RadrootsNostrSignerConnectEvaluation,
    695     ) -> RadrootsNostrSignerConnectProposal {
    696         match evaluation {
    697             RadrootsNostrSignerConnectEvaluation::RegistrationRequired(proposal) => proposal,
    698             other => panic!("unexpected connect evaluation: {other:?}"),
    699         }
    700     }
    701 
    702     fn expect_lookup_connection(
    703         lookup: RadrootsNostrSignerSessionLookup,
    704     ) -> RadrootsNostrSignerConnectionRecord {
    705         match lookup {
    706             RadrootsNostrSignerSessionLookup::Connection(found) => *found,
    707             other => panic!("unexpected session lookup: {other:?}"),
    708         }
    709     }
    710 
    711     fn expect_begun_workflow_id(
    712         transition: RadrootsNostrSignerPublishTransition,
    713     ) -> RadrootsNostrSignerWorkflowId {
    714         match transition {
    715             RadrootsNostrSignerPublishTransition::Begun(workflow) => workflow.workflow_id,
    716             other => panic!("unexpected begin transition: {other:?}"),
    717         }
    718     }
    719 
    720     fn expect_finalized_transition(
    721         transition: RadrootsNostrSignerPublishTransition,
    722     ) -> (
    723         RadrootsNostrSignerWorkflowId,
    724         RadrootsNostrSignerConnectionRecord,
    725     ) {
    726         match transition {
    727             RadrootsNostrSignerPublishTransition::Finalized {
    728                 workflow_id,
    729                 connection,
    730             } => (workflow_id, *connection),
    731             other => panic!("unexpected finalize transition: {other:?}"),
    732         }
    733     }
    734 
    735     struct StubBackend {
    736         signer_identity: Option<RadrootsIdentityPublic>,
    737         signer_identity_error: Option<&'static str>,
    738         sign_error_message: Option<&'static str>,
    739     }
    740 
    741     #[derive(Default)]
    742     struct ToggleSaveStore {
    743         state: RwLock<RadrootsNostrSignerStoreState>,
    744         mode: AtomicU8,
    745     }
    746 
    747     impl ToggleSaveStore {
    748         fn set_mode(&self, mode: u8) {
    749             self.mode.store(mode, Ordering::SeqCst);
    750         }
    751     }
    752 
    753     impl RadrootsNostrSignerStore for ToggleSaveStore {
    754         fn load(&self) -> Result<RadrootsNostrSignerStoreState, RadrootsNostrSignerError> {
    755             let guard = self.state.read().map_err(|_| {
    756                 RadrootsNostrSignerError::Store("toggle store lock poisoned".into())
    757             })?;
    758             Ok(guard.clone())
    759         }
    760 
    761         fn save(
    762             &self,
    763             state: &RadrootsNostrSignerStoreState,
    764         ) -> Result<(), RadrootsNostrSignerError> {
    765             match self.mode.load(Ordering::SeqCst) {
    766                 1 => Err(RadrootsNostrSignerError::Store("save failed".into())),
    767                 2 => panic!("toggle save panic"),
    768                 _ => {
    769                     let mut guard = self.state.write().map_err(|_| {
    770                         RadrootsNostrSignerError::Store("toggle store lock poisoned".into())
    771                     })?;
    772                     *guard = state.clone();
    773                     Ok(())
    774                 }
    775             }
    776         }
    777     }
    778 
    779     impl RadrootsNostrSignerBackend for StubBackend {
    780         fn signer_identity(
    781             &self,
    782         ) -> Result<Option<RadrootsIdentityPublic>, RadrootsNostrSignerError> {
    783             if let Some(message) = self.signer_identity_error {
    784                 return Err(RadrootsNostrSignerError::InvalidState(message.into()));
    785             }
    786             Ok(self.signer_identity.clone())
    787         }
    788 
    789         fn set_signer_identity(
    790             &self,
    791             _signer_identity: RadrootsIdentityPublic,
    792         ) -> Result<(), RadrootsNostrSignerError> {
    793             unreachable!("set_signer_identity not used in tests")
    794         }
    795 
    796         fn capabilities(
    797             &self,
    798         ) -> Result<RadrootsNostrSignerBackendCapabilities, RadrootsNostrSignerError> {
    799             unreachable!("capabilities not used in tests")
    800         }
    801 
    802         fn list_connections(
    803             &self,
    804         ) -> Result<Vec<RadrootsNostrSignerConnectionRecord>, RadrootsNostrSignerError> {
    805             unreachable!("list_connections not used in tests")
    806         }
    807 
    808         fn get_connection(
    809             &self,
    810             _connection_id: &crate::model::RadrootsNostrSignerConnectionId,
    811         ) -> Result<Option<RadrootsNostrSignerConnectionRecord>, RadrootsNostrSignerError> {
    812             unreachable!("get_connection not used in tests")
    813         }
    814 
    815         fn list_publish_workflows(
    816             &self,
    817         ) -> Result<Vec<RadrootsNostrSignerPublishWorkflowRecord>, RadrootsNostrSignerError>
    818         {
    819             unreachable!("list_publish_workflows not used in tests")
    820         }
    821 
    822         fn get_publish_workflow(
    823             &self,
    824             _workflow_id: &RadrootsNostrSignerWorkflowId,
    825         ) -> Result<Option<RadrootsNostrSignerPublishWorkflowRecord>, RadrootsNostrSignerError>
    826         {
    827             unreachable!("get_publish_workflow not used in tests")
    828         }
    829 
    830         fn find_connections_by_client_public_key(
    831             &self,
    832             _client_public_key: &nostr::PublicKey,
    833         ) -> Result<Vec<RadrootsNostrSignerConnectionRecord>, RadrootsNostrSignerError> {
    834             unreachable!("find_connections_by_client_public_key not used in tests")
    835         }
    836 
    837         fn find_connection_by_connect_secret(
    838             &self,
    839             _connect_secret: &str,
    840         ) -> Result<Option<RadrootsNostrSignerConnectionRecord>, RadrootsNostrSignerError> {
    841             unreachable!("find_connection_by_connect_secret not used in tests")
    842         }
    843 
    844         fn lookup_session(
    845             &self,
    846             _client_public_key: &nostr::PublicKey,
    847             _connect_secret: Option<&str>,
    848         ) -> Result<RadrootsNostrSignerSessionLookup, RadrootsNostrSignerError> {
    849             unreachable!("lookup_session not used in tests")
    850         }
    851 
    852         fn evaluate_connect_request(
    853             &self,
    854             _client_public_key: nostr::PublicKey,
    855             _request: RadrootsNostrConnectRequest,
    856         ) -> Result<RadrootsNostrSignerConnectEvaluation, RadrootsNostrSignerError> {
    857             unreachable!("evaluate_connect_request not used in tests")
    858         }
    859 
    860         fn register_connection(
    861             &self,
    862             _draft: RadrootsNostrSignerConnectionDraft,
    863         ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> {
    864             unreachable!("register_connection not used in tests")
    865         }
    866 
    867         fn set_granted_permissions(
    868             &self,
    869             _connection_id: &crate::model::RadrootsNostrSignerConnectionId,
    870             _granted_permissions: radroots_nostr_connect::prelude::RadrootsNostrConnectPermissions,
    871         ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> {
    872             unreachable!("set_granted_permissions not used in tests")
    873         }
    874 
    875         fn approve_connection(
    876             &self,
    877             _connection_id: &crate::model::RadrootsNostrSignerConnectionId,
    878             _granted_permissions: radroots_nostr_connect::prelude::RadrootsNostrConnectPermissions,
    879         ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> {
    880             unreachable!("approve_connection not used in tests")
    881         }
    882 
    883         fn reject_connection(
    884             &self,
    885             _connection_id: &crate::model::RadrootsNostrSignerConnectionId,
    886             _reason: Option<String>,
    887         ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> {
    888             unreachable!("reject_connection not used in tests")
    889         }
    890 
    891         fn revoke_connection(
    892             &self,
    893             _connection_id: &crate::model::RadrootsNostrSignerConnectionId,
    894             _reason: Option<String>,
    895         ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> {
    896             unreachable!("revoke_connection not used in tests")
    897         }
    898 
    899         fn update_relays(
    900             &self,
    901             _connection_id: &crate::model::RadrootsNostrSignerConnectionId,
    902             _relays: Vec<nostr::RelayUrl>,
    903         ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> {
    904             unreachable!("update_relays not used in tests")
    905         }
    906 
    907         fn require_auth_challenge(
    908             &self,
    909             _connection_id: &crate::model::RadrootsNostrSignerConnectionId,
    910             _auth_url: &str,
    911         ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> {
    912             unreachable!("require_auth_challenge not used in tests")
    913         }
    914 
    915         fn set_pending_request(
    916             &self,
    917             _connection_id: &crate::model::RadrootsNostrSignerConnectionId,
    918             _request_message: RadrootsNostrConnectRequestMessage,
    919         ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> {
    920             unreachable!("set_pending_request not used in tests")
    921         }
    922 
    923         fn authorize_auth_challenge(
    924             &self,
    925             _connection_id: &crate::model::RadrootsNostrSignerConnectionId,
    926         ) -> Result<crate::model::RadrootsNostrSignerAuthorizationOutcome, RadrootsNostrSignerError>
    927         {
    928             unreachable!("authorize_auth_challenge not used in tests")
    929         }
    930 
    931         fn restore_pending_auth_challenge(
    932             &self,
    933             _connection_id: &crate::model::RadrootsNostrSignerConnectionId,
    934             _pending_request: crate::model::RadrootsNostrSignerPendingRequest,
    935         ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> {
    936             unreachable!("restore_pending_auth_challenge not used in tests")
    937         }
    938 
    939         fn begin_connect_secret_publish_finalization(
    940             &self,
    941             _connection_id: &crate::model::RadrootsNostrSignerConnectionId,
    942         ) -> Result<RadrootsNostrSignerPublishTransition, RadrootsNostrSignerError> {
    943             unreachable!("begin_connect_secret_publish_finalization not used in tests")
    944         }
    945 
    946         fn begin_auth_replay_publish_finalization(
    947             &self,
    948             _connection_id: &crate::model::RadrootsNostrSignerConnectionId,
    949         ) -> Result<RadrootsNostrSignerPublishTransition, RadrootsNostrSignerError> {
    950             unreachable!("begin_auth_replay_publish_finalization not used in tests")
    951         }
    952 
    953         fn mark_publish_workflow_published(
    954             &self,
    955             _workflow_id: &RadrootsNostrSignerWorkflowId,
    956         ) -> Result<RadrootsNostrSignerPublishTransition, RadrootsNostrSignerError> {
    957             unreachable!("mark_publish_workflow_published not used in tests")
    958         }
    959 
    960         fn finalize_publish_workflow(
    961             &self,
    962             _workflow_id: &RadrootsNostrSignerWorkflowId,
    963         ) -> Result<RadrootsNostrSignerPublishTransition, RadrootsNostrSignerError> {
    964             unreachable!("finalize_publish_workflow not used in tests")
    965         }
    966 
    967         fn cancel_publish_workflow(
    968             &self,
    969             _workflow_id: &RadrootsNostrSignerWorkflowId,
    970         ) -> Result<RadrootsNostrSignerPublishTransition, RadrootsNostrSignerError> {
    971             unreachable!("cancel_publish_workflow not used in tests")
    972         }
    973 
    974         fn mark_authenticated(
    975             &self,
    976             _connection_id: &crate::model::RadrootsNostrSignerConnectionId,
    977         ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> {
    978             unreachable!("mark_authenticated not used in tests")
    979         }
    980 
    981         fn mark_connect_secret_consumed(
    982             &self,
    983             _connection_id: &crate::model::RadrootsNostrSignerConnectionId,
    984         ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> {
    985             unreachable!("mark_connect_secret_consumed not used in tests")
    986         }
    987 
    988         fn evaluate_request(
    989             &self,
    990             _connection_id: &crate::model::RadrootsNostrSignerConnectionId,
    991             _request_message: RadrootsNostrConnectRequestMessage,
    992         ) -> Result<crate::evaluation::RadrootsNostrSignerRequestEvaluation, RadrootsNostrSignerError>
    993         {
    994             unreachable!("evaluate_request not used in tests")
    995         }
    996 
    997         fn evaluate_auth_replay_publish_workflow(
    998             &self,
    999             _workflow_id: &RadrootsNostrSignerWorkflowId,
   1000         ) -> Result<crate::evaluation::RadrootsNostrSignerRequestEvaluation, RadrootsNostrSignerError>
   1001         {
   1002             unreachable!("evaluate_auth_replay_publish_workflow not used in tests")
   1003         }
   1004 
   1005         fn record_request(
   1006             &self,
   1007             _connection_id: &crate::model::RadrootsNostrSignerConnectionId,
   1008             _request_id: &str,
   1009             _method: RadrootsNostrConnectMethod,
   1010             _decision: RadrootsNostrSignerRequestDecision,
   1011             _message: Option<String>,
   1012         ) -> Result<crate::model::RadrootsNostrSignerRequestAuditRecord, RadrootsNostrSignerError>
   1013         {
   1014             unreachable!("record_request not used in tests")
   1015         }
   1016 
   1017         fn sign_unsigned_event(
   1018             &self,
   1019             _unsigned_event: nostr::UnsignedEvent,
   1020         ) -> Result<super::RadrootsNostrSignerSignOutput, RadrootsNostrSignerError> {
   1021             match self.sign_error_message {
   1022                 Some(message) => Err(RadrootsNostrSignerError::InvalidState(message.into())),
   1023                 None => unreachable!("sign_unsigned_event success path not used in tests"),
   1024             }
   1025         }
   1026     }
   1027 
   1028     #[test]
   1029     fn embedded_backend_bootstraps_signer_identity_and_capabilities() {
   1030         let identity = embedded_identity(0x90);
   1031         let backend = RadrootsNostrEmbeddedSignerBackend::new_in_memory(identity.clone())
   1032             .expect("embedded backend");
   1033 
   1034         let signer_identity = backend
   1035             .signer_identity()
   1036             .expect("signer identity")
   1037             .expect("present");
   1038         assert_eq!(signer_identity.id, identity.to_public().id);
   1039 
   1040         let capabilities = backend.capabilities().expect("capabilities");
   1041         let local = capabilities.local_signer.clone().expect("local signer");
   1042         assert_eq!(local.public_identity.id, identity.to_public().id);
   1043         assert!(local.is_secret_backed());
   1044         assert!(capabilities.remote_sessions.is_empty());
   1045         assert_eq!(capabilities.all_signers().len(), 1);
   1046         let manager_identity = backend
   1047             .manager()
   1048             .signer_identity()
   1049             .expect("manager signer identity")
   1050             .expect("stored signer identity");
   1051         assert!(same_public_identity_key(
   1052             &manager_identity,
   1053             &identity.to_public()
   1054         ));
   1055         assert_eq!(backend.local_identity().public_key(), identity.public_key());
   1056     }
   1057 
   1058     #[test]
   1059     fn embedded_backend_rejects_mismatched_manager_identity() {
   1060         let manager = RadrootsNostrSignerManager::new_in_memory();
   1061         manager
   1062             .set_signer_identity(fixture_bob_identity())
   1063             .expect("set signer identity");
   1064 
   1065         let error = RadrootsNostrEmbeddedSignerBackend::new(manager, embedded_identity(0x91))
   1066             .err()
   1067             .expect("mismatched identity");
   1068         assert!(
   1069             error
   1070                 .to_string()
   1071                 .contains("embedded signer identity does not match")
   1072         );
   1073     }
   1074 
   1075     #[test]
   1076     fn embedded_backend_accepts_matching_manager_identity_and_setter_delegate() {
   1077         let identity = embedded_identity(0x97);
   1078         let manager = RadrootsNostrSignerManager::new_in_memory();
   1079         let public_identity = identity.to_public();
   1080         manager
   1081             .set_signer_identity(public_identity.clone())
   1082             .expect("prime manager identity");
   1083 
   1084         let backend = RadrootsNostrEmbeddedSignerBackend::new(manager, identity.clone())
   1085             .expect("matching embedded backend");
   1086         let backend_trait: &dyn RadrootsNostrSignerBackend = &backend;
   1087 
   1088         assert_eq!(backend.local_identity().public_key(), identity.public_key());
   1089         assert!(same_public_identity_key(
   1090             backend_trait
   1091                 .signer_identity()
   1092                 .expect("signer identity")
   1093                 .as_ref()
   1094                 .expect("present"),
   1095             &public_identity
   1096         ));
   1097 
   1098         backend_trait
   1099             .set_signer_identity(public_identity.clone())
   1100             .expect("delegate set signer identity");
   1101         let manager_identity = backend
   1102             .manager()
   1103             .signer_identity()
   1104             .expect("manager signer identity")
   1105             .expect("stored signer identity");
   1106         assert!(same_public_identity_key(
   1107             &manager_identity,
   1108             &public_identity
   1109         ));
   1110     }
   1111 
   1112     #[test]
   1113     fn sign_event_builder_propagates_identity_and_sign_errors() {
   1114         let missing_identity_backend = StubBackend {
   1115             signer_identity: None,
   1116             signer_identity_error: None,
   1117             sign_error_message: Some("sign should not be called"),
   1118         };
   1119         let err = missing_identity_backend
   1120             .sign_event_builder(EventBuilder::new(Kind::TextNote, "missing"))
   1121             .expect_err("missing identity");
   1122         assert!(matches!(
   1123             err,
   1124             RadrootsNostrSignerError::MissingSignerIdentity
   1125         ));
   1126 
   1127         let identity_error_backend = StubBackend {
   1128             signer_identity: None,
   1129             signer_identity_error: Some("stub signer identity failure"),
   1130             sign_error_message: Some("sign should not be called"),
   1131         };
   1132         let err = identity_error_backend
   1133             .sign_event_builder(EventBuilder::new(Kind::TextNote, "identity-error"))
   1134             .expect_err("signer identity error");
   1135         assert!(err.to_string().contains("stub signer identity failure"));
   1136 
   1137         let mut invalid_identity = synthetic_public_identity(0xaa);
   1138         invalid_identity.public_key_hex = "invalid".into();
   1139         let invalid_identity_backend = StubBackend {
   1140             signer_identity: Some(invalid_identity),
   1141             signer_identity_error: None,
   1142             sign_error_message: Some("sign should not be called"),
   1143         };
   1144         let err = invalid_identity_backend
   1145             .sign_event_builder(EventBuilder::new(Kind::TextNote, "invalid"))
   1146             .expect_err("invalid signer identity");
   1147         assert!(err.to_string().contains("identity public key is invalid"));
   1148 
   1149         let signing_error_backend = StubBackend {
   1150             signer_identity: Some(synthetic_public_identity(0xab)),
   1151             signer_identity_error: None,
   1152             sign_error_message: Some("stub sign failure"),
   1153         };
   1154         let err = signing_error_backend
   1155             .sign_event_builder(EventBuilder::new(Kind::TextNote, "sign-failure"))
   1156             .expect_err("sign failure");
   1157         assert!(err.to_string().contains("stub sign failure"));
   1158     }
   1159 
   1160     #[test]
   1161     fn capabilities_only_include_active_remote_sessions() {
   1162         let identity = embedded_identity(0xac);
   1163         let backend = RadrootsNostrEmbeddedSignerBackend::new_in_memory(identity.clone())
   1164             .expect("embedded backend");
   1165         let backend_trait: &dyn RadrootsNostrSignerBackend = &backend;
   1166 
   1167         let active = backend_trait
   1168             .register_connection(RadrootsNostrSignerConnectionDraft::new(
   1169                 synthetic_public_key(0xad),
   1170                 synthetic_public_identity(0xae),
   1171             ))
   1172             .expect("register active");
   1173 
   1174         let pending = backend_trait
   1175             .register_connection(
   1176                 RadrootsNostrSignerConnectionDraft::new(
   1177                     synthetic_public_key(0xaf),
   1178                     synthetic_public_identity(0xb0),
   1179                 )
   1180                 .with_approval_requirement(RadrootsNostrSignerApprovalRequirement::ExplicitUser),
   1181             )
   1182             .expect("register pending");
   1183         let rejected = backend_trait
   1184             .register_connection(RadrootsNostrSignerConnectionDraft::new(
   1185                 synthetic_public_key(0xb1),
   1186                 synthetic_public_identity(0xb2),
   1187             ))
   1188             .expect("register rejected");
   1189         backend_trait
   1190             .reject_connection(&rejected.connection_id, Some("rejected".into()))
   1191             .expect("reject connection");
   1192 
   1193         let capabilities = backend_trait.capabilities().expect("capabilities");
   1194         assert_eq!(capabilities.remote_sessions.len(), 1);
   1195         assert_eq!(
   1196             capabilities.remote_sessions[0].connection_id,
   1197             active.connection_id
   1198         );
   1199         assert_ne!(
   1200             capabilities.remote_sessions[0].connection_id,
   1201             pending.connection_id
   1202         );
   1203     }
   1204 
   1205     #[test]
   1206     fn embedded_backend_propagates_missing_publish_targets() {
   1207         let identity = embedded_identity(0xb3);
   1208         let backend =
   1209             RadrootsNostrEmbeddedSignerBackend::new_in_memory(identity).expect("embedded backend");
   1210         let backend_trait: &dyn RadrootsNostrSignerBackend = &backend;
   1211 
   1212         let missing_connection_id =
   1213             crate::model::RadrootsNostrSignerConnectionId::parse("conn-backend-missing")
   1214                 .expect("connection id");
   1215         let missing_workflow_id =
   1216             RadrootsNostrSignerWorkflowId::parse("wf-backend-missing").expect("workflow id");
   1217 
   1218         assert!(
   1219             backend_trait
   1220                 .begin_connect_secret_publish_finalization(&missing_connection_id)
   1221                 .expect_err("missing connect workflow")
   1222                 .to_string()
   1223                 .contains("connection not found")
   1224         );
   1225         assert!(
   1226             backend_trait
   1227                 .begin_auth_replay_publish_finalization(&missing_connection_id)
   1228                 .expect_err("missing auth workflow")
   1229                 .to_string()
   1230                 .contains("connection not found")
   1231         );
   1232         assert!(
   1233             backend_trait
   1234                 .mark_publish_workflow_published(&missing_workflow_id)
   1235                 .expect_err("missing published workflow")
   1236                 .to_string()
   1237                 .contains("publish workflow not found")
   1238         );
   1239         assert!(
   1240             backend_trait
   1241                 .finalize_publish_workflow(&missing_workflow_id)
   1242                 .expect_err("missing finalized workflow")
   1243                 .to_string()
   1244                 .contains("publish workflow not found")
   1245         );
   1246         assert!(
   1247             backend_trait
   1248                 .cancel_publish_workflow(&missing_workflow_id)
   1249                 .expect_err("missing cancelled workflow")
   1250                 .to_string()
   1251                 .contains("publish workflow not found")
   1252         );
   1253     }
   1254 
   1255     #[test]
   1256     fn embedded_backend_reports_manager_read_and_save_failures() {
   1257         let save_fail_store = Arc::new(ToggleSaveStore::default());
   1258         save_fail_store.set_mode(1);
   1259         let save_fail_manager =
   1260             RadrootsNostrSignerManager::new(save_fail_store).expect("save-fail manager");
   1261         let err = match RadrootsNostrEmbeddedSignerBackend::new(
   1262             save_fail_manager,
   1263             embedded_identity(0xb4),
   1264         ) {
   1265             Ok(_) => panic!("expected save failure"),
   1266             Err(err) => err,
   1267         };
   1268         assert!(err.to_string().contains("save failed"));
   1269 
   1270         let poisoned_store = Arc::new(ToggleSaveStore::default());
   1271         let poisoned_manager =
   1272             RadrootsNostrSignerManager::new(poisoned_store.clone()).expect("poison manager");
   1273         let backend = RadrootsNostrEmbeddedSignerBackend::new(
   1274             poisoned_manager.clone(),
   1275             embedded_identity(0xb5),
   1276         )
   1277         .expect("embedded backend");
   1278         poisoned_store.set_mode(2);
   1279         assert!(
   1280             catch_unwind(AssertUnwindSafe(|| {
   1281                 let _ = backend
   1282                     .manager()
   1283                     .set_signer_identity(fixture_bob_identity());
   1284             }))
   1285             .is_err()
   1286         );
   1287 
   1288         let err = backend.capabilities().expect_err("poisoned capabilities");
   1289         assert!(err.to_string().contains("signer state lock poisoned"));
   1290 
   1291         let err = match RadrootsNostrEmbeddedSignerBackend::new(
   1292             poisoned_manager,
   1293             embedded_identity(0xb5),
   1294         ) {
   1295             Ok(_) => panic!("expected poisoned new failure"),
   1296             Err(err) => err,
   1297         };
   1298         assert!(err.to_string().contains("signer state lock poisoned"));
   1299     }
   1300 
   1301     #[test]
   1302     fn embedded_backend_sign_unsigned_event_rejects_invalid_precomputed_id() {
   1303         let identity = embedded_identity(0xb6);
   1304         let backend = RadrootsNostrEmbeddedSignerBackend::new_in_memory(identity.clone())
   1305             .expect("embedded backend");
   1306         let backend_trait: &dyn RadrootsNostrSignerBackend = &backend;
   1307 
   1308         let mut unsigned_event =
   1309             EventBuilder::new(Kind::TextNote, "hello").build(identity.public_key());
   1310         unsigned_event.id = Some(EventId::all_zeros());
   1311         let err = backend_trait
   1312             .sign_unsigned_event(unsigned_event)
   1313             .expect_err("invalid precomputed id");
   1314         assert!(err.to_string().starts_with("sign error:"));
   1315     }
   1316 
   1317     #[test]
   1318     fn embedded_backend_trait_delegates_connect_and_publish_workflow_methods() {
   1319         let identity = embedded_identity(0x92);
   1320         let backend = RadrootsNostrEmbeddedSignerBackend::new_in_memory(identity.clone())
   1321             .expect("embedded backend");
   1322         let backend: &dyn RadrootsNostrSignerBackend = &backend;
   1323 
   1324         let evaluation = backend
   1325             .evaluate_connect_request(
   1326                 synthetic_public_key(0x93),
   1327                 RadrootsNostrConnectRequest::Connect {
   1328                     remote_signer_public_key: identity.public_key(),
   1329                     secret: Some("connect-secret".into()),
   1330                     requested_permissions: vec![RadrootsNostrConnectPermission::new(
   1331                         RadrootsNostrConnectMethod::Ping,
   1332                     )]
   1333                     .into(),
   1334                 },
   1335             )
   1336             .expect("connect evaluation");
   1337         let proposal = expect_registration_required(evaluation);
   1338         let connection = backend
   1339             .register_connection(
   1340                 proposal
   1341                     .into_connection_draft(synthetic_public_identity(0x94))
   1342                     .with_relays(vec![primary_relay()]),
   1343             )
   1344             .expect("register connection");
   1345 
   1346         let capabilities = backend.capabilities().expect("capabilities");
   1347         assert_eq!(capabilities.remote_sessions.len(), 1);
   1348 
   1349         let begun = backend
   1350             .begin_connect_secret_publish_finalization(&connection.connection_id)
   1351             .expect("begin workflow");
   1352         let workflow_id = expect_begun_workflow_id(begun.clone());
   1353         assert_eq!(
   1354             begun.workflow().expect("begun workflow").connection_id,
   1355             connection.connection_id
   1356         );
   1357 
   1358         let published = backend
   1359             .mark_publish_workflow_published(&workflow_id)
   1360             .expect("mark published");
   1361         assert!(matches!(
   1362             published,
   1363             RadrootsNostrSignerPublishTransition::MarkedPublished(_)
   1364         ));
   1365 
   1366         let finalized = backend
   1367             .finalize_publish_workflow(&workflow_id)
   1368             .expect("finalize workflow");
   1369         let (finalized_workflow_id, finalized_connection) = expect_finalized_transition(finalized);
   1370         assert_eq!(finalized_workflow_id, workflow_id);
   1371         assert!(finalized_connection.connect_secret_is_consumed());
   1372 
   1373         let audit = backend
   1374             .record_request(
   1375                 &connection.connection_id,
   1376                 "req-1",
   1377                 RadrootsNostrConnectMethod::Ping,
   1378                 RadrootsNostrSignerRequestDecision::Allowed,
   1379                 None,
   1380             )
   1381             .expect("record request");
   1382         assert_eq!(audit.method, RadrootsNostrConnectMethod::Ping);
   1383     }
   1384 
   1385     #[test]
   1386     fn embedded_backend_delegates_lookup_state_and_auth_workflow_methods() {
   1387         let identity = embedded_identity(0xa0);
   1388         let backend = RadrootsNostrEmbeddedSignerBackend::new_in_memory(identity.clone())
   1389             .expect("embedded backend");
   1390         let backend_trait: &dyn RadrootsNostrSignerBackend = &backend;
   1391 
   1392         let connect_evaluation = backend_trait
   1393             .evaluate_connect_request(
   1394                 synthetic_public_key(0xa1),
   1395                 RadrootsNostrConnectRequest::Connect {
   1396                     remote_signer_public_key: identity.public_key(),
   1397                     secret: Some("connect-secret-2".into()),
   1398                     requested_permissions: vec![RadrootsNostrConnectPermission::new(
   1399                         RadrootsNostrConnectMethod::Ping,
   1400                     )]
   1401                     .into(),
   1402                 },
   1403             )
   1404             .expect("connect evaluation");
   1405         let connect_proposal = expect_registration_required(connect_evaluation);
   1406         let connection = backend_trait
   1407             .register_connection(
   1408                 connect_proposal
   1409                     .into_connection_draft(synthetic_public_identity(0xa2))
   1410                     .with_relays(vec![primary_relay()]),
   1411             )
   1412             .expect("register connect-secret connection");
   1413 
   1414         assert_eq!(backend_trait.list_connections().expect("list").len(), 1);
   1415         assert_eq!(
   1416             backend_trait
   1417                 .get_connection(&connection.connection_id)
   1418                 .expect("get connection")
   1419                 .expect("stored connection")
   1420                 .connection_id,
   1421             connection.connection_id
   1422         );
   1423         assert_eq!(
   1424             backend_trait
   1425                 .find_connections_by_client_public_key(&connection.client_public_key)
   1426                 .expect("find by client key")
   1427                 .len(),
   1428             1
   1429         );
   1430         assert_eq!(
   1431             backend_trait
   1432                 .find_connection_by_connect_secret("connect-secret-2")
   1433                 .expect("find by secret")
   1434                 .expect("stored by secret")
   1435                 .connection_id,
   1436             connection.connection_id
   1437         );
   1438         let looked_up = expect_lookup_connection(
   1439             backend_trait
   1440                 .lookup_session(&connection.client_public_key, Some("connect-secret-2"))
   1441                 .expect("lookup session"),
   1442         );
   1443         assert_eq!(looked_up.connection_id, connection.connection_id);
   1444 
   1445         let with_relays = backend_trait
   1446             .update_relays(
   1447                 &connection.connection_id,
   1448                 vec![primary_relay(), secondary_relay()],
   1449             )
   1450             .expect("update relays");
   1451         assert_eq!(with_relays.relays.len(), 2);
   1452 
   1453         let evaluation = backend_trait
   1454             .evaluate_request(
   1455                 &connection.connection_id,
   1456                 RadrootsNostrConnectRequestMessage::new(
   1457                     "req-ping",
   1458                     RadrootsNostrConnectRequest::Ping,
   1459                 ),
   1460             )
   1461             .expect("evaluate request");
   1462         assert!(matches!(
   1463             evaluation.action,
   1464             RadrootsNostrSignerRequestAction::Allowed { .. }
   1465         ));
   1466 
   1467         let authenticated = backend_trait
   1468             .mark_authenticated(&connection.connection_id)
   1469             .expect("mark authenticated");
   1470         assert!(authenticated.last_authenticated_at_unix.is_some());
   1471 
   1472         let pending_connection = backend_trait
   1473             .register_connection(
   1474                 RadrootsNostrSignerConnectionDraft::new(
   1475                     synthetic_public_key(0xab),
   1476                     synthetic_public_identity(0xac),
   1477                 )
   1478                 .with_requested_permissions(
   1479                     vec![RadrootsNostrConnectPermission::new(
   1480                         RadrootsNostrConnectMethod::Ping,
   1481                     )]
   1482                     .into(),
   1483                 )
   1484                 .with_approval_requirement(RadrootsNostrSignerApprovalRequirement::ExplicitUser),
   1485             )
   1486             .expect("register pending connection");
   1487         let granted_permissions: radroots_nostr_connect::prelude::RadrootsNostrConnectPermissions =
   1488             vec![RadrootsNostrConnectPermission::new(
   1489                 RadrootsNostrConnectMethod::Ping,
   1490             )]
   1491             .into();
   1492         let granted = backend_trait
   1493             .set_granted_permissions(
   1494                 &pending_connection.connection_id,
   1495                 granted_permissions.clone(),
   1496             )
   1497             .expect("set granted permissions");
   1498         assert_eq!(granted.connection_id, pending_connection.connection_id);
   1499 
   1500         let approved = backend_trait
   1501             .approve_connection(&pending_connection.connection_id, granted_permissions)
   1502             .expect("approve connection");
   1503         assert_eq!(approved.status, RadrootsNostrSignerConnectionStatus::Active);
   1504 
   1505         let begun = backend_trait
   1506             .begin_connect_secret_publish_finalization(&connection.connection_id)
   1507             .expect("begin connect workflow");
   1508         let workflow = begun.workflow().expect("begun workflow").clone();
   1509         assert!(begun.finalized_connection().is_none());
   1510         assert_eq!(
   1511             backend_trait
   1512                 .list_publish_workflows()
   1513                 .expect("list publish workflows")
   1514                 .len(),
   1515             1
   1516         );
   1517         assert_eq!(
   1518             backend_trait
   1519                 .get_publish_workflow(&workflow.workflow_id)
   1520                 .expect("get publish workflow")
   1521                 .expect("stored workflow")
   1522                 .workflow_id,
   1523             workflow.workflow_id
   1524         );
   1525 
   1526         let published = backend_trait
   1527             .mark_publish_workflow_published(&workflow.workflow_id)
   1528             .expect("mark publish workflow");
   1529         assert_eq!(
   1530             published
   1531                 .workflow()
   1532                 .expect("published workflow")
   1533                 .workflow_id,
   1534             workflow.workflow_id
   1535         );
   1536 
   1537         let finalized = backend_trait
   1538             .finalize_publish_workflow(&workflow.workflow_id)
   1539             .expect("finalize workflow");
   1540         assert!(finalized.workflow().is_none());
   1541         assert_eq!(
   1542             finalized
   1543                 .finalized_connection()
   1544                 .expect("finalized connection")
   1545                 .connection_id,
   1546             connection.connection_id
   1547         );
   1548 
   1549         let audit = backend_trait
   1550             .record_request(
   1551                 &connection.connection_id,
   1552                 "req-audit",
   1553                 RadrootsNostrConnectMethod::Ping,
   1554                 RadrootsNostrSignerRequestDecision::Allowed,
   1555                 None,
   1556             )
   1557             .expect("record request");
   1558         assert_eq!(audit.connection_id, connection.connection_id);
   1559 
   1560         let consumed_connection = backend_trait
   1561             .register_connection(
   1562                 RadrootsNostrSignerConnectionDraft::new(
   1563                     synthetic_public_key(0xa3),
   1564                     synthetic_public_identity(0xa4),
   1565                 )
   1566                 .with_connect_secret("manual-secret"),
   1567             )
   1568             .expect("register consumed connection");
   1569         let consumed = backend_trait
   1570             .mark_connect_secret_consumed(&consumed_connection.connection_id)
   1571             .expect("mark connect secret consumed");
   1572         assert!(consumed.connect_secret_is_consumed());
   1573 
   1574         let rejected = backend_trait
   1575             .register_connection(RadrootsNostrSignerConnectionDraft::new(
   1576                 synthetic_public_key(0xa5),
   1577                 synthetic_public_identity(0xa6),
   1578             ))
   1579             .expect("register rejected connection");
   1580         let rejected = backend_trait
   1581             .reject_connection(&rejected.connection_id, Some("rejected".into()))
   1582             .expect("reject connection");
   1583         assert_eq!(
   1584             rejected.status,
   1585             RadrootsNostrSignerConnectionStatus::Rejected
   1586         );
   1587 
   1588         let auth_connection = backend_trait
   1589             .register_connection(
   1590                 RadrootsNostrSignerConnectionDraft::new(
   1591                     synthetic_public_key(0xa7),
   1592                     synthetic_public_identity(0xa8),
   1593                 )
   1594                 .with_requested_permissions(
   1595                     vec![RadrootsNostrConnectPermission::new(
   1596                         RadrootsNostrConnectMethod::Ping,
   1597                     )]
   1598                     .into(),
   1599                 ),
   1600             )
   1601             .expect("register auth connection");
   1602         backend_trait
   1603             .require_auth_challenge(
   1604                 &auth_connection.connection_id,
   1605                 "https://api.example.com/auth",
   1606             )
   1607             .expect("require auth challenge");
   1608         let pending = backend_trait
   1609             .set_pending_request(
   1610                 &auth_connection.connection_id,
   1611                 RadrootsNostrConnectRequestMessage::new(
   1612                     "req-auth-replay",
   1613                     RadrootsNostrConnectRequest::Ping,
   1614                 ),
   1615             )
   1616             .expect("set pending request");
   1617         assert!(pending.pending_request.is_some());
   1618         let authorized = backend_trait
   1619             .authorize_auth_challenge(&auth_connection.connection_id)
   1620             .expect("authorize auth challenge");
   1621         let pending_request = authorized.pending_request.expect("pending request");
   1622         let restored = backend_trait
   1623             .restore_pending_auth_challenge(&auth_connection.connection_id, pending_request.clone())
   1624             .expect("restore pending auth challenge");
   1625         assert_eq!(restored.pending_request.as_ref(), Some(&pending_request));
   1626 
   1627         let auth_workflow = backend_trait
   1628             .begin_auth_replay_publish_finalization(&auth_connection.connection_id)
   1629             .expect("begin auth replay")
   1630             .workflow()
   1631             .expect("auth replay workflow")
   1632             .clone();
   1633         let replay_evaluation = backend_trait
   1634             .evaluate_auth_replay_publish_workflow(&auth_workflow.workflow_id)
   1635             .expect("evaluate auth replay workflow");
   1636         assert_eq!(
   1637             replay_evaluation.connection.connection_id,
   1638             auth_connection.connection_id
   1639         );
   1640         let cancelled = backend_trait
   1641             .cancel_publish_workflow(&auth_workflow.workflow_id)
   1642             .expect("cancel auth workflow");
   1643         assert_eq!(
   1644             cancelled
   1645                 .workflow()
   1646                 .expect("cancelled workflow")
   1647                 .workflow_id,
   1648             auth_workflow.workflow_id
   1649         );
   1650 
   1651         let revoked = backend_trait
   1652             .revoke_connection(&auth_connection.connection_id, Some("revoked".into()))
   1653             .expect("revoke connection");
   1654         assert_eq!(revoked.status, RadrootsNostrSignerConnectionStatus::Revoked);
   1655     }
   1656 
   1657     #[test]
   1658     fn embedded_backend_signs_builder_with_local_capability() {
   1659         let identity = embedded_identity(0x95);
   1660         let backend = RadrootsNostrEmbeddedSignerBackend::new_in_memory(identity.clone())
   1661             .expect("embedded backend");
   1662         let backend_trait: &dyn RadrootsNostrSignerBackend = &backend;
   1663 
   1664         let output = backend_trait
   1665             .sign_event_builder(EventBuilder::new(Kind::TextNote, "hello"))
   1666             .expect("sign event builder");
   1667         let direct_output =
   1668             <RadrootsNostrEmbeddedSignerBackend as RadrootsNostrSignerBackend>::sign_unsigned_event(
   1669                 &backend,
   1670                 EventBuilder::new(Kind::TextNote, "hello-direct").build(identity.public_key()),
   1671             )
   1672             .expect("sign unsigned event");
   1673 
   1674         assert_eq!(output.event.pubkey, identity.public_key());
   1675         assert_eq!(direct_output.event.pubkey, identity.public_key());
   1676         let local = output.signer.local_account().expect("local signer");
   1677         assert_eq!(local.public_identity.id, identity.to_public().id);
   1678         assert!(local.is_secret_backed());
   1679     }
   1680 
   1681     #[test]
   1682     fn embedded_backend_can_prepare_and_cancel_auth_replay_workflow() {
   1683         let identity = embedded_identity(0x96);
   1684         let backend = RadrootsNostrEmbeddedSignerBackend::new_in_memory(identity.clone())
   1685             .expect("embedded backend");
   1686         let backend: &dyn RadrootsNostrSignerBackend = &backend;
   1687 
   1688         let connection = backend
   1689             .register_connection(
   1690                 RadrootsNostrSignerConnectionDraft::new(
   1691                     synthetic_public_key(0x97),
   1692                     synthetic_public_identity(0x98),
   1693                 )
   1694                 .with_requested_permissions(
   1695                     vec![RadrootsNostrConnectPermission::new(
   1696                         RadrootsNostrConnectMethod::Ping,
   1697                     )]
   1698                     .into(),
   1699                 ),
   1700             )
   1701             .expect("register connection");
   1702         backend
   1703             .require_auth_challenge(&connection.connection_id, "https://api.example.com/auth")
   1704             .expect("require auth");
   1705         backend
   1706             .set_pending_request(
   1707                 &connection.connection_id,
   1708                 RadrootsNostrConnectRequestMessage::new(
   1709                     "req-auth",
   1710                     RadrootsNostrConnectRequest::Ping,
   1711                 ),
   1712             )
   1713             .expect("set pending request");
   1714 
   1715         let begun = backend
   1716             .begin_auth_replay_publish_finalization(&connection.connection_id)
   1717             .expect("begin auth replay");
   1718         let workflow_id = begun
   1719             .workflow()
   1720             .expect("begun auth replay workflow")
   1721             .workflow_id
   1722             .clone();
   1723 
   1724         let cancelled = backend
   1725             .cancel_publish_workflow(&workflow_id)
   1726             .expect("cancel workflow");
   1727         assert!(matches!(
   1728             cancelled,
   1729             RadrootsNostrSignerPublishTransition::Cancelled(_)
   1730         ));
   1731     }
   1732 
   1733     #[test]
   1734     fn backend_capabilities_all_signers_supports_remote_only_and_identity_helpers() {
   1735         let remote = crate::capability::RadrootsNostrRemoteSessionSignerCapability::new(
   1736             crate::model::RadrootsNostrSignerConnectionId::new_v7(),
   1737             synthetic_public_identity(0xb0),
   1738             synthetic_public_identity(0xb1),
   1739         );
   1740         let capabilities = RadrootsNostrSignerBackendCapabilities::new(None, vec![remote.clone()]);
   1741 
   1742         assert_eq!(
   1743             capabilities.all_signers(),
   1744             vec![
   1745                 crate::capability::RadrootsNostrSignerCapability::RemoteSession(Box::new(remote,))
   1746             ]
   1747         );
   1748 
   1749         let valid_identity = synthetic_public_identity(0xb2);
   1750         assert!(same_public_identity_key(&valid_identity, &valid_identity));
   1751         assert_eq!(
   1752             parse_identity_public_key(&valid_identity).expect("valid public key"),
   1753             synthetic_public_key(0xb2)
   1754         );
   1755         let mut valid_identity_with_different_hex = valid_identity.clone();
   1756         valid_identity_with_different_hex.public_key_hex =
   1757             synthetic_public_identity(0xb3).public_key_hex;
   1758         assert!(!same_public_identity_key(
   1759             &valid_identity,
   1760             &valid_identity_with_different_hex
   1761         ));
   1762 
   1763         let invalid_identity: RadrootsIdentityPublic = serde_json::from_value(json!({
   1764             "id": "not-a-public-key",
   1765             "public_key_hex": "not-a-public-key",
   1766             "public_key_npub": "npub1invalid"
   1767         }))
   1768         .expect("invalid identity payload");
   1769         let error = parse_identity_public_key(&invalid_identity)
   1770             .err()
   1771             .expect("invalid public identity");
   1772         assert!(error.to_string().contains("identity public key is invalid"));
   1773     }
   1774 
   1775     #[test]
   1776     fn backend_test_helpers_reject_unexpected_variants() {
   1777         let connection = RadrootsNostrSignerConnectionRecord::new(
   1778             crate::model::RadrootsNostrSignerConnectionId::new_v7(),
   1779             synthetic_public_identity(0xb4),
   1780             RadrootsNostrSignerConnectionDraft::new(
   1781                 synthetic_public_key(0xb5),
   1782                 synthetic_public_identity(0xb6),
   1783             ),
   1784             1,
   1785         );
   1786         let workflow = RadrootsNostrSignerPublishWorkflowRecord::new_connect_secret_finalization(
   1787             connection.connection_id.clone(),
   1788             1,
   1789         );
   1790 
   1791         assert!(
   1792             std::panic::catch_unwind(|| {
   1793                 expect_registration_required(
   1794                     RadrootsNostrSignerConnectEvaluation::ExistingConnection(Box::new(
   1795                         connection.clone(),
   1796                     )),
   1797                 )
   1798             })
   1799             .is_err()
   1800         );
   1801         assert!(
   1802             std::panic::catch_unwind(|| {
   1803                 expect_lookup_connection(RadrootsNostrSignerSessionLookup::None)
   1804             })
   1805             .is_err()
   1806         );
   1807         assert!(
   1808             std::panic::catch_unwind(|| {
   1809                 expect_begun_workflow_id(RadrootsNostrSignerPublishTransition::cancelled(
   1810                     workflow.clone(),
   1811                 ))
   1812             })
   1813             .is_err()
   1814         );
   1815         assert!(
   1816             std::panic::catch_unwind(|| {
   1817                 expect_finalized_transition(RadrootsNostrSignerPublishTransition::begun(workflow))
   1818             })
   1819             .is_err()
   1820         );
   1821     }
   1822 }