lib

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

manager.rs (160929B)


      1 use crate::error::RadrootsNostrSignerError;
      2 use crate::evaluation::{
      3     RadrootsNostrSignerConnectEvaluation, RadrootsNostrSignerConnectProposal,
      4     RadrootsNostrSignerRequestAction, RadrootsNostrSignerRequestEvaluation,
      5     RadrootsNostrSignerSessionLookup, request_allowed_by_permissions,
      6     required_permission_for_request, response_hint_for_request,
      7 };
      8 use crate::model::{
      9     RADROOTS_NOSTR_SIGNER_STORE_VERSION, RadrootsNostrSignerApprovalRequirement,
     10     RadrootsNostrSignerApprovalState, RadrootsNostrSignerAuthChallenge,
     11     RadrootsNostrSignerAuthState, RadrootsNostrSignerAuthorizationOutcome,
     12     RadrootsNostrSignerConnectSecretHash, RadrootsNostrSignerConnectionDraft,
     13     RadrootsNostrSignerConnectionId, RadrootsNostrSignerConnectionRecord,
     14     RadrootsNostrSignerConnectionStatus, RadrootsNostrSignerPendingRequest,
     15     RadrootsNostrSignerPermissionGrant, RadrootsNostrSignerPublishWorkflowKind,
     16     RadrootsNostrSignerPublishWorkflowRecord, RadrootsNostrSignerPublishWorkflowState,
     17     RadrootsNostrSignerRequestAuditRecord, RadrootsNostrSignerRequestDecision,
     18     RadrootsNostrSignerRequestId, RadrootsNostrSignerStoreState, RadrootsNostrSignerWorkflowId,
     19 };
     20 use crate::store::{RadrootsNostrMemorySignerStore, RadrootsNostrSignerStore};
     21 use nostr::{PublicKey, RelayUrl};
     22 use radroots_identity::RadrootsIdentityPublic;
     23 use radroots_nostr_connect::prelude::{
     24     RadrootsNostrConnectMethod, RadrootsNostrConnectPermissions, RadrootsNostrConnectRequest,
     25     RadrootsNostrConnectRequestMessage,
     26 };
     27 use std::sync::{Arc, RwLock};
     28 use std::time::{SystemTime, UNIX_EPOCH};
     29 
     30 #[derive(Clone)]
     31 pub struct RadrootsNostrSignerManager {
     32     store: Arc<dyn RadrootsNostrSignerStore>,
     33     state: Arc<RwLock<RadrootsNostrSignerStoreState>>,
     34 }
     35 
     36 impl RadrootsNostrSignerManager {
     37     pub fn new_in_memory() -> Self {
     38         Self {
     39             store: Arc::new(RadrootsNostrMemorySignerStore::new()),
     40             state: Arc::new(RwLock::new(RadrootsNostrSignerStoreState::default())),
     41         }
     42     }
     43 
     44     pub fn new(store: Arc<dyn RadrootsNostrSignerStore>) -> Result<Self, RadrootsNostrSignerError> {
     45         let state = store.load()?;
     46         if state.version != RADROOTS_NOSTR_SIGNER_STORE_VERSION {
     47             return Err(RadrootsNostrSignerError::InvalidState(format!(
     48                 "unsupported signer schema version {}",
     49                 state.version
     50             )));
     51         }
     52 
     53         Ok(Self {
     54             store,
     55             state: Arc::new(RwLock::new(state)),
     56         })
     57     }
     58 
     59     pub fn signer_identity(
     60         &self,
     61     ) -> Result<Option<RadrootsIdentityPublic>, RadrootsNostrSignerError> {
     62         let guard = self
     63             .state
     64             .read()
     65             .map_err(|_| RadrootsNostrSignerError::Store("signer state lock poisoned".into()))?;
     66         Ok(guard.signer_identity.clone())
     67     }
     68 
     69     pub fn set_signer_identity(
     70         &self,
     71         signer_identity: RadrootsIdentityPublic,
     72     ) -> Result<(), RadrootsNostrSignerError> {
     73         validate_public_identity(&signer_identity)?;
     74         self.update_state(|state| {
     75             state.signer_identity = Some(signer_identity);
     76             Ok(())
     77         })
     78     }
     79 
     80     pub fn list_connections(
     81         &self,
     82     ) -> Result<Vec<RadrootsNostrSignerConnectionRecord>, RadrootsNostrSignerError> {
     83         let guard = self
     84             .state
     85             .read()
     86             .map_err(|_| RadrootsNostrSignerError::Store("signer state lock poisoned".into()))?;
     87         Ok(guard.connections.clone())
     88     }
     89 
     90     pub fn get_connection(
     91         &self,
     92         connection_id: &RadrootsNostrSignerConnectionId,
     93     ) -> Result<Option<RadrootsNostrSignerConnectionRecord>, RadrootsNostrSignerError> {
     94         let guard = self
     95             .state
     96             .read()
     97             .map_err(|_| RadrootsNostrSignerError::Store("signer state lock poisoned".into()))?;
     98         Ok(guard
     99             .connections
    100             .iter()
    101             .find(|record| &record.connection_id == connection_id)
    102             .cloned())
    103     }
    104 
    105     pub fn list_publish_workflows(
    106         &self,
    107     ) -> Result<Vec<RadrootsNostrSignerPublishWorkflowRecord>, RadrootsNostrSignerError> {
    108         let guard = self
    109             .state
    110             .read()
    111             .map_err(|_| RadrootsNostrSignerError::Store("signer state lock poisoned".into()))?;
    112         Ok(guard.publish_workflows.clone())
    113     }
    114 
    115     pub fn get_publish_workflow(
    116         &self,
    117         workflow_id: &RadrootsNostrSignerWorkflowId,
    118     ) -> Result<Option<RadrootsNostrSignerPublishWorkflowRecord>, RadrootsNostrSignerError> {
    119         let guard = self
    120             .state
    121             .read()
    122             .map_err(|_| RadrootsNostrSignerError::Store("signer state lock poisoned".into()))?;
    123         Ok(guard
    124             .publish_workflows
    125             .iter()
    126             .find(|record| &record.workflow_id == workflow_id)
    127             .cloned())
    128     }
    129 
    130     pub fn find_connections_by_client_public_key(
    131         &self,
    132         client_public_key: &PublicKey,
    133     ) -> Result<Vec<RadrootsNostrSignerConnectionRecord>, RadrootsNostrSignerError> {
    134         let guard = self
    135             .state
    136             .read()
    137             .map_err(|_| RadrootsNostrSignerError::Store("signer state lock poisoned".into()))?;
    138         Ok(guard
    139             .connections
    140             .iter()
    141             .filter(|record| &record.client_public_key == client_public_key)
    142             .cloned()
    143             .collect())
    144     }
    145 
    146     pub fn find_connection_by_connect_secret(
    147         &self,
    148         connect_secret: &str,
    149     ) -> Result<Option<RadrootsNostrSignerConnectionRecord>, RadrootsNostrSignerError> {
    150         let Some(connect_secret_hash) =
    151             RadrootsNostrSignerConnectSecretHash::from_secret(connect_secret)
    152         else {
    153             return Ok(None);
    154         };
    155 
    156         let guard = self
    157             .state
    158             .read()
    159             .map_err(|_| RadrootsNostrSignerError::Store("signer state lock poisoned".into()))?;
    160         Ok(guard
    161             .connections
    162             .iter()
    163             .find(|record| {
    164                 record.connect_secret_hash.as_ref() == Some(&connect_secret_hash)
    165                     && (!record.is_terminal() || record.connect_secret_is_consumed())
    166             })
    167             .cloned())
    168     }
    169 
    170     pub fn lookup_session(
    171         &self,
    172         client_public_key: &PublicKey,
    173         connect_secret: Option<&str>,
    174     ) -> Result<RadrootsNostrSignerSessionLookup, RadrootsNostrSignerError> {
    175         if let Some(connect_secret) = connect_secret
    176             && let Some(connection) = self.find_connection_by_connect_secret(connect_secret)?
    177         {
    178             if &connection.client_public_key != client_public_key {
    179                 return Err(RadrootsNostrSignerError::InvalidState(
    180                     "connect secret is bound to a different client public key".into(),
    181                 ));
    182             }
    183             return Ok(RadrootsNostrSignerSessionLookup::Connection(Box::new(
    184                 connection,
    185             )));
    186         }
    187 
    188         let mut matches = self.find_connections_by_client_public_key(client_public_key)?;
    189         matches.retain(|record| !record.is_terminal());
    190         Ok(match matches.len() {
    191             0 => RadrootsNostrSignerSessionLookup::None,
    192             1 => RadrootsNostrSignerSessionLookup::Connection(Box::new(matches.remove(0))),
    193             _ => RadrootsNostrSignerSessionLookup::Ambiguous(matches),
    194         })
    195     }
    196 
    197     pub fn evaluate_connect_request(
    198         &self,
    199         client_public_key: PublicKey,
    200         request: RadrootsNostrConnectRequest,
    201     ) -> Result<RadrootsNostrSignerConnectEvaluation, RadrootsNostrSignerError> {
    202         let RadrootsNostrConnectRequest::Connect {
    203             remote_signer_public_key,
    204             secret,
    205             requested_permissions,
    206         } = request
    207         else {
    208             return Err(RadrootsNostrSignerError::InvalidState(
    209                 "connect evaluation requires a connect request".into(),
    210             ));
    211         };
    212 
    213         let (connect_secret, existing_connection) =
    214             self.resolve_connect_request_context(remote_signer_public_key, secret)?;
    215         if let Some(connection) = existing_connection {
    216             if connection.client_public_key != client_public_key {
    217                 return Err(RadrootsNostrSignerError::InvalidState(
    218                     "connect secret is bound to a different client public key".into(),
    219                 ));
    220             }
    221             return Ok(RadrootsNostrSignerConnectEvaluation::ExistingConnection(
    222                 Box::new(connection),
    223             ));
    224         }
    225 
    226         Ok(RadrootsNostrSignerConnectEvaluation::RegistrationRequired(
    227             RadrootsNostrSignerConnectProposal {
    228                 client_public_key,
    229                 connect_secret,
    230                 requested_permissions: normalize_permissions(requested_permissions),
    231             },
    232         ))
    233     }
    234 
    235     pub fn list_audit_records(
    236         &self,
    237     ) -> Result<Vec<RadrootsNostrSignerRequestAuditRecord>, RadrootsNostrSignerError> {
    238         let guard = self
    239             .state
    240             .read()
    241             .map_err(|_| RadrootsNostrSignerError::Store("signer state lock poisoned".into()))?;
    242         Ok(guard.audit_records.clone())
    243     }
    244 
    245     pub fn audit_records_for_connection(
    246         &self,
    247         connection_id: &RadrootsNostrSignerConnectionId,
    248     ) -> Result<Vec<RadrootsNostrSignerRequestAuditRecord>, RadrootsNostrSignerError> {
    249         let guard = self
    250             .state
    251             .read()
    252             .map_err(|_| RadrootsNostrSignerError::Store("signer state lock poisoned".into()))?;
    253         Ok(guard
    254             .audit_records
    255             .iter()
    256             .filter(|record| &record.connection_id == connection_id)
    257             .cloned()
    258             .collect())
    259     }
    260 
    261     pub fn register_connection(
    262         &self,
    263         draft: RadrootsNostrSignerConnectionDraft,
    264     ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> {
    265         self.update_state_with(|state| {
    266             let signer_identity = state
    267                 .signer_identity
    268                 .clone()
    269                 .ok_or(RadrootsNostrSignerError::MissingSignerIdentity)?;
    270             validate_public_identity(&signer_identity)?;
    271             validate_public_identity(&draft.user_identity)?;
    272 
    273             let connect_secret_hash = draft
    274                 .connect_secret
    275                 .as_deref()
    276                 .and_then(RadrootsNostrSignerConnectSecretHash::from_secret);
    277             if let Some(secret_hash) = connect_secret_hash.as_ref()
    278                 && state.connections.iter().any(|record| {
    279                     record.connect_secret_hash.as_ref() == Some(secret_hash)
    280                         && (!record.is_terminal() || record.connect_secret_is_consumed())
    281                 })
    282             {
    283                 return Err(RadrootsNostrSignerError::ConnectSecretAlreadyInUse);
    284             }
    285 
    286             if state.connections.iter().any(|record| {
    287                 !record.is_terminal()
    288                     && record.client_public_key == draft.client_public_key
    289                     && record.user_identity.id == draft.user_identity.id
    290             }) {
    291                 return Err(RadrootsNostrSignerError::ConnectionAlreadyExists {
    292                     client_public_key: draft.client_public_key.to_hex(),
    293                     user_identity_id: draft.user_identity.id.to_string(),
    294                 });
    295             }
    296 
    297             let created_at_unix = now_unix_secs();
    298             let record = RadrootsNostrSignerConnectionRecord::new(
    299                 RadrootsNostrSignerConnectionId::new_v7(),
    300                 signer_identity,
    301                 RadrootsNostrSignerConnectionDraft {
    302                     client_public_key: draft.client_public_key,
    303                     user_identity: draft.user_identity,
    304                     connect_secret: draft.connect_secret,
    305                     requested_permissions: normalize_permissions(draft.requested_permissions),
    306                     relays: normalize_relays(draft.relays),
    307                     approval_requirement: draft.approval_requirement,
    308                 },
    309                 created_at_unix,
    310             );
    311             state.connections.push(record.clone());
    312             Ok(record)
    313         })
    314     }
    315 
    316     pub fn set_granted_permissions(
    317         &self,
    318         connection_id: &RadrootsNostrSignerConnectionId,
    319         granted_permissions: RadrootsNostrConnectPermissions,
    320     ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> {
    321         self.update_state_with(|state| {
    322             let updated_at_unix = now_unix_secs();
    323             let record = find_connection_mut(state, connection_id)?;
    324             if record.is_terminal() {
    325                 return Err(RadrootsNostrSignerError::InvalidState(format!(
    326                     "cannot update granted permissions for {} connection",
    327                     status_label(record.status)
    328                 )));
    329             }
    330 
    331             let granted_permissions = normalize_permissions(granted_permissions);
    332             validate_granted_permissions(&record.requested_permissions, &granted_permissions)?;
    333             record.granted_permissions = granted_permissions
    334                 .as_slice()
    335                 .iter()
    336                 .cloned()
    337                 .map(|permission| {
    338                     RadrootsNostrSignerPermissionGrant::new(permission, updated_at_unix)
    339                 })
    340                 .collect();
    341             record.touch_updated(updated_at_unix);
    342             Ok(record.clone())
    343         })
    344     }
    345 
    346     pub fn approve_connection(
    347         &self,
    348         connection_id: &RadrootsNostrSignerConnectionId,
    349         granted_permissions: RadrootsNostrConnectPermissions,
    350     ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> {
    351         self.update_state_with(|state| {
    352             let updated_at_unix = now_unix_secs();
    353             let record = find_connection_mut(state, connection_id)?;
    354             if record.approval_requirement != RadrootsNostrSignerApprovalRequirement::ExplicitUser {
    355                 return Err(RadrootsNostrSignerError::InvalidState(
    356                     "approval not required for connection".into(),
    357                 ));
    358             }
    359             if record.is_terminal() {
    360                 return Err(RadrootsNostrSignerError::InvalidState(format!(
    361                     "cannot approve {} connection",
    362                     status_label(record.status)
    363                 )));
    364             }
    365 
    366             let granted_permissions = normalize_permissions(granted_permissions);
    367             validate_granted_permissions(&record.requested_permissions, &granted_permissions)?;
    368             record.granted_permissions = granted_permissions
    369                 .as_slice()
    370                 .iter()
    371                 .cloned()
    372                 .map(|permission| {
    373                     RadrootsNostrSignerPermissionGrant::new(permission, updated_at_unix)
    374                 })
    375                 .collect();
    376             record.approval_state = RadrootsNostrSignerApprovalState::Approved;
    377             record.status = RadrootsNostrSignerConnectionStatus::Active;
    378             record.status_reason = None;
    379             record.touch_updated(updated_at_unix);
    380             Ok(record.clone())
    381         })
    382     }
    383 
    384     pub fn reject_connection(
    385         &self,
    386         connection_id: &RadrootsNostrSignerConnectionId,
    387         reason: Option<String>,
    388     ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> {
    389         self.update_state_with(|state| {
    390             let updated_at_unix = now_unix_secs();
    391             let record = find_connection_mut(state, connection_id)?;
    392             if record.is_terminal() {
    393                 return Err(RadrootsNostrSignerError::InvalidState(format!(
    394                     "cannot reject {} connection",
    395                     status_label(record.status)
    396                 )));
    397             }
    398 
    399             record.approval_state = RadrootsNostrSignerApprovalState::Rejected;
    400             record.status = RadrootsNostrSignerConnectionStatus::Rejected;
    401             record.status_reason = normalize_optional_string(reason);
    402             record.touch_updated(updated_at_unix);
    403             Ok(record.clone())
    404         })
    405     }
    406 
    407     pub fn revoke_connection(
    408         &self,
    409         connection_id: &RadrootsNostrSignerConnectionId,
    410         reason: Option<String>,
    411     ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> {
    412         self.update_state_with(|state| {
    413             let updated_at_unix = now_unix_secs();
    414             let record = find_connection_mut(state, connection_id)?;
    415             if record.status == RadrootsNostrSignerConnectionStatus::Revoked {
    416                 return Err(RadrootsNostrSignerError::InvalidState(
    417                     "connection already revoked".into(),
    418                 ));
    419             }
    420 
    421             record.status = RadrootsNostrSignerConnectionStatus::Revoked;
    422             record.status_reason = normalize_optional_string(reason);
    423             record.touch_updated(updated_at_unix);
    424             Ok(record.clone())
    425         })
    426     }
    427 
    428     pub fn update_relays(
    429         &self,
    430         connection_id: &RadrootsNostrSignerConnectionId,
    431         relays: Vec<RelayUrl>,
    432     ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> {
    433         self.update_state_with(|state| {
    434             let updated_at_unix = now_unix_secs();
    435             let record = find_connection_mut(state, connection_id)?;
    436             if record.is_terminal() {
    437                 return Err(RadrootsNostrSignerError::InvalidState(format!(
    438                     "cannot update relays for {} connection",
    439                     status_label(record.status)
    440                 )));
    441             }
    442 
    443             record.relays = normalize_relays(relays);
    444             record.touch_updated(updated_at_unix);
    445             Ok(record.clone())
    446         })
    447     }
    448 
    449     pub fn require_auth_challenge(
    450         &self,
    451         connection_id: &RadrootsNostrSignerConnectionId,
    452         auth_url: impl AsRef<str>,
    453     ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> {
    454         self.update_state_with(|state| {
    455             let required_at_unix = now_unix_secs();
    456             let record = find_connection_mut(state, connection_id)?;
    457             if record.is_terminal() {
    458                 return Err(RadrootsNostrSignerError::InvalidState(format!(
    459                     "cannot require auth for {} connection",
    460                     status_label(record.status)
    461                 )));
    462             }
    463 
    464             let challenge =
    465                 RadrootsNostrSignerAuthChallenge::new(auth_url.as_ref(), required_at_unix)?;
    466             record.require_auth_challenge(challenge);
    467             Ok(record.clone())
    468         })
    469     }
    470 
    471     pub fn set_pending_request(
    472         &self,
    473         connection_id: &RadrootsNostrSignerConnectionId,
    474         request_message: RadrootsNostrConnectRequestMessage,
    475     ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> {
    476         self.update_state_with(|state| {
    477             let record = find_connection_mut(state, connection_id)?;
    478             if record.is_terminal() {
    479                 return Err(RadrootsNostrSignerError::InvalidState(format!(
    480                     "cannot set pending request for {} connection",
    481                     status_label(record.status)
    482                 )));
    483             }
    484             if record.auth_state != RadrootsNostrSignerAuthState::Pending {
    485                 return Err(RadrootsNostrSignerError::InvalidState(
    486                     "auth challenge not pending for connection".into(),
    487                 ));
    488             }
    489 
    490             let pending_request =
    491                 RadrootsNostrSignerPendingRequest::new(request_message, now_unix_secs())?;
    492             record.set_pending_request(pending_request);
    493             Ok(record.clone())
    494         })
    495     }
    496 
    497     pub fn authorize_auth_challenge(
    498         &self,
    499         connection_id: &RadrootsNostrSignerConnectionId,
    500     ) -> Result<RadrootsNostrSignerAuthorizationOutcome, RadrootsNostrSignerError> {
    501         self.update_state_with(|state| {
    502             let record = find_connection_mut(state, connection_id)?;
    503             if record.is_terminal() {
    504                 return Err(RadrootsNostrSignerError::InvalidState(format!(
    505                     "cannot authorize auth challenge for {} connection",
    506                     status_label(record.status)
    507                 )));
    508             }
    509             if record.auth_state != RadrootsNostrSignerAuthState::Pending {
    510                 return Err(RadrootsNostrSignerError::InvalidState(
    511                     "auth challenge not pending for connection".into(),
    512                 ));
    513             }
    514 
    515             let pending_request = record.authorize_auth_challenge(now_unix_secs());
    516             Ok(RadrootsNostrSignerAuthorizationOutcome::new(
    517                 record.clone(),
    518                 pending_request,
    519             ))
    520         })
    521     }
    522 
    523     pub fn restore_pending_auth_challenge(
    524         &self,
    525         connection_id: &RadrootsNostrSignerConnectionId,
    526         pending_request: RadrootsNostrSignerPendingRequest,
    527     ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> {
    528         self.update_state_with(|state| {
    529             let restored_at_unix = now_unix_secs();
    530             let record = find_connection_mut(state, connection_id)?;
    531             if record.is_terminal() {
    532                 return Err(RadrootsNostrSignerError::InvalidState(format!(
    533                     "cannot restore auth challenge for {} connection",
    534                     status_label(record.status)
    535                 )));
    536             }
    537             if record.auth_state != RadrootsNostrSignerAuthState::Authorized {
    538                 return Err(RadrootsNostrSignerError::InvalidState(
    539                     "auth challenge not authorized for connection".into(),
    540                 ));
    541             }
    542             if record.auth_challenge.is_none() {
    543                 return Err(RadrootsNostrSignerError::InvalidState(
    544                     "auth challenge missing for connection".into(),
    545                 ));
    546             }
    547 
    548             record.restore_pending_auth_challenge(pending_request, restored_at_unix);
    549             Ok(record.clone())
    550         })
    551     }
    552 
    553     pub fn begin_connect_secret_publish_finalization(
    554         &self,
    555         connection_id: &RadrootsNostrSignerConnectionId,
    556     ) -> Result<RadrootsNostrSignerPublishWorkflowRecord, RadrootsNostrSignerError> {
    557         self.update_state_with(|state| {
    558             let connection_index = find_connection_index(state, connection_id)?;
    559             let record = &state.connections[connection_index];
    560             if record.is_terminal() {
    561                 return Err(RadrootsNostrSignerError::InvalidState(format!(
    562                     "cannot begin connect secret finalization for {} connection",
    563                     status_label(record.status)
    564                 )));
    565             }
    566             if record.connect_secret_hash.is_none() {
    567                 return Err(RadrootsNostrSignerError::InvalidState(
    568                     "connection does not have a connect secret".into(),
    569                 ));
    570             }
    571             if record.connect_secret_is_consumed() {
    572                 return Err(RadrootsNostrSignerError::InvalidState(
    573                     "connect secret already consumed for connection".into(),
    574                 ));
    575             }
    576             ensure_no_active_publish_workflow(
    577                 state,
    578                 connection_id,
    579                 RadrootsNostrSignerPublishWorkflowKind::ConnectSecretFinalization,
    580             )?;
    581 
    582             let workflow =
    583                 RadrootsNostrSignerPublishWorkflowRecord::new_connect_secret_finalization(
    584                     connection_id.clone(),
    585                     now_unix_secs(),
    586                 );
    587             state.publish_workflows.push(workflow.clone());
    588             Ok(workflow)
    589         })
    590     }
    591 
    592     pub fn begin_auth_replay_publish_finalization(
    593         &self,
    594         connection_id: &RadrootsNostrSignerConnectionId,
    595     ) -> Result<RadrootsNostrSignerPublishWorkflowRecord, RadrootsNostrSignerError> {
    596         self.update_state_with(|state| {
    597             let authorized_at_unix = now_unix_secs();
    598             let connection_index = find_connection_index(state, connection_id)?;
    599             let record = &state.connections[connection_index];
    600             if record.is_terminal() {
    601                 return Err(RadrootsNostrSignerError::InvalidState(format!(
    602                     "cannot begin auth replay finalization for {} connection",
    603                     status_label(record.status)
    604                 )));
    605             }
    606             if record.auth_state != RadrootsNostrSignerAuthState::Pending {
    607                 return Err(RadrootsNostrSignerError::InvalidState(
    608                     "auth challenge not pending for connection".into(),
    609                 ));
    610             }
    611             if record.auth_challenge.is_none() {
    612                 return Err(RadrootsNostrSignerError::InvalidState(
    613                     "auth challenge missing for connection".into(),
    614                 ));
    615             }
    616             let pending_request = record.pending_request.clone().ok_or_else(|| {
    617                 RadrootsNostrSignerError::InvalidState(
    618                     "pending request missing for auth replay finalization".into(),
    619                 )
    620             })?;
    621             ensure_no_active_publish_workflow(
    622                 state,
    623                 connection_id,
    624                 RadrootsNostrSignerPublishWorkflowKind::AuthReplayFinalization,
    625             )?;
    626 
    627             let workflow = RadrootsNostrSignerPublishWorkflowRecord::new_auth_replay_finalization(
    628                 connection_id.clone(),
    629                 pending_request,
    630                 authorized_at_unix,
    631             );
    632             state.publish_workflows.push(workflow.clone());
    633             Ok(workflow)
    634         })
    635     }
    636 
    637     pub fn mark_publish_workflow_published(
    638         &self,
    639         workflow_id: &RadrootsNostrSignerWorkflowId,
    640     ) -> Result<RadrootsNostrSignerPublishWorkflowRecord, RadrootsNostrSignerError> {
    641         self.update_state_with(|state| {
    642             let workflow = find_publish_workflow_mut(state, workflow_id)?;
    643             workflow.mark_published(now_unix_secs());
    644             Ok(workflow.clone())
    645         })
    646     }
    647 
    648     pub fn finalize_publish_workflow(
    649         &self,
    650         workflow_id: &RadrootsNostrSignerWorkflowId,
    651     ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> {
    652         self.update_state_with(|state| {
    653             let workflow_index = find_publish_workflow_index(state, workflow_id)?;
    654             let workflow = state.publish_workflows[workflow_index].clone();
    655             if workflow.state != RadrootsNostrSignerPublishWorkflowState::PublishedPendingFinalize {
    656                 return Err(RadrootsNostrSignerError::InvalidState(
    657                     "publish workflow has not reached published state".into(),
    658                 ));
    659             }
    660 
    661             let record = find_connection_mut(state, &workflow.connection_id)?;
    662             let finalized = match workflow.kind {
    663                 RadrootsNostrSignerPublishWorkflowKind::ConnectSecretFinalization => {
    664                     if record.connect_secret_hash.is_none() {
    665                         return Err(RadrootsNostrSignerError::InvalidState(
    666                             "connection does not have a connect secret".into(),
    667                         ));
    668                     }
    669                     if record.connect_secret_is_consumed() {
    670                         return Err(RadrootsNostrSignerError::InvalidState(
    671                             "connect secret already consumed for connection".into(),
    672                         ));
    673                     }
    674                     record.mark_connect_secret_consumed(now_unix_secs());
    675                     record.clone()
    676                 }
    677                 RadrootsNostrSignerPublishWorkflowKind::AuthReplayFinalization => {
    678                     if record.auth_state != RadrootsNostrSignerAuthState::Pending {
    679                         return Err(RadrootsNostrSignerError::InvalidState(
    680                             "auth challenge not pending for connection".into(),
    681                         ));
    682                     }
    683                     if record.auth_challenge.is_none() {
    684                         return Err(RadrootsNostrSignerError::InvalidState(
    685                             "auth challenge missing for connection".into(),
    686                         ));
    687                     }
    688                     let expected_pending_request =
    689                         workflow.pending_request.clone().ok_or_else(|| {
    690                             RadrootsNostrSignerError::InvalidState(
    691                                 "auth replay workflow missing pending request".into(),
    692                             )
    693                         })?;
    694                     if record.pending_request.as_ref() != Some(&expected_pending_request) {
    695                         return Err(RadrootsNostrSignerError::InvalidState(
    696                             "pending request does not match auth replay workflow".into(),
    697                         ));
    698                     }
    699                     let authorized_at_unix = workflow.authorized_at_unix.ok_or_else(|| {
    700                         RadrootsNostrSignerError::InvalidState(
    701                             "auth replay workflow missing authorized timestamp".into(),
    702                         )
    703                     })?;
    704                     let replay = record.authorize_auth_challenge(authorized_at_unix);
    705                     debug_assert_eq!(
    706                         replay.as_ref(),
    707                         Some(&expected_pending_request),
    708                         "auth replay finalization returned unexpected pending request"
    709                     );
    710                     record.clone()
    711                 }
    712             };
    713 
    714             state.publish_workflows.remove(workflow_index);
    715             Ok(finalized)
    716         })
    717     }
    718 
    719     pub fn cancel_publish_workflow(
    720         &self,
    721         workflow_id: &RadrootsNostrSignerWorkflowId,
    722     ) -> Result<RadrootsNostrSignerPublishWorkflowRecord, RadrootsNostrSignerError> {
    723         self.update_state_with(|state| {
    724             let workflow_index = find_publish_workflow_index(state, workflow_id)?;
    725             Ok(state.publish_workflows.remove(workflow_index))
    726         })
    727     }
    728 
    729     pub fn mark_authenticated(
    730         &self,
    731         connection_id: &RadrootsNostrSignerConnectionId,
    732     ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> {
    733         self.update_state_with(|state| {
    734             let authenticated_at_unix = now_unix_secs();
    735             let record = find_connection_mut(state, connection_id)?;
    736             record.mark_authenticated(authenticated_at_unix);
    737             Ok(record.clone())
    738         })
    739     }
    740 
    741     pub fn mark_connect_secret_consumed(
    742         &self,
    743         connection_id: &RadrootsNostrSignerConnectionId,
    744     ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> {
    745         self.update_state_with(|state| {
    746             let consumed_at_unix = now_unix_secs();
    747             let record = find_connection_mut(state, connection_id)?;
    748             if record.connect_secret_hash.is_none() {
    749                 return Err(RadrootsNostrSignerError::InvalidState(
    750                     "connection does not have a connect secret".into(),
    751                 ));
    752             }
    753             record.mark_connect_secret_consumed(consumed_at_unix);
    754             Ok(record.clone())
    755         })
    756     }
    757 
    758     pub fn evaluate_request(
    759         &self,
    760         connection_id: &RadrootsNostrSignerConnectionId,
    761         request_message: RadrootsNostrConnectRequestMessage,
    762     ) -> Result<RadrootsNostrSignerRequestEvaluation, RadrootsNostrSignerError> {
    763         if matches!(
    764             request_message.request,
    765             RadrootsNostrConnectRequest::Connect { .. }
    766         ) {
    767             return Err(RadrootsNostrSignerError::InvalidState(
    768                 "connect requests must be evaluated via evaluate_connect_request".into(),
    769             ));
    770         }
    771 
    772         self.update_state_with(|state| {
    773             let request_at_unix = now_unix_secs();
    774             let request_id = RadrootsNostrSignerRequestId::parse(&request_message.id)?;
    775             let record = find_connection_mut(state, connection_id)?;
    776             let method = request_message.request.method();
    777             let action = evaluate_request_action(record, &request_message, request_at_unix)?;
    778             record.mark_request(request_at_unix);
    779 
    780             let audit = RadrootsNostrSignerRequestAuditRecord::new(
    781                 request_id.clone(),
    782                 connection_id.clone(),
    783                 method.clone(),
    784                 request_decision(&action),
    785                 action.audit_message(),
    786                 request_at_unix,
    787             );
    788             let connection = record.clone();
    789             state.audit_records.push(audit.clone());
    790 
    791             Ok(RadrootsNostrSignerRequestEvaluation {
    792                 request_id,
    793                 method,
    794                 connection,
    795                 audit,
    796                 action,
    797             })
    798         })
    799     }
    800 
    801     pub fn evaluate_auth_replay_publish_workflow(
    802         &self,
    803         workflow_id: &RadrootsNostrSignerWorkflowId,
    804     ) -> Result<RadrootsNostrSignerRequestEvaluation, RadrootsNostrSignerError> {
    805         self.update_state_with(|state| {
    806             let request_at_unix = now_unix_secs();
    807             let workflow = state
    808                 .publish_workflows
    809                 .iter()
    810                 .find(|record| &record.workflow_id == workflow_id)
    811                 .cloned()
    812                 .ok_or_else(|| {
    813                     RadrootsNostrSignerError::PublishWorkflowNotFound(workflow_id.to_string())
    814                 })?;
    815             if workflow.kind != RadrootsNostrSignerPublishWorkflowKind::AuthReplayFinalization {
    816                 return Err(RadrootsNostrSignerError::InvalidState(
    817                     "publish workflow is not an auth replay finalization".into(),
    818                 ));
    819             }
    820 
    821             let pending_request = workflow.pending_request.clone().ok_or_else(|| {
    822                 RadrootsNostrSignerError::InvalidState(
    823                     "auth replay workflow missing pending request".into(),
    824                 )
    825             })?;
    826             let request_message = pending_request.request_message();
    827             let request_id = pending_request.request_id();
    828             let method = request_message.request.method();
    829 
    830             let record = find_connection_mut(state, &workflow.connection_id)?;
    831             if record.is_terminal() {
    832                 return Err(RadrootsNostrSignerError::InvalidState(format!(
    833                     "cannot evaluate auth replay workflow for {} connection",
    834                     status_label(record.status)
    835                 )));
    836             }
    837             if record.auth_state != RadrootsNostrSignerAuthState::Pending {
    838                 return Err(RadrootsNostrSignerError::InvalidState(
    839                     "auth challenge not pending for connection".into(),
    840                 ));
    841             }
    842             if record.pending_request.as_ref() != Some(&pending_request) {
    843                 return Err(RadrootsNostrSignerError::InvalidState(
    844                     "pending request does not match auth replay workflow".into(),
    845                 ));
    846             }
    847 
    848             let mut effective_connection = record.clone();
    849             effective_connection.auth_state = RadrootsNostrSignerAuthState::Authorized;
    850             effective_connection.pending_request = None;
    851             if let Some(auth_challenge) = effective_connection.auth_challenge.as_mut() {
    852                 auth_challenge.authorized_at_unix = workflow.authorized_at_unix;
    853             }
    854             let request = &request_message;
    855             let action =
    856                 evaluate_request_action(&mut effective_connection, request, request_at_unix)?;
    857             effective_connection.mark_request(request_at_unix);
    858             record.mark_request(request_at_unix);
    859 
    860             let audit = RadrootsNostrSignerRequestAuditRecord::new(
    861                 request_id.clone(),
    862                 workflow.connection_id.clone(),
    863                 method.clone(),
    864                 request_decision(&action),
    865                 action.audit_message(),
    866                 request_at_unix,
    867             );
    868             state.audit_records.push(audit.clone());
    869 
    870             Ok(RadrootsNostrSignerRequestEvaluation {
    871                 request_id,
    872                 method,
    873                 connection: effective_connection,
    874                 audit,
    875                 action,
    876             })
    877         })
    878     }
    879 
    880     pub fn record_request(
    881         &self,
    882         connection_id: &RadrootsNostrSignerConnectionId,
    883         request_id: impl AsRef<str>,
    884         method: RadrootsNostrConnectMethod,
    885         decision: RadrootsNostrSignerRequestDecision,
    886         message: Option<String>,
    887     ) -> Result<RadrootsNostrSignerRequestAuditRecord, RadrootsNostrSignerError> {
    888         self.update_state_with(|state| {
    889             let created_at_unix = now_unix_secs();
    890             let request_id = RadrootsNostrSignerRequestId::parse(request_id.as_ref())?;
    891             let record = find_connection_mut(state, connection_id)?;
    892             record.mark_request(created_at_unix);
    893 
    894             let audit = RadrootsNostrSignerRequestAuditRecord::new(
    895                 request_id,
    896                 connection_id.clone(),
    897                 method,
    898                 decision,
    899                 normalize_optional_string(message),
    900                 created_at_unix,
    901             );
    902             state.audit_records.push(audit.clone());
    903             Ok(audit)
    904         })
    905     }
    906 
    907     #[cfg_attr(coverage_nightly, coverage(off))]
    908     fn update_state(
    909         &self,
    910         update: impl FnOnce(&mut RadrootsNostrSignerStoreState) -> Result<(), RadrootsNostrSignerError>,
    911     ) -> Result<(), RadrootsNostrSignerError> {
    912         self.update_state_with(|state| {
    913             update(state)?;
    914             Ok(())
    915         })
    916     }
    917 
    918     #[cfg_attr(coverage_nightly, coverage(off))]
    919     fn update_state_with<T>(
    920         &self,
    921         update: impl FnOnce(&mut RadrootsNostrSignerStoreState) -> Result<T, RadrootsNostrSignerError>,
    922     ) -> Result<T, RadrootsNostrSignerError> {
    923         let mut guard = self
    924             .state
    925             .write()
    926             .map_err(|_| RadrootsNostrSignerError::Store("signer state lock poisoned".into()))?;
    927         let mut next = guard.clone();
    928         let value = update(&mut next)?;
    929         self.store.save(&next)?;
    930         *guard = next;
    931         Ok(value)
    932     }
    933 
    934     fn resolve_connect_request_context(
    935         &self,
    936         remote_signer_public_key: PublicKey,
    937         secret: Option<String>,
    938     ) -> Result<
    939         (Option<String>, Option<RadrootsNostrSignerConnectionRecord>),
    940         RadrootsNostrSignerError,
    941     > {
    942         let signer_identity = self
    943             .signer_identity()?
    944             .ok_or(RadrootsNostrSignerError::MissingSignerIdentity)?;
    945         let signer_public_key = parse_identity_public_key(&signer_identity)?;
    946         if remote_signer_public_key != signer_public_key {
    947             return Err(RadrootsNostrSignerError::InvalidState(
    948                 "remote signer public key mismatch".into(),
    949             ));
    950         }
    951 
    952         let connect_secret = normalize_optional_string(secret);
    953         let existing_connection =
    954             self.find_connection_by_connect_secret(connect_secret.as_deref().unwrap_or_default())?;
    955         Ok((connect_secret, existing_connection))
    956     }
    957 }
    958 
    959 fn find_connection_mut<'a>(
    960     state: &'a mut RadrootsNostrSignerStoreState,
    961     connection_id: &RadrootsNostrSignerConnectionId,
    962 ) -> Result<&'a mut RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> {
    963     state
    964         .connections
    965         .iter_mut()
    966         .find(|record| &record.connection_id == connection_id)
    967         .ok_or_else(|| RadrootsNostrSignerError::ConnectionNotFound(connection_id.to_string()))
    968 }
    969 
    970 fn find_connection_index(
    971     state: &RadrootsNostrSignerStoreState,
    972     connection_id: &RadrootsNostrSignerConnectionId,
    973 ) -> Result<usize, RadrootsNostrSignerError> {
    974     for (index, record) in state.connections.iter().enumerate() {
    975         if &record.connection_id == connection_id {
    976             return Ok(index);
    977         }
    978     }
    979     Err(RadrootsNostrSignerError::ConnectionNotFound(
    980         connection_id.to_string(),
    981     ))
    982 }
    983 
    984 fn find_publish_workflow_index(
    985     state: &RadrootsNostrSignerStoreState,
    986     workflow_id: &RadrootsNostrSignerWorkflowId,
    987 ) -> Result<usize, RadrootsNostrSignerError> {
    988     state
    989         .publish_workflows
    990         .iter()
    991         .position(|record| &record.workflow_id == workflow_id)
    992         .ok_or_else(|| RadrootsNostrSignerError::PublishWorkflowNotFound(workflow_id.to_string()))
    993 }
    994 
    995 fn find_publish_workflow_mut<'a>(
    996     state: &'a mut RadrootsNostrSignerStoreState,
    997     workflow_id: &RadrootsNostrSignerWorkflowId,
    998 ) -> Result<&'a mut RadrootsNostrSignerPublishWorkflowRecord, RadrootsNostrSignerError> {
    999     state
   1000         .publish_workflows
   1001         .iter_mut()
   1002         .find(|record| &record.workflow_id == workflow_id)
   1003         .ok_or_else(|| RadrootsNostrSignerError::PublishWorkflowNotFound(workflow_id.to_string()))
   1004 }
   1005 
   1006 fn ensure_no_active_publish_workflow(
   1007     state: &RadrootsNostrSignerStoreState,
   1008     connection_id: &RadrootsNostrSignerConnectionId,
   1009     kind: RadrootsNostrSignerPublishWorkflowKind,
   1010 ) -> Result<(), RadrootsNostrSignerError> {
   1011     if state
   1012         .publish_workflows
   1013         .iter()
   1014         .any(|record| &record.connection_id == connection_id && record.kind == kind)
   1015     {
   1016         return Err(RadrootsNostrSignerError::InvalidState(format!(
   1017             "publish workflow already active for {}",
   1018             publish_workflow_kind_label(kind)
   1019         )));
   1020     }
   1021     Ok(())
   1022 }
   1023 
   1024 fn validate_public_identity(
   1025     identity: &RadrootsIdentityPublic,
   1026 ) -> Result<(), RadrootsNostrSignerError> {
   1027     if identity.id.as_str() != identity.public_key_hex {
   1028         return Err(RadrootsNostrSignerError::InvalidState(
   1029             "public identity id does not match public key".into(),
   1030         ));
   1031     }
   1032     Ok(())
   1033 }
   1034 
   1035 fn validate_granted_permissions(
   1036     requested_permissions: &RadrootsNostrConnectPermissions,
   1037     granted_permissions: &RadrootsNostrConnectPermissions,
   1038 ) -> Result<(), RadrootsNostrSignerError> {
   1039     if requested_permissions.is_empty() {
   1040         return Ok(());
   1041     }
   1042 
   1043     let requested = requested_permissions.as_slice();
   1044     if let Some(permission) = granted_permissions
   1045         .as_slice()
   1046         .iter()
   1047         .find(|permission| !requested.contains(permission))
   1048     {
   1049         return Err(RadrootsNostrSignerError::InvalidGrantedPermission(
   1050             permission.to_string(),
   1051         ));
   1052     }
   1053     Ok(())
   1054 }
   1055 
   1056 fn evaluate_request_action(
   1057     record: &mut RadrootsNostrSignerConnectionRecord,
   1058     request_message: &RadrootsNostrConnectRequestMessage,
   1059     request_at_unix: u64,
   1060 ) -> Result<RadrootsNostrSignerRequestAction, RadrootsNostrSignerError> {
   1061     if record.is_terminal() {
   1062         return Ok(RadrootsNostrSignerRequestAction::Denied {
   1063             reason: format!("connection is {}", status_label(record.status)),
   1064         });
   1065     }
   1066     if record.status != RadrootsNostrSignerConnectionStatus::Active {
   1067         return Ok(RadrootsNostrSignerRequestAction::Denied {
   1068             reason: format!("connection is {}", status_label(record.status)),
   1069         });
   1070     }
   1071     if record.auth_state == RadrootsNostrSignerAuthState::Pending {
   1072         let auth_challenge =
   1073             record
   1074                 .auth_challenge
   1075                 .clone()
   1076                 .ok_or(RadrootsNostrSignerError::InvalidState(
   1077                     "auth challenge missing for pending auth state".into(),
   1078                 ))?;
   1079         let pending_request =
   1080             RadrootsNostrSignerPendingRequest::new(request_message.clone(), request_at_unix)?;
   1081         record.set_pending_request(pending_request.clone());
   1082         return Ok(RadrootsNostrSignerRequestAction::Challenged {
   1083             auth_challenge,
   1084             pending_request,
   1085         });
   1086     }
   1087 
   1088     let effective_permissions = record.effective_permissions();
   1089     if !request_allowed_by_permissions(&effective_permissions, &request_message.request) {
   1090         return Ok(RadrootsNostrSignerRequestAction::Denied {
   1091             reason: format!("unauthorized {}", request_message.request.method()),
   1092         });
   1093     }
   1094 
   1095     Ok(RadrootsNostrSignerRequestAction::Allowed {
   1096         required_permission: required_permission_for_request(&request_message.request),
   1097         response_hint: response_hint_for_request(record, &request_message.request)?,
   1098     })
   1099 }
   1100 
   1101 fn normalize_permissions(
   1102     permissions: RadrootsNostrConnectPermissions,
   1103 ) -> RadrootsNostrConnectPermissions {
   1104     let mut permissions = permissions.into_vec();
   1105     permissions.sort();
   1106     permissions.dedup();
   1107     permissions.into()
   1108 }
   1109 
   1110 fn normalize_relays(relays: Vec<RelayUrl>) -> Vec<RelayUrl> {
   1111     let mut relays = relays;
   1112     relays.sort_by(|left, right| left.as_str().cmp(right.as_str()));
   1113     relays.dedup_by(|left, right| left.as_str() == right.as_str());
   1114     relays
   1115 }
   1116 
   1117 fn normalize_optional_string(value: Option<String>) -> Option<String> {
   1118     value.and_then(|value| {
   1119         let trimmed = value.trim().to_owned();
   1120         if trimmed.is_empty() {
   1121             None
   1122         } else {
   1123             Some(trimmed)
   1124         }
   1125     })
   1126 }
   1127 
   1128 fn status_label(status: RadrootsNostrSignerConnectionStatus) -> &'static str {
   1129     match status {
   1130         RadrootsNostrSignerConnectionStatus::Pending => "pending",
   1131         RadrootsNostrSignerConnectionStatus::Active => "active",
   1132         RadrootsNostrSignerConnectionStatus::Rejected => "rejected",
   1133         RadrootsNostrSignerConnectionStatus::Revoked => "revoked",
   1134     }
   1135 }
   1136 
   1137 fn publish_workflow_kind_label(kind: RadrootsNostrSignerPublishWorkflowKind) -> &'static str {
   1138     match kind {
   1139         RadrootsNostrSignerPublishWorkflowKind::ConnectSecretFinalization => {
   1140             "connect_secret_finalization"
   1141         }
   1142         RadrootsNostrSignerPublishWorkflowKind::AuthReplayFinalization => {
   1143             "auth_replay_finalization"
   1144         }
   1145     }
   1146 }
   1147 
   1148 fn request_decision(
   1149     action: &RadrootsNostrSignerRequestAction,
   1150 ) -> RadrootsNostrSignerRequestDecision {
   1151     match action {
   1152         RadrootsNostrSignerRequestAction::Allowed { .. } => {
   1153             RadrootsNostrSignerRequestDecision::Allowed
   1154         }
   1155         RadrootsNostrSignerRequestAction::Denied { .. } => {
   1156             RadrootsNostrSignerRequestDecision::Denied
   1157         }
   1158         RadrootsNostrSignerRequestAction::Challenged { .. } => {
   1159             RadrootsNostrSignerRequestDecision::Challenged
   1160         }
   1161     }
   1162 }
   1163 
   1164 fn parse_identity_public_key(
   1165     identity: &RadrootsIdentityPublic,
   1166 ) -> Result<PublicKey, RadrootsNostrSignerError> {
   1167     PublicKey::parse(identity.public_key_hex.as_str())
   1168         .or_else(|_| PublicKey::from_hex(identity.public_key_hex.as_str()))
   1169         .map_err(|_| {
   1170             RadrootsNostrSignerError::InvalidState("identity public key is invalid".into())
   1171         })
   1172 }
   1173 
   1174 fn now_unix_secs() -> u64 {
   1175     SystemTime::now()
   1176         .duration_since(UNIX_EPOCH)
   1177         .map(|duration| duration.as_secs())
   1178         .unwrap_or(0)
   1179 }
   1180 
   1181 #[cfg(test)]
   1182 #[cfg_attr(coverage_nightly, coverage(off))]
   1183 mod tests {
   1184     use super::*;
   1185     use crate::evaluation::{
   1186         RadrootsNostrSignerConnectEvaluation, RadrootsNostrSignerRequestAction,
   1187         RadrootsNostrSignerRequestResponseHint, RadrootsNostrSignerSessionLookup,
   1188     };
   1189     use crate::store::RadrootsNostrSignerStore;
   1190     use crate::test_support::{
   1191         api_primary_https, fixture_alice_identity, primary_relay, secondary_relay,
   1192         synthetic_public_identity, synthetic_public_key, tertiary_relay,
   1193     };
   1194     use nostr::{PublicKey, Timestamp, UnsignedEvent};
   1195     use radroots_identity::RadrootsIdentityPublic;
   1196     use radroots_nostr_connect::prelude::RadrootsNostrConnectPermission;
   1197     use serde_json::json;
   1198     use std::sync::Arc;
   1199     use std::thread;
   1200 
   1201     fn public_identity(index: u32) -> RadrootsIdentityPublic {
   1202         synthetic_public_identity(index)
   1203     }
   1204 
   1205     fn invalid_public_identity(index: u32) -> RadrootsIdentityPublic {
   1206         let mut identity = public_identity(index);
   1207         identity.id =
   1208             radroots_identity::RadrootsIdentityId::parse(&public_key(0xff).to_hex()).expect("id");
   1209         identity
   1210     }
   1211 
   1212     fn public_key(index: u32) -> PublicKey {
   1213         synthetic_public_key(index)
   1214     }
   1215 
   1216     fn permission(
   1217         method: RadrootsNostrConnectMethod,
   1218         parameter: Option<&str>,
   1219     ) -> RadrootsNostrConnectPermission {
   1220         match parameter {
   1221             Some(parameter) => RadrootsNostrConnectPermission::with_parameter(method, parameter),
   1222             None => RadrootsNostrConnectPermission::new(method),
   1223         }
   1224     }
   1225 
   1226     fn request_message(id: &str) -> RadrootsNostrConnectRequestMessage {
   1227         RadrootsNostrConnectRequestMessage::new(
   1228             id,
   1229             radroots_nostr_connect::prelude::RadrootsNostrConnectRequest::Ping,
   1230         )
   1231     }
   1232 
   1233     fn request_message_with_request(
   1234         id: &str,
   1235         request: RadrootsNostrConnectRequest,
   1236     ) -> RadrootsNostrConnectRequestMessage {
   1237         RadrootsNostrConnectRequestMessage::new(id, request)
   1238     }
   1239 
   1240     fn unsigned_event(kind: u16) -> UnsignedEvent {
   1241         serde_json::from_value(json!({
   1242             "pubkey": public_key(0xa1).to_hex(),
   1243             "created_at": Timestamp::from(1).as_secs(),
   1244             "kind": kind,
   1245             "tags": [],
   1246             "content": "hello"
   1247         }))
   1248         .expect("unsigned event")
   1249     }
   1250 
   1251     #[cfg_attr(coverage_nightly, coverage(off))]
   1252     fn expect_connection_lookup(
   1253         lookup: RadrootsNostrSignerSessionLookup,
   1254     ) -> RadrootsNostrSignerConnectionRecord {
   1255         match lookup {
   1256             RadrootsNostrSignerSessionLookup::Connection(found) => *found,
   1257             other => panic!("unexpected lookup result: {other:?}"),
   1258         }
   1259     }
   1260 
   1261     #[cfg_attr(coverage_nightly, coverage(off))]
   1262     fn expect_ambiguous_lookup(
   1263         lookup: RadrootsNostrSignerSessionLookup,
   1264     ) -> Vec<RadrootsNostrSignerConnectionRecord> {
   1265         match lookup {
   1266             RadrootsNostrSignerSessionLookup::Ambiguous(found) => found,
   1267             other => panic!("unexpected ambiguous lookup result: {other:?}"),
   1268         }
   1269     }
   1270 
   1271     #[cfg_attr(coverage_nightly, coverage(off))]
   1272     fn expect_existing_connect(
   1273         evaluation: RadrootsNostrSignerConnectEvaluation,
   1274     ) -> RadrootsNostrSignerConnectionRecord {
   1275         match evaluation {
   1276             RadrootsNostrSignerConnectEvaluation::ExistingConnection(found) => *found,
   1277             other => panic!("unexpected existing connect result: {other:?}"),
   1278         }
   1279     }
   1280 
   1281     #[cfg_attr(coverage_nightly, coverage(off))]
   1282     fn expect_registration_connect(
   1283         evaluation: RadrootsNostrSignerConnectEvaluation,
   1284     ) -> crate::evaluation::RadrootsNostrSignerConnectProposal {
   1285         match evaluation {
   1286             RadrootsNostrSignerConnectEvaluation::RegistrationRequired(proposal) => proposal,
   1287             other => panic!("unexpected registration connect result: {other:?}"),
   1288         }
   1289     }
   1290 
   1291     #[cfg_attr(coverage_nightly, coverage(off))]
   1292     fn expect_none_lookup(lookup: RadrootsNostrSignerSessionLookup) {
   1293         match lookup {
   1294             RadrootsNostrSignerSessionLookup::None => {}
   1295             other => panic!("unexpected non-empty lookup result: {other:?}"),
   1296         }
   1297     }
   1298 
   1299     #[cfg_attr(coverage_nightly, coverage(off))]
   1300     fn expect_allowed_user_public_key(action: &RadrootsNostrSignerRequestAction) {
   1301         match action {
   1302             RadrootsNostrSignerRequestAction::Allowed {
   1303                 required_permission: None,
   1304                 response_hint: RadrootsNostrSignerRequestResponseHint::UserPublicKey(_),
   1305             } => {}
   1306             other => panic!("unexpected allowed pubkey action: {other:?}"),
   1307         }
   1308     }
   1309 
   1310     #[cfg_attr(coverage_nightly, coverage(off))]
   1311     fn expect_allowed_without_response_hint(action: &RadrootsNostrSignerRequestAction) {
   1312         match action {
   1313             RadrootsNostrSignerRequestAction::Allowed {
   1314                 required_permission: Some(_),
   1315                 response_hint: RadrootsNostrSignerRequestResponseHint::None,
   1316             } => {}
   1317             other => panic!("unexpected allowed no-hint action: {other:?}"),
   1318         }
   1319     }
   1320 
   1321     #[cfg_attr(coverage_nightly, coverage(off))]
   1322     fn expect_challenged_action(action: &RadrootsNostrSignerRequestAction) {
   1323         match action {
   1324             RadrootsNostrSignerRequestAction::Challenged { .. } => {}
   1325             other => panic!("unexpected challenged action: {other:?}"),
   1326         }
   1327     }
   1328 
   1329     fn poison_manager_state(manager: &RadrootsNostrSignerManager) {
   1330         let shared = manager.state.clone();
   1331         let _ = thread::spawn(move || {
   1332             let _guard = shared.write().expect("write");
   1333             panic!("poison signer state");
   1334         })
   1335         .join();
   1336     }
   1337 
   1338     fn assert_same_public_identity(left: &RadrootsIdentityPublic, right: &RadrootsIdentityPublic) {
   1339         assert_eq!(left.id.as_str(), right.id.as_str());
   1340         assert_eq!(left.public_key_hex, right.public_key_hex);
   1341         assert_eq!(left.public_key_npub, right.public_key_npub);
   1342     }
   1343 
   1344     fn assert_same_connection(
   1345         left: &RadrootsNostrSignerConnectionRecord,
   1346         right: &RadrootsNostrSignerConnectionRecord,
   1347     ) {
   1348         assert_eq!(left.connection_id, right.connection_id);
   1349         assert_eq!(left.client_public_key, right.client_public_key);
   1350         assert_same_public_identity(&left.signer_identity, &right.signer_identity);
   1351         assert_same_public_identity(&left.user_identity, &right.user_identity);
   1352         assert_eq!(left.connect_secret_hash, right.connect_secret_hash);
   1353         assert_eq!(
   1354             left.connect_secret_consumed_at_unix,
   1355             right.connect_secret_consumed_at_unix
   1356         );
   1357         assert_eq!(left.requested_permissions, right.requested_permissions);
   1358         assert_eq!(left.granted_permissions, right.granted_permissions);
   1359         assert_eq!(left.relays, right.relays);
   1360         assert_eq!(left.approval_requirement, right.approval_requirement);
   1361         assert_eq!(left.approval_state, right.approval_state);
   1362         assert_eq!(left.auth_state, right.auth_state);
   1363         assert_eq!(left.auth_challenge, right.auth_challenge);
   1364         assert_eq!(left.pending_request, right.pending_request);
   1365         assert_eq!(left.status, right.status);
   1366         assert_eq!(left.status_reason, right.status_reason);
   1367         assert_eq!(left.created_at_unix, right.created_at_unix);
   1368         assert_eq!(left.updated_at_unix, right.updated_at_unix);
   1369         assert_eq!(
   1370             left.last_authenticated_at_unix,
   1371             right.last_authenticated_at_unix
   1372         );
   1373         assert_eq!(left.last_request_at_unix, right.last_request_at_unix);
   1374     }
   1375 
   1376     struct LoadErrorStore;
   1377 
   1378     impl RadrootsNostrSignerStore for LoadErrorStore {
   1379         fn load(&self) -> Result<RadrootsNostrSignerStoreState, RadrootsNostrSignerError> {
   1380             Err(RadrootsNostrSignerError::Store("store load failed".into()))
   1381         }
   1382 
   1383         fn save(
   1384             &self,
   1385             _state: &RadrootsNostrSignerStoreState,
   1386         ) -> Result<(), RadrootsNostrSignerError> {
   1387             Ok(())
   1388         }
   1389     }
   1390 
   1391     struct SaveErrorStore {
   1392         state: RwLock<RadrootsNostrSignerStoreState>,
   1393     }
   1394 
   1395     impl SaveErrorStore {
   1396         fn new(state: RadrootsNostrSignerStoreState) -> Self {
   1397             Self {
   1398                 state: RwLock::new(state),
   1399             }
   1400         }
   1401     }
   1402 
   1403     impl RadrootsNostrSignerStore for SaveErrorStore {
   1404         fn load(&self) -> Result<RadrootsNostrSignerStoreState, RadrootsNostrSignerError> {
   1405             self.state
   1406                 .read()
   1407                 .map(|guard| guard.clone())
   1408                 .map_err(|_| RadrootsNostrSignerError::Store("save error store poisoned".into()))
   1409         }
   1410 
   1411         fn save(
   1412             &self,
   1413             _state: &RadrootsNostrSignerStoreState,
   1414         ) -> Result<(), RadrootsNostrSignerError> {
   1415             Err(RadrootsNostrSignerError::Store("store save failed".into()))
   1416         }
   1417     }
   1418 
   1419     #[test]
   1420     fn manager_new_in_memory_and_invalid_schema_paths() {
   1421         let manager = RadrootsNostrSignerManager::new_in_memory();
   1422         assert!(
   1423             manager
   1424                 .signer_identity()
   1425                 .expect("signer identity")
   1426                 .is_none()
   1427         );
   1428 
   1429         let load_error_store = Arc::new(LoadErrorStore);
   1430         load_error_store
   1431             .save(&RadrootsNostrSignerStoreState::default())
   1432             .expect("load error store save");
   1433         let load_result = RadrootsNostrSignerManager::new(load_error_store);
   1434         assert!(load_result.is_err());
   1435         let err = load_result.err().expect("load error");
   1436         assert!(err.to_string().contains("store load failed"));
   1437 
   1438         let store = Arc::new(RadrootsNostrMemorySignerStore::new());
   1439         let mut state = RadrootsNostrSignerStoreState::default();
   1440         state.version = 2;
   1441         store.save(&state).expect("save");
   1442         let version_result = RadrootsNostrSignerManager::new(store);
   1443         assert!(version_result.is_err());
   1444         let err = version_result.err().expect("invalid version");
   1445         assert!(
   1446             err.to_string()
   1447                 .contains("unsupported signer schema version")
   1448         );
   1449     }
   1450 
   1451     #[test]
   1452     fn set_signer_identity_validates_and_persists() {
   1453         let manager = RadrootsNostrSignerManager::new_in_memory();
   1454         let signer_identity = fixture_alice_identity();
   1455         manager
   1456             .set_signer_identity(signer_identity.clone())
   1457             .expect("set signer");
   1458 
   1459         let loaded = manager
   1460             .signer_identity()
   1461             .expect("identity")
   1462             .expect("loaded");
   1463         assert_same_public_identity(&loaded, &signer_identity);
   1464 
   1465         let err = manager
   1466             .set_signer_identity(invalid_public_identity(0x2))
   1467             .expect_err("invalid identity");
   1468         assert!(
   1469             err.to_string()
   1470                 .contains("public identity id does not match public key")
   1471         );
   1472     }
   1473 
   1474     #[test]
   1475     fn register_connection_requires_signer_identity_and_normalizes_inputs() {
   1476         let manager = RadrootsNostrSignerManager::new_in_memory();
   1477         let err = manager
   1478             .register_connection(RadrootsNostrSignerConnectionDraft::new(
   1479                 public_key(0x3),
   1480                 public_identity(0x4),
   1481             ))
   1482             .expect_err("missing signer");
   1483         assert!(err.to_string().contains("missing signer identity"));
   1484 
   1485         manager
   1486             .set_signer_identity(public_identity(0x5))
   1487             .expect("set signer");
   1488 
   1489         let sign_event = permission(RadrootsNostrConnectMethod::SignEvent, Some("kind:1"));
   1490         let ping = permission(RadrootsNostrConnectMethod::Ping, None);
   1491         let record = manager
   1492             .register_connection(
   1493                 RadrootsNostrSignerConnectionDraft::new(public_key(0x6), public_identity(0x7))
   1494                     .with_connect_secret(" secret ")
   1495                     .with_requested_permissions(
   1496                         vec![sign_event.clone(), ping.clone(), sign_event.clone()].into(),
   1497                     )
   1498                     .with_relays(vec![primary_relay(), secondary_relay(), secondary_relay()]),
   1499             )
   1500             .expect("register");
   1501 
   1502         assert!(
   1503             record
   1504                 .connect_secret_hash
   1505                 .as_ref()
   1506                 .expect("connect secret hash")
   1507                 .matches_secret("secret")
   1508         );
   1509         assert_eq!(record.status, RadrootsNostrSignerConnectionStatus::Active);
   1510         assert_eq!(
   1511             record.approval_state,
   1512             RadrootsNostrSignerApprovalState::NotRequired
   1513         );
   1514         assert_eq!(record.auth_state, RadrootsNostrSignerAuthState::NotRequired);
   1515         assert_eq!(record.requested_permissions.as_slice(), &[sign_event, ping]);
   1516         assert_eq!(record.relays, vec![secondary_relay(), primary_relay()]);
   1517     }
   1518 
   1519     #[test]
   1520     fn register_connection_enforces_identity_and_uniqueness_rules() {
   1521         let manager = RadrootsNostrSignerManager::new_in_memory();
   1522         manager
   1523             .set_signer_identity(public_identity(0x8))
   1524             .expect("set signer");
   1525 
   1526         let user_identity = public_identity(0x9);
   1527         let client_public_key = public_key(0x10);
   1528         let pending = manager
   1529             .register_connection(
   1530                 RadrootsNostrSignerConnectionDraft::new(client_public_key, user_identity.clone())
   1531                     .with_connect_secret("shared-secret")
   1532                     .with_approval_requirement(
   1533                         RadrootsNostrSignerApprovalRequirement::ExplicitUser,
   1534                     ),
   1535             )
   1536             .expect("register");
   1537         assert_eq!(pending.status, RadrootsNostrSignerConnectionStatus::Pending);
   1538 
   1539         let duplicate_connection = manager
   1540             .register_connection(
   1541                 RadrootsNostrSignerConnectionDraft::new(client_public_key, user_identity)
   1542                     .with_connect_secret("other-secret"),
   1543             )
   1544             .expect_err("duplicate connection");
   1545         assert!(
   1546             duplicate_connection
   1547                 .to_string()
   1548                 .contains("connection already exists")
   1549         );
   1550 
   1551         let duplicate_secret = manager
   1552             .register_connection(
   1553                 RadrootsNostrSignerConnectionDraft::new(public_key(0x11), public_identity(0x12))
   1554                     .with_connect_secret("shared-secret"),
   1555             )
   1556             .expect_err("duplicate secret");
   1557         assert!(
   1558             duplicate_secret
   1559                 .to_string()
   1560                 .contains("connect secret already in use")
   1561         );
   1562 
   1563         let invalid_user = manager
   1564             .register_connection(RadrootsNostrSignerConnectionDraft::new(
   1565                 public_key(0x13),
   1566                 invalid_public_identity(0x14),
   1567             ))
   1568             .expect_err("invalid user identity");
   1569         assert!(
   1570             invalid_user
   1571                 .to_string()
   1572                 .contains("public identity id does not match public key")
   1573         );
   1574     }
   1575 
   1576     #[test]
   1577     fn manager_query_helpers_find_connections() {
   1578         let manager = RadrootsNostrSignerManager::new_in_memory();
   1579         manager
   1580             .set_signer_identity(public_identity(0x15))
   1581             .expect("set signer");
   1582 
   1583         let client_public_key = public_key(0x16);
   1584         let record = manager
   1585             .register_connection(
   1586                 RadrootsNostrSignerConnectionDraft::new(client_public_key, public_identity(0x17))
   1587                     .with_connect_secret("lookup-secret"),
   1588             )
   1589             .expect("register");
   1590 
   1591         let by_id = manager
   1592             .get_connection(&record.connection_id)
   1593             .expect("get connection");
   1594         let by_client = manager
   1595             .find_connections_by_client_public_key(&client_public_key)
   1596             .expect("find by client");
   1597         let by_secret = manager
   1598             .find_connection_by_connect_secret(" lookup-secret ")
   1599             .expect("find by secret");
   1600         let empty_secret = manager
   1601             .find_connection_by_connect_secret("   ")
   1602             .expect("empty secret");
   1603         let all_connections = manager.list_connections().expect("list connections");
   1604 
   1605         assert_same_connection(&by_id.expect("by id"), &record);
   1606         assert_eq!(by_client.len(), 1);
   1607         assert_same_connection(&by_client[0], &record);
   1608         assert_same_connection(&by_secret.expect("by secret"), &record);
   1609         assert!(empty_secret.is_none());
   1610         assert_eq!(all_connections.len(), 1);
   1611         assert_same_connection(&all_connections[0], &record);
   1612     }
   1613 
   1614     #[test]
   1615     fn granted_permissions_and_approval_enforce_subset_rules() {
   1616         let manager = RadrootsNostrSignerManager::new_in_memory();
   1617         manager
   1618             .set_signer_identity(public_identity(0x18))
   1619             .expect("set signer");
   1620         let requested = vec![
   1621             permission(RadrootsNostrConnectMethod::SignEvent, Some("kind:1")),
   1622             permission(RadrootsNostrConnectMethod::Ping, None),
   1623         ];
   1624         let granted = vec![requested[1].clone()];
   1625         let invalid = vec![permission(
   1626             RadrootsNostrConnectMethod::Nip44Encrypt,
   1627             Some("kind:1"),
   1628         )];
   1629         let pending = manager
   1630             .register_connection(
   1631                 RadrootsNostrSignerConnectionDraft::new(public_key(0x19), public_identity(0x20))
   1632                     .with_requested_permissions(requested.clone().into())
   1633                     .with_approval_requirement(
   1634                         RadrootsNostrSignerApprovalRequirement::ExplicitUser,
   1635                     ),
   1636             )
   1637             .expect("register");
   1638 
   1639         let invalid_set = manager
   1640             .set_granted_permissions(&pending.connection_id, invalid.clone().into())
   1641             .expect_err("invalid set grants");
   1642         assert!(
   1643             invalid_set
   1644                 .to_string()
   1645                 .contains("invalid granted permission")
   1646         );
   1647 
   1648         let set_grants = manager
   1649             .set_granted_permissions(&pending.connection_id, granted.clone().into())
   1650             .expect("set grants");
   1651         assert_eq!(
   1652             set_grants.granted_permissions().as_slice(),
   1653             granted.as_slice()
   1654         );
   1655         assert_eq!(
   1656             set_grants.status,
   1657             RadrootsNostrSignerConnectionStatus::Pending
   1658         );
   1659 
   1660         let approved = manager
   1661             .approve_connection(&pending.connection_id, granted.clone().into())
   1662             .expect("approve");
   1663         assert_eq!(approved.status, RadrootsNostrSignerConnectionStatus::Active);
   1664         assert_eq!(
   1665             approved.approval_state,
   1666             RadrootsNostrSignerApprovalState::Approved
   1667         );
   1668         assert_eq!(
   1669             approved.granted_permissions().as_slice(),
   1670             granted.as_slice()
   1671         );
   1672 
   1673         let reapprove = manager
   1674             .approve_connection(&pending.connection_id, granted.into())
   1675             .expect("reapprove active");
   1676         assert_eq!(
   1677             reapprove.status,
   1678             RadrootsNostrSignerConnectionStatus::Active
   1679         );
   1680 
   1681         let auto = manager
   1682             .register_connection(RadrootsNostrSignerConnectionDraft::new(
   1683                 public_key(0x21),
   1684                 public_identity(0x22),
   1685             ))
   1686             .expect("register auto");
   1687         let err = manager
   1688             .approve_connection(
   1689                 &auto.connection_id,
   1690                 RadrootsNostrConnectPermissions::default(),
   1691             )
   1692             .expect_err("approval not required");
   1693         assert!(err.to_string().contains("approval not required"));
   1694 
   1695         let terminal_pending = manager
   1696             .register_connection(
   1697                 RadrootsNostrSignerConnectionDraft::new(public_key(0x40), public_identity(0x41))
   1698                     .with_connect_secret("terminal-secret")
   1699                     .with_approval_requirement(
   1700                         RadrootsNostrSignerApprovalRequirement::ExplicitUser,
   1701                     ),
   1702             )
   1703             .expect("register terminal");
   1704         manager
   1705             .reject_connection(&terminal_pending.connection_id, Some("terminal".into()))
   1706             .expect("reject terminal");
   1707         let terminal_approve = manager
   1708             .approve_connection(
   1709                 &terminal_pending.connection_id,
   1710                 vec![requested[0].clone()].into(),
   1711             )
   1712             .expect_err("approve rejected");
   1713         assert!(
   1714             terminal_approve
   1715                 .to_string()
   1716                 .contains("cannot approve rejected connection")
   1717         );
   1718 
   1719         let unrestricted = manager
   1720             .register_connection(RadrootsNostrSignerConnectionDraft::new(
   1721                 public_key(0x23),
   1722                 public_identity(0x24),
   1723             ))
   1724             .expect("register unrestricted");
   1725         let unrestricted_grants = manager
   1726             .set_granted_permissions(&unrestricted.connection_id, invalid.into())
   1727             .expect("unrestricted grants");
   1728         assert_eq!(unrestricted_grants.granted_permissions.len(), 1);
   1729     }
   1730 
   1731     #[test]
   1732     fn reject_revoke_and_relay_updates_cover_terminal_paths() {
   1733         let manager = RadrootsNostrSignerManager::new_in_memory();
   1734         manager
   1735             .set_signer_identity(public_identity(0x25))
   1736             .expect("set signer");
   1737         let rejected = manager
   1738             .register_connection(
   1739                 RadrootsNostrSignerConnectionDraft::new(public_key(0x26), public_identity(0x27))
   1740                     .with_connect_secret("shared-secret")
   1741                     .with_approval_requirement(
   1742                         RadrootsNostrSignerApprovalRequirement::ExplicitUser,
   1743                     ),
   1744             )
   1745             .expect("register reject");
   1746         let rejected = manager
   1747             .reject_connection(&rejected.connection_id, Some("denied".into()))
   1748             .expect("reject");
   1749         assert_eq!(
   1750             rejected.status,
   1751             RadrootsNostrSignerConnectionStatus::Rejected
   1752         );
   1753         assert_eq!(rejected.status_reason.as_deref(), Some("denied"));
   1754 
   1755         let reject_err = manager
   1756             .reject_connection(&rejected.connection_id, None)
   1757             .expect_err("reject terminal");
   1758         assert!(
   1759             reject_err
   1760                 .to_string()
   1761                 .contains("cannot reject rejected connection")
   1762         );
   1763 
   1764         let relay_err = manager
   1765             .update_relays(&rejected.connection_id, vec![primary_relay()])
   1766             .expect_err("update rejected");
   1767         assert!(
   1768             relay_err
   1769                 .to_string()
   1770                 .contains("cannot update relays for rejected connection")
   1771         );
   1772         let rejected_lookup = manager
   1773             .find_connection_by_connect_secret("shared-secret")
   1774             .expect("lookup rejected secret");
   1775         assert!(rejected_lookup.is_none());
   1776 
   1777         let active = manager
   1778             .register_connection(RadrootsNostrSignerConnectionDraft::new(
   1779                 public_key(0x28),
   1780                 public_identity(0x29),
   1781             ))
   1782             .expect("register active");
   1783         let active = manager
   1784             .update_relays(
   1785                 &active.connection_id,
   1786                 vec![tertiary_relay(), secondary_relay(), secondary_relay()],
   1787             )
   1788             .expect("update relays");
   1789         assert_eq!(active.relays, vec![secondary_relay(), tertiary_relay()]);
   1790 
   1791         let revoked = manager
   1792             .revoke_connection(&active.connection_id, Some("manual".into()))
   1793             .expect("revoke");
   1794         assert_eq!(revoked.status, RadrootsNostrSignerConnectionStatus::Revoked);
   1795         assert_eq!(revoked.status_reason.as_deref(), Some("manual"));
   1796 
   1797         let revoke_again = manager
   1798             .revoke_connection(&active.connection_id, None)
   1799             .expect_err("revoke twice");
   1800         assert!(
   1801             revoke_again
   1802                 .to_string()
   1803                 .contains("connection already revoked")
   1804         );
   1805 
   1806         let grants_err = manager
   1807             .set_granted_permissions(
   1808                 &active.connection_id,
   1809                 vec![permission(RadrootsNostrConnectMethod::Ping, None)].into(),
   1810             )
   1811             .expect_err("update grants revoked");
   1812         assert!(
   1813             grants_err
   1814                 .to_string()
   1815                 .contains("cannot update granted permissions for revoked connection")
   1816         );
   1817 
   1818         let require_auth_err = manager
   1819             .require_auth_challenge(&active.connection_id, api_primary_https())
   1820             .expect_err("require auth revoked");
   1821         assert!(
   1822             require_auth_err
   1823                 .to_string()
   1824                 .contains("cannot require auth for revoked connection")
   1825         );
   1826 
   1827         let pending_request_err = manager
   1828             .set_pending_request(&active.connection_id, request_message("req-terminal"))
   1829             .expect_err("pending request revoked");
   1830         assert!(
   1831             pending_request_err
   1832                 .to_string()
   1833                 .contains("cannot set pending request for revoked connection")
   1834         );
   1835 
   1836         let authorize_auth_err = manager
   1837             .authorize_auth_challenge(&active.connection_id)
   1838             .expect_err("authorize auth revoked");
   1839         assert!(
   1840             authorize_auth_err
   1841                 .to_string()
   1842                 .contains("cannot authorize auth challenge for revoked connection")
   1843         );
   1844     }
   1845 
   1846     #[test]
   1847     fn authentication_and_request_audit_paths_are_recorded() {
   1848         let manager = RadrootsNostrSignerManager::new_in_memory();
   1849         manager
   1850             .set_signer_identity(public_identity(0x30))
   1851             .expect("set signer");
   1852         let record = manager
   1853             .register_connection(RadrootsNostrSignerConnectionDraft::new(
   1854                 public_key(0x31),
   1855                 public_identity(0x32),
   1856             ))
   1857             .expect("register");
   1858 
   1859         let authenticated = manager
   1860             .mark_authenticated(&record.connection_id)
   1861             .expect("auth");
   1862         assert!(authenticated.last_authenticated_at_unix.is_some());
   1863 
   1864         let consumed = manager
   1865             .mark_connect_secret_consumed(&record.connection_id)
   1866             .expect_err("consume missing secret");
   1867         assert!(
   1868             consumed
   1869                 .to_string()
   1870                 .contains("connection does not have a connect secret")
   1871         );
   1872 
   1873         let audit = manager
   1874             .record_request(
   1875                 &record.connection_id,
   1876                 " request-1 ",
   1877                 RadrootsNostrConnectMethod::Ping,
   1878                 RadrootsNostrSignerRequestDecision::Challenged,
   1879                 Some(" challenge ".into()),
   1880             )
   1881             .expect("record request");
   1882         assert_eq!(audit.request_id.as_str(), "request-1");
   1883         assert_eq!(audit.message.as_deref(), Some("challenge"));
   1884 
   1885         let blank_message_audit = manager
   1886             .record_request(
   1887                 &record.connection_id,
   1888                 "request-2",
   1889                 RadrootsNostrConnectMethod::Ping,
   1890                 RadrootsNostrSignerRequestDecision::Denied,
   1891                 Some("   ".into()),
   1892             )
   1893             .expect("record blank message");
   1894         assert!(blank_message_audit.message.is_none());
   1895 
   1896         let all_audits = manager.list_audit_records().expect("list audits");
   1897         let connection_audits = manager
   1898             .audit_records_for_connection(&record.connection_id)
   1899             .expect("connection audits");
   1900         let stored = manager
   1901             .get_connection(&record.connection_id)
   1902             .expect("get")
   1903             .expect("stored");
   1904         assert_eq!(all_audits, vec![audit.clone(), blank_message_audit.clone()]);
   1905         assert_eq!(connection_audits, vec![audit, blank_message_audit]);
   1906         assert!(stored.last_request_at_unix.is_some());
   1907 
   1908         let request_err = manager
   1909             .record_request(
   1910                 &record.connection_id,
   1911                 "   ",
   1912                 RadrootsNostrConnectMethod::Ping,
   1913                 RadrootsNostrSignerRequestDecision::Denied,
   1914                 None,
   1915             )
   1916             .expect_err("invalid request id");
   1917         assert!(request_err.to_string().contains("invalid request id"));
   1918     }
   1919 
   1920     #[test]
   1921     fn auth_challenge_and_pending_request_state_are_persisted_and_replayed() {
   1922         let manager = RadrootsNostrSignerManager::new_in_memory();
   1923         manager
   1924             .set_signer_identity(public_identity(0x34))
   1925             .expect("set signer");
   1926         let record = manager
   1927             .register_connection(RadrootsNostrSignerConnectionDraft::new(
   1928                 public_key(0x35),
   1929                 public_identity(0x36),
   1930             ))
   1931             .expect("register");
   1932 
   1933         let required = manager
   1934             .require_auth_challenge(
   1935                 &record.connection_id,
   1936                 format!(" {}/flow ", api_primary_https()).as_str(),
   1937             )
   1938             .expect("require auth");
   1939         assert_eq!(required.auth_state, RadrootsNostrSignerAuthState::Pending);
   1940         assert_eq!(
   1941             required
   1942                 .auth_challenge
   1943                 .as_ref()
   1944                 .expect("auth challenge")
   1945                 .auth_url,
   1946             format!("{}/flow", api_primary_https())
   1947         );
   1948         assert!(required.pending_request.is_none());
   1949 
   1950         let pending = manager
   1951             .set_pending_request(&record.connection_id, request_message(" req-auth "))
   1952             .expect("set pending request");
   1953         assert_eq!(
   1954             pending
   1955                 .pending_request
   1956                 .as_ref()
   1957                 .expect("pending request")
   1958                 .request_id()
   1959                 .as_str(),
   1960             "req-auth"
   1961         );
   1962 
   1963         let authorized = manager
   1964             .authorize_auth_challenge(&record.connection_id)
   1965             .expect("authorize");
   1966         assert_eq!(
   1967             authorized.connection.auth_state,
   1968             RadrootsNostrSignerAuthState::Authorized
   1969         );
   1970         assert!(authorized.connection.last_authenticated_at_unix.is_some());
   1971         assert!(authorized.connection.pending_request.is_none());
   1972         assert_eq!(
   1973             authorized
   1974                 .pending_request
   1975                 .as_ref()
   1976                 .expect("replayed request")
   1977                 .request_message()
   1978                 .id,
   1979             "req-auth"
   1980         );
   1981         assert_eq!(
   1982             authorized
   1983                 .connection
   1984                 .auth_challenge
   1985                 .as_ref()
   1986                 .expect("authorized challenge")
   1987                 .authorized_at_unix,
   1988             authorized.connection.last_authenticated_at_unix
   1989         );
   1990 
   1991         let invalid_url = manager
   1992             .require_auth_challenge(&record.connection_id, "not-a-url")
   1993             .expect_err("invalid auth url");
   1994         assert!(invalid_url.to_string().contains("invalid auth url"));
   1995 
   1996         let no_pending_auth = manager
   1997             .set_pending_request(&record.connection_id, request_message("req-again"))
   1998             .expect_err("pending request without auth challenge");
   1999         assert!(
   2000             no_pending_auth
   2001                 .to_string()
   2002                 .contains("auth challenge not pending for connection")
   2003         );
   2004 
   2005         let no_authorize = manager
   2006             .authorize_auth_challenge(&record.connection_id)
   2007             .expect_err("authorize without pending auth challenge");
   2008         assert!(
   2009             no_authorize
   2010                 .to_string()
   2011                 .contains("auth challenge not pending for connection")
   2012         );
   2013     }
   2014 
   2015     #[test]
   2016     fn restored_authorized_auth_challenge_requeues_pending_request() {
   2017         let manager = RadrootsNostrSignerManager::new_in_memory();
   2018         manager
   2019             .set_signer_identity(public_identity(0x134))
   2020             .expect("set signer");
   2021         let record = manager
   2022             .register_connection(RadrootsNostrSignerConnectionDraft::new(
   2023                 public_key(0x135),
   2024                 public_identity(0x136),
   2025             ))
   2026             .expect("register");
   2027 
   2028         manager
   2029             .require_auth_challenge(
   2030                 &record.connection_id,
   2031                 format!("{}/flow", api_primary_https()).as_str(),
   2032             )
   2033             .expect("require auth");
   2034         manager
   2035             .set_pending_request(&record.connection_id, request_message("req-replay"))
   2036             .expect("set pending");
   2037 
   2038         let authorized = manager
   2039             .authorize_auth_challenge(&record.connection_id)
   2040             .expect("authorize");
   2041         let pending_request = authorized.pending_request.expect("pending request");
   2042 
   2043         let restored = manager
   2044             .restore_pending_auth_challenge(&record.connection_id, pending_request.clone())
   2045             .expect("restore pending challenge");
   2046         assert_eq!(restored.auth_state, RadrootsNostrSignerAuthState::Pending);
   2047         assert_eq!(
   2048             restored
   2049                 .auth_challenge
   2050                 .as_ref()
   2051                 .expect("challenge")
   2052                 .authorized_at_unix,
   2053             None
   2054         );
   2055         assert!(restored.last_authenticated_at_unix.is_none());
   2056         assert_eq!(
   2057             restored
   2058                 .pending_request
   2059                 .as_ref()
   2060                 .expect("pending request")
   2061                 .request_id()
   2062                 .as_str(),
   2063             pending_request.request_id().as_str()
   2064         );
   2065     }
   2066 
   2067     #[test]
   2068     fn connect_secret_consumption_persists_and_remains_idempotent() {
   2069         let manager = RadrootsNostrSignerManager::new_in_memory();
   2070         manager
   2071             .set_signer_identity(public_identity(0x37))
   2072             .expect("set signer");
   2073         let record = manager
   2074             .register_connection(
   2075                 RadrootsNostrSignerConnectionDraft::new(public_key(0x38), public_identity(0x39))
   2076                     .with_connect_secret("one-shot-secret"),
   2077             )
   2078             .expect("register");
   2079 
   2080         let consumed = manager
   2081             .mark_connect_secret_consumed(&record.connection_id)
   2082             .expect("consume secret");
   2083         assert!(consumed.connect_secret_is_consumed());
   2084         assert!(consumed.connect_secret_consumed_at_unix.is_some());
   2085 
   2086         let consumed_again = manager
   2087             .mark_connect_secret_consumed(&record.connection_id)
   2088             .expect("consume secret again");
   2089         assert_eq!(
   2090             consumed_again.connect_secret_consumed_at_unix,
   2091             consumed.connect_secret_consumed_at_unix
   2092         );
   2093 
   2094         let found = manager
   2095             .find_connection_by_connect_secret("one-shot-secret")
   2096             .expect("find consumed secret")
   2097             .expect("stored secret");
   2098         assert!(found.connect_secret_is_consumed());
   2099         assert_eq!(
   2100             found.connect_secret_consumed_at_unix,
   2101             consumed.connect_secret_consumed_at_unix
   2102         );
   2103     }
   2104 
   2105     #[test]
   2106     fn connect_secret_publish_workflow_is_persisted_and_finalized() {
   2107         let manager = RadrootsNostrSignerManager::new_in_memory();
   2108         manager
   2109             .set_signer_identity(public_identity(0x237))
   2110             .expect("set signer");
   2111         let record = manager
   2112             .register_connection(
   2113                 RadrootsNostrSignerConnectionDraft::new(public_key(0x238), public_identity(0x239))
   2114                     .with_connect_secret("workflow-secret"),
   2115             )
   2116             .expect("register");
   2117 
   2118         let workflow = manager
   2119             .begin_connect_secret_publish_finalization(&record.connection_id)
   2120             .expect("begin workflow");
   2121         assert_eq!(
   2122             workflow.kind,
   2123             RadrootsNostrSignerPublishWorkflowKind::ConnectSecretFinalization
   2124         );
   2125         assert_eq!(
   2126             workflow.state,
   2127             RadrootsNostrSignerPublishWorkflowState::PendingPublish
   2128         );
   2129         assert!(workflow.pending_request.is_none());
   2130         assert!(
   2131             !manager
   2132                 .get_connection(&record.connection_id)
   2133                 .expect("get")
   2134                 .expect("stored")
   2135                 .connect_secret_is_consumed()
   2136         );
   2137         assert_eq!(
   2138             manager.list_publish_workflows().expect("list workflows"),
   2139             vec![workflow.clone()]
   2140         );
   2141 
   2142         let published = manager
   2143             .mark_publish_workflow_published(&workflow.workflow_id)
   2144             .expect("mark published");
   2145         assert_eq!(
   2146             published.state,
   2147             RadrootsNostrSignerPublishWorkflowState::PublishedPendingFinalize
   2148         );
   2149 
   2150         let finalized = manager
   2151             .finalize_publish_workflow(&workflow.workflow_id)
   2152             .expect("finalize workflow");
   2153         assert!(finalized.connect_secret_is_consumed());
   2154         assert!(
   2155             manager
   2156                 .list_publish_workflows()
   2157                 .expect("list workflows")
   2158                 .is_empty()
   2159         );
   2160         assert!(
   2161             manager
   2162                 .find_connection_by_connect_secret("workflow-secret")
   2163                 .expect("find secret")
   2164                 .expect("stored")
   2165                 .connect_secret_is_consumed()
   2166         );
   2167     }
   2168 
   2169     #[test]
   2170     fn auth_replay_publish_workflow_is_persisted_and_finalized() {
   2171         let manager = RadrootsNostrSignerManager::new_in_memory();
   2172         manager
   2173             .set_signer_identity(public_identity(0x23a))
   2174             .expect("set signer");
   2175         let record = manager
   2176             .register_connection(RadrootsNostrSignerConnectionDraft::new(
   2177                 public_key(0x23b),
   2178                 public_identity(0x23c),
   2179             ))
   2180             .expect("register");
   2181 
   2182         manager
   2183             .require_auth_challenge(
   2184                 &record.connection_id,
   2185                 format!("{}/flow", api_primary_https()).as_str(),
   2186             )
   2187             .expect("require auth");
   2188         let pending = manager
   2189             .set_pending_request(&record.connection_id, request_message("req-auth-workflow"))
   2190             .expect("set pending");
   2191         let pending_request = pending.pending_request.expect("pending request");
   2192 
   2193         let workflow = manager
   2194             .begin_auth_replay_publish_finalization(&record.connection_id)
   2195             .expect("begin auth replay workflow");
   2196         assert_eq!(
   2197             workflow.kind,
   2198             RadrootsNostrSignerPublishWorkflowKind::AuthReplayFinalization
   2199         );
   2200         assert_eq!(workflow.pending_request.as_ref(), Some(&pending_request));
   2201         assert!(workflow.authorized_at_unix.is_some());
   2202 
   2203         let stored_before_publish = manager
   2204             .get_connection(&record.connection_id)
   2205             .expect("get")
   2206             .expect("stored");
   2207         assert_eq!(
   2208             stored_before_publish.auth_state,
   2209             RadrootsNostrSignerAuthState::Pending
   2210         );
   2211         assert_eq!(
   2212             stored_before_publish.pending_request.as_ref(),
   2213             Some(&pending_request)
   2214         );
   2215 
   2216         manager
   2217             .mark_publish_workflow_published(&workflow.workflow_id)
   2218             .expect("mark published");
   2219         let finalized = manager
   2220             .finalize_publish_workflow(&workflow.workflow_id)
   2221             .expect("finalize auth replay");
   2222         assert_eq!(
   2223             finalized.auth_state,
   2224             RadrootsNostrSignerAuthState::Authorized
   2225         );
   2226         assert!(finalized.pending_request.is_none());
   2227         assert_eq!(
   2228             finalized
   2229                 .auth_challenge
   2230                 .as_ref()
   2231                 .expect("challenge")
   2232                 .authorized_at_unix,
   2233             workflow.authorized_at_unix
   2234         );
   2235         assert_eq!(
   2236             finalized.last_authenticated_at_unix,
   2237             workflow.authorized_at_unix
   2238         );
   2239         assert!(
   2240             manager
   2241                 .list_publish_workflows()
   2242                 .expect("list workflows")
   2243                 .is_empty()
   2244         );
   2245     }
   2246 
   2247     #[test]
   2248     fn canceling_auth_replay_publish_workflow_preserves_pending_request() {
   2249         let manager = RadrootsNostrSignerManager::new_in_memory();
   2250         manager
   2251             .set_signer_identity(public_identity(0x23d))
   2252             .expect("set signer");
   2253         let record = manager
   2254             .register_connection(RadrootsNostrSignerConnectionDraft::new(
   2255                 public_key(0x23e),
   2256                 public_identity(0x23f),
   2257             ))
   2258             .expect("register");
   2259 
   2260         manager
   2261             .require_auth_challenge(
   2262                 &record.connection_id,
   2263                 format!("{}/flow", api_primary_https()).as_str(),
   2264             )
   2265             .expect("require auth");
   2266         let pending = manager
   2267             .set_pending_request(&record.connection_id, request_message("req-auth-cancel"))
   2268             .expect("set pending");
   2269         let pending_request = pending.pending_request.expect("pending request");
   2270 
   2271         let workflow = manager
   2272             .begin_auth_replay_publish_finalization(&record.connection_id)
   2273             .expect("begin auth replay workflow");
   2274         let canceled = manager
   2275             .cancel_publish_workflow(&workflow.workflow_id)
   2276             .expect("cancel workflow");
   2277         assert_eq!(canceled.workflow_id, workflow.workflow_id);
   2278 
   2279         let stored = manager
   2280             .get_connection(&record.connection_id)
   2281             .expect("get")
   2282             .expect("stored");
   2283         assert_eq!(stored.auth_state, RadrootsNostrSignerAuthState::Pending);
   2284         assert_eq!(stored.pending_request.as_ref(), Some(&pending_request));
   2285         assert!(
   2286             manager
   2287                 .list_publish_workflows()
   2288                 .expect("list workflows")
   2289                 .is_empty()
   2290         );
   2291     }
   2292 
   2293     #[test]
   2294     fn evaluate_auth_replay_publish_workflow_uses_authorized_view_without_mutating_state() {
   2295         let manager = RadrootsNostrSignerManager::new_in_memory();
   2296         manager
   2297             .set_signer_identity(public_identity(0x240))
   2298             .expect("set signer");
   2299         let record = manager
   2300             .register_connection(RadrootsNostrSignerConnectionDraft::new(
   2301                 public_key(0x241),
   2302                 public_identity(0x242),
   2303             ))
   2304             .expect("register");
   2305 
   2306         manager
   2307             .set_granted_permissions(
   2308                 &record.connection_id,
   2309                 vec!["get_public_key".parse().expect("permission")].into(),
   2310             )
   2311             .expect("grant permissions");
   2312         manager
   2313             .require_auth_challenge(
   2314                 &record.connection_id,
   2315                 format!("{}/flow", api_primary_https()).as_str(),
   2316             )
   2317             .expect("require auth");
   2318         let pending = manager
   2319             .set_pending_request(
   2320                 &record.connection_id,
   2321                 RadrootsNostrConnectRequestMessage::new(
   2322                     "req-auth-preview",
   2323                     RadrootsNostrConnectRequest::GetPublicKey,
   2324                 ),
   2325             )
   2326             .expect("set pending");
   2327         let pending_request = pending.pending_request.expect("pending request");
   2328 
   2329         let workflow = manager
   2330             .begin_auth_replay_publish_finalization(&record.connection_id)
   2331             .expect("begin auth replay workflow");
   2332         let evaluation = manager
   2333             .evaluate_auth_replay_publish_workflow(&workflow.workflow_id)
   2334             .expect("evaluate auth replay workflow");
   2335 
   2336         assert_eq!(
   2337             evaluation.request_id.as_str(),
   2338             pending_request.request_id().as_str()
   2339         );
   2340         assert_eq!(
   2341             evaluation.connection.auth_state,
   2342             RadrootsNostrSignerAuthState::Authorized
   2343         );
   2344         assert!(evaluation.connection.pending_request.is_none());
   2345         assert!(matches!(
   2346             evaluation.action,
   2347             RadrootsNostrSignerRequestAction::Allowed { .. }
   2348         ));
   2349 
   2350         let stored = manager
   2351             .get_connection(&record.connection_id)
   2352             .expect("get")
   2353             .expect("stored");
   2354         assert_eq!(stored.auth_state, RadrootsNostrSignerAuthState::Pending);
   2355         assert_eq!(stored.pending_request.as_ref(), Some(&pending_request));
   2356     }
   2357 
   2358     #[test]
   2359     fn publish_workflow_duplicate_and_missing_paths_are_rejected() {
   2360         let manager = RadrootsNostrSignerManager::new_in_memory();
   2361         manager
   2362             .set_signer_identity(public_identity(0x240))
   2363             .expect("set signer");
   2364         let record = manager
   2365             .register_connection(
   2366                 RadrootsNostrSignerConnectionDraft::new(public_key(0x241), public_identity(0x242))
   2367                     .with_connect_secret("duplicate-secret"),
   2368             )
   2369             .expect("register");
   2370 
   2371         let workflow = manager
   2372             .begin_connect_secret_publish_finalization(&record.connection_id)
   2373             .expect("begin workflow");
   2374         let duplicate = manager
   2375             .begin_connect_secret_publish_finalization(&record.connection_id)
   2376             .expect_err("duplicate workflow");
   2377         assert!(
   2378             duplicate
   2379                 .to_string()
   2380                 .contains("publish workflow already active")
   2381         );
   2382 
   2383         let missing_workflow_id = RadrootsNostrSignerWorkflowId::parse("wf-missing").expect("id");
   2384         let missing_mark = manager
   2385             .mark_publish_workflow_published(&missing_workflow_id)
   2386             .expect_err("missing mark");
   2387         let missing_finalize = manager
   2388             .finalize_publish_workflow(&missing_workflow_id)
   2389             .expect_err("missing finalize");
   2390         let missing_cancel = manager
   2391             .cancel_publish_workflow(&missing_workflow_id)
   2392             .expect_err("missing cancel");
   2393 
   2394         for err in [missing_mark, missing_finalize, missing_cancel] {
   2395             assert!(err.to_string().contains("publish workflow not found"));
   2396         }
   2397 
   2398         let unpublished_finalize = manager
   2399             .finalize_publish_workflow(&workflow.workflow_id)
   2400             .expect_err("unpublished finalize");
   2401         assert!(
   2402             unpublished_finalize
   2403                 .to_string()
   2404                 .contains("publish workflow has not reached published state")
   2405         );
   2406     }
   2407 
   2408     #[test]
   2409     fn publish_workflow_entrypoints_reject_invalid_connection_states() {
   2410         let manager = RadrootsNostrSignerManager::new_in_memory();
   2411         manager
   2412             .set_signer_identity(public_identity(0x300))
   2413             .expect("set signer");
   2414         let missing_connection_id =
   2415             RadrootsNostrSignerConnectionId::parse("conn-missing-publish").expect("connection id");
   2416         let restore_pending_request =
   2417             RadrootsNostrSignerPendingRequest::new(request_message("req-restore-invalid"), 61)
   2418                 .expect("pending request");
   2419 
   2420         let missing_restore_err = manager
   2421             .restore_pending_auth_challenge(&missing_connection_id, restore_pending_request.clone())
   2422             .expect_err("missing restore connection");
   2423         assert!(
   2424             missing_restore_err
   2425                 .to_string()
   2426                 .contains("connection not found")
   2427         );
   2428 
   2429         let terminal_restore = manager
   2430             .register_connection(RadrootsNostrSignerConnectionDraft::new(
   2431                 public_key(0x301),
   2432                 public_identity(0x302),
   2433             ))
   2434             .expect("register terminal restore");
   2435         manager
   2436             .reject_connection(&terminal_restore.connection_id, Some("closed".into()))
   2437             .expect("reject terminal restore");
   2438         let terminal_restore_err = manager
   2439             .restore_pending_auth_challenge(
   2440                 &terminal_restore.connection_id,
   2441                 restore_pending_request.clone(),
   2442             )
   2443             .expect_err("terminal restore error");
   2444         assert!(
   2445             terminal_restore_err
   2446                 .to_string()
   2447                 .contains("cannot restore auth challenge for rejected connection")
   2448         );
   2449 
   2450         let unauthorized_restore = manager
   2451             .register_connection(RadrootsNostrSignerConnectionDraft::new(
   2452                 public_key(0x303),
   2453                 public_identity(0x304),
   2454             ))
   2455             .expect("register unauthorized restore");
   2456         let unauthorized_restore_err = manager
   2457             .restore_pending_auth_challenge(
   2458                 &unauthorized_restore.connection_id,
   2459                 restore_pending_request.clone(),
   2460             )
   2461             .expect_err("unauthorized restore error");
   2462         assert!(
   2463             unauthorized_restore_err
   2464                 .to_string()
   2465                 .contains("auth challenge not authorized for connection")
   2466         );
   2467 
   2468         let missing_challenge_restore = manager
   2469             .register_connection(RadrootsNostrSignerConnectionDraft::new(
   2470                 public_key(0x305),
   2471                 public_identity(0x306),
   2472             ))
   2473             .expect("register missing challenge restore");
   2474         manager
   2475             .require_auth_challenge(
   2476                 &missing_challenge_restore.connection_id,
   2477                 format!("{}/restore", api_primary_https()).as_str(),
   2478             )
   2479             .expect("require auth");
   2480         manager
   2481             .set_pending_request(
   2482                 &missing_challenge_restore.connection_id,
   2483                 request_message("req-restore-missing-challenge"),
   2484             )
   2485             .expect("set pending");
   2486         let replay = manager
   2487             .authorize_auth_challenge(&missing_challenge_restore.connection_id)
   2488             .expect("authorize")
   2489             .pending_request
   2490             .expect("pending request");
   2491         {
   2492             let mut state = manager.state.write().expect("write");
   2493             let record = state
   2494                 .connections
   2495                 .iter_mut()
   2496                 .find(|record| record.connection_id == missing_challenge_restore.connection_id)
   2497                 .expect("stored connection");
   2498             record.auth_challenge = None;
   2499         }
   2500         let missing_challenge_restore_err = manager
   2501             .restore_pending_auth_challenge(&missing_challenge_restore.connection_id, replay)
   2502             .expect_err("missing challenge restore error");
   2503         assert!(
   2504             missing_challenge_restore_err
   2505                 .to_string()
   2506                 .contains("auth challenge missing for connection")
   2507         );
   2508 
   2509         let terminal_connect = manager
   2510             .register_connection(
   2511                 RadrootsNostrSignerConnectionDraft::new(public_key(0x307), public_identity(0x308))
   2512                     .with_connect_secret("terminal-connect-secret"),
   2513             )
   2514             .expect("register terminal connect");
   2515         manager
   2516             .reject_connection(&terminal_connect.connection_id, Some("closed".into()))
   2517             .expect("reject terminal connect");
   2518         let terminal_connect_err = manager
   2519             .begin_connect_secret_publish_finalization(&terminal_connect.connection_id)
   2520             .expect_err("terminal connect workflow");
   2521         assert!(
   2522             terminal_connect_err
   2523                 .to_string()
   2524                 .contains("cannot begin connect secret finalization for rejected connection")
   2525         );
   2526 
   2527         let no_secret_connect = manager
   2528             .register_connection(RadrootsNostrSignerConnectionDraft::new(
   2529                 public_key(0x309),
   2530                 public_identity(0x30a),
   2531             ))
   2532             .expect("register no secret connect");
   2533         let no_secret_connect_err = manager
   2534             .begin_connect_secret_publish_finalization(&no_secret_connect.connection_id)
   2535             .expect_err("missing secret workflow");
   2536         assert!(
   2537             no_secret_connect_err
   2538                 .to_string()
   2539                 .contains("connection does not have a connect secret")
   2540         );
   2541 
   2542         let consumed_connect = manager
   2543             .register_connection(
   2544                 RadrootsNostrSignerConnectionDraft::new(public_key(0x30b), public_identity(0x30c))
   2545                     .with_connect_secret("consumed-connect-secret"),
   2546             )
   2547             .expect("register consumed connect");
   2548         manager
   2549             .mark_connect_secret_consumed(&consumed_connect.connection_id)
   2550             .expect("consume connect secret");
   2551         let consumed_connect_err = manager
   2552             .begin_connect_secret_publish_finalization(&consumed_connect.connection_id)
   2553             .expect_err("consumed secret workflow");
   2554         assert!(
   2555             consumed_connect_err
   2556                 .to_string()
   2557                 .contains("connect secret already consumed for connection")
   2558         );
   2559 
   2560         let missing_mark_consumed_err = manager
   2561             .mark_connect_secret_consumed(&missing_connection_id)
   2562             .expect_err("missing mark connect secret consumed");
   2563         assert!(
   2564             missing_mark_consumed_err
   2565                 .to_string()
   2566                 .contains("connection not found")
   2567         );
   2568 
   2569         let terminal_auth = manager
   2570             .register_connection(RadrootsNostrSignerConnectionDraft::new(
   2571                 public_key(0x30d),
   2572                 public_identity(0x30e),
   2573             ))
   2574             .expect("register terminal auth");
   2575         manager
   2576             .reject_connection(&terminal_auth.connection_id, Some("closed".into()))
   2577             .expect("reject terminal auth");
   2578         let terminal_auth_err = manager
   2579             .begin_auth_replay_publish_finalization(&terminal_auth.connection_id)
   2580             .expect_err("terminal auth workflow");
   2581         assert!(
   2582             terminal_auth_err
   2583                 .to_string()
   2584                 .contains("cannot begin auth replay finalization for rejected connection")
   2585         );
   2586 
   2587         let not_pending_auth = manager
   2588             .register_connection(RadrootsNostrSignerConnectionDraft::new(
   2589                 public_key(0x30f),
   2590                 public_identity(0x310),
   2591             ))
   2592             .expect("register not pending auth");
   2593         let not_pending_auth_err = manager
   2594             .begin_auth_replay_publish_finalization(&not_pending_auth.connection_id)
   2595             .expect_err("not pending auth workflow");
   2596         assert!(
   2597             not_pending_auth_err
   2598                 .to_string()
   2599                 .contains("auth challenge not pending for connection")
   2600         );
   2601 
   2602         let missing_challenge_auth = manager
   2603             .register_connection(RadrootsNostrSignerConnectionDraft::new(
   2604                 public_key(0x311),
   2605                 public_identity(0x312),
   2606             ))
   2607             .expect("register missing challenge auth");
   2608         manager
   2609             .require_auth_challenge(
   2610                 &missing_challenge_auth.connection_id,
   2611                 format!("{}/auth-missing-challenge", api_primary_https()).as_str(),
   2612             )
   2613             .expect("require auth");
   2614         {
   2615             let mut state = manager.state.write().expect("write");
   2616             let record = state
   2617                 .connections
   2618                 .iter_mut()
   2619                 .find(|record| record.connection_id == missing_challenge_auth.connection_id)
   2620                 .expect("stored connection");
   2621             record.auth_challenge = None;
   2622         }
   2623         let missing_challenge_auth_err = manager
   2624             .begin_auth_replay_publish_finalization(&missing_challenge_auth.connection_id)
   2625             .expect_err("missing challenge auth workflow");
   2626         assert!(
   2627             missing_challenge_auth_err
   2628                 .to_string()
   2629                 .contains("auth challenge missing for connection")
   2630         );
   2631 
   2632         let missing_pending_auth = manager
   2633             .register_connection(RadrootsNostrSignerConnectionDraft::new(
   2634                 public_key(0x313),
   2635                 public_identity(0x314),
   2636             ))
   2637             .expect("register missing pending auth");
   2638         manager
   2639             .require_auth_challenge(
   2640                 &missing_pending_auth.connection_id,
   2641                 format!("{}/auth-missing-pending", api_primary_https()).as_str(),
   2642             )
   2643             .expect("require auth");
   2644         let missing_pending_auth_err = manager
   2645             .begin_auth_replay_publish_finalization(&missing_pending_auth.connection_id)
   2646             .expect_err("missing pending auth workflow");
   2647         assert!(
   2648             missing_pending_auth_err
   2649                 .to_string()
   2650                 .contains("pending request missing for auth replay finalization")
   2651         );
   2652 
   2653         let duplicate_auth = manager
   2654             .register_connection(RadrootsNostrSignerConnectionDraft::new(
   2655                 public_key(0x315),
   2656                 public_identity(0x316),
   2657             ))
   2658             .expect("register duplicate auth");
   2659         manager
   2660             .require_auth_challenge(
   2661                 &duplicate_auth.connection_id,
   2662                 format!("{}/auth-duplicate", api_primary_https()).as_str(),
   2663             )
   2664             .expect("require auth");
   2665         manager
   2666             .set_pending_request(
   2667                 &duplicate_auth.connection_id,
   2668                 request_message("req-auth-duplicate"),
   2669             )
   2670             .expect("set pending");
   2671         manager
   2672             .begin_auth_replay_publish_finalization(&duplicate_auth.connection_id)
   2673             .expect("begin auth workflow");
   2674         let duplicate_auth_err = manager
   2675             .begin_auth_replay_publish_finalization(&duplicate_auth.connection_id)
   2676             .expect_err("duplicate auth workflow");
   2677         assert!(
   2678             duplicate_auth_err
   2679                 .to_string()
   2680                 .contains("publish workflow already active for auth_replay_finalization")
   2681         );
   2682     }
   2683 
   2684     #[test]
   2685     fn publish_workflow_finalize_and_evaluate_reject_corrupted_states() {
   2686         let manager = RadrootsNostrSignerManager::new_in_memory();
   2687         manager
   2688             .set_signer_identity(public_identity(0x320))
   2689             .expect("set signer");
   2690 
   2691         let missing_workflow_id =
   2692             RadrootsNostrSignerWorkflowId::parse("wf-evaluate-missing").expect("workflow id");
   2693         let missing_evaluate_err = manager
   2694             .evaluate_auth_replay_publish_workflow(&missing_workflow_id)
   2695             .expect_err("missing workflow evaluate");
   2696         assert!(
   2697             missing_evaluate_err
   2698                 .to_string()
   2699                 .contains("publish workflow not found")
   2700         );
   2701 
   2702         let connect_kind_record = manager
   2703             .register_connection(
   2704                 RadrootsNostrSignerConnectionDraft::new(public_key(0x321), public_identity(0x322))
   2705                     .with_connect_secret("evaluate-connect-kind"),
   2706             )
   2707             .expect("register connect kind");
   2708         let connect_kind_workflow = manager
   2709             .begin_connect_secret_publish_finalization(&connect_kind_record.connection_id)
   2710             .expect("begin connect workflow");
   2711         let wrong_kind_err = manager
   2712             .evaluate_auth_replay_publish_workflow(&connect_kind_workflow.workflow_id)
   2713             .expect_err("wrong workflow kind");
   2714         assert!(
   2715             wrong_kind_err
   2716                 .to_string()
   2717                 .contains("publish workflow is not an auth replay finalization")
   2718         );
   2719 
   2720         let connect_missing_secret_record = manager
   2721             .register_connection(
   2722                 RadrootsNostrSignerConnectionDraft::new(public_key(0x323), public_identity(0x324))
   2723                     .with_connect_secret("missing-secret-finalize"),
   2724             )
   2725             .expect("register connect missing secret");
   2726         let connect_missing_secret_workflow = manager
   2727             .begin_connect_secret_publish_finalization(&connect_missing_secret_record.connection_id)
   2728             .expect("begin connect missing secret workflow");
   2729         manager
   2730             .mark_publish_workflow_published(&connect_missing_secret_workflow.workflow_id)
   2731             .expect("mark connect missing secret workflow");
   2732         {
   2733             let mut state = manager.state.write().expect("write");
   2734             let record = state
   2735                 .connections
   2736                 .iter_mut()
   2737                 .find(|record| record.connection_id == connect_missing_secret_record.connection_id)
   2738                 .expect("stored connection");
   2739             record.connect_secret_hash = None;
   2740             record.connect_secret_consumed_at_unix = None;
   2741         }
   2742         let connect_missing_secret_err = manager
   2743             .finalize_publish_workflow(&connect_missing_secret_workflow.workflow_id)
   2744             .expect_err("missing connect secret finalize");
   2745         assert!(
   2746             connect_missing_secret_err
   2747                 .to_string()
   2748                 .contains("connection does not have a connect secret")
   2749         );
   2750 
   2751         let connect_consumed_record = manager
   2752             .register_connection(
   2753                 RadrootsNostrSignerConnectionDraft::new(public_key(0x325), public_identity(0x326))
   2754                     .with_connect_secret("consumed-secret-finalize"),
   2755             )
   2756             .expect("register connect consumed");
   2757         let connect_consumed_workflow = manager
   2758             .begin_connect_secret_publish_finalization(&connect_consumed_record.connection_id)
   2759             .expect("begin connect consumed workflow");
   2760         manager
   2761             .mark_publish_workflow_published(&connect_consumed_workflow.workflow_id)
   2762             .expect("mark connect consumed workflow");
   2763         {
   2764             let mut state = manager.state.write().expect("write");
   2765             let record = state
   2766                 .connections
   2767                 .iter_mut()
   2768                 .find(|record| record.connection_id == connect_consumed_record.connection_id)
   2769                 .expect("stored connection");
   2770             record.connect_secret_consumed_at_unix = Some(88);
   2771         }
   2772         let connect_consumed_err = manager
   2773             .finalize_publish_workflow(&connect_consumed_workflow.workflow_id)
   2774             .expect_err("consumed connect secret finalize");
   2775         assert!(
   2776             connect_consumed_err
   2777                 .to_string()
   2778                 .contains("connect secret already consumed for connection")
   2779         );
   2780 
   2781         let start_auth_replay_workflow = |suffix: u32,
   2782                                           request_id: &str|
   2783          -> (
   2784             RadrootsNostrSignerConnectionRecord,
   2785             RadrootsNostrSignerPublishWorkflowRecord,
   2786             RadrootsNostrSignerPendingRequest,
   2787         ) {
   2788             let record = manager
   2789                 .register_connection(RadrootsNostrSignerConnectionDraft::new(
   2790                     public_key(0x330 + suffix),
   2791                     public_identity(0x340 + suffix),
   2792                 ))
   2793                 .expect("register auth workflow");
   2794             manager
   2795                 .require_auth_challenge(
   2796                     &record.connection_id,
   2797                     format!("{}/auth-workflow-{suffix}", api_primary_https()).as_str(),
   2798                 )
   2799                 .expect("require auth");
   2800             let pending = manager
   2801                 .set_pending_request(&record.connection_id, request_message(request_id))
   2802                 .expect("set pending");
   2803             let pending_request = pending.pending_request.expect("pending request");
   2804             let workflow = manager
   2805                 .begin_auth_replay_publish_finalization(&record.connection_id)
   2806                 .expect("begin auth workflow");
   2807             (record, workflow, pending_request)
   2808         };
   2809 
   2810         let (missing_pending_record, missing_pending_workflow, _) =
   2811             start_auth_replay_workflow(0, "req-eval-missing-pending");
   2812         {
   2813             let mut state = manager.state.write().expect("write");
   2814             let workflow = state
   2815                 .publish_workflows
   2816                 .iter_mut()
   2817                 .find(|workflow| workflow.workflow_id == missing_pending_workflow.workflow_id)
   2818                 .expect("stored workflow");
   2819             workflow.pending_request = None;
   2820         }
   2821         let missing_pending_eval_err = manager
   2822             .evaluate_auth_replay_publish_workflow(&missing_pending_workflow.workflow_id)
   2823             .expect_err("missing pending evaluate");
   2824         assert!(
   2825             missing_pending_eval_err
   2826                 .to_string()
   2827                 .contains("auth replay workflow missing pending request")
   2828         );
   2829         {
   2830             let mut state = manager.state.write().expect("write");
   2831             state
   2832                 .publish_workflows
   2833                 .retain(|workflow| workflow.workflow_id != missing_pending_workflow.workflow_id);
   2834             state
   2835                 .connections
   2836                 .retain(|record| record.connection_id != missing_pending_record.connection_id);
   2837         }
   2838 
   2839         let (missing_challenge_eval_record, missing_challenge_eval_workflow, pending_request) =
   2840             start_auth_replay_workflow(1, "req-eval-no-challenge");
   2841         {
   2842             let mut state = manager.state.write().expect("write");
   2843             let record = state
   2844                 .connections
   2845                 .iter_mut()
   2846                 .find(|record| record.connection_id == missing_challenge_eval_record.connection_id)
   2847                 .expect("stored connection");
   2848             record.auth_challenge = None;
   2849         }
   2850         let evaluation = manager
   2851             .evaluate_auth_replay_publish_workflow(&missing_challenge_eval_workflow.workflow_id)
   2852             .expect("evaluate without challenge");
   2853         assert_eq!(
   2854             evaluation.request_id.as_str(),
   2855             pending_request.request_id().as_str()
   2856         );
   2857         assert_eq!(
   2858             evaluation.connection.auth_state,
   2859             RadrootsNostrSignerAuthState::Authorized
   2860         );
   2861         assert!(evaluation.connection.pending_request.is_none());
   2862 
   2863         let (invalid_identity_eval_record, invalid_identity_eval_workflow, _) =
   2864             start_auth_replay_workflow(10, "req-eval-invalid-identity");
   2865         {
   2866             let mut state = manager.state.write().expect("write");
   2867             let pending_request = RadrootsNostrSignerPendingRequest::new(
   2868                 request_message_with_request(
   2869                     "req-eval-invalid-identity",
   2870                     RadrootsNostrConnectRequest::GetSessionCapability,
   2871                 ),
   2872                 81,
   2873             )
   2874             .expect("pending request");
   2875             let workflow = state
   2876                 .publish_workflows
   2877                 .iter_mut()
   2878                 .find(|workflow| workflow.workflow_id == invalid_identity_eval_workflow.workflow_id)
   2879                 .expect("stored workflow");
   2880             workflow.pending_request = Some(pending_request.clone());
   2881             let record = state
   2882                 .connections
   2883                 .iter_mut()
   2884                 .find(|record| record.connection_id == invalid_identity_eval_record.connection_id)
   2885                 .expect("stored connection");
   2886             record.pending_request = Some(pending_request);
   2887             record.user_identity.public_key_hex = "invalid".into();
   2888         }
   2889         let invalid_identity_eval_err = manager
   2890             .evaluate_auth_replay_publish_workflow(&invalid_identity_eval_workflow.workflow_id)
   2891             .expect_err("invalid identity evaluate");
   2892         assert!(
   2893             invalid_identity_eval_err
   2894                 .to_string()
   2895                 .contains("user identity public key is invalid")
   2896         );
   2897 
   2898         let (terminal_eval_record, terminal_eval_workflow, _) =
   2899             start_auth_replay_workflow(2, "req-eval-terminal");
   2900         {
   2901             let mut state = manager.state.write().expect("write");
   2902             let record = state
   2903                 .connections
   2904                 .iter_mut()
   2905                 .find(|record| record.connection_id == terminal_eval_record.connection_id)
   2906                 .expect("stored connection");
   2907             record.status = RadrootsNostrSignerConnectionStatus::Rejected;
   2908         }
   2909         let terminal_eval_err = manager
   2910             .evaluate_auth_replay_publish_workflow(&terminal_eval_workflow.workflow_id)
   2911             .expect_err("terminal evaluate");
   2912         assert!(
   2913             terminal_eval_err
   2914                 .to_string()
   2915                 .contains("cannot evaluate auth replay workflow for rejected connection")
   2916         );
   2917 
   2918         let (not_pending_eval_record, not_pending_eval_workflow, _) =
   2919             start_auth_replay_workflow(3, "req-eval-not-pending");
   2920         {
   2921             let mut state = manager.state.write().expect("write");
   2922             let record = state
   2923                 .connections
   2924                 .iter_mut()
   2925                 .find(|record| record.connection_id == not_pending_eval_record.connection_id)
   2926                 .expect("stored connection");
   2927             record.auth_state = RadrootsNostrSignerAuthState::Authorized;
   2928         }
   2929         let not_pending_eval_err = manager
   2930             .evaluate_auth_replay_publish_workflow(&not_pending_eval_workflow.workflow_id)
   2931             .expect_err("not pending evaluate");
   2932         assert!(
   2933             not_pending_eval_err
   2934                 .to_string()
   2935                 .contains("auth challenge not pending for connection")
   2936         );
   2937 
   2938         let (mismatch_eval_record, mismatch_eval_workflow, _) =
   2939             start_auth_replay_workflow(4, "req-eval-mismatch");
   2940         {
   2941             let mut state = manager.state.write().expect("write");
   2942             let record = state
   2943                 .connections
   2944                 .iter_mut()
   2945                 .find(|record| record.connection_id == mismatch_eval_record.connection_id)
   2946                 .expect("stored connection");
   2947             record.pending_request = Some(
   2948                 RadrootsNostrSignerPendingRequest::new(
   2949                     request_message("req-eval-mismatch-other"),
   2950                     77,
   2951                 )
   2952                 .expect("mismatched pending request"),
   2953             );
   2954         }
   2955         let mismatch_eval_err = manager
   2956             .evaluate_auth_replay_publish_workflow(&mismatch_eval_workflow.workflow_id)
   2957             .expect_err("mismatch evaluate");
   2958         assert!(
   2959             mismatch_eval_err
   2960                 .to_string()
   2961                 .contains("pending request does not match auth replay workflow")
   2962         );
   2963 
   2964         let start_published_auth_workflow = |suffix: u32, request_id: &str| {
   2965             let (record, workflow, pending_request) =
   2966                 start_auth_replay_workflow(suffix, request_id);
   2967             let published = manager
   2968                 .mark_publish_workflow_published(&workflow.workflow_id)
   2969                 .expect("mark published");
   2970             (record, published, pending_request)
   2971         };
   2972 
   2973         let (auth_not_pending_record, auth_not_pending_workflow, _) =
   2974             start_published_auth_workflow(5, "req-finalize-not-pending");
   2975         {
   2976             let mut state = manager.state.write().expect("write");
   2977             let record = state
   2978                 .connections
   2979                 .iter_mut()
   2980                 .find(|record| record.connection_id == auth_not_pending_record.connection_id)
   2981                 .expect("stored connection");
   2982             record.auth_state = RadrootsNostrSignerAuthState::Authorized;
   2983         }
   2984         let auth_not_pending_err = manager
   2985             .finalize_publish_workflow(&auth_not_pending_workflow.workflow_id)
   2986             .expect_err("not pending finalize");
   2987         assert!(
   2988             auth_not_pending_err
   2989                 .to_string()
   2990                 .contains("auth challenge not pending for connection")
   2991         );
   2992 
   2993         let (missing_connection_finalize_record, missing_connection_finalize_workflow, _) =
   2994             start_published_auth_workflow(11, "req-finalize-missing-connection");
   2995         {
   2996             let mut state = manager.state.write().expect("write");
   2997             let workflow = state
   2998                 .publish_workflows
   2999                 .iter_mut()
   3000                 .find(|workflow| {
   3001                     workflow.workflow_id == missing_connection_finalize_workflow.workflow_id
   3002                 })
   3003                 .expect("stored workflow");
   3004             workflow.connection_id =
   3005                 RadrootsNostrSignerConnectionId::parse("conn-finalize-missing")
   3006                     .expect("connection id");
   3007         }
   3008         let missing_connection_finalize_err = manager
   3009             .finalize_publish_workflow(&missing_connection_finalize_workflow.workflow_id)
   3010             .expect_err("missing connection finalize");
   3011         assert!(
   3012             missing_connection_finalize_err
   3013                 .to_string()
   3014                 .contains("connection not found")
   3015         );
   3016         {
   3017             let mut state = manager.state.write().expect("write");
   3018             state.publish_workflows.retain(|workflow| {
   3019                 workflow.workflow_id != missing_connection_finalize_workflow.workflow_id
   3020             });
   3021             state.connections.retain(|record| {
   3022                 record.connection_id != missing_connection_finalize_record.connection_id
   3023             });
   3024         }
   3025 
   3026         let (auth_missing_challenge_record, auth_missing_challenge_workflow, _) =
   3027             start_published_auth_workflow(6, "req-finalize-missing-challenge");
   3028         {
   3029             let mut state = manager.state.write().expect("write");
   3030             let record = state
   3031                 .connections
   3032                 .iter_mut()
   3033                 .find(|record| record.connection_id == auth_missing_challenge_record.connection_id)
   3034                 .expect("stored connection");
   3035             record.auth_challenge = None;
   3036         }
   3037         let auth_missing_challenge_err = manager
   3038             .finalize_publish_workflow(&auth_missing_challenge_workflow.workflow_id)
   3039             .expect_err("missing challenge finalize");
   3040         assert!(
   3041             auth_missing_challenge_err
   3042                 .to_string()
   3043                 .contains("auth challenge missing for connection")
   3044         );
   3045 
   3046         let (workflow_missing_pending_record, workflow_missing_pending_workflow, _) =
   3047             start_published_auth_workflow(7, "req-finalize-workflow-missing-pending");
   3048         {
   3049             let mut state = manager.state.write().expect("write");
   3050             let workflow = state
   3051                 .publish_workflows
   3052                 .iter_mut()
   3053                 .find(|workflow| {
   3054                     workflow.workflow_id == workflow_missing_pending_workflow.workflow_id
   3055                 })
   3056                 .expect("stored workflow");
   3057             workflow.pending_request = None;
   3058         }
   3059         let workflow_missing_pending_err = manager
   3060             .finalize_publish_workflow(&workflow_missing_pending_workflow.workflow_id)
   3061             .expect_err("workflow missing pending finalize");
   3062         assert!(
   3063             workflow_missing_pending_err
   3064                 .to_string()
   3065                 .contains("auth replay workflow missing pending request")
   3066         );
   3067         {
   3068             let mut state = manager.state.write().expect("write");
   3069             state.publish_workflows.retain(|workflow| {
   3070                 workflow.workflow_id != workflow_missing_pending_workflow.workflow_id
   3071             });
   3072             state.connections.retain(|record| {
   3073                 record.connection_id != workflow_missing_pending_record.connection_id
   3074             });
   3075         }
   3076 
   3077         let (mismatch_finalize_record, mismatch_finalize_workflow, _) =
   3078             start_published_auth_workflow(8, "req-finalize-mismatch");
   3079         {
   3080             let mut state = manager.state.write().expect("write");
   3081             let record = state
   3082                 .connections
   3083                 .iter_mut()
   3084                 .find(|record| record.connection_id == mismatch_finalize_record.connection_id)
   3085                 .expect("stored connection");
   3086             record.pending_request = Some(
   3087                 RadrootsNostrSignerPendingRequest::new(
   3088                     request_message("req-finalize-mismatch-other"),
   3089                     78,
   3090                 )
   3091                 .expect("mismatched pending request"),
   3092             );
   3093         }
   3094         let mismatch_finalize_err = manager
   3095             .finalize_publish_workflow(&mismatch_finalize_workflow.workflow_id)
   3096             .expect_err("mismatch finalize");
   3097         assert!(
   3098             mismatch_finalize_err
   3099                 .to_string()
   3100                 .contains("pending request does not match auth replay workflow")
   3101         );
   3102 
   3103         let (missing_authorized_record, missing_authorized_workflow, _) =
   3104             start_published_auth_workflow(9, "req-finalize-missing-authorized");
   3105         {
   3106             let mut state = manager.state.write().expect("write");
   3107             let workflow = state
   3108                 .publish_workflows
   3109                 .iter_mut()
   3110                 .find(|workflow| workflow.workflow_id == missing_authorized_workflow.workflow_id)
   3111                 .expect("stored workflow");
   3112             workflow.authorized_at_unix = None;
   3113         }
   3114         let missing_authorized_err = manager
   3115             .finalize_publish_workflow(&missing_authorized_workflow.workflow_id)
   3116             .expect_err("missing authorized finalize");
   3117         assert!(
   3118             missing_authorized_err
   3119                 .to_string()
   3120                 .contains("auth replay workflow missing authorized timestamp")
   3121         );
   3122         {
   3123             let mut state = manager.state.write().expect("write");
   3124             state
   3125                 .publish_workflows
   3126                 .retain(|workflow| workflow.workflow_id != missing_authorized_workflow.workflow_id);
   3127             state
   3128                 .connections
   3129                 .retain(|record| record.connection_id != missing_authorized_record.connection_id);
   3130         }
   3131 
   3132         let (missing_connection_eval_record, missing_connection_eval_workflow, _) =
   3133             start_auth_replay_workflow(12, "req-eval-missing-connection");
   3134         {
   3135             let mut state = manager.state.write().expect("write");
   3136             let workflow = state
   3137                 .publish_workflows
   3138                 .iter_mut()
   3139                 .find(|workflow| {
   3140                     workflow.workflow_id == missing_connection_eval_workflow.workflow_id
   3141                 })
   3142                 .expect("stored workflow");
   3143             workflow.connection_id =
   3144                 RadrootsNostrSignerConnectionId::parse("conn-evaluate-missing")
   3145                     .expect("connection id");
   3146         }
   3147         let missing_connection_eval_err = manager
   3148             .evaluate_auth_replay_publish_workflow(&missing_connection_eval_workflow.workflow_id)
   3149             .expect_err("missing connection evaluate");
   3150         assert!(
   3151             missing_connection_eval_err
   3152                 .to_string()
   3153                 .contains("connection not found")
   3154         );
   3155         {
   3156             let mut state = manager.state.write().expect("write");
   3157             state.publish_workflows.retain(|workflow| {
   3158                 workflow.workflow_id != missing_connection_eval_workflow.workflow_id
   3159             });
   3160             state.connections.retain(|record| {
   3161                 record.connection_id != missing_connection_eval_record.connection_id
   3162             });
   3163         }
   3164     }
   3165 
   3166     #[test]
   3167     fn manager_reports_missing_connections_and_save_failures() {
   3168         let manager = RadrootsNostrSignerManager::new_in_memory();
   3169         let missing_id = RadrootsNostrSignerConnectionId::parse("missing").expect("id");
   3170         let missing_get = manager.get_connection(&missing_id).expect("missing get");
   3171         assert!(missing_get.is_none());
   3172 
   3173         let mark_err = manager
   3174             .mark_authenticated(&missing_id)
   3175             .expect_err("missing auth");
   3176         assert!(mark_err.to_string().contains("connection not found"));
   3177 
   3178         let save_error_store =
   3179             Arc::new(SaveErrorStore::new(RadrootsNostrSignerStoreState::default()));
   3180         let loaded_state = save_error_store.load().expect("load save error store");
   3181         assert_eq!(loaded_state.version, RADROOTS_NOSTR_SIGNER_STORE_VERSION);
   3182         let manager = RadrootsNostrSignerManager::new(save_error_store).expect("manager");
   3183         let err = manager
   3184             .set_signer_identity(public_identity(0x33))
   3185             .expect_err("save error");
   3186         assert!(err.to_string().contains("store save failed"));
   3187 
   3188         let signer_identity = public_identity(0x243);
   3189         let connection = RadrootsNostrSignerConnectionRecord::new(
   3190             RadrootsNostrSignerConnectionId::parse("conn-save-error").expect("id"),
   3191             signer_identity.clone(),
   3192             RadrootsNostrSignerConnectionDraft::new(public_key(0x244), public_identity(0x245))
   3193                 .with_connect_secret("save-error-secret"),
   3194             1,
   3195         );
   3196         let manager = RadrootsNostrSignerManager::new(Arc::new(SaveErrorStore::new(
   3197             RadrootsNostrSignerStoreState {
   3198                 version: RADROOTS_NOSTR_SIGNER_STORE_VERSION,
   3199                 signer_identity: Some(signer_identity),
   3200                 connections: vec![connection.clone()],
   3201                 audit_records: Vec::new(),
   3202                 publish_workflows: Vec::new(),
   3203             },
   3204         )))
   3205         .expect("manager with preloaded state");
   3206         let workflow_err = manager
   3207             .begin_connect_secret_publish_finalization(&connection.connection_id)
   3208             .expect_err("workflow save error");
   3209         assert!(workflow_err.to_string().contains("store save failed"));
   3210     }
   3211 
   3212     #[test]
   3213     fn mutation_methods_cover_remaining_error_paths() {
   3214         let manager = RadrootsNostrSignerManager::new_in_memory();
   3215         manager
   3216             .set_signer_identity(public_identity(0x51))
   3217             .expect("set signer");
   3218 
   3219         let missing_id = RadrootsNostrSignerConnectionId::parse("missing-2").expect("id");
   3220         let missing_permissions: RadrootsNostrConnectPermissions =
   3221             vec![permission(RadrootsNostrConnectMethod::Ping, None)].into();
   3222 
   3223         let missing_grants = manager
   3224             .set_granted_permissions(&missing_id, missing_permissions.clone())
   3225             .expect_err("missing grants");
   3226         let missing_approve = manager
   3227             .approve_connection(&missing_id, RadrootsNostrConnectPermissions::default())
   3228             .expect_err("missing approve");
   3229         let missing_reject = manager
   3230             .reject_connection(&missing_id, None)
   3231             .expect_err("missing reject");
   3232         let missing_revoke = manager
   3233             .revoke_connection(&missing_id, None)
   3234             .expect_err("missing revoke");
   3235         let missing_relays = manager
   3236             .update_relays(&missing_id, vec![primary_relay()])
   3237             .expect_err("missing relays");
   3238         let missing_require_auth = manager
   3239             .require_auth_challenge(&missing_id, api_primary_https())
   3240             .expect_err("missing require auth");
   3241         let missing_pending_request = manager
   3242             .set_pending_request(&missing_id, request_message("req-missing-2"))
   3243             .expect_err("missing pending request");
   3244         let missing_begin_connect_workflow = manager
   3245             .begin_connect_secret_publish_finalization(&missing_id)
   3246             .expect_err("missing connect workflow");
   3247         let missing_begin_auth_workflow = manager
   3248             .begin_auth_replay_publish_finalization(&missing_id)
   3249             .expect_err("missing auth workflow");
   3250         let missing_authorize_auth = manager
   3251             .authorize_auth_challenge(&missing_id)
   3252             .expect_err("missing authorize auth");
   3253         let missing_request = manager
   3254             .record_request(
   3255                 &missing_id,
   3256                 "req-missing",
   3257                 RadrootsNostrConnectMethod::Ping,
   3258                 RadrootsNostrSignerRequestDecision::Denied,
   3259                 None,
   3260             )
   3261             .expect_err("missing request");
   3262 
   3263         for err in [
   3264             missing_grants,
   3265             missing_approve,
   3266             missing_reject,
   3267             missing_revoke,
   3268             missing_relays,
   3269             missing_require_auth,
   3270             missing_pending_request,
   3271             missing_begin_connect_workflow,
   3272             missing_begin_auth_workflow,
   3273             missing_authorize_auth,
   3274             missing_request,
   3275         ] {
   3276             assert!(err.to_string().contains("connection not found"));
   3277         }
   3278 
   3279         let requested = vec![permission(RadrootsNostrConnectMethod::Ping, None)];
   3280         let pending = manager
   3281             .register_connection(
   3282                 RadrootsNostrSignerConnectionDraft::new(public_key(0x52), public_identity(0x53))
   3283                     .with_requested_permissions(requested.into())
   3284                     .with_approval_requirement(
   3285                         RadrootsNostrSignerApprovalRequirement::ExplicitUser,
   3286                     ),
   3287             )
   3288             .expect("register pending");
   3289         let invalid_approve = manager
   3290             .approve_connection(
   3291                 &pending.connection_id,
   3292                 vec![permission(
   3293                     RadrootsNostrConnectMethod::Nip44Encrypt,
   3294                     Some("kind:1"),
   3295                 )]
   3296                 .into(),
   3297             )
   3298             .expect_err("invalid approve grants");
   3299         assert!(
   3300             invalid_approve
   3301                 .to_string()
   3302                 .contains("invalid granted permission")
   3303         );
   3304 
   3305         let auth_required = manager
   3306             .require_auth_challenge(&pending.connection_id, api_primary_https())
   3307             .expect("require auth");
   3308         assert_eq!(
   3309             auth_required.auth_state,
   3310             RadrootsNostrSignerAuthState::Pending
   3311         );
   3312 
   3313         let invalid_pending_request = manager
   3314             .set_pending_request(&pending.connection_id, request_message("   "))
   3315             .expect_err("invalid pending request id");
   3316         assert!(
   3317             invalid_pending_request
   3318                 .to_string()
   3319                 .contains("invalid request id")
   3320         );
   3321 
   3322         let update_state_err = manager
   3323             .update_state(|_| Err(RadrootsNostrSignerError::InvalidState("manual".into())))
   3324             .expect_err("update_state error");
   3325         assert!(update_state_err.to_string().contains("manual"));
   3326     }
   3327 
   3328     #[test]
   3329     fn register_connection_rejects_invalid_persisted_signer_identity() {
   3330         let store = Arc::new(RadrootsNostrMemorySignerStore::new());
   3331         let mut state = RadrootsNostrSignerStoreState::default();
   3332         state.signer_identity = Some(invalid_public_identity(0x54));
   3333         store.save(&state).expect("seed state");
   3334 
   3335         let manager = RadrootsNostrSignerManager::new(store).expect("manager");
   3336         let err = manager
   3337             .register_connection(RadrootsNostrSignerConnectionDraft::new(
   3338                 public_key(0x55),
   3339                 public_identity(0x56),
   3340             ))
   3341             .expect_err("invalid signer identity");
   3342         assert!(
   3343             err.to_string()
   3344                 .contains("public identity id does not match public key")
   3345         );
   3346     }
   3347 
   3348     #[test]
   3349     fn manager_reports_poisoned_state_lock() {
   3350         let manager = RadrootsNostrSignerManager::new_in_memory();
   3351         poison_manager_state(&manager);
   3352 
   3353         let identity = manager.signer_identity().expect_err("poisoned read");
   3354         assert!(identity.to_string().contains("signer state lock poisoned"));
   3355     }
   3356 
   3357     #[test]
   3358     fn read_helpers_report_poisoned_state_lock() {
   3359         let manager = RadrootsNostrSignerManager::new_in_memory();
   3360         poison_manager_state(&manager);
   3361 
   3362         let connection_id = RadrootsNostrSignerConnectionId::parse("conn-1").expect("id");
   3363         let client_public_key = public_key(0x47);
   3364 
   3365         let get_err = manager
   3366             .get_connection(&connection_id)
   3367             .expect_err("poisoned get");
   3368         let list_err = manager.list_connections().expect_err("poisoned list");
   3369         let audit_list_err = manager
   3370             .list_audit_records()
   3371             .expect_err("poisoned audit list");
   3372         let audit_for_connection_err = manager
   3373             .audit_records_for_connection(&connection_id)
   3374             .expect_err("poisoned audit connection");
   3375         let workflow_list_err = manager
   3376             .list_publish_workflows()
   3377             .expect_err("poisoned workflow list");
   3378         let workflow_get_err = manager
   3379             .get_publish_workflow(&RadrootsNostrSignerWorkflowId::parse("wf-poison").expect("id"))
   3380             .expect_err("poisoned workflow get");
   3381         let find_secret_err = manager
   3382             .find_connection_by_connect_secret("secret")
   3383             .expect_err("poisoned secret lookup");
   3384         let find_client_err = manager
   3385             .find_connections_by_client_public_key(&client_public_key)
   3386             .expect_err("poisoned client lookup");
   3387         let lookup_secret_err = manager
   3388             .lookup_session(&client_public_key, Some("secret"))
   3389             .expect_err("poisoned session secret lookup");
   3390         let lookup_client_err = manager
   3391             .lookup_session(&client_public_key, None)
   3392             .expect_err("poisoned session client lookup");
   3393 
   3394         for err in [
   3395             get_err,
   3396             list_err,
   3397             audit_list_err,
   3398             audit_for_connection_err,
   3399             workflow_list_err,
   3400             workflow_get_err,
   3401             find_secret_err,
   3402             find_client_err,
   3403             lookup_secret_err,
   3404             lookup_client_err,
   3405         ] {
   3406             assert!(err.to_string().contains("signer state lock poisoned"));
   3407         }
   3408     }
   3409 
   3410     #[test]
   3411     fn evaluate_connect_request_reports_poisoned_state_lock() {
   3412         let store = Arc::new(RadrootsNostrMemorySignerStore::new());
   3413         let signer_identity = public_identity(0x57);
   3414         let mut state = RadrootsNostrSignerStoreState::default();
   3415         state.signer_identity = Some(signer_identity.clone());
   3416         store.save(&state).expect("save state");
   3417 
   3418         let manager = RadrootsNostrSignerManager::new(store).expect("manager");
   3419         poison_manager_state(&manager);
   3420 
   3421         let err = manager
   3422             .evaluate_connect_request(
   3423                 public_key(0x58),
   3424                 RadrootsNostrConnectRequest::Connect {
   3425                     remote_signer_public_key: PublicKey::parse(
   3426                         signer_identity.public_key_hex.as_str(),
   3427                     )
   3428                     .expect("signer public key"),
   3429                     secret: Some("secret".into()),
   3430                     requested_permissions: RadrootsNostrConnectPermissions::default(),
   3431                 },
   3432             )
   3433             .expect_err("poisoned connect evaluation");
   3434         assert!(err.to_string().contains("signer state lock poisoned"));
   3435     }
   3436 
   3437     #[test]
   3438     fn mutation_helpers_report_poisoned_state_lock() {
   3439         let manager = RadrootsNostrSignerManager::new_in_memory();
   3440         poison_manager_state(&manager);
   3441 
   3442         let signer_identity = public_identity(0x48);
   3443         let connection_id = RadrootsNostrSignerConnectionId::parse("conn-2").expect("id");
   3444         let workflow_id = RadrootsNostrSignerWorkflowId::parse("wf-2").expect("id");
   3445         let connect_draft =
   3446             RadrootsNostrSignerConnectionDraft::new(public_key(0x49), public_identity(0x50));
   3447 
   3448         let set_signer_err = manager
   3449             .set_signer_identity(signer_identity)
   3450             .expect_err("poisoned set signer");
   3451         let register_err = manager
   3452             .register_connection(connect_draft)
   3453             .expect_err("poisoned register");
   3454         let grants_err = manager
   3455             .set_granted_permissions(
   3456                 &connection_id,
   3457                 vec![permission(RadrootsNostrConnectMethod::Ping, None)].into(),
   3458             )
   3459             .expect_err("poisoned set grants");
   3460         let approve_err = manager
   3461             .approve_connection(&connection_id, RadrootsNostrConnectPermissions::default())
   3462             .expect_err("poisoned approve");
   3463         let reject_err = manager
   3464             .reject_connection(&connection_id, Some("reason".into()))
   3465             .expect_err("poisoned reject");
   3466         let revoke_err = manager
   3467             .revoke_connection(&connection_id, Some("reason".into()))
   3468             .expect_err("poisoned revoke");
   3469         let update_relays_err = manager
   3470             .update_relays(&connection_id, vec![primary_relay()])
   3471             .expect_err("poisoned relays");
   3472         let require_auth_err = manager
   3473             .require_auth_challenge(&connection_id, api_primary_https())
   3474             .expect_err("poisoned require auth");
   3475         let set_pending_request_err = manager
   3476             .set_pending_request(&connection_id, request_message("req-2"))
   3477             .expect_err("poisoned set pending request");
   3478         let authorize_auth_err = manager
   3479             .authorize_auth_challenge(&connection_id)
   3480             .expect_err("poisoned authorize auth");
   3481         let begin_connect_workflow_err = manager
   3482             .begin_connect_secret_publish_finalization(&connection_id)
   3483             .expect_err("poisoned connect workflow");
   3484         let begin_auth_workflow_err = manager
   3485             .begin_auth_replay_publish_finalization(&connection_id)
   3486             .expect_err("poisoned auth workflow");
   3487         let mark_workflow_err = manager
   3488             .mark_publish_workflow_published(&workflow_id)
   3489             .expect_err("poisoned mark workflow");
   3490         let finalize_workflow_err = manager
   3491             .finalize_publish_workflow(&workflow_id)
   3492             .expect_err("poisoned finalize workflow");
   3493         let cancel_workflow_err = manager
   3494             .cancel_publish_workflow(&workflow_id)
   3495             .expect_err("poisoned cancel workflow");
   3496         let auth_err = manager
   3497             .mark_authenticated(&connection_id)
   3498             .expect_err("poisoned auth");
   3499         let request_err = manager
   3500             .record_request(
   3501                 &connection_id,
   3502                 "req-1",
   3503                 RadrootsNostrConnectMethod::Ping,
   3504                 RadrootsNostrSignerRequestDecision::Allowed,
   3505                 None,
   3506             )
   3507             .expect_err("poisoned request");
   3508 
   3509         for err in [
   3510             set_signer_err,
   3511             register_err,
   3512             grants_err,
   3513             approve_err,
   3514             reject_err,
   3515             revoke_err,
   3516             update_relays_err,
   3517             require_auth_err,
   3518             set_pending_request_err,
   3519             authorize_auth_err,
   3520             begin_connect_workflow_err,
   3521             begin_auth_workflow_err,
   3522             mark_workflow_err,
   3523             finalize_workflow_err,
   3524             cancel_workflow_err,
   3525             auth_err,
   3526             request_err,
   3527         ] {
   3528             assert!(err.to_string().contains("signer state lock poisoned"));
   3529         }
   3530     }
   3531 
   3532     #[test]
   3533     fn save_error_store_reports_poisoned_load_lock() {
   3534         let store = SaveErrorStore::new(RadrootsNostrSignerStoreState::default());
   3535         let shared = Arc::new(store);
   3536         let poison = shared.clone();
   3537         let _ = thread::spawn(move || {
   3538             let _guard = poison.state.write().expect("write");
   3539             panic!("poison save error store");
   3540         })
   3541         .join();
   3542 
   3543         let err = shared.load().expect_err("poisoned load");
   3544         assert!(err.to_string().contains("save error store poisoned"));
   3545     }
   3546 
   3547     #[test]
   3548     fn helpers_cover_status_labels_and_consumed_secret_reuse_rules() {
   3549         assert_eq!(
   3550             status_label(RadrootsNostrSignerConnectionStatus::Pending),
   3551             "pending"
   3552         );
   3553         assert_eq!(
   3554             status_label(RadrootsNostrSignerConnectionStatus::Active),
   3555             "active"
   3556         );
   3557         assert_eq!(
   3558             status_label(RadrootsNostrSignerConnectionStatus::Rejected),
   3559             "rejected"
   3560         );
   3561         assert_eq!(
   3562             status_label(RadrootsNostrSignerConnectionStatus::Revoked),
   3563             "revoked"
   3564         );
   3565 
   3566         let manager = RadrootsNostrSignerManager::new_in_memory();
   3567         manager
   3568             .set_signer_identity(public_identity(0x42))
   3569             .expect("set signer");
   3570 
   3571         let initial = manager
   3572             .register_connection(
   3573                 RadrootsNostrSignerConnectionDraft::new(public_key(0x43), public_identity(0x44))
   3574                     .with_connect_secret("reusable-secret")
   3575                     .with_approval_requirement(
   3576                         RadrootsNostrSignerApprovalRequirement::ExplicitUser,
   3577                     ),
   3578             )
   3579             .expect("register initial");
   3580         manager
   3581             .reject_connection(&initial.connection_id, Some("closed".into()))
   3582             .expect("reject initial");
   3583 
   3584         let reused = manager
   3585             .register_connection(
   3586                 RadrootsNostrSignerConnectionDraft::new(public_key(0x45), public_identity(0x46))
   3587                     .with_connect_secret("reusable-secret"),
   3588             )
   3589             .expect("register reused secret");
   3590 
   3591         assert!(
   3592             reused
   3593                 .connect_secret_hash
   3594                 .as_ref()
   3595                 .expect("connect secret hash")
   3596                 .matches_secret("reusable-secret")
   3597         );
   3598 
   3599         let consumed = manager
   3600             .mark_connect_secret_consumed(&reused.connection_id)
   3601             .expect("consume secret");
   3602         assert!(consumed.connect_secret_is_consumed());
   3603         manager
   3604             .reject_connection(&reused.connection_id, Some("closed".into()))
   3605             .expect("reject consumed");
   3606 
   3607         let blocked_reuse = manager
   3608             .register_connection(
   3609                 RadrootsNostrSignerConnectionDraft::new(public_key(0x47), public_identity(0x48))
   3610                     .with_connect_secret("reusable-secret"),
   3611             )
   3612             .expect_err("block consumed secret reuse");
   3613         assert!(matches!(
   3614             blocked_reuse,
   3615             RadrootsNostrSignerError::ConnectSecretAlreadyInUse
   3616         ));
   3617     }
   3618 
   3619     #[test]
   3620     fn session_lookup_and_connect_evaluation_cover_new_paths() {
   3621         let manager = RadrootsNostrSignerManager::new_in_memory();
   3622         let signer_identity = public_identity(0x60);
   3623         let signer_public_key =
   3624             PublicKey::parse(signer_identity.public_key_hex.as_str()).expect("signer public key");
   3625         manager
   3626             .set_signer_identity(signer_identity)
   3627             .expect("set signer");
   3628 
   3629         let client_public_key = public_key(0x61);
   3630         let primary = manager
   3631             .register_connection(
   3632                 RadrootsNostrSignerConnectionDraft::new(client_public_key, public_identity(0x62))
   3633                     .with_connect_secret("connect-secret"),
   3634             )
   3635             .expect("register primary");
   3636 
   3637         let single_lookup = manager
   3638             .lookup_session(&client_public_key, None)
   3639             .expect("lookup single");
   3640         assert_same_connection(&expect_connection_lookup(single_lookup), &primary);
   3641 
   3642         let secret_lookup = manager
   3643             .lookup_session(&client_public_key, Some("connect-secret"))
   3644             .expect("lookup by secret");
   3645         assert_same_connection(&expect_connection_lookup(secret_lookup), &primary);
   3646         let missing_secret_lookup = manager
   3647             .lookup_session(&client_public_key, Some("missing-secret"))
   3648             .expect("lookup missing secret");
   3649         assert_same_connection(&expect_connection_lookup(missing_secret_lookup), &primary);
   3650 
   3651         let second = manager
   3652             .register_connection(
   3653                 RadrootsNostrSignerConnectionDraft::new(client_public_key, public_identity(0x63))
   3654                     .with_connect_secret("second-secret"),
   3655             )
   3656             .expect("register second");
   3657 
   3658         let ambiguous_by_missing_secret = manager
   3659             .lookup_session(&client_public_key, Some("missing-secret"))
   3660             .expect("lookup missing secret after second");
   3661         let found = expect_ambiguous_lookup(ambiguous_by_missing_secret);
   3662         assert_eq!(found.len(), 2);
   3663         assert_same_connection(&found[0], &primary);
   3664         assert_same_connection(&found[1], &second);
   3665         let ambiguous_lookup = manager
   3666             .lookup_session(&client_public_key, None)
   3667             .expect("lookup ambiguous");
   3668         let found = expect_ambiguous_lookup(ambiguous_lookup);
   3669         assert_eq!(found.len(), 2);
   3670         assert_same_connection(&found[0], &primary);
   3671         assert_same_connection(&found[1], &second);
   3672 
   3673         let mismatch_secret = manager
   3674             .lookup_session(&public_key(0x64), Some("connect-secret"))
   3675             .expect_err("secret mismatch");
   3676         assert!(
   3677             mismatch_secret
   3678                 .to_string()
   3679                 .contains("different client public key")
   3680         );
   3681 
   3682         let none_lookup = manager
   3683             .lookup_session(&public_key(0x65), None)
   3684             .expect("lookup none");
   3685         expect_none_lookup(none_lookup);
   3686 
   3687         let non_connect_err = manager
   3688             .evaluate_connect_request(client_public_key, RadrootsNostrConnectRequest::Ping)
   3689             .expect_err("non-connect evaluation");
   3690         assert!(
   3691             non_connect_err
   3692                 .to_string()
   3693                 .contains("connect evaluation requires a connect request")
   3694         );
   3695 
   3696         let missing_signer_err = RadrootsNostrSignerManager::new_in_memory()
   3697             .evaluate_connect_request(
   3698                 client_public_key,
   3699                 RadrootsNostrConnectRequest::Connect {
   3700                     remote_signer_public_key: signer_public_key,
   3701                     secret: None,
   3702                     requested_permissions: RadrootsNostrConnectPermissions::default(),
   3703                 },
   3704             )
   3705             .expect_err("missing signer");
   3706         assert_eq!(missing_signer_err.to_string(), "missing signer identity");
   3707 
   3708         let signer_mismatch_err = manager
   3709             .evaluate_connect_request(
   3710                 client_public_key,
   3711                 RadrootsNostrConnectRequest::Connect {
   3712                     remote_signer_public_key: public_key(0x66),
   3713                     secret: None,
   3714                     requested_permissions: RadrootsNostrConnectPermissions::default(),
   3715                 },
   3716             )
   3717             .expect_err("signer mismatch");
   3718         assert!(
   3719             signer_mismatch_err
   3720                 .to_string()
   3721                 .contains("remote signer public key mismatch")
   3722         );
   3723 
   3724         let existing_connect = manager
   3725             .evaluate_connect_request(
   3726                 client_public_key,
   3727                 RadrootsNostrConnectRequest::Connect {
   3728                     remote_signer_public_key: signer_public_key,
   3729                     secret: Some(" connect-secret ".into()),
   3730                     requested_permissions: vec![
   3731                         permission(RadrootsNostrConnectMethod::Ping, None),
   3732                         permission(RadrootsNostrConnectMethod::Ping, None),
   3733                     ]
   3734                     .into(),
   3735                 },
   3736             )
   3737             .expect("existing connect request");
   3738         assert_same_connection(&expect_existing_connect(existing_connect), &primary);
   3739 
   3740         let registration_connect = manager
   3741             .evaluate_connect_request(
   3742                 public_key(0x67),
   3743                 RadrootsNostrConnectRequest::Connect {
   3744                     remote_signer_public_key: signer_public_key,
   3745                     secret: Some(" fresh-secret ".into()),
   3746                     requested_permissions: vec![
   3747                         permission(RadrootsNostrConnectMethod::Ping, None),
   3748                         permission(RadrootsNostrConnectMethod::SignEvent, Some("kind:1")),
   3749                         permission(RadrootsNostrConnectMethod::Ping, None),
   3750                     ]
   3751                     .into(),
   3752                 },
   3753             )
   3754             .expect("registration connect request");
   3755         let proposal = expect_registration_connect(registration_connect);
   3756         assert_eq!(proposal.client_public_key, public_key(0x67));
   3757         assert_eq!(proposal.connect_secret.as_deref(), Some("fresh-secret"));
   3758         assert_eq!(
   3759             proposal.requested_permissions.as_slice(),
   3760             &[
   3761                 permission(RadrootsNostrConnectMethod::SignEvent, Some("kind:1")),
   3762                 permission(RadrootsNostrConnectMethod::Ping, None),
   3763             ]
   3764         );
   3765 
   3766         let existing_secret_mismatch = manager
   3767             .evaluate_connect_request(
   3768                 public_key(0x68),
   3769                 RadrootsNostrConnectRequest::Connect {
   3770                     remote_signer_public_key: signer_public_key,
   3771                     secret: Some("connect-secret".into()),
   3772                     requested_permissions: RadrootsNostrConnectPermissions::default(),
   3773                 },
   3774             )
   3775             .expect_err("existing secret mismatch");
   3776         assert!(
   3777             existing_secret_mismatch
   3778                 .to_string()
   3779                 .contains("different client public key")
   3780         );
   3781 
   3782         let store = Arc::new(RadrootsNostrMemorySignerStore::new());
   3783         let mut invalid_state = RadrootsNostrSignerStoreState::default();
   3784         let mut invalid_identity = public_identity(0x69);
   3785         invalid_identity.public_key_hex = "invalid".into();
   3786         invalid_state.signer_identity = Some(invalid_identity);
   3787         store
   3788             .save(&invalid_state)
   3789             .expect("save invalid signer state");
   3790         let invalid_manager = RadrootsNostrSignerManager::new(store).expect("invalid manager");
   3791         let invalid_signer_err = invalid_manager
   3792             .evaluate_connect_request(
   3793                 public_key(0x70),
   3794                 RadrootsNostrConnectRequest::Connect {
   3795                     remote_signer_public_key: signer_public_key,
   3796                     secret: None,
   3797                     requested_permissions: RadrootsNostrConnectPermissions::default(),
   3798                 },
   3799             )
   3800             .expect_err("invalid signer public key");
   3801         assert!(
   3802             invalid_signer_err
   3803                 .to_string()
   3804                 .contains("identity public key is invalid")
   3805         );
   3806     }
   3807 
   3808     #[test]
   3809     fn evaluate_request_covers_allowed_denied_and_challenged_paths() {
   3810         let manager = RadrootsNostrSignerManager::new_in_memory();
   3811         manager
   3812             .set_signer_identity(public_identity(0x71))
   3813             .expect("set signer");
   3814 
   3815         let active = manager
   3816             .register_connection(
   3817                 RadrootsNostrSignerConnectionDraft::new(public_key(0x72), public_identity(0x73))
   3818                     .with_requested_permissions(
   3819                         vec![permission(
   3820                             RadrootsNostrConnectMethod::SignEvent,
   3821                             Some("kind:1"),
   3822                         )]
   3823                         .into(),
   3824                     ),
   3825             )
   3826             .expect("register active");
   3827 
   3828         let get_public_key = manager
   3829             .evaluate_request(
   3830                 &active.connection_id,
   3831                 request_message_with_request("req-get", RadrootsNostrConnectRequest::GetPublicKey),
   3832             )
   3833             .expect("evaluate get_public_key");
   3834         expect_allowed_user_public_key(&get_public_key.action);
   3835         assert_eq!(
   3836             get_public_key.audit.decision,
   3837             RadrootsNostrSignerRequestDecision::Allowed
   3838         );
   3839         assert!(get_public_key.denied_reason().is_none());
   3840 
   3841         let allowed_sign = manager
   3842             .evaluate_request(
   3843                 &active.connection_id,
   3844                 request_message_with_request(
   3845                     "req-sign-1",
   3846                     RadrootsNostrConnectRequest::SignEvent(unsigned_event(1)),
   3847                 ),
   3848             )
   3849             .expect("evaluate sign allowed");
   3850         expect_allowed_without_response_hint(&allowed_sign.action);
   3851 
   3852         let denied_sign = manager
   3853             .evaluate_request(
   3854                 &active.connection_id,
   3855                 request_message_with_request(
   3856                     "req-sign-2",
   3857                     RadrootsNostrConnectRequest::SignEvent(unsigned_event(2)),
   3858                 ),
   3859             )
   3860             .expect("evaluate sign denied");
   3861         assert_eq!(denied_sign.denied_reason(), Some("unauthorized sign_event"));
   3862         assert_eq!(
   3863             denied_sign.audit.decision,
   3864             RadrootsNostrSignerRequestDecision::Denied
   3865         );
   3866 
   3867         let pending = manager
   3868             .register_connection(
   3869                 RadrootsNostrSignerConnectionDraft::new(public_key(0x74), public_identity(0x75))
   3870                     .with_approval_requirement(
   3871                         RadrootsNostrSignerApprovalRequirement::ExplicitUser,
   3872                     ),
   3873             )
   3874             .expect("register pending");
   3875         let pending_eval = manager
   3876             .evaluate_request(&pending.connection_id, request_message("req-pending"))
   3877             .expect("evaluate pending");
   3878         assert_eq!(pending_eval.denied_reason(), Some("connection is pending"));
   3879 
   3880         let challenged = manager
   3881             .register_connection(RadrootsNostrSignerConnectionDraft::new(
   3882                 public_key(0x76),
   3883                 public_identity(0x77),
   3884             ))
   3885             .expect("register challenged");
   3886         manager
   3887             .require_auth_challenge(&challenged.connection_id, api_primary_https())
   3888             .expect("require auth challenge");
   3889         let challenged_eval = manager
   3890             .evaluate_request(&challenged.connection_id, request_message("req-auth"))
   3891             .expect("evaluate challenged");
   3892         expect_challenged_action(&challenged_eval.action);
   3893         assert_eq!(
   3894             challenged_eval.audit.decision,
   3895             RadrootsNostrSignerRequestDecision::Challenged
   3896         );
   3897         assert_eq!(
   3898             challenged_eval
   3899                 .connection
   3900                 .pending_request
   3901                 .as_ref()
   3902                 .expect("pending request")
   3903                 .request_id()
   3904                 .as_str(),
   3905             "req-auth"
   3906         );
   3907 
   3908         let rejected = manager
   3909             .reject_connection(&challenged.connection_id, Some("closed".into()))
   3910             .expect("reject challenged");
   3911         let rejected_eval = manager
   3912             .evaluate_request(&rejected.connection_id, request_message("req-rejected"))
   3913             .expect("evaluate rejected");
   3914         assert_eq!(
   3915             rejected_eval.denied_reason(),
   3916             Some("connection is rejected")
   3917         );
   3918 
   3919         let connect_eval_err = manager
   3920             .evaluate_request(
   3921                 &active.connection_id,
   3922                 request_message_with_request(
   3923                     "req-connect",
   3924                     RadrootsNostrConnectRequest::Connect {
   3925                         remote_signer_public_key: active.client_public_key,
   3926                         secret: None,
   3927                         requested_permissions: RadrootsNostrConnectPermissions::default(),
   3928                     },
   3929                 ),
   3930             )
   3931             .expect_err("connect through evaluate_request");
   3932         assert!(
   3933             connect_eval_err
   3934                 .to_string()
   3935                 .contains("evaluate_connect_request")
   3936         );
   3937     }
   3938 
   3939     #[test]
   3940     fn evaluate_request_reports_invalid_corrupted_auth_state() {
   3941         let store = Arc::new(RadrootsNostrMemorySignerStore::new());
   3942         let signer_identity = public_identity(0x78);
   3943         let mut state = RadrootsNostrSignerStoreState::default();
   3944         state.signer_identity = Some(signer_identity.clone());
   3945         let mut record = RadrootsNostrSignerConnectionRecord::new(
   3946             RadrootsNostrSignerConnectionId::new_v7(),
   3947             signer_identity,
   3948             RadrootsNostrSignerConnectionDraft::new(public_key(0x79), public_identity(0x80)),
   3949             1,
   3950         );
   3951         record.auth_state = RadrootsNostrSignerAuthState::Pending;
   3952         record.auth_challenge = None;
   3953         state.connections.push(record.clone());
   3954         store.save(&state).expect("save corrupted auth state");
   3955 
   3956         let manager = RadrootsNostrSignerManager::new(store).expect("manager");
   3957         let err = manager
   3958             .evaluate_request(&record.connection_id, request_message("req-corrupt"))
   3959             .expect_err("corrupted auth evaluation");
   3960         assert!(err.to_string().contains("auth challenge missing"));
   3961     }
   3962 
   3963     #[test]
   3964     fn evaluate_request_reports_invalid_request_id_and_missing_connection() {
   3965         let manager = RadrootsNostrSignerManager::new_in_memory();
   3966         manager
   3967             .set_signer_identity(public_identity(0x81))
   3968             .expect("set signer");
   3969 
   3970         let active = manager
   3971             .register_connection(RadrootsNostrSignerConnectionDraft::new(
   3972                 public_key(0x82),
   3973                 public_identity(0x83),
   3974             ))
   3975             .expect("register active");
   3976 
   3977         let invalid_request_id = manager
   3978             .evaluate_request(
   3979                 &active.connection_id,
   3980                 request_message_with_request("   ", RadrootsNostrConnectRequest::Ping),
   3981             )
   3982             .expect_err("invalid request id");
   3983         assert!(
   3984             invalid_request_id
   3985                 .to_string()
   3986                 .contains("invalid request id")
   3987         );
   3988 
   3989         let missing_connection = manager
   3990             .evaluate_request(
   3991                 &RadrootsNostrSignerConnectionId::new_v7(),
   3992                 request_message("req-missing"),
   3993             )
   3994             .expect_err("missing connection");
   3995         assert!(
   3996             missing_connection
   3997                 .to_string()
   3998                 .contains("connection not found")
   3999         );
   4000     }
   4001 
   4002     #[test]
   4003     fn evaluate_request_action_reports_pending_request_and_response_hint_errors() {
   4004         let mut pending_record = RadrootsNostrSignerConnectionRecord::new(
   4005             RadrootsNostrSignerConnectionId::new_v7(),
   4006             public_identity(0x84),
   4007             RadrootsNostrSignerConnectionDraft::new(public_key(0x85), public_identity(0x86)),
   4008             1,
   4009         );
   4010         pending_record.status = RadrootsNostrSignerConnectionStatus::Active;
   4011         pending_record.auth_state = RadrootsNostrSignerAuthState::Pending;
   4012         pending_record.auth_challenge =
   4013             Some(RadrootsNostrSignerAuthChallenge::new(api_primary_https(), 1).expect("challenge"));
   4014         let invalid_pending = evaluate_request_action(
   4015             &mut pending_record,
   4016             &request_message_with_request("   ", RadrootsNostrConnectRequest::Ping),
   4017             1,
   4018         )
   4019         .expect_err("invalid pending request");
   4020         assert!(invalid_pending.to_string().contains("invalid request id"));
   4021 
   4022         let mut invalid_user_record = RadrootsNostrSignerConnectionRecord::new(
   4023             RadrootsNostrSignerConnectionId::new_v7(),
   4024             public_identity(0x87),
   4025             RadrootsNostrSignerConnectionDraft::new(public_key(0x88), public_identity(0x89)),
   4026             1,
   4027         );
   4028         invalid_user_record.status = RadrootsNostrSignerConnectionStatus::Active;
   4029         invalid_user_record.user_identity.public_key_hex = "invalid".into();
   4030         let response_hint_err = evaluate_request_action(
   4031             &mut invalid_user_record,
   4032             &request_message_with_request("req-get", RadrootsNostrConnectRequest::GetPublicKey),
   4033             1,
   4034         )
   4035         .expect_err("invalid response hint");
   4036         assert!(
   4037             response_hint_err
   4038                 .to_string()
   4039                 .contains("user identity public key is invalid")
   4040         );
   4041     }
   4042 }