lib

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

nip46.rs (79758B)


      1 use nostr::UnsignedEvent;
      2 use radroots_identity::RadrootsIdentityPublic;
      3 use radroots_nostr::prelude::{
      4     RadrootsNostrEvent, RadrootsNostrEventBuilder, RadrootsNostrFilter, RadrootsNostrKind,
      5     RadrootsNostrPublicKey, RadrootsNostrRelayUrl, RadrootsNostrTag, RadrootsNostrTimestamp,
      6     radroots_nostr_filter_tag,
      7 };
      8 use radroots_nostr_connect::prelude::{
      9     RADROOTS_NOSTR_CONNECT_RPC_KIND, RadrootsNostrConnectError, RadrootsNostrConnectPermissions,
     10     RadrootsNostrConnectRequest, RadrootsNostrConnectRequestMessage, RadrootsNostrConnectResponse,
     11 };
     12 
     13 use crate::backend::RadrootsNostrSignerBackend;
     14 use crate::error::RadrootsNostrSignerError;
     15 use crate::evaluation::{
     16     RadrootsNostrSignerConnectEvaluation, RadrootsNostrSignerRequestAction,
     17     RadrootsNostrSignerRequestEvaluation, RadrootsNostrSignerRequestResponseHint,
     18     RadrootsNostrSignerSessionLookup,
     19 };
     20 use crate::model::{
     21     RadrootsNostrSignerApprovalRequirement, RadrootsNostrSignerConnectionId,
     22     RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerRequestAuditRecord,
     23     RadrootsNostrSignerRequestDecision,
     24 };
     25 
     26 pub trait RadrootsNostrSignerNip46Signer: Clone + Send + Sync {
     27     fn signer_public_key_hex(&self) -> String;
     28     fn decrypt_request(
     29         &self,
     30         client_public_key: &RadrootsNostrPublicKey,
     31         ciphertext: &str,
     32     ) -> Result<String, RadrootsNostrSignerError>;
     33     fn encrypt_response(
     34         &self,
     35         client_public_key: &RadrootsNostrPublicKey,
     36         payload: &str,
     37     ) -> Result<String, RadrootsNostrSignerError>;
     38     fn user_identity(&self) -> RadrootsIdentityPublic;
     39     fn sign_user_event(
     40         &self,
     41         unsigned_event: UnsignedEvent,
     42     ) -> Result<RadrootsNostrEvent, RadrootsNostrSignerError>;
     43     fn nip04_encrypt(
     44         &self,
     45         public_key: &RadrootsNostrPublicKey,
     46         plaintext: &str,
     47     ) -> Result<String, RadrootsNostrSignerError>;
     48     fn nip04_decrypt(
     49         &self,
     50         public_key: &RadrootsNostrPublicKey,
     51         ciphertext: &str,
     52     ) -> Result<String, RadrootsNostrSignerError>;
     53     fn nip44_encrypt(
     54         &self,
     55         public_key: &RadrootsNostrPublicKey,
     56         plaintext: &str,
     57     ) -> Result<String, RadrootsNostrSignerError>;
     58     fn nip44_decrypt(
     59         &self,
     60         public_key: &RadrootsNostrPublicKey,
     61         ciphertext: &str,
     62     ) -> Result<String, RadrootsNostrSignerError>;
     63 }
     64 
     65 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
     66 pub enum RadrootsNostrSignerNip46ConnectDecision {
     67     Allow,
     68     RequireApproval,
     69     Deny,
     70 }
     71 
     72 pub trait RadrootsNostrSignerNip46Policy<B: RadrootsNostrSignerBackend>:
     73     Clone + Send + Sync
     74 {
     75     fn connect_decision(
     76         &self,
     77         client_public_key: &RadrootsNostrPublicKey,
     78     ) -> RadrootsNostrSignerNip46ConnectDecision;
     79 
     80     fn connect_rate_limit_denied_reason(
     81         &self,
     82         client_public_key: &RadrootsNostrPublicKey,
     83     ) -> Option<String>;
     84 
     85     fn approval_requirement_for_client(
     86         &self,
     87         client_public_key: &RadrootsNostrPublicKey,
     88     ) -> Option<RadrootsNostrSignerApprovalRequirement>;
     89 
     90     fn filtered_requested_permissions(
     91         &self,
     92         requested_permissions: &RadrootsNostrConnectPermissions,
     93     ) -> RadrootsNostrConnectPermissions;
     94 
     95     fn auto_granted_permissions(
     96         &self,
     97         requested_permissions: &RadrootsNostrConnectPermissions,
     98     ) -> RadrootsNostrConnectPermissions;
     99 
    100     fn prepare_request(
    101         &self,
    102         backend: &B,
    103         connection: &RadrootsNostrSignerConnectionRecord,
    104         request_message: &RadrootsNostrConnectRequestMessage,
    105     ) -> Result<Option<String>, RadrootsNostrSignerError>;
    106 }
    107 
    108 #[derive(Clone)]
    109 pub struct RadrootsNostrSignerNip46Codec<S> {
    110     signer: S,
    111 }
    112 
    113 #[derive(Clone)]
    114 pub struct RadrootsNostrSignerNip46Handler<B, P, S> {
    115     backend: B,
    116     policy: P,
    117     relays: Vec<RadrootsNostrRelayUrl>,
    118     codec: RadrootsNostrSignerNip46Codec<S>,
    119 }
    120 
    121 #[derive(Debug, Clone, PartialEq, Eq)]
    122 pub enum RadrootsNostrSignerHandledRequest {
    123     Respond {
    124         response: Box<RadrootsNostrConnectResponse>,
    125         connection_id: Option<RadrootsNostrSignerConnectionId>,
    126         consume_connect_secret_for: Option<RadrootsNostrSignerConnectionId>,
    127     },
    128     Ignore,
    129 }
    130 
    131 #[derive(Debug, Clone)]
    132 pub struct RadrootsNostrSignerHandledRequestOutcome {
    133     pub handled_request: RadrootsNostrSignerHandledRequest,
    134     pub audit: Option<RadrootsNostrSignerRequestAuditRecord>,
    135 }
    136 
    137 enum RadrootsNostrSignerPreparedRequestEvaluation {
    138     Denied {
    139         reason: String,
    140         audit: RadrootsNostrSignerRequestAuditRecord,
    141     },
    142     Evaluation(Box<RadrootsNostrSignerRequestEvaluation>),
    143 }
    144 
    145 impl<S: RadrootsNostrSignerNip46Signer> RadrootsNostrSignerNip46Codec<S> {
    146     pub fn new(signer: S) -> Self {
    147         Self { signer }
    148     }
    149 
    150     pub fn filter(&self) -> Result<RadrootsNostrFilter, RadrootsNostrSignerError> {
    151         let filter = RadrootsNostrFilter::new()
    152             .kind(RadrootsNostrKind::Custom(RADROOTS_NOSTR_CONNECT_RPC_KIND))
    153             .since(RadrootsNostrTimestamp::now());
    154         Ok(radroots_nostr_filter_tag(
    155             filter,
    156             "p",
    157             vec![self.signer.signer_public_key_hex()],
    158         )?)
    159     }
    160 
    161     pub fn parse_request_event(
    162         &self,
    163         event: &RadrootsNostrEvent,
    164     ) -> Result<RadrootsNostrConnectRequestMessage, RadrootsNostrSignerError> {
    165         let decrypted = self.signer.decrypt_request(&event.pubkey, &event.content)?;
    166         Ok(serde_json::from_str(&decrypted).map_err(RadrootsNostrConnectError::from)?)
    167     }
    168 
    169     pub fn build_response_event(
    170         &self,
    171         client_public_key: RadrootsNostrPublicKey,
    172         request_id: impl Into<String>,
    173         response: RadrootsNostrConnectResponse,
    174     ) -> Result<RadrootsNostrEventBuilder, RadrootsNostrSignerError> {
    175         let envelope = response.into_envelope(request_id.into())?;
    176         let payload = serde_json::to_string(&envelope).map_err(RadrootsNostrConnectError::from)?;
    177         let ciphertext = self.signer.encrypt_response(&client_public_key, &payload)?;
    178 
    179         Ok(RadrootsNostrEventBuilder::new(
    180             RadrootsNostrKind::Custom(RADROOTS_NOSTR_CONNECT_RPC_KIND),
    181             ciphertext,
    182         )
    183         .tags(vec![RadrootsNostrTag::public_key(client_public_key)]))
    184     }
    185 
    186     pub fn sign_event_response(
    187         &self,
    188         unsigned_event: UnsignedEvent,
    189     ) -> Result<RadrootsNostrConnectResponse, RadrootsNostrSignerError> {
    190         let user_public_key = self.signer.user_identity().public_key_hex;
    191         if unsigned_event.pubkey.to_hex() != user_public_key {
    192             return Ok(RadrootsNostrConnectResponse::Error {
    193                 result: None,
    194                 error: "sign_event pubkey does not match the managed user identity".to_owned(),
    195             });
    196         }
    197 
    198         match self.signer.sign_user_event(unsigned_event) {
    199             Ok(event) => Ok(RadrootsNostrConnectResponse::SignedEvent(event)),
    200             Err(error) => Ok(RadrootsNostrConnectResponse::Error {
    201                 result: None,
    202                 error: format!("failed to sign event: {error}"),
    203             }),
    204         }
    205     }
    206 
    207     pub fn crypto_response(
    208         &self,
    209         request: RadrootsNostrConnectRequest,
    210     ) -> Result<RadrootsNostrConnectResponse, RadrootsNostrSignerError> {
    211         Ok(match request {
    212             RadrootsNostrConnectRequest::Nip04Encrypt {
    213                 public_key,
    214                 plaintext,
    215             } => match self.signer.nip04_encrypt(&public_key, &plaintext) {
    216                 Ok(ciphertext) => RadrootsNostrConnectResponse::Nip04Encrypt(ciphertext),
    217                 Err(error) => RadrootsNostrConnectResponse::Error {
    218                     result: None,
    219                     error: format!("nip04 encrypt failed: {error}"),
    220                 },
    221             },
    222             RadrootsNostrConnectRequest::Nip04Decrypt {
    223                 public_key,
    224                 ciphertext,
    225             } => match self.signer.nip04_decrypt(&public_key, &ciphertext) {
    226                 Ok(plaintext) => RadrootsNostrConnectResponse::Nip04Decrypt(plaintext),
    227                 Err(error) => RadrootsNostrConnectResponse::Error {
    228                     result: None,
    229                     error: format!("nip04 decrypt failed: {error}"),
    230                 },
    231             },
    232             RadrootsNostrConnectRequest::Nip44Encrypt {
    233                 public_key,
    234                 plaintext,
    235             } => match self.signer.nip44_encrypt(&public_key, &plaintext) {
    236                 Ok(ciphertext) => RadrootsNostrConnectResponse::Nip44Encrypt(ciphertext),
    237                 Err(error) => RadrootsNostrConnectResponse::Error {
    238                     result: None,
    239                     error: format!("nip44 encrypt failed: {error}"),
    240                 },
    241             },
    242             RadrootsNostrConnectRequest::Nip44Decrypt {
    243                 public_key,
    244                 ciphertext,
    245             } => match self.signer.nip44_decrypt(&public_key, &ciphertext) {
    246                 Ok(plaintext) => RadrootsNostrConnectResponse::Nip44Decrypt(plaintext),
    247                 Err(error) => RadrootsNostrConnectResponse::Error {
    248                     result: None,
    249                     error: format!("nip44 decrypt failed: {error}"),
    250                 },
    251             },
    252             other => RadrootsNostrConnectResponse::Error {
    253                 result: None,
    254                 error: format!("request `{}` is not a crypto method", other.method()),
    255             },
    256         })
    257     }
    258 }
    259 
    260 impl<B, P, S> RadrootsNostrSignerNip46Handler<B, P, S>
    261 where
    262     B: RadrootsNostrSignerBackend + Clone,
    263     P: RadrootsNostrSignerNip46Policy<B>,
    264     S: RadrootsNostrSignerNip46Signer,
    265 {
    266     pub fn new(backend: B, policy: P, relays: Vec<RadrootsNostrRelayUrl>, signer: S) -> Self {
    267         Self {
    268             backend,
    269             policy,
    270             relays,
    271             codec: RadrootsNostrSignerNip46Codec::new(signer),
    272         }
    273     }
    274 
    275     pub fn filter(&self) -> Result<RadrootsNostrFilter, RadrootsNostrSignerError> {
    276         self.codec.filter()
    277     }
    278 
    279     pub fn parse_request_event(
    280         &self,
    281         event: &RadrootsNostrEvent,
    282     ) -> Result<RadrootsNostrConnectRequestMessage, RadrootsNostrSignerError> {
    283         self.codec.parse_request_event(event)
    284     }
    285 
    286     pub fn build_response_event(
    287         &self,
    288         client_public_key: RadrootsNostrPublicKey,
    289         request_id: impl Into<String>,
    290         response: RadrootsNostrConnectResponse,
    291     ) -> Result<RadrootsNostrEventBuilder, RadrootsNostrSignerError> {
    292         self.codec
    293             .build_response_event(client_public_key, request_id, response)
    294     }
    295 
    296     pub fn handle_request(
    297         &self,
    298         client_public_key: RadrootsNostrPublicKey,
    299         request_message: RadrootsNostrConnectRequestMessage,
    300     ) -> Result<RadrootsNostrSignerHandledRequestOutcome, RadrootsNostrSignerError> {
    301         match request_message.request.clone() {
    302             RadrootsNostrConnectRequest::Connect { secret, .. } => {
    303                 self.handle_connect_request(client_public_key, request_message.request, secret)
    304             }
    305             RadrootsNostrConnectRequest::SignEvent(unsigned_event) => {
    306                 self.handle_sign_event_request(client_public_key, request_message, unsigned_event)
    307             }
    308             RadrootsNostrConnectRequest::Nip04Encrypt { .. }
    309             | RadrootsNostrConnectRequest::Nip04Decrypt { .. }
    310             | RadrootsNostrConnectRequest::Nip44Encrypt { .. }
    311             | RadrootsNostrConnectRequest::Nip44Decrypt { .. } => {
    312                 self.handle_crypto_request(client_public_key, request_message)
    313             }
    314             RadrootsNostrConnectRequest::GetPublicKey
    315             | RadrootsNostrConnectRequest::GetSessionCapability
    316             | RadrootsNostrConnectRequest::Ping
    317             | RadrootsNostrConnectRequest::SwitchRelays => {
    318                 self.handle_base_request(client_public_key, request_message)
    319             }
    320             _ => Ok(RadrootsNostrSignerHandledRequestOutcome::new(
    321                 RadrootsNostrSignerHandledRequest::respond(RadrootsNostrConnectResponse::Error {
    322                     result: None,
    323                     error: format!(
    324                         "method `{}` is not implemented yet",
    325                         request_message.request.method()
    326                     ),
    327                 }),
    328                 None,
    329             )),
    330         }
    331     }
    332 
    333     pub fn handle_authorized_request_evaluation(
    334         &self,
    335         request_message: RadrootsNostrConnectRequestMessage,
    336         evaluation: RadrootsNostrSignerRequestEvaluation,
    337     ) -> Result<RadrootsNostrSignerHandledRequestOutcome, RadrootsNostrSignerError> {
    338         let audit = evaluation.audit.clone();
    339         let handled_request = self.handled_request_for_evaluation(request_message, evaluation)?;
    340         Ok(RadrootsNostrSignerHandledRequestOutcome::new(
    341             handled_request,
    342             Some(audit),
    343         ))
    344     }
    345 
    346     fn handle_connect_request(
    347         &self,
    348         client_public_key: RadrootsNostrPublicKey,
    349         request: RadrootsNostrConnectRequest,
    350         secret: Option<String>,
    351     ) -> Result<RadrootsNostrSignerHandledRequestOutcome, RadrootsNostrSignerError> {
    352         let connect_decision = self.policy.connect_decision(&client_public_key);
    353         if let Some(connect_secret) = secret.as_deref()
    354             && let Some(connection) = self
    355                 .backend
    356                 .find_connection_by_connect_secret(connect_secret)?
    357             && connection.connect_secret_is_consumed()
    358         {
    359             return Ok(RadrootsNostrSignerHandledRequestOutcome::ignore());
    360         }
    361         if !matches!(
    362             connect_decision,
    363             RadrootsNostrSignerNip46ConnectDecision::Deny
    364         ) && let Some(reason) = self
    365             .policy
    366             .connect_rate_limit_denied_reason(&client_public_key)
    367         {
    368             return Ok(RadrootsNostrSignerHandledRequestOutcome::respond(
    369                 RadrootsNostrConnectResponse::Error {
    370                     result: None,
    371                     error: reason,
    372                 },
    373             ));
    374         }
    375 
    376         let evaluation = self
    377             .backend
    378             .evaluate_connect_request(client_public_key, request)?;
    379 
    380         match evaluation {
    381             RadrootsNostrSignerConnectEvaluation::ExistingConnection(connection) => {
    382                 if matches!(
    383                     connect_decision,
    384                     RadrootsNostrSignerNip46ConnectDecision::Deny
    385                 ) {
    386                     return Ok(RadrootsNostrSignerHandledRequestOutcome::respond(
    387                         RadrootsNostrConnectResponse::Error {
    388                             result: None,
    389                             error: "client public key denied by policy".to_owned(),
    390                         },
    391                     ));
    392                 }
    393                 Ok(RadrootsNostrSignerHandledRequestOutcome::new(
    394                     connect_response_outcome(&connection, secret),
    395                     None,
    396                 ))
    397             }
    398             RadrootsNostrSignerConnectEvaluation::RegistrationRequired(proposal) => {
    399                 let requested_permissions = self
    400                     .policy
    401                     .filtered_requested_permissions(&proposal.requested_permissions);
    402                 let Some(approval_requirement) = self
    403                     .policy
    404                     .approval_requirement_for_client(&client_public_key)
    405                 else {
    406                     return Ok(RadrootsNostrSignerHandledRequestOutcome::respond(
    407                         RadrootsNostrConnectResponse::Error {
    408                             result: None,
    409                             error: "client public key denied by policy".to_owned(),
    410                         },
    411                     ));
    412                 };
    413                 let draft = proposal
    414                     .into_connection_draft(self.codec.signer.user_identity())
    415                     .with_requested_permissions(requested_permissions)
    416                     .with_relays(self.relays.clone())
    417                     .with_approval_requirement(approval_requirement);
    418                 let connection = self.backend.register_connection(draft)?;
    419                 if approval_requirement == RadrootsNostrSignerApprovalRequirement::NotRequired {
    420                     let granted_permissions = self
    421                         .policy
    422                         .auto_granted_permissions(&connection.requested_permissions);
    423                     let _ = self
    424                         .backend
    425                         .set_granted_permissions(&connection.connection_id, granted_permissions)?;
    426                 }
    427                 Ok(RadrootsNostrSignerHandledRequestOutcome::new(
    428                     connect_response_outcome(&connection, secret),
    429                     None,
    430                 ))
    431             }
    432         }
    433     }
    434 
    435     fn handle_base_request(
    436         &self,
    437         client_public_key: RadrootsNostrPublicKey,
    438         request_message: RadrootsNostrConnectRequestMessage,
    439     ) -> Result<RadrootsNostrSignerHandledRequestOutcome, RadrootsNostrSignerError> {
    440         let connection = match self.lookup_connection(client_public_key)? {
    441             Ok(connection) => connection,
    442             Err(response) => {
    443                 return Ok(RadrootsNostrSignerHandledRequestOutcome::respond(response));
    444             }
    445         };
    446 
    447         match self.evaluate_request_with_policy(&connection, request_message)? {
    448             RadrootsNostrSignerPreparedRequestEvaluation::Denied { reason, audit } => {
    449                 Ok(RadrootsNostrSignerHandledRequestOutcome::new(
    450                     RadrootsNostrSignerHandledRequest::respond_for_connection(
    451                         Some(connection.connection_id.clone()),
    452                         RadrootsNostrConnectResponse::Error {
    453                             result: None,
    454                             error: reason,
    455                         },
    456                     ),
    457                     Some(audit),
    458                 ))
    459             }
    460             RadrootsNostrSignerPreparedRequestEvaluation::Evaluation(evaluation) => {
    461                 let evaluation = *evaluation;
    462                 let audit = evaluation.audit.clone();
    463                 let response_hint = match &evaluation.action {
    464                     RadrootsNostrSignerRequestAction::Allowed { response_hint, .. } => {
    465                         Some(response_hint.clone())
    466                     }
    467                     _ => None,
    468                 };
    469                 Ok(RadrootsNostrSignerHandledRequestOutcome::new(
    470                     handled_request_for_action(&evaluation.connection, evaluation.action, || {
    471                         Ok(response_from_hint(
    472                             &evaluation.connection,
    473                             response_hint.expect("allowed action carries response hint"),
    474                         ))
    475                     })?,
    476                     Some(audit),
    477                 ))
    478             }
    479         }
    480     }
    481 
    482     fn handle_sign_event_request(
    483         &self,
    484         client_public_key: RadrootsNostrPublicKey,
    485         request_message: RadrootsNostrConnectRequestMessage,
    486         unsigned_event: UnsignedEvent,
    487     ) -> Result<RadrootsNostrSignerHandledRequestOutcome, RadrootsNostrSignerError> {
    488         let connection = match self.lookup_connection(client_public_key)? {
    489             Ok(connection) => connection,
    490             Err(response) => {
    491                 return Ok(RadrootsNostrSignerHandledRequestOutcome::respond(response));
    492             }
    493         };
    494 
    495         match self.evaluate_request_with_policy(&connection, request_message)? {
    496             RadrootsNostrSignerPreparedRequestEvaluation::Denied { reason, audit } => {
    497                 Ok(RadrootsNostrSignerHandledRequestOutcome::new(
    498                     RadrootsNostrSignerHandledRequest::respond_for_connection(
    499                         Some(connection.connection_id.clone()),
    500                         RadrootsNostrConnectResponse::Error {
    501                             result: None,
    502                             error: reason,
    503                         },
    504                     ),
    505                     Some(audit),
    506                 ))
    507             }
    508             RadrootsNostrSignerPreparedRequestEvaluation::Evaluation(evaluation) => {
    509                 let evaluation = *evaluation;
    510                 Ok(RadrootsNostrSignerHandledRequestOutcome::new(
    511                     self.handled_request_for_authorized_action(
    512                         &evaluation.connection,
    513                         evaluation.action,
    514                         || self.codec.sign_event_response(unsigned_event),
    515                     )?,
    516                     Some(evaluation.audit),
    517                 ))
    518             }
    519         }
    520     }
    521 
    522     fn handle_crypto_request(
    523         &self,
    524         client_public_key: RadrootsNostrPublicKey,
    525         request_message: RadrootsNostrConnectRequestMessage,
    526     ) -> Result<RadrootsNostrSignerHandledRequestOutcome, RadrootsNostrSignerError> {
    527         let request = request_message.request.clone();
    528         let connection = match self.lookup_connection(client_public_key)? {
    529             Ok(connection) => connection,
    530             Err(response) => {
    531                 return Ok(RadrootsNostrSignerHandledRequestOutcome::respond(response));
    532             }
    533         };
    534 
    535         match self.evaluate_request_with_policy(&connection, request_message)? {
    536             RadrootsNostrSignerPreparedRequestEvaluation::Denied { reason, audit } => {
    537                 Ok(RadrootsNostrSignerHandledRequestOutcome::new(
    538                     RadrootsNostrSignerHandledRequest::respond_for_connection(
    539                         Some(connection.connection_id.clone()),
    540                         RadrootsNostrConnectResponse::Error {
    541                             result: None,
    542                             error: reason,
    543                         },
    544                     ),
    545                     Some(audit),
    546                 ))
    547             }
    548             RadrootsNostrSignerPreparedRequestEvaluation::Evaluation(evaluation) => {
    549                 let evaluation = *evaluation;
    550                 Ok(RadrootsNostrSignerHandledRequestOutcome::new(
    551                     self.handled_request_for_authorized_action(
    552                         &evaluation.connection,
    553                         evaluation.action,
    554                         || self.codec.crypto_response(request),
    555                     )?,
    556                     Some(evaluation.audit),
    557                 ))
    558             }
    559         }
    560     }
    561 
    562     fn handled_request_for_evaluation(
    563         &self,
    564         request_message: RadrootsNostrConnectRequestMessage,
    565         evaluation: RadrootsNostrSignerRequestEvaluation,
    566     ) -> Result<RadrootsNostrSignerHandledRequest, RadrootsNostrSignerError> {
    567         match request_message.request.clone() {
    568             RadrootsNostrConnectRequest::SignEvent(unsigned_event) => self
    569                 .handled_request_for_authorized_action(
    570                     &evaluation.connection,
    571                     evaluation.action,
    572                     || self.codec.sign_event_response(unsigned_event),
    573                 ),
    574             RadrootsNostrConnectRequest::Nip04Encrypt { .. }
    575             | RadrootsNostrConnectRequest::Nip04Decrypt { .. }
    576             | RadrootsNostrConnectRequest::Nip44Encrypt { .. }
    577             | RadrootsNostrConnectRequest::Nip44Decrypt { .. } => self
    578                 .handled_request_for_authorized_action(
    579                     &evaluation.connection,
    580                     evaluation.action,
    581                     || self.codec.crypto_response(request_message.request),
    582                 ),
    583             RadrootsNostrConnectRequest::GetPublicKey
    584             | RadrootsNostrConnectRequest::GetSessionCapability
    585             | RadrootsNostrConnectRequest::Ping
    586             | RadrootsNostrConnectRequest::SwitchRelays => {
    587                 let response_hint = match &evaluation.action {
    588                     RadrootsNostrSignerRequestAction::Allowed { response_hint, .. } => {
    589                         Some(response_hint.clone())
    590                     }
    591                     _ => None,
    592                 };
    593                 self.handled_request_for_authorized_action(
    594                     &evaluation.connection,
    595                     evaluation.action,
    596                     || {
    597                         Ok(response_from_hint(
    598                             &evaluation.connection,
    599                             response_hint.expect("allowed action carries response hint"),
    600                         ))
    601                     },
    602                 )
    603             }
    604             other => Ok(RadrootsNostrSignerHandledRequest::respond_for_connection(
    605                 Some(evaluation.connection.connection_id.clone()),
    606                 RadrootsNostrConnectResponse::Error {
    607                     result: None,
    608                     error: format!("method `{}` is not implemented yet", other.method()),
    609                 },
    610             )),
    611         }
    612     }
    613 
    614     fn handled_request_for_authorized_action<F>(
    615         &self,
    616         connection: &RadrootsNostrSignerConnectionRecord,
    617         action: RadrootsNostrSignerRequestAction,
    618         on_allowed: F,
    619     ) -> Result<RadrootsNostrSignerHandledRequest, RadrootsNostrSignerError>
    620     where
    621         F: FnOnce() -> Result<RadrootsNostrConnectResponse, RadrootsNostrSignerError>,
    622     {
    623         handled_request_for_action(connection, action, on_allowed)
    624     }
    625 
    626     fn evaluate_request_with_policy(
    627         &self,
    628         connection: &RadrootsNostrSignerConnectionRecord,
    629         request_message: RadrootsNostrConnectRequestMessage,
    630     ) -> Result<RadrootsNostrSignerPreparedRequestEvaluation, RadrootsNostrSignerError> {
    631         if let Some(reason) =
    632             self.policy
    633                 .prepare_request(&self.backend, connection, &request_message)?
    634         {
    635             let audit = self.backend.record_request(
    636                 &connection.connection_id,
    637                 &request_message.id,
    638                 request_message.request.method(),
    639                 RadrootsNostrSignerRequestDecision::Denied,
    640                 Some(reason.clone()),
    641             )?;
    642             return Ok(RadrootsNostrSignerPreparedRequestEvaluation::Denied { reason, audit });
    643         }
    644 
    645         Ok(RadrootsNostrSignerPreparedRequestEvaluation::Evaluation(
    646             Box::new(
    647                 self.backend
    648                     .evaluate_request(&connection.connection_id, request_message)?,
    649             ),
    650         ))
    651     }
    652 
    653     fn lookup_connection(
    654         &self,
    655         client_public_key: RadrootsNostrPublicKey,
    656     ) -> Result<
    657         Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrConnectResponse>,
    658         RadrootsNostrSignerError,
    659     > {
    660         Ok(
    661             match self.backend.lookup_session(&client_public_key, None)? {
    662                 RadrootsNostrSignerSessionLookup::Connection(connection) => Ok(*connection),
    663                 RadrootsNostrSignerSessionLookup::None => {
    664                     Err(RadrootsNostrConnectResponse::Error {
    665                         result: None,
    666                         error: "unauthorized".to_owned(),
    667                     })
    668                 }
    669                 RadrootsNostrSignerSessionLookup::Ambiguous(_) => {
    670                     Err(RadrootsNostrConnectResponse::Error {
    671                         result: None,
    672                         error: "ambiguous client sessions".to_owned(),
    673                     })
    674                 }
    675             },
    676         )
    677     }
    678 }
    679 
    680 impl RadrootsNostrSignerHandledRequest {
    681     pub fn respond(response: RadrootsNostrConnectResponse) -> Self {
    682         Self::respond_for_connection(None, response)
    683     }
    684 
    685     pub fn respond_for_connection(
    686         connection_id: Option<RadrootsNostrSignerConnectionId>,
    687         response: RadrootsNostrConnectResponse,
    688     ) -> Self {
    689         Self::Respond {
    690             response: Box::new(response),
    691             connection_id,
    692             consume_connect_secret_for: None,
    693         }
    694     }
    695 
    696     pub fn into_publish_parts(
    697         self,
    698     ) -> Option<(
    699         RadrootsNostrConnectResponse,
    700         Option<RadrootsNostrSignerConnectionId>,
    701         Option<RadrootsNostrSignerConnectionId>,
    702     )> {
    703         match self {
    704             Self::Respond {
    705                 response,
    706                 connection_id,
    707                 consume_connect_secret_for,
    708             } => Some((*response, connection_id, consume_connect_secret_for)),
    709             Self::Ignore => None,
    710         }
    711     }
    712 }
    713 
    714 impl RadrootsNostrSignerHandledRequestOutcome {
    715     pub fn new(
    716         handled_request: RadrootsNostrSignerHandledRequest,
    717         audit: Option<RadrootsNostrSignerRequestAuditRecord>,
    718     ) -> Self {
    719         Self {
    720             handled_request,
    721             audit,
    722         }
    723     }
    724 
    725     pub fn respond(response: RadrootsNostrConnectResponse) -> Self {
    726         Self::new(RadrootsNostrSignerHandledRequest::respond(response), None)
    727     }
    728 
    729     pub fn ignore() -> Self {
    730         Self::new(RadrootsNostrSignerHandledRequest::Ignore, None)
    731     }
    732 }
    733 
    734 pub fn connect_response_outcome(
    735     connection: &RadrootsNostrSignerConnectionRecord,
    736     secret: Option<String>,
    737 ) -> RadrootsNostrSignerHandledRequest {
    738     let consume_connect_secret_for = secret.as_ref().map(|_| connection.connection_id.clone());
    739     RadrootsNostrSignerHandledRequest::Respond {
    740         response: Box::new(match secret {
    741             Some(secret) => RadrootsNostrConnectResponse::ConnectSecretEcho(secret),
    742             None => RadrootsNostrConnectResponse::ConnectAcknowledged,
    743         }),
    744         connection_id: Some(connection.connection_id.clone()),
    745         consume_connect_secret_for,
    746     }
    747 }
    748 
    749 pub fn response_from_hint(
    750     connection: &RadrootsNostrSignerConnectionRecord,
    751     hint: RadrootsNostrSignerRequestResponseHint,
    752 ) -> RadrootsNostrConnectResponse {
    753     match hint {
    754         RadrootsNostrSignerRequestResponseHint::Pong => RadrootsNostrConnectResponse::Pong,
    755         RadrootsNostrSignerRequestResponseHint::UserPublicKey(public_key) => {
    756             RadrootsNostrConnectResponse::UserPublicKey(public_key)
    757         }
    758         RadrootsNostrSignerRequestResponseHint::RemoteSessionCapability(capability) => {
    759             RadrootsNostrConnectResponse::RemoteSessionCapability(capability)
    760         }
    761         RadrootsNostrSignerRequestResponseHint::RelayList(relays) => {
    762             if relays == connection.relays {
    763                 RadrootsNostrConnectResponse::RelayList(relays)
    764             } else {
    765                 RadrootsNostrConnectResponse::RelayList(connection.relays.clone())
    766             }
    767         }
    768         RadrootsNostrSignerRequestResponseHint::None => RadrootsNostrConnectResponse::Error {
    769             result: None,
    770             error: "request evaluation did not provide a response hint".to_owned(),
    771         },
    772     }
    773 }
    774 
    775 pub fn handled_request_for_action<F>(
    776     connection: &RadrootsNostrSignerConnectionRecord,
    777     action: RadrootsNostrSignerRequestAction,
    778     on_allowed: F,
    779 ) -> Result<RadrootsNostrSignerHandledRequest, RadrootsNostrSignerError>
    780 where
    781     F: FnOnce() -> Result<RadrootsNostrConnectResponse, RadrootsNostrSignerError>,
    782 {
    783     Ok(match action {
    784         RadrootsNostrSignerRequestAction::Denied { reason } => {
    785             RadrootsNostrSignerHandledRequest::respond_for_connection(
    786                 Some(connection.connection_id.clone()),
    787                 RadrootsNostrConnectResponse::Error {
    788                     result: None,
    789                     error: reason,
    790                 },
    791             )
    792         }
    793         RadrootsNostrSignerRequestAction::Challenged { auth_challenge, .. } => {
    794             RadrootsNostrSignerHandledRequest::respond_for_connection(
    795                 Some(connection.connection_id.clone()),
    796                 RadrootsNostrConnectResponse::AuthUrl(auth_challenge.auth_url),
    797             )
    798         }
    799         RadrootsNostrSignerRequestAction::Allowed { .. } => {
    800             RadrootsNostrSignerHandledRequest::respond_for_connection(
    801                 Some(connection.connection_id.clone()),
    802                 on_allowed()?,
    803             )
    804         }
    805     })
    806 }
    807 
    808 #[cfg(test)]
    809 #[cfg_attr(coverage_nightly, coverage(off))]
    810 mod tests {
    811     use super::{
    812         RadrootsNostrSignerHandledRequest, RadrootsNostrSignerHandledRequestOutcome,
    813         RadrootsNostrSignerNip46ConnectDecision, RadrootsNostrSignerNip46Handler,
    814         RadrootsNostrSignerNip46Policy, RadrootsNostrSignerNip46Signer,
    815     };
    816     use crate::backend::{RadrootsNostrEmbeddedSignerBackend, RadrootsNostrSignerBackend};
    817     use crate::error::RadrootsNostrSignerError;
    818     use crate::evaluation::{
    819         RadrootsNostrSignerRequestAction, RadrootsNostrSignerRequestResponseHint,
    820     };
    821     use crate::model::{
    822         RadrootsNostrSignerApprovalRequirement, RadrootsNostrSignerAuthChallenge,
    823         RadrootsNostrSignerAuthState, RadrootsNostrSignerConnectionDraft,
    824         RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerPendingRequest,
    825     };
    826     use crate::test_support::{fixture_alice_identity, fixture_carol_public_key, primary_relay};
    827     use nostr::{Keys, Timestamp, UnsignedEvent};
    828     use radroots_identity::{RadrootsIdentity, RadrootsIdentityPublic};
    829     use radroots_nostr::prelude::{
    830         RadrootsNostrEvent, RadrootsNostrEventBuilder, RadrootsNostrKind, RadrootsNostrPublicKey,
    831         RadrootsNostrTagKind,
    832     };
    833     use radroots_nostr_connect::prelude::{
    834         RADROOTS_NOSTR_CONNECT_RPC_KIND, RadrootsNostrConnectMethod,
    835         RadrootsNostrConnectPermission, RadrootsNostrConnectPermissions,
    836         RadrootsNostrConnectRemoteSessionCapability, RadrootsNostrConnectRequest,
    837         RadrootsNostrConnectRequestMessage, RadrootsNostrConnectResponse,
    838     };
    839 
    840     #[derive(Clone)]
    841     struct TestSigner {
    842         signer_identity: RadrootsIdentity,
    843         user_identity: RadrootsIdentity,
    844         sign_events: bool,
    845         fail_crypto: bool,
    846     }
    847 
    848     #[derive(Clone)]
    849     struct TestPolicy {
    850         connect_decision: RadrootsNostrSignerNip46ConnectDecision,
    851         rate_limit_reason: Option<&'static str>,
    852         approval_requirement: Option<RadrootsNostrSignerApprovalRequirement>,
    853         prepare_denial: Option<&'static str>,
    854     }
    855 
    856     impl Default for TestPolicy {
    857         fn default() -> Self {
    858             Self {
    859                 connect_decision: RadrootsNostrSignerNip46ConnectDecision::Allow,
    860                 rate_limit_reason: None,
    861                 approval_requirement: Some(RadrootsNostrSignerApprovalRequirement::NotRequired),
    862                 prepare_denial: None,
    863             }
    864         }
    865     }
    866 
    867     impl RadrootsNostrSignerNip46Signer for TestSigner {
    868         fn signer_public_key_hex(&self) -> String {
    869             self.signer_identity.public_key().to_hex()
    870         }
    871 
    872         fn decrypt_request(
    873             &self,
    874             _client_public_key: &RadrootsNostrPublicKey,
    875             ciphertext: &str,
    876         ) -> Result<String, RadrootsNostrSignerError> {
    877             Ok(ciphertext.to_owned())
    878         }
    879 
    880         fn encrypt_response(
    881             &self,
    882             _client_public_key: &RadrootsNostrPublicKey,
    883             payload: &str,
    884         ) -> Result<String, RadrootsNostrSignerError> {
    885             Ok(payload.to_owned())
    886         }
    887 
    888         fn user_identity(&self) -> RadrootsIdentityPublic {
    889             self.user_identity.to_public()
    890         }
    891 
    892         fn sign_user_event(
    893             &self,
    894             unsigned_event: UnsignedEvent,
    895         ) -> Result<RadrootsNostrEvent, RadrootsNostrSignerError> {
    896             if self.sign_events {
    897                 return unsigned_event
    898                     .sign_with_keys(self.user_identity.keys())
    899                     .map_err(|error| RadrootsNostrSignerError::Sign(error.to_string()));
    900             }
    901             Err(RadrootsNostrSignerError::Sign(
    902                 "test signer does not sign events".to_owned(),
    903             ))
    904         }
    905 
    906         fn nip04_encrypt(
    907             &self,
    908             _public_key: &RadrootsNostrPublicKey,
    909             plaintext: &str,
    910         ) -> Result<String, RadrootsNostrSignerError> {
    911             if self.fail_crypto {
    912                 return Err(RadrootsNostrSignerError::Sign(
    913                     "test crypto failure".to_owned(),
    914                 ));
    915             }
    916             Ok(plaintext.to_owned())
    917         }
    918 
    919         fn nip04_decrypt(
    920             &self,
    921             _public_key: &RadrootsNostrPublicKey,
    922             ciphertext: &str,
    923         ) -> Result<String, RadrootsNostrSignerError> {
    924             if self.fail_crypto {
    925                 return Err(RadrootsNostrSignerError::Sign(
    926                     "test crypto failure".to_owned(),
    927                 ));
    928             }
    929             Ok(ciphertext.to_owned())
    930         }
    931 
    932         fn nip44_encrypt(
    933             &self,
    934             _public_key: &RadrootsNostrPublicKey,
    935             plaintext: &str,
    936         ) -> Result<String, RadrootsNostrSignerError> {
    937             if self.fail_crypto {
    938                 return Err(RadrootsNostrSignerError::Sign(
    939                     "test crypto failure".to_owned(),
    940                 ));
    941             }
    942             Ok(plaintext.to_owned())
    943         }
    944 
    945         fn nip44_decrypt(
    946             &self,
    947             _public_key: &RadrootsNostrPublicKey,
    948             ciphertext: &str,
    949         ) -> Result<String, RadrootsNostrSignerError> {
    950             if self.fail_crypto {
    951                 return Err(RadrootsNostrSignerError::Sign(
    952                     "test crypto failure".to_owned(),
    953                 ));
    954             }
    955             Ok(ciphertext.to_owned())
    956         }
    957     }
    958 
    959     impl<B: RadrootsNostrSignerBackend> RadrootsNostrSignerNip46Policy<B> for TestPolicy {
    960         fn connect_decision(
    961             &self,
    962             _client_public_key: &RadrootsNostrPublicKey,
    963         ) -> RadrootsNostrSignerNip46ConnectDecision {
    964             self.connect_decision
    965         }
    966 
    967         fn connect_rate_limit_denied_reason(
    968             &self,
    969             _client_public_key: &RadrootsNostrPublicKey,
    970         ) -> Option<String> {
    971             self.rate_limit_reason.map(ToOwned::to_owned)
    972         }
    973 
    974         fn approval_requirement_for_client(
    975             &self,
    976             _client_public_key: &RadrootsNostrPublicKey,
    977         ) -> Option<RadrootsNostrSignerApprovalRequirement> {
    978             self.approval_requirement
    979         }
    980 
    981         fn filtered_requested_permissions(
    982             &self,
    983             requested_permissions: &RadrootsNostrConnectPermissions,
    984         ) -> RadrootsNostrConnectPermissions {
    985             requested_permissions.clone()
    986         }
    987 
    988         fn auto_granted_permissions(
    989             &self,
    990             requested_permissions: &RadrootsNostrConnectPermissions,
    991         ) -> RadrootsNostrConnectPermissions {
    992             requested_permissions.clone()
    993         }
    994 
    995         fn prepare_request(
    996             &self,
    997             _backend: &B,
    998             _connection: &crate::model::RadrootsNostrSignerConnectionRecord,
    999             _request_message: &RadrootsNostrConnectRequestMessage,
   1000         ) -> Result<Option<String>, RadrootsNostrSignerError> {
   1001             Ok(self.prepare_denial.map(ToOwned::to_owned))
   1002         }
   1003     }
   1004 
   1005     fn test_signer() -> TestSigner {
   1006         test_signer_with_options(false, false)
   1007     }
   1008 
   1009     fn test_signer_with_options(sign_events: bool, fail_crypto: bool) -> TestSigner {
   1010         TestSigner {
   1011             signer_identity: RadrootsIdentity::from_secret_key_str(
   1012                 "1111111111111111111111111111111111111111111111111111111111111111",
   1013             )
   1014             .expect("signer identity"),
   1015             user_identity: RadrootsIdentity::from_secret_key_str(
   1016                 "2222222222222222222222222222222222222222222222222222222222222222",
   1017             )
   1018             .expect("user identity"),
   1019             sign_events,
   1020             fail_crypto,
   1021         }
   1022     }
   1023 
   1024     fn embedded_backend() -> RadrootsNostrEmbeddedSignerBackend {
   1025         RadrootsNostrEmbeddedSignerBackend::new(
   1026             crate::manager::RadrootsNostrSignerManager::new_in_memory(),
   1027             test_signer().signer_identity.clone(),
   1028         )
   1029         .expect("embedded backend")
   1030     }
   1031 
   1032     fn handler_with_backend(
   1033         backend: RadrootsNostrEmbeddedSignerBackend,
   1034     ) -> RadrootsNostrSignerNip46Handler<RadrootsNostrEmbeddedSignerBackend, TestPolicy, TestSigner>
   1035     {
   1036         handler_with_policy(backend, TestPolicy::default())
   1037     }
   1038 
   1039     fn handler_with_policy(
   1040         backend: RadrootsNostrEmbeddedSignerBackend,
   1041         policy: TestPolicy,
   1042     ) -> RadrootsNostrSignerNip46Handler<RadrootsNostrEmbeddedSignerBackend, TestPolicy, TestSigner>
   1043     {
   1044         RadrootsNostrSignerNip46Handler::new(backend, policy, vec![primary_relay()], test_signer())
   1045     }
   1046 
   1047     fn connect_request(secret: Option<&str>) -> RadrootsNostrConnectRequestMessage {
   1048         connect_request_with_permissions(
   1049             secret,
   1050             vec![RadrootsNostrConnectPermission::new(
   1051                 RadrootsNostrConnectMethod::Nip04Encrypt,
   1052             )],
   1053         )
   1054     }
   1055 
   1056     fn connect_request_with_permissions(
   1057         secret: Option<&str>,
   1058         permissions: Vec<RadrootsNostrConnectPermission>,
   1059     ) -> RadrootsNostrConnectRequestMessage {
   1060         let signer_public_key = test_signer().signer_identity.public_key();
   1061         RadrootsNostrConnectRequestMessage::new(
   1062             "req-connect",
   1063             RadrootsNostrConnectRequest::Connect {
   1064                 remote_signer_public_key: signer_public_key,
   1065                 secret: secret.map(ToOwned::to_owned),
   1066                 requested_permissions: permissions.into(),
   1067             },
   1068         )
   1069     }
   1070 
   1071     fn all_runtime_permissions() -> Vec<RadrootsNostrConnectPermission> {
   1072         vec![
   1073             RadrootsNostrConnectPermission::new(RadrootsNostrConnectMethod::SignEvent),
   1074             RadrootsNostrConnectPermission::new(RadrootsNostrConnectMethod::Nip04Encrypt),
   1075             RadrootsNostrConnectPermission::new(RadrootsNostrConnectMethod::Nip04Decrypt),
   1076             RadrootsNostrConnectPermission::new(RadrootsNostrConnectMethod::Nip44Encrypt),
   1077             RadrootsNostrConnectPermission::new(RadrootsNostrConnectMethod::Nip44Decrypt),
   1078             RadrootsNostrConnectPermission::new(RadrootsNostrConnectMethod::SwitchRelays),
   1079         ]
   1080     }
   1081 
   1082     fn request_message(
   1083         id: &str,
   1084         request: RadrootsNostrConnectRequest,
   1085     ) -> RadrootsNostrConnectRequestMessage {
   1086         RadrootsNostrConnectRequestMessage::new(id, request)
   1087     }
   1088 
   1089     fn unsigned_user_event(kind: u16) -> UnsignedEvent {
   1090         serde_json::from_value(serde_json::json!({
   1091             "pubkey": test_signer().user_identity.public_key().to_hex(),
   1092             "created_at": Timestamp::from(1).as_secs(),
   1093             "kind": kind,
   1094             "tags": [],
   1095             "content": "hello",
   1096         }))
   1097         .expect("unsigned event")
   1098     }
   1099 
   1100     fn registered_connection(
   1101         backend: &RadrootsNostrEmbeddedSignerBackend,
   1102         client_public_key: &RadrootsNostrPublicKey,
   1103     ) -> RadrootsNostrSignerConnectionRecord {
   1104         backend
   1105             .find_connections_by_client_public_key(client_public_key)
   1106             .expect("connections")
   1107             .into_iter()
   1108             .next()
   1109             .expect("connection")
   1110     }
   1111 
   1112     fn connect_with_permissions(
   1113         handler: &RadrootsNostrSignerNip46Handler<
   1114             RadrootsNostrEmbeddedSignerBackend,
   1115             TestPolicy,
   1116             TestSigner,
   1117         >,
   1118         client_public_key: RadrootsNostrPublicKey,
   1119         permissions: Vec<RadrootsNostrConnectPermission>,
   1120     ) {
   1121         let outcome = handler
   1122             .handle_request(
   1123                 client_public_key,
   1124                 connect_request_with_permissions(None, permissions),
   1125             )
   1126             .expect("connect");
   1127         assert!(matches!(
   1128             outcome.handled_request,
   1129             RadrootsNostrSignerHandledRequest::Respond { .. }
   1130         ));
   1131     }
   1132 
   1133     fn response_from_outcome(
   1134         outcome: RadrootsNostrSignerHandledRequestOutcome,
   1135     ) -> RadrootsNostrConnectResponse {
   1136         match outcome.handled_request {
   1137             RadrootsNostrSignerHandledRequest::Respond { response, .. } => *response,
   1138             other => panic!("unexpected handled request: {other:?}"),
   1139         }
   1140     }
   1141 
   1142     #[test]
   1143     fn codec_and_handler_facades_cover_rpc_event_surface() {
   1144         let codec = super::RadrootsNostrSignerNip46Codec::new(test_signer());
   1145         let _ = codec.filter().expect("codec filter");
   1146         let client_public_key = fixture_carol_public_key();
   1147         let request = request_message("req-parse", RadrootsNostrConnectRequest::Ping);
   1148         let raw = serde_json::to_string(&request).expect("serialize request");
   1149         let event = RadrootsNostrEventBuilder::new(
   1150             RadrootsNostrKind::Custom(RADROOTS_NOSTR_CONNECT_RPC_KIND),
   1151             raw,
   1152         )
   1153         .sign_with_keys(&Keys::generate())
   1154         .expect("sign request event");
   1155 
   1156         let parsed = codec.parse_request_event(&event).expect("parse request");
   1157         assert_eq!(parsed, request);
   1158 
   1159         let response_builder = codec
   1160             .build_response_event(
   1161                 client_public_key,
   1162                 "req-parse",
   1163                 RadrootsNostrConnectResponse::Pong,
   1164             )
   1165             .expect("response builder");
   1166         let response_event = response_builder.build(test_signer().signer_identity.public_key());
   1167         assert_eq!(
   1168             response_event.kind,
   1169             RadrootsNostrKind::Custom(RADROOTS_NOSTR_CONNECT_RPC_KIND)
   1170         );
   1171         assert!(response_event.tags.iter().any(|tag| {
   1172             tag.kind() == RadrootsNostrTagKind::p()
   1173                 && tag.content() == Some(client_public_key.to_hex().as_str())
   1174         }));
   1175 
   1176         let handler = handler_with_backend(embedded_backend());
   1177         let _ = handler.filter().expect("handler filter");
   1178         assert_eq!(
   1179             handler.parse_request_event(&event).expect("handler parse"),
   1180             request
   1181         );
   1182         let handler_event = handler
   1183             .build_response_event(
   1184                 client_public_key,
   1185                 "req-handler",
   1186                 RadrootsNostrConnectResponse::ConnectAcknowledged,
   1187             )
   1188             .expect("handler response")
   1189             .build(test_signer().signer_identity.public_key());
   1190         assert_eq!(
   1191             handler_event.kind,
   1192             RadrootsNostrKind::Custom(RADROOTS_NOSTR_CONNECT_RPC_KIND)
   1193         );
   1194     }
   1195 
   1196     #[test]
   1197     fn codec_crypto_and_signing_responses_cover_method_matrix() {
   1198         let codec = super::RadrootsNostrSignerNip46Codec::new(test_signer());
   1199         let client_public_key = fixture_carol_public_key();
   1200 
   1201         assert_eq!(
   1202             codec
   1203                 .crypto_response(RadrootsNostrConnectRequest::Nip04Encrypt {
   1204                     public_key: client_public_key,
   1205                     plaintext: "plain".to_owned(),
   1206                 })
   1207                 .expect("nip04 encrypt"),
   1208             RadrootsNostrConnectResponse::Nip04Encrypt("plain".to_owned())
   1209         );
   1210         assert_eq!(
   1211             codec
   1212                 .crypto_response(RadrootsNostrConnectRequest::Nip04Decrypt {
   1213                     public_key: client_public_key,
   1214                     ciphertext: "cipher".to_owned(),
   1215                 })
   1216                 .expect("nip04 decrypt"),
   1217             RadrootsNostrConnectResponse::Nip04Decrypt("cipher".to_owned())
   1218         );
   1219         assert_eq!(
   1220             codec
   1221                 .crypto_response(RadrootsNostrConnectRequest::Nip44Encrypt {
   1222                     public_key: client_public_key,
   1223                     plaintext: "plain44".to_owned(),
   1224                 })
   1225                 .expect("nip44 encrypt"),
   1226             RadrootsNostrConnectResponse::Nip44Encrypt("plain44".to_owned())
   1227         );
   1228         assert_eq!(
   1229             codec
   1230                 .crypto_response(RadrootsNostrConnectRequest::Nip44Decrypt {
   1231                     public_key: client_public_key,
   1232                     ciphertext: "cipher44".to_owned(),
   1233                 })
   1234                 .expect("nip44 decrypt"),
   1235             RadrootsNostrConnectResponse::Nip44Decrypt("cipher44".to_owned())
   1236         );
   1237 
   1238         let non_crypto = codec
   1239             .crypto_response(RadrootsNostrConnectRequest::Ping)
   1240             .expect("non crypto response");
   1241         assert!(matches!(
   1242             non_crypto,
   1243             RadrootsNostrConnectResponse::Error { .. }
   1244         ));
   1245 
   1246         let failing_codec =
   1247             super::RadrootsNostrSignerNip46Codec::new(test_signer_with_options(false, true));
   1248         for request in [
   1249             RadrootsNostrConnectRequest::Nip04Encrypt {
   1250                 public_key: client_public_key,
   1251                 plaintext: "plain".to_owned(),
   1252             },
   1253             RadrootsNostrConnectRequest::Nip04Decrypt {
   1254                 public_key: client_public_key,
   1255                 ciphertext: "cipher".to_owned(),
   1256             },
   1257             RadrootsNostrConnectRequest::Nip44Encrypt {
   1258                 public_key: client_public_key,
   1259                 plaintext: "plain44".to_owned(),
   1260             },
   1261             RadrootsNostrConnectRequest::Nip44Decrypt {
   1262                 public_key: client_public_key,
   1263                 ciphertext: "cipher44".to_owned(),
   1264             },
   1265         ] {
   1266             assert!(matches!(
   1267                 failing_codec
   1268                     .crypto_response(request)
   1269                     .expect("failing crypto response"),
   1270                 RadrootsNostrConnectResponse::Error { .. }
   1271             ));
   1272         }
   1273 
   1274         let signing = codec
   1275             .sign_event_response(unsigned_user_event(1))
   1276             .expect("signing response");
   1277         match signing {
   1278             RadrootsNostrConnectResponse::Error { error, .. } => {
   1279                 assert!(error.contains("failed to sign event"));
   1280             }
   1281             other => panic!("unexpected sign response: {other:?}"),
   1282         }
   1283 
   1284         let signed =
   1285             super::RadrootsNostrSignerNip46Codec::new(test_signer_with_options(true, false))
   1286                 .sign_event_response(unsigned_user_event(1))
   1287                 .expect("signed response");
   1288         assert!(matches!(
   1289             signed,
   1290             RadrootsNostrConnectResponse::SignedEvent(_)
   1291         ));
   1292     }
   1293 
   1294     #[test]
   1295     fn handler_connect_policy_paths_cover_registration_branches() {
   1296         let client_public_key = fixture_carol_public_key();
   1297 
   1298         let rate_limited = handler_with_policy(
   1299             embedded_backend(),
   1300             TestPolicy {
   1301                 rate_limit_reason: Some("slow down"),
   1302                 ..TestPolicy::default()
   1303             },
   1304         )
   1305         .handle_request(client_public_key, connect_request(None))
   1306         .expect("rate limit outcome");
   1307         assert_eq!(
   1308             response_from_outcome(rate_limited),
   1309             RadrootsNostrConnectResponse::Error {
   1310                 result: None,
   1311                 error: "slow down".to_owned(),
   1312             }
   1313         );
   1314 
   1315         let denied_registration = handler_with_policy(
   1316             embedded_backend(),
   1317             TestPolicy {
   1318                 approval_requirement: None,
   1319                 ..TestPolicy::default()
   1320             },
   1321         )
   1322         .handle_request(client_public_key, connect_request(None))
   1323         .expect("registration denial");
   1324         assert_eq!(
   1325             response_from_outcome(denied_registration),
   1326             RadrootsNostrConnectResponse::Error {
   1327                 result: None,
   1328                 error: "client public key denied by policy".to_owned(),
   1329             }
   1330         );
   1331 
   1332         let approval_backend = embedded_backend();
   1333         let approval_handler = handler_with_policy(
   1334             approval_backend.clone(),
   1335             TestPolicy {
   1336                 approval_requirement: Some(RadrootsNostrSignerApprovalRequirement::ExplicitUser),
   1337                 ..TestPolicy::default()
   1338             },
   1339         );
   1340         let _ = approval_handler
   1341             .handle_request(client_public_key, connect_request(None))
   1342             .expect("approval connect");
   1343         let approval_connection = registered_connection(&approval_backend, &client_public_key);
   1344         assert_eq!(
   1345             approval_connection.approval_requirement,
   1346             RadrootsNostrSignerApprovalRequirement::ExplicitUser
   1347         );
   1348     }
   1349 
   1350     #[test]
   1351     fn handler_connect_existing_connection_paths_cover_policy_edges() {
   1352         let client_public_key = fixture_carol_public_key();
   1353         let secret = "connect-secret";
   1354         let existing_backend = embedded_backend();
   1355         let existing_handler = handler_with_backend(existing_backend.clone());
   1356 
   1357         let first = existing_handler
   1358             .handle_request(client_public_key, connect_request(Some(secret)))
   1359             .expect("initial connect");
   1360         assert_eq!(
   1361             response_from_outcome(first),
   1362             RadrootsNostrConnectResponse::ConnectSecretEcho(secret.to_owned())
   1363         );
   1364         let existing = existing_handler
   1365             .handle_request(client_public_key, connect_request(Some(secret)))
   1366             .expect("existing connect by secret");
   1367         assert_eq!(
   1368             response_from_outcome(existing),
   1369             RadrootsNostrConnectResponse::ConnectSecretEcho(secret.to_owned())
   1370         );
   1371 
   1372         let denied_backend = embedded_backend();
   1373         let denied_handler = handler_with_backend(denied_backend.clone());
   1374         let _ = denied_handler
   1375             .handle_request(client_public_key, connect_request(Some(secret)))
   1376             .expect("denied seed connect");
   1377         let denying_handler = handler_with_policy(
   1378             denied_backend,
   1379             TestPolicy {
   1380                 connect_decision: RadrootsNostrSignerNip46ConnectDecision::Deny,
   1381                 ..TestPolicy::default()
   1382             },
   1383         );
   1384         let denied = denying_handler
   1385             .handle_request(client_public_key, connect_request(Some(secret)))
   1386             .expect("existing connect denied");
   1387         assert_eq!(
   1388             response_from_outcome(denied),
   1389             RadrootsNostrConnectResponse::Error {
   1390                 result: None,
   1391                 error: "client public key denied by policy".to_owned(),
   1392             }
   1393         );
   1394     }
   1395 
   1396     #[test]
   1397     fn handler_request_paths_cover_base_sign_crypto_denied_and_challenged() {
   1398         let backend = embedded_backend();
   1399         let handler = handler_with_backend(backend.clone());
   1400         let client_public_key = fixture_carol_public_key();
   1401         connect_with_permissions(&handler, client_public_key, all_runtime_permissions());
   1402 
   1403         assert!(matches!(
   1404             response_from_outcome(
   1405                 handler
   1406                     .handle_request(
   1407                         client_public_key,
   1408                         request_message("req-pubkey", RadrootsNostrConnectRequest::GetPublicKey),
   1409                     )
   1410                     .expect("pubkey")
   1411             ),
   1412             RadrootsNostrConnectResponse::UserPublicKey(_)
   1413         ));
   1414         assert!(matches!(
   1415             response_from_outcome(
   1416                 handler
   1417                     .handle_request(
   1418                         client_public_key,
   1419                         request_message(
   1420                             "req-capability",
   1421                             RadrootsNostrConnectRequest::GetSessionCapability,
   1422                         ),
   1423                     )
   1424                     .expect("capability")
   1425             ),
   1426             RadrootsNostrConnectResponse::RemoteSessionCapability(_)
   1427         ));
   1428         assert_eq!(
   1429             response_from_outcome(
   1430                 handler
   1431                     .handle_request(
   1432                         client_public_key,
   1433                         request_message("req-relays", RadrootsNostrConnectRequest::SwitchRelays),
   1434                     )
   1435                     .expect("relays")
   1436             ),
   1437             RadrootsNostrConnectResponse::RelayList(vec![primary_relay()])
   1438         );
   1439         assert!(matches!(
   1440             response_from_outcome(
   1441                 handler
   1442                     .handle_request(
   1443                         client_public_key,
   1444                         request_message(
   1445                             "req-sign",
   1446                             RadrootsNostrConnectRequest::SignEvent(unsigned_user_event(1)),
   1447                         ),
   1448                     )
   1449                     .expect("sign")
   1450             ),
   1451             RadrootsNostrConnectResponse::Error { .. }
   1452         ));
   1453         assert_eq!(
   1454             response_from_outcome(
   1455                 handler
   1456                     .handle_request(
   1457                         client_public_key,
   1458                         request_message(
   1459                             "req-nip04-decrypt",
   1460                             RadrootsNostrConnectRequest::Nip04Decrypt {
   1461                                 public_key: client_public_key,
   1462                                 ciphertext: "cipher".to_owned(),
   1463                             },
   1464                         ),
   1465                     )
   1466                     .expect("nip04 decrypt")
   1467             ),
   1468             RadrootsNostrConnectResponse::Nip04Decrypt("cipher".to_owned())
   1469         );
   1470         assert_eq!(
   1471             response_from_outcome(
   1472                 handler
   1473                     .handle_request(
   1474                         client_public_key,
   1475                         request_message(
   1476                             "req-nip44-encrypt",
   1477                             RadrootsNostrConnectRequest::Nip44Encrypt {
   1478                                 public_key: client_public_key,
   1479                                 plaintext: "plain".to_owned(),
   1480                             },
   1481                         ),
   1482                     )
   1483                     .expect("nip44 encrypt")
   1484             ),
   1485             RadrootsNostrConnectResponse::Nip44Encrypt("plain".to_owned())
   1486         );
   1487 
   1488         let unimplemented = handler
   1489             .handle_request(
   1490                 client_public_key,
   1491                 request_message(
   1492                     "req-custom",
   1493                     RadrootsNostrConnectRequest::Custom {
   1494                         method: RadrootsNostrConnectMethod::Custom("publish_note".to_owned()),
   1495                         params: vec![],
   1496                     },
   1497                 ),
   1498             )
   1499             .expect("custom");
   1500         assert!(matches!(
   1501             response_from_outcome(unimplemented),
   1502             RadrootsNostrConnectResponse::Error { .. }
   1503         ));
   1504 
   1505         let limited_backend = embedded_backend();
   1506         let limited_handler = handler_with_backend(limited_backend);
   1507         connect_with_permissions(
   1508             &limited_handler,
   1509             client_public_key,
   1510             vec![RadrootsNostrConnectPermission::new(
   1511                 RadrootsNostrConnectMethod::Nip04Encrypt,
   1512             )],
   1513         );
   1514         let denied_crypto = limited_handler
   1515             .handle_request(
   1516                 client_public_key,
   1517                 request_message(
   1518                     "req-denied",
   1519                     RadrootsNostrConnectRequest::Nip04Decrypt {
   1520                         public_key: client_public_key,
   1521                         ciphertext: "cipher".to_owned(),
   1522                     },
   1523                 ),
   1524             )
   1525             .expect("denied crypto");
   1526         assert!(matches!(
   1527             response_from_outcome(denied_crypto),
   1528             RadrootsNostrConnectResponse::Error { .. }
   1529         ));
   1530 
   1531         let denied_backend = embedded_backend();
   1532         let open_handler = handler_with_backend(denied_backend.clone());
   1533         connect_with_permissions(&open_handler, client_public_key, all_runtime_permissions());
   1534         let denying_handler = handler_with_policy(
   1535             denied_backend,
   1536             TestPolicy {
   1537                 prepare_denial: Some("policy blocked"),
   1538                 ..TestPolicy::default()
   1539             },
   1540         );
   1541         let denied_base = denying_handler
   1542             .handle_request(
   1543                 client_public_key,
   1544                 request_message("req-policy-denied", RadrootsNostrConnectRequest::Ping),
   1545             )
   1546             .expect("policy denied");
   1547         assert!(matches!(
   1548             response_from_outcome(denied_base),
   1549             RadrootsNostrConnectResponse::Error { .. }
   1550         ));
   1551         let denied_sign = denying_handler
   1552             .handle_request(
   1553                 client_public_key,
   1554                 request_message(
   1555                     "req-policy-denied-sign",
   1556                     RadrootsNostrConnectRequest::SignEvent(unsigned_user_event(1)),
   1557                 ),
   1558             )
   1559             .expect("policy denied sign");
   1560         assert!(matches!(
   1561             response_from_outcome(denied_sign),
   1562             RadrootsNostrConnectResponse::Error { .. }
   1563         ));
   1564         let denied_crypto = denying_handler
   1565             .handle_request(
   1566                 client_public_key,
   1567                 request_message(
   1568                     "req-policy-denied-crypto",
   1569                     RadrootsNostrConnectRequest::Nip44Encrypt {
   1570                         public_key: client_public_key,
   1571                         plaintext: "plain".to_owned(),
   1572                     },
   1573                 ),
   1574             )
   1575             .expect("policy denied crypto");
   1576         assert!(matches!(
   1577             response_from_outcome(denied_crypto),
   1578             RadrootsNostrConnectResponse::Error { .. }
   1579         ));
   1580 
   1581         let challenge_backend = embedded_backend();
   1582         let challenge_handler = handler_with_backend(challenge_backend.clone());
   1583         connect_with_permissions(
   1584             &challenge_handler,
   1585             client_public_key,
   1586             all_runtime_permissions(),
   1587         );
   1588         let challenged = registered_connection(&challenge_backend, &client_public_key);
   1589         challenge_backend
   1590             .manager()
   1591             .require_auth_challenge(&challenged.connection_id, "https://example.test/auth")
   1592             .expect("require challenge");
   1593         let auth_url = challenge_handler
   1594             .handle_request(
   1595                 client_public_key,
   1596                 request_message("req-challenge", RadrootsNostrConnectRequest::Ping),
   1597             )
   1598             .expect("challenge");
   1599         assert_eq!(
   1600             response_from_outcome(auth_url),
   1601             RadrootsNostrConnectResponse::AuthUrl("https://example.test/auth".to_owned())
   1602         );
   1603     }
   1604 
   1605     #[test]
   1606     fn handler_rejects_unauthorized_base_sign_and_crypto_requests() {
   1607         let handler = handler_with_backend(embedded_backend());
   1608         let client_public_key = fixture_carol_public_key();
   1609 
   1610         for request in [
   1611             RadrootsNostrConnectRequest::Ping,
   1612             RadrootsNostrConnectRequest::SignEvent(unsigned_user_event(1)),
   1613             RadrootsNostrConnectRequest::Nip04Decrypt {
   1614                 public_key: client_public_key,
   1615                 ciphertext: "cipher".to_owned(),
   1616             },
   1617         ] {
   1618             let outcome = handler
   1619                 .handle_request(
   1620                     client_public_key,
   1621                     request_message("req-unauthorized", request),
   1622                 )
   1623                 .expect("unauthorized request");
   1624             assert_eq!(
   1625                 response_from_outcome(outcome),
   1626                 RadrootsNostrConnectResponse::Error {
   1627                     result: None,
   1628                     error: "unauthorized".to_owned(),
   1629                 }
   1630             );
   1631         }
   1632     }
   1633 
   1634     #[test]
   1635     fn handler_allowed_sign_and_crypto_requests_execute_codec_paths() {
   1636         let backend = embedded_backend();
   1637         let handler = RadrootsNostrSignerNip46Handler::new(
   1638             backend,
   1639             TestPolicy::default(),
   1640             vec![primary_relay()],
   1641             test_signer_with_options(true, false),
   1642         );
   1643         let client_public_key = fixture_carol_public_key();
   1644         connect_with_permissions(
   1645             &handler,
   1646             client_public_key,
   1647             vec![
   1648                 RadrootsNostrConnectPermission::with_parameter(
   1649                     RadrootsNostrConnectMethod::SignEvent,
   1650                     "kind:1",
   1651                 ),
   1652                 RadrootsNostrConnectPermission::new(RadrootsNostrConnectMethod::Nip04Encrypt),
   1653             ],
   1654         );
   1655 
   1656         assert!(matches!(
   1657             response_from_outcome(
   1658                 handler
   1659                     .handle_request(
   1660                         client_public_key,
   1661                         request_message(
   1662                             "req-allowed-sign",
   1663                             RadrootsNostrConnectRequest::SignEvent(unsigned_user_event(1)),
   1664                         ),
   1665                     )
   1666                     .expect("allowed sign")
   1667             ),
   1668             RadrootsNostrConnectResponse::SignedEvent(_)
   1669         ));
   1670         assert_eq!(
   1671             response_from_outcome(
   1672                 handler
   1673                     .handle_request(
   1674                         client_public_key,
   1675                         request_message(
   1676                             "req-allowed-nip04-encrypt",
   1677                             RadrootsNostrConnectRequest::Nip04Encrypt {
   1678                                 public_key: client_public_key,
   1679                                 plaintext: "plain".to_owned(),
   1680                             },
   1681                         ),
   1682                     )
   1683                     .expect("allowed nip04 encrypt")
   1684             ),
   1685             RadrootsNostrConnectResponse::Nip04Encrypt("plain".to_owned())
   1686         );
   1687     }
   1688 
   1689     #[test]
   1690     fn handler_authorized_evaluation_facade_covers_request_variants() {
   1691         let backend = embedded_backend();
   1692         let handler = handler_with_backend(backend.clone());
   1693         let client_public_key = fixture_carol_public_key();
   1694         connect_with_permissions(&handler, client_public_key, all_runtime_permissions());
   1695         let connection = registered_connection(&backend, &client_public_key);
   1696 
   1697         let base = request_message("req-eval-ping", RadrootsNostrConnectRequest::Ping);
   1698         let base_eval = backend
   1699             .evaluate_request(&connection.connection_id, base.clone())
   1700             .expect("base evaluation");
   1701         assert_eq!(
   1702             response_from_outcome(
   1703                 handler
   1704                     .handle_authorized_request_evaluation(base, base_eval)
   1705                     .expect("base authorized")
   1706             ),
   1707             RadrootsNostrConnectResponse::Pong
   1708         );
   1709         let mut denied_base_eval = backend
   1710             .evaluate_request(
   1711                 &connection.connection_id,
   1712                 request_message("req-eval-denied-ping", RadrootsNostrConnectRequest::Ping),
   1713             )
   1714             .expect("denied base evaluation");
   1715         denied_base_eval.action = RadrootsNostrSignerRequestAction::Denied {
   1716             reason: "blocked".to_owned(),
   1717         };
   1718         assert!(matches!(
   1719             response_from_outcome(
   1720                 handler
   1721                     .handle_authorized_request_evaluation(
   1722                         request_message("req-eval-denied-ping", RadrootsNostrConnectRequest::Ping),
   1723                         denied_base_eval,
   1724                     )
   1725                     .expect("denied base authorized")
   1726             ),
   1727             RadrootsNostrConnectResponse::Error { .. }
   1728         ));
   1729 
   1730         let crypto = request_message(
   1731             "req-eval-crypto",
   1732             RadrootsNostrConnectRequest::Nip44Decrypt {
   1733                 public_key: client_public_key,
   1734                 ciphertext: "sealed".to_owned(),
   1735             },
   1736         );
   1737         let crypto_eval = backend
   1738             .evaluate_request(&connection.connection_id, crypto.clone())
   1739             .expect("crypto evaluation");
   1740         assert_eq!(
   1741             response_from_outcome(
   1742                 handler
   1743                     .handle_authorized_request_evaluation(crypto, crypto_eval)
   1744                     .expect("crypto authorized")
   1745             ),
   1746             RadrootsNostrConnectResponse::Nip44Decrypt("sealed".to_owned())
   1747         );
   1748 
   1749         let sign = request_message(
   1750             "req-eval-sign",
   1751             RadrootsNostrConnectRequest::SignEvent(unsigned_user_event(1)),
   1752         );
   1753         let sign_eval = backend
   1754             .evaluate_request(&connection.connection_id, sign.clone())
   1755             .expect("sign evaluation");
   1756         assert!(matches!(
   1757             response_from_outcome(
   1758                 handler
   1759                     .handle_authorized_request_evaluation(sign, sign_eval)
   1760                     .expect("sign authorized")
   1761             ),
   1762             RadrootsNostrConnectResponse::Error { .. }
   1763         ));
   1764 
   1765         let custom = request_message(
   1766             "req-eval-custom",
   1767             RadrootsNostrConnectRequest::Custom {
   1768                 method: RadrootsNostrConnectMethod::Custom("do_work".to_owned()),
   1769                 params: vec![],
   1770             },
   1771         );
   1772         let custom_eval = backend
   1773             .evaluate_request(&connection.connection_id, custom.clone())
   1774             .expect("custom evaluation");
   1775         assert!(matches!(
   1776             response_from_outcome(
   1777                 handler
   1778                     .handle_authorized_request_evaluation(custom, custom_eval)
   1779                     .expect("custom authorized")
   1780             ),
   1781             RadrootsNostrConnectResponse::Error { .. }
   1782         ));
   1783     }
   1784 
   1785     #[test]
   1786     fn standalone_response_helpers_cover_publish_parts_and_hints() {
   1787         let backend = embedded_backend();
   1788         let handler = handler_with_backend(backend.clone());
   1789         let client_public_key = fixture_carol_public_key();
   1790         connect_with_permissions(&handler, client_public_key, all_runtime_permissions());
   1791         let connection = registered_connection(&backend, &client_public_key);
   1792 
   1793         let parts = super::connect_response_outcome(&connection, Some("secret".to_owned()))
   1794             .into_publish_parts()
   1795             .expect("publish parts");
   1796         assert_eq!(
   1797             parts.0,
   1798             RadrootsNostrConnectResponse::ConnectSecretEcho("secret".to_owned())
   1799         );
   1800         assert_eq!(parts.1, Some(connection.connection_id.clone()));
   1801         assert_eq!(parts.2, Some(connection.connection_id.clone()));
   1802         assert!(
   1803             RadrootsNostrSignerHandledRequest::Ignore
   1804                 .into_publish_parts()
   1805                 .is_none()
   1806         );
   1807         assert!(
   1808             RadrootsNostrSignerHandledRequest::respond(RadrootsNostrConnectResponse::Pong)
   1809                 .into_publish_parts()
   1810                 .is_some()
   1811         );
   1812         assert_eq!(
   1813             response_from_outcome(RadrootsNostrSignerHandledRequestOutcome::respond(
   1814                 RadrootsNostrConnectResponse::Pong,
   1815             )),
   1816             RadrootsNostrConnectResponse::Pong
   1817         );
   1818 
   1819         assert_eq!(
   1820             super::response_from_hint(
   1821                 &connection,
   1822                 RadrootsNostrSignerRequestResponseHint::UserPublicKey(client_public_key),
   1823             ),
   1824             RadrootsNostrConnectResponse::UserPublicKey(client_public_key)
   1825         );
   1826         let capability = RadrootsNostrConnectRemoteSessionCapability {
   1827             user_public_key: client_public_key,
   1828             relays: vec![primary_relay()],
   1829             permissions: all_runtime_permissions().into(),
   1830         };
   1831         assert_eq!(
   1832             super::response_from_hint(
   1833                 &connection,
   1834                 RadrootsNostrSignerRequestResponseHint::RemoteSessionCapability(capability.clone(),),
   1835             ),
   1836             RadrootsNostrConnectResponse::RemoteSessionCapability(capability)
   1837         );
   1838         assert_eq!(
   1839             super::response_from_hint(
   1840                 &connection,
   1841                 RadrootsNostrSignerRequestResponseHint::RelayList(vec![primary_relay()]),
   1842             ),
   1843             RadrootsNostrConnectResponse::RelayList(vec![primary_relay()])
   1844         );
   1845         assert_eq!(
   1846             super::response_from_hint(
   1847                 &connection,
   1848                 RadrootsNostrSignerRequestResponseHint::RelayList(Vec::new()),
   1849             ),
   1850             RadrootsNostrConnectResponse::RelayList(vec![primary_relay()])
   1851         );
   1852         assert!(matches!(
   1853             super::response_from_hint(&connection, RadrootsNostrSignerRequestResponseHint::None),
   1854             RadrootsNostrConnectResponse::Error { .. }
   1855         ));
   1856 
   1857         let denied = super::handled_request_for_action(
   1858             &connection,
   1859             RadrootsNostrSignerRequestAction::Denied {
   1860                 reason: "blocked".to_owned(),
   1861             },
   1862             || Ok(RadrootsNostrConnectResponse::Pong),
   1863         )
   1864         .expect("denied action");
   1865         assert!(matches!(
   1866             denied,
   1867             RadrootsNostrSignerHandledRequest::Respond { .. }
   1868         ));
   1869 
   1870         let allowed = super::handled_request_for_action(
   1871             &connection,
   1872             RadrootsNostrSignerRequestAction::Allowed {
   1873                 required_permission: None,
   1874                 response_hint: RadrootsNostrSignerRequestResponseHint::Pong,
   1875             },
   1876             || Ok(RadrootsNostrConnectResponse::Pong),
   1877         )
   1878         .expect("allowed action");
   1879         assert!(matches!(
   1880             allowed,
   1881             RadrootsNostrSignerHandledRequest::Respond { .. }
   1882         ));
   1883 
   1884         let challenged = super::handled_request_for_action(
   1885             &connection,
   1886             RadrootsNostrSignerRequestAction::Challenged {
   1887                 auth_challenge: RadrootsNostrSignerAuthChallenge::new(
   1888                     "https://example.test/auth",
   1889                     1,
   1890                 )
   1891                 .expect("challenge"),
   1892                 pending_request: RadrootsNostrSignerPendingRequest::new(
   1893                     request_message("req-pending", RadrootsNostrConnectRequest::Ping),
   1894                     1,
   1895                 )
   1896                 .expect("pending"),
   1897             },
   1898             || Ok(RadrootsNostrConnectResponse::Pong),
   1899         )
   1900         .expect("challenged action");
   1901         assert!(matches!(
   1902             challenged,
   1903             RadrootsNostrSignerHandledRequest::Respond { .. }
   1904         ));
   1905     }
   1906 
   1907     #[test]
   1908     fn handler_reports_ambiguous_client_sessions() {
   1909         let backend = embedded_backend();
   1910         let client_public_key = fixture_carol_public_key();
   1911         backend
   1912             .register_connection(RadrootsNostrSignerConnectionDraft::new(
   1913                 client_public_key,
   1914                 test_signer().user_identity.to_public(),
   1915             ))
   1916             .expect("first connection");
   1917         backend
   1918             .register_connection(RadrootsNostrSignerConnectionDraft::new(
   1919                 client_public_key,
   1920                 RadrootsIdentity::from_secret_key_str(
   1921                     "3333333333333333333333333333333333333333333333333333333333333333",
   1922                 )
   1923                 .expect("second user identity")
   1924                 .to_public(),
   1925             ))
   1926             .expect("second connection");
   1927 
   1928         let outcome = handler_with_backend(backend)
   1929             .handle_request(
   1930                 client_public_key,
   1931                 request_message("req-ambiguous", RadrootsNostrConnectRequest::Ping),
   1932             )
   1933             .expect("ambiguous request");
   1934         assert_eq!(
   1935             response_from_outcome(outcome),
   1936             RadrootsNostrConnectResponse::Error {
   1937                 result: None,
   1938                 error: "ambiguous client sessions".to_owned(),
   1939             }
   1940         );
   1941     }
   1942 
   1943     #[test]
   1944     fn handler_registers_connections_and_returns_audit_for_authorized_requests() {
   1945         let backend = embedded_backend();
   1946         let handler = handler_with_backend(backend.clone());
   1947         let client_public_key = fixture_carol_public_key();
   1948 
   1949         let connect = handler
   1950             .handle_request(client_public_key, connect_request(None))
   1951             .expect("connect outcome");
   1952         assert!(connect.audit.is_none());
   1953         match connect.handled_request {
   1954             RadrootsNostrSignerHandledRequest::Respond { response, .. } => {
   1955                 assert_eq!(*response, RadrootsNostrConnectResponse::ConnectAcknowledged);
   1956             }
   1957             other => panic!("unexpected connect outcome: {other:?}"),
   1958         }
   1959 
   1960         let ping = handler
   1961             .handle_request(
   1962                 client_public_key,
   1963                 RadrootsNostrConnectRequestMessage::new(
   1964                     "req-ping",
   1965                     RadrootsNostrConnectRequest::Ping,
   1966                 ),
   1967             )
   1968             .expect("ping outcome");
   1969         match ping.handled_request {
   1970             RadrootsNostrSignerHandledRequest::Respond { response, .. } => {
   1971                 assert_eq!(*response, RadrootsNostrConnectResponse::Pong);
   1972             }
   1973             other => panic!("unexpected ping outcome: {other:?}"),
   1974         }
   1975         let audit = ping.audit.expect("audit");
   1976         assert_eq!(audit.request_id.as_str(), "req-ping");
   1977         assert_eq!(
   1978             backend
   1979                 .find_connections_by_client_public_key(&client_public_key)
   1980                 .expect("connections")
   1981                 .len(),
   1982             1
   1983         );
   1984     }
   1985 
   1986     #[test]
   1987     fn handler_ignores_reused_consumed_connect_secrets() {
   1988         let backend = embedded_backend();
   1989         let handler = handler_with_backend(backend.clone());
   1990         let client_public_key = fixture_carol_public_key();
   1991         let secret = "connect-secret";
   1992 
   1993         let first = handler
   1994             .handle_request(client_public_key, connect_request(Some(secret)))
   1995             .expect("first connect");
   1996         assert!(first.audit.is_none());
   1997 
   1998         let connection = backend
   1999             .find_connections_by_client_public_key(&client_public_key)
   2000             .expect("connections")
   2001             .into_iter()
   2002             .next()
   2003             .expect("connection");
   2004         backend
   2005             .mark_connect_secret_consumed(&connection.connection_id)
   2006             .expect("consume secret");
   2007 
   2008         let reused = handler_with_backend(backend)
   2009             .handle_request(client_public_key, connect_request(Some(secret)))
   2010             .expect("reused outcome");
   2011         assert_eq!(
   2012             reused.handled_request,
   2013             RadrootsNostrSignerHandledRequest::Ignore
   2014         );
   2015     }
   2016 
   2017     #[test]
   2018     fn sign_event_response_rejects_wrong_user_pubkey() {
   2019         let codec = super::RadrootsNostrSignerNip46Codec::new(test_signer());
   2020         let response = codec
   2021             .sign_event_response(
   2022                 serde_json::from_value(serde_json::json!({
   2023                     "pubkey": fixture_alice_identity().public_key_hex,
   2024                     "created_at": 1,
   2025                     "kind": 1,
   2026                     "tags": [],
   2027                     "content": "hello",
   2028                 }))
   2029                 .expect("unsigned event"),
   2030             )
   2031             .expect("response");
   2032 
   2033         assert_eq!(
   2034             response,
   2035             RadrootsNostrConnectResponse::Error {
   2036                 result: None,
   2037                 error: "sign_event pubkey does not match the managed user identity".to_owned(),
   2038             }
   2039         );
   2040     }
   2041 
   2042     #[test]
   2043     fn connect_decision_enum_covers_all_states() {
   2044         assert_eq!(
   2045             [
   2046                 RadrootsNostrSignerNip46ConnectDecision::Allow,
   2047                 RadrootsNostrSignerNip46ConnectDecision::RequireApproval,
   2048                 RadrootsNostrSignerNip46ConnectDecision::Deny,
   2049             ]
   2050             .len(),
   2051             3
   2052         );
   2053     }
   2054 
   2055     #[test]
   2056     fn connect_request_keeps_requested_permissions() {
   2057         let request = connect_request(None);
   2058         assert_eq!(
   2059             request.request,
   2060             RadrootsNostrConnectRequest::Connect {
   2061                 remote_signer_public_key: test_signer().signer_identity.public_key(),
   2062                 secret: None,
   2063                 requested_permissions: vec![RadrootsNostrConnectPermission::new(
   2064                     RadrootsNostrConnectMethod::Nip04Encrypt,
   2065                 )]
   2066                 .into(),
   2067             }
   2068         );
   2069     }
   2070 
   2071     #[test]
   2072     fn handler_registration_initializes_non_terminal_connection_state() {
   2073         let backend = embedded_backend();
   2074         let handler = handler_with_backend(backend.clone());
   2075         let _ = handler
   2076             .handle_request(fixture_carol_public_key(), connect_request(None))
   2077             .expect("connect");
   2078         let connection = backend
   2079             .find_connections_by_client_public_key(&fixture_carol_public_key())
   2080             .expect("connections")
   2081             .into_iter()
   2082             .next()
   2083             .expect("connection");
   2084         assert!(matches!(
   2085             connection.auth_state,
   2086             RadrootsNostrSignerAuthState::NotRequired
   2087                 | RadrootsNostrSignerAuthState::Pending
   2088                 | RadrootsNostrSignerAuthState::Authorized
   2089         ));
   2090         assert_eq!(
   2091             connection.user_identity.id,
   2092             test_signer().user_identity().id
   2093         );
   2094     }
   2095 }