myc

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

nip46.rs (67720B)


      1 use std::future::Future;
      2 use std::sync::Arc;
      3 
      4 use radroots_nostr::prelude::{
      5     RadrootsNostrEvent, RadrootsNostrFilter, RadrootsNostrKind, RadrootsNostrPublicKey,
      6     RadrootsNostrRelayPoolNotification, RadrootsNostrRelayUrl,
      7 };
      8 use radroots_nostr_connect::prelude::{
      9     RADROOTS_NOSTR_CONNECT_RPC_KIND, RadrootsNostrConnectRequestMessage,
     10     RadrootsNostrConnectResponse,
     11 };
     12 use radroots_nostr_signer::prelude::{
     13     RadrootsNostrSignerConnectionId, RadrootsNostrSignerHandledRequestOutcome,
     14     RadrootsNostrSignerNip46Handler, RadrootsNostrSignerNip46Signer,
     15     RadrootsNostrSignerRequestEvaluation, RadrootsNostrSignerWorkflowId,
     16 };
     17 use tokio::sync::broadcast;
     18 
     19 #[cfg(test)]
     20 use radroots_nostr_signer::prelude::RadrootsNostrSignerHandledRequest;
     21 
     22 use crate::app::MycSignerContext;
     23 use crate::app::backend::MycSignerBackend;
     24 use crate::audit::{MycOperationAuditKind, MycOperationAuditOutcome, MycOperationAuditRecord};
     25 use crate::error::MycError;
     26 use crate::outbox::{MycDeliveryOutboxKind, MycDeliveryOutboxRecord, MycDeliveryOutboxStore};
     27 use crate::transport::MycNostrTransport;
     28 
     29 type MycNip46CoreHandler = RadrootsNostrSignerNip46Handler<
     30     MycSignerBackend,
     31     crate::policy::MycPolicyContext,
     32     MycNip46Signer,
     33 >;
     34 
     35 #[derive(Clone)]
     36 pub struct MycNip46Handler {
     37     signer: MycSignerContext,
     38     handler: MycNip46CoreHandler,
     39 }
     40 
     41 pub struct MycNip46Service {
     42     handler: MycNip46Handler,
     43     transport: MycNostrTransport,
     44     delivery_outbox_store: Arc<dyn MycDeliveryOutboxStore>,
     45 }
     46 
     47 type MycNip46HandledOutcome = RadrootsNostrSignerHandledRequestOutcome;
     48 
     49 #[derive(Clone)]
     50 struct MycNip46Signer {
     51     signer: MycSignerContext,
     52 }
     53 
     54 impl RadrootsNostrSignerNip46Signer for MycNip46Signer {
     55     fn signer_public_key_hex(&self) -> String {
     56         self.signer.signer_public_identity().public_key_hex
     57     }
     58 
     59     fn decrypt_request(
     60         &self,
     61         client_public_key: &RadrootsNostrPublicKey,
     62         ciphertext: &str,
     63     ) -> Result<String, radroots_nostr_signer::prelude::RadrootsNostrSignerError> {
     64         self.signer
     65             .signer_identity()
     66             .nip44_decrypt(client_public_key, ciphertext)
     67             .map_err(|error| {
     68                 radroots_nostr_signer::prelude::RadrootsNostrSignerError::Sign(error.to_string())
     69             })
     70     }
     71 
     72     fn encrypt_response(
     73         &self,
     74         client_public_key: &RadrootsNostrPublicKey,
     75         payload: &str,
     76     ) -> Result<String, radroots_nostr_signer::prelude::RadrootsNostrSignerError> {
     77         self.signer
     78             .signer_identity()
     79             .nip44_encrypt(client_public_key, payload.to_owned())
     80             .map_err(|error| {
     81                 radroots_nostr_signer::prelude::RadrootsNostrSignerError::Sign(error.to_string())
     82             })
     83     }
     84 
     85     fn user_identity(&self) -> radroots_identity::RadrootsIdentityPublic {
     86         self.signer.user_public_identity()
     87     }
     88 
     89     fn sign_user_event(
     90         &self,
     91         unsigned_event: nostr::UnsignedEvent,
     92     ) -> Result<RadrootsNostrEvent, radroots_nostr_signer::prelude::RadrootsNostrSignerError> {
     93         self.signer
     94             .user_identity()
     95             .sign_unsigned_event(unsigned_event, "managed user sign_event")
     96             .map_err(|error| {
     97                 radroots_nostr_signer::prelude::RadrootsNostrSignerError::Sign(error.to_string())
     98             })
     99     }
    100 
    101     fn nip04_encrypt(
    102         &self,
    103         public_key: &RadrootsNostrPublicKey,
    104         plaintext: &str,
    105     ) -> Result<String, radroots_nostr_signer::prelude::RadrootsNostrSignerError> {
    106         self.signer
    107             .user_identity()
    108             .nip04_encrypt(public_key, plaintext.to_owned())
    109             .map_err(|error| {
    110                 radroots_nostr_signer::prelude::RadrootsNostrSignerError::Sign(error.to_string())
    111             })
    112     }
    113 
    114     fn nip04_decrypt(
    115         &self,
    116         public_key: &RadrootsNostrPublicKey,
    117         ciphertext: &str,
    118     ) -> Result<String, radroots_nostr_signer::prelude::RadrootsNostrSignerError> {
    119         self.signer
    120             .user_identity()
    121             .nip04_decrypt(public_key, ciphertext.to_owned())
    122             .map_err(|error| {
    123                 radroots_nostr_signer::prelude::RadrootsNostrSignerError::Sign(error.to_string())
    124             })
    125     }
    126 
    127     fn nip44_encrypt(
    128         &self,
    129         public_key: &RadrootsNostrPublicKey,
    130         plaintext: &str,
    131     ) -> Result<String, radroots_nostr_signer::prelude::RadrootsNostrSignerError> {
    132         self.signer
    133             .user_identity()
    134             .nip44_encrypt(public_key, plaintext.to_owned())
    135             .map_err(|error| {
    136                 radroots_nostr_signer::prelude::RadrootsNostrSignerError::Sign(error.to_string())
    137             })
    138     }
    139 
    140     fn nip44_decrypt(
    141         &self,
    142         public_key: &RadrootsNostrPublicKey,
    143         ciphertext: &str,
    144     ) -> Result<String, radroots_nostr_signer::prelude::RadrootsNostrSignerError> {
    145         self.signer
    146             .user_identity()
    147             .nip44_decrypt(public_key, ciphertext.to_owned())
    148             .map_err(|error| {
    149                 radroots_nostr_signer::prelude::RadrootsNostrSignerError::Sign(error.to_string())
    150             })
    151     }
    152 }
    153 
    154 impl MycNip46Handler {
    155     pub fn new(signer: MycSignerContext, relays: Vec<RadrootsNostrRelayUrl>) -> Self {
    156         let handler = RadrootsNostrSignerNip46Handler::new(
    157             MycSignerBackend::new(signer.clone()),
    158             signer.policy().clone(),
    159             relays,
    160             MycNip46Signer {
    161                 signer: signer.clone(),
    162             },
    163         );
    164         Self { signer, handler }
    165     }
    166 
    167     pub fn filter(&self) -> Result<RadrootsNostrFilter, MycError> {
    168         self.handler.filter().map_err(Into::into)
    169     }
    170 
    171     pub fn parse_request_event(
    172         &self,
    173         event: &RadrootsNostrEvent,
    174     ) -> Result<RadrootsNostrConnectRequestMessage, MycError> {
    175         self.handler.parse_request_event(event).map_err(Into::into)
    176     }
    177 
    178     pub fn build_response_event(
    179         &self,
    180         client_public_key: RadrootsNostrPublicKey,
    181         request_id: impl Into<String>,
    182         response: RadrootsNostrConnectResponse,
    183     ) -> Result<radroots_nostr::prelude::RadrootsNostrEventBuilder, MycError> {
    184         self.handler
    185             .build_response_event(client_public_key, request_id, response)
    186             .map_err(Into::into)
    187     }
    188 
    189     pub(crate) fn handle_request(
    190         &self,
    191         client_public_key: RadrootsNostrPublicKey,
    192         request_message: RadrootsNostrConnectRequestMessage,
    193     ) -> Result<MycNip46HandledOutcome, MycError> {
    194         self.handler
    195             .handle_request(client_public_key, request_message)
    196             .map_err(Into::into)
    197     }
    198 
    199     #[cfg(test)]
    200     fn handle_request_response(
    201         &self,
    202         client_public_key: RadrootsNostrPublicKey,
    203         request_message: RadrootsNostrConnectRequestMessage,
    204     ) -> Result<RadrootsNostrConnectResponse, MycError> {
    205         match self.handle_request(client_public_key, request_message)? {
    206             MycNip46HandledOutcome {
    207                 handled_request: RadrootsNostrSignerHandledRequest::Respond { response, .. },
    208                 ..
    209             } => Ok(response),
    210             MycNip46HandledOutcome {
    211                 handled_request: RadrootsNostrSignerHandledRequest::Ignore,
    212                 ..
    213             } => Err(MycError::InvalidOperation(
    214                 "request was ignored without a response".to_owned(),
    215             )),
    216         }
    217     }
    218 
    219     pub(crate) fn handle_authorized_request_evaluation(
    220         &self,
    221         request_message: RadrootsNostrConnectRequestMessage,
    222         evaluation: RadrootsNostrSignerRequestEvaluation,
    223     ) -> Result<MycNip46HandledOutcome, MycError> {
    224         self.handler
    225             .handle_authorized_request_evaluation(request_message, evaluation)
    226             .map_err(Into::into)
    227     }
    228 }
    229 
    230 impl MycNip46Service {
    231     pub fn new(
    232         signer: MycSignerContext,
    233         transport: MycNostrTransport,
    234         delivery_outbox_store: Arc<dyn MycDeliveryOutboxStore>,
    235     ) -> Self {
    236         let handler = MycNip46Handler::new(signer, transport.relays().to_vec());
    237         Self {
    238             handler,
    239             transport,
    240             delivery_outbox_store,
    241         }
    242     }
    243 
    244     pub async fn run(&self) -> Result<(), MycError> {
    245         self.run_until(std::future::pending()).await
    246     }
    247 
    248     pub async fn run_until<F>(&self, shutdown: F) -> Result<(), MycError>
    249     where
    250         F: Future<Output = ()>,
    251     {
    252         tokio::pin!(shutdown);
    253         self.transport.connect().await?;
    254 
    255         let filter = self.handler.filter()?;
    256         let mut notifications = self.transport.client().notifications();
    257         let subscription = self.transport.client().subscribe(filter, None).await?;
    258         tracing::info!(
    259             subscription_id = %subscription.val,
    260             relay_count = self.transport.relays().len(),
    261             "myc NIP-46 listener subscribed"
    262         );
    263 
    264         loop {
    265             let notification = tokio::select! {
    266                 _ = &mut shutdown => return Ok(()),
    267                 notification = notifications.recv() => {
    268                     match notification {
    269                         Ok(notification) => notification,
    270                         Err(broadcast::error::RecvError::Lagged(_)) => continue,
    271                         Err(broadcast::error::RecvError::Closed) => {
    272                             return Err(MycError::Nip46ListenerClosed);
    273                         }
    274                     }
    275                 }
    276             };
    277             let RadrootsNostrRelayPoolNotification::Event { event, .. } = notification else {
    278                 continue;
    279             };
    280             let event = *event;
    281             if event.kind != RadrootsNostrKind::Custom(RADROOTS_NOSTR_CONNECT_RPC_KIND) {
    282                 continue;
    283             }
    284 
    285             let request_message = match self.handler.parse_request_event(&event) {
    286                 Ok(message) => message,
    287                 Err(error) => {
    288                     tracing::warn!(error = %error, "discarding invalid NIP-46 request event");
    289                     continue;
    290                 }
    291             };
    292 
    293             let request_id = request_message.id.clone();
    294             let handled_outcome = match self.handler.handle_request(event.pubkey, request_message) {
    295                 Ok(handled_outcome) => handled_outcome,
    296                 Err(error) => {
    297                     tracing::warn!(error = %error, "failed to handle NIP-46 request");
    298                     MycNip46HandledOutcome::respond(RadrootsNostrConnectResponse::Error {
    299                         result: None,
    300                         error: error.to_string(),
    301                     })
    302                 }
    303             };
    304             if let Some(audit) = handled_outcome.audit.as_ref() {
    305                 self.handler.signer.record_signer_request_audit(audit);
    306             }
    307             let Some((response, connection_id, consume_connect_secret_for)) =
    308                 handled_outcome.handled_request.into_publish_parts()
    309             else {
    310                 tracing::debug!(
    311                     request_id = %request_id,
    312                     client_public_key = %event.pubkey,
    313                     "ignoring NIP-46 request without response"
    314                 );
    315                 continue;
    316             };
    317 
    318             let response_event =
    319                 self.handler
    320                     .build_response_event(event.pubkey, request_id.as_str(), response)?;
    321             let response_event = match self
    322                 .handler
    323                 .signer
    324                 .signer_identity()
    325                 .sign_event_builder(response_event, "NIP-46 response")
    326             {
    327                 Ok(event) => event,
    328                 Err(error) => {
    329                     self.record_listener_publish_local_rejection(
    330                         connection_id.as_ref(),
    331                         request_id.as_str(),
    332                         format!("failed to sign NIP-46 response event: {error}"),
    333                     );
    334                     continue;
    335                 }
    336             };
    337 
    338             let mut workflow_id = None;
    339             if let Some(connect_connection_id) = consume_connect_secret_for.as_ref() {
    340                 let manager = match self.handler.signer.load_signer_manager() {
    341                     Ok(manager) => manager,
    342                     Err(error) => {
    343                         self.record_listener_publish_local_rejection(
    344                             connection_id.as_ref(),
    345                             request_id.as_str(),
    346                             error.to_string(),
    347                         );
    348                         continue;
    349                     }
    350                 };
    351                 match manager.begin_connect_secret_publish_finalization(connect_connection_id) {
    352                     Ok(workflow) => workflow_id = Some(workflow.workflow_id),
    353                     Err(error) => {
    354                         self.record_listener_publish_local_rejection(
    355                             connection_id.as_ref(),
    356                             request_id.as_str(),
    357                             format!(
    358                                 "failed to begin connect-secret publish finalization workflow: {error}"
    359                             ),
    360                         );
    361                         continue;
    362                     }
    363                 }
    364             }
    365 
    366             let outbox_record = match self.build_listener_outbox_record(
    367                 response_event.clone(),
    368                 connection_id.as_ref(),
    369                 request_id.as_str(),
    370                 workflow_id.as_ref(),
    371             ) {
    372                 Ok(record) => record,
    373                 Err(error) => {
    374                     let error = self
    375                         .cancel_listener_publish_workflow_if_needed(workflow_id.as_ref(), error);
    376                     self.record_listener_publish_local_rejection(
    377                         connection_id.as_ref(),
    378                         request_id.as_str(),
    379                         error.to_string(),
    380                     );
    381                     continue;
    382                 }
    383             };
    384             if let Err(error) = self.delivery_outbox_store.enqueue(&outbox_record) {
    385                 let error =
    386                     self.cancel_listener_publish_workflow_if_needed(workflow_id.as_ref(), error);
    387                 self.record_listener_publish_local_rejection(
    388                     connection_id.as_ref(),
    389                     request_id.as_str(),
    390                     error.to_string(),
    391                 );
    392                 continue;
    393             }
    394             let publish_outcome = match self
    395                 .transport
    396                 .publish_event("NIP-46 response publish", &response_event)
    397                 .await
    398             {
    399                 Ok(publish_outcome) => publish_outcome,
    400                 Err(error) => {
    401                     let mut error = self.record_listener_outbox_failure(&outbox_record, error);
    402                     error = self
    403                         .cancel_listener_publish_workflow_if_needed(workflow_id.as_ref(), error);
    404                     self.record_listener_publish_error(
    405                         connection_id.as_ref(),
    406                         request_id.as_str(),
    407                         &error,
    408                     );
    409                     continue;
    410                 }
    411             };
    412             if let Some(workflow_id) = workflow_id.as_ref() {
    413                 let manager = match self.handler.signer.load_signer_manager() {
    414                     Ok(manager) => manager,
    415                     Err(error) => {
    416                         self.record_listener_publish_post_publish_failure(
    417                             connection_id.as_ref(),
    418                             request_id.as_str(),
    419                             &publish_outcome,
    420                             format!(
    421                                 "failed to load signer manager for publish finalization: {error}"
    422                             ),
    423                         );
    424                         continue;
    425                     }
    426                 };
    427                 if let Err(error) = manager.mark_publish_workflow_published(workflow_id) {
    428                     self.record_listener_publish_post_publish_failure(
    429                         connection_id.as_ref(),
    430                         request_id.as_str(),
    431                         &publish_outcome,
    432                         format!("failed to mark signer publish workflow as published: {error}"),
    433                     );
    434                     continue;
    435                 }
    436             }
    437             if let Err(error) = self.delivery_outbox_store.mark_published_pending_finalize(
    438                 &outbox_record.job_id,
    439                 publish_outcome.attempt_count,
    440             ) {
    441                 self.record_listener_publish_post_publish_failure(
    442                     connection_id.as_ref(),
    443                     request_id.as_str(),
    444                     &publish_outcome,
    445                     format!("failed to persist delivery outbox published state: {error}"),
    446                 );
    447                 continue;
    448             }
    449             if let Some(workflow_id) = workflow_id.as_ref() {
    450                 let manager = match self.handler.signer.load_signer_manager() {
    451                     Ok(manager) => manager,
    452                     Err(error) => {
    453                         self.record_listener_publish_post_publish_failure(
    454                             connection_id.as_ref(),
    455                             request_id.as_str(),
    456                             &publish_outcome,
    457                             format!("failed to load signer manager for publish workflow finalization: {error}"),
    458                         );
    459                         continue;
    460                     }
    461                 };
    462                 if let Err(error) = manager.finalize_publish_workflow(workflow_id) {
    463                     self.record_listener_publish_post_publish_failure(
    464                         connection_id.as_ref(),
    465                         request_id.as_str(),
    466                         &publish_outcome,
    467                         format!("failed to finalize signer publish workflow: {error}"),
    468                     );
    469                     continue;
    470                 }
    471             }
    472             if let Err(error) = self
    473                 .delivery_outbox_store
    474                 .mark_finalized(&outbox_record.job_id)
    475             {
    476                 self.record_listener_publish_post_publish_failure(
    477                     connection_id.as_ref(),
    478                     request_id.as_str(),
    479                     &publish_outcome,
    480                     format!("failed to finalize delivery outbox job: {error}"),
    481                 );
    482                 continue;
    483             }
    484             self.record_listener_publish_success(
    485                 connection_id.as_ref(),
    486                 request_id.as_str(),
    487                 &publish_outcome,
    488             );
    489         }
    490     }
    491 
    492     fn build_listener_outbox_record(
    493         &self,
    494         response_event: RadrootsNostrEvent,
    495         connection_id: Option<&RadrootsNostrSignerConnectionId>,
    496         request_id: &str,
    497         workflow_id: Option<&RadrootsNostrSignerWorkflowId>,
    498     ) -> Result<MycDeliveryOutboxRecord, MycError> {
    499         let mut record = MycDeliveryOutboxRecord::new(
    500             MycDeliveryOutboxKind::ListenerResponsePublish,
    501             response_event,
    502             self.transport.relays().to_vec(),
    503         )?
    504         .with_request_id(request_id.to_owned());
    505         if let Some(connection_id) = connection_id {
    506             record = record.with_connection_id(connection_id);
    507         }
    508         if let Some(workflow_id) = workflow_id {
    509             record = record.with_signer_publish_workflow_id(workflow_id);
    510         }
    511         Ok(record)
    512     }
    513 
    514     fn cancel_listener_publish_workflow_if_needed(
    515         &self,
    516         workflow_id: Option<&RadrootsNostrSignerWorkflowId>,
    517         error: MycError,
    518     ) -> MycError {
    519         let Some(workflow_id) = workflow_id else {
    520             return error;
    521         };
    522         match self
    523             .handler
    524             .signer
    525             .load_signer_manager()
    526             .and_then(|manager| {
    527                 manager
    528                     .cancel_publish_workflow(workflow_id)
    529                     .map(|_| ())
    530                     .map_err(Into::into)
    531             }) {
    532             Ok(()) => error,
    533             Err(cancel_error) => MycError::InvalidOperation(format!(
    534                 "{error}; additionally failed to cancel listener publish workflow: {cancel_error}"
    535             )),
    536         }
    537     }
    538 
    539     fn record_listener_outbox_failure(
    540         &self,
    541         outbox_record: &MycDeliveryOutboxRecord,
    542         error: MycError,
    543     ) -> MycError {
    544         let publish_attempt_count = error.publish_attempt_count().unwrap_or_default();
    545         let failure_summary = error
    546             .publish_rejection_details()
    547             .map(ToOwned::to_owned)
    548             .unwrap_or_else(|| error.to_string());
    549         match self.delivery_outbox_store.mark_failed(
    550             &outbox_record.job_id,
    551             publish_attempt_count,
    552             &failure_summary,
    553         ) {
    554             Ok(_) => error,
    555             Err(outbox_error) => MycError::InvalidOperation(format!(
    556                 "{error}; additionally failed to persist listener publish failure to the outbox: {outbox_error}"
    557             )),
    558         }
    559     }
    560 
    561     fn record_listener_publish_local_rejection(
    562         &self,
    563         connection_id: Option<&RadrootsNostrSignerConnectionId>,
    564         request_id: &str,
    565         summary: impl Into<String>,
    566     ) {
    567         self.handler
    568             .signer
    569             .record_operation_audit(&MycOperationAuditRecord::new(
    570                 MycOperationAuditKind::ListenerResponsePublish,
    571                 MycOperationAuditOutcome::Rejected,
    572                 connection_id,
    573                 Some(request_id),
    574                 self.transport.relays().len(),
    575                 0,
    576                 summary.into(),
    577             ));
    578     }
    579 
    580     fn record_listener_publish_error(
    581         &self,
    582         connection_id: Option<&RadrootsNostrSignerConnectionId>,
    583         request_id: &str,
    584         error: &MycError,
    585     ) {
    586         let mut record = MycOperationAuditRecord::new(
    587             MycOperationAuditKind::ListenerResponsePublish,
    588             MycOperationAuditOutcome::Rejected,
    589             connection_id,
    590             Some(request_id),
    591             error
    592                 .publish_rejection_counts()
    593                 .map(|(relay_count, _)| relay_count)
    594                 .unwrap_or(self.transport.relays().len()),
    595             error
    596                 .publish_rejection_counts()
    597                 .map(|(_, acknowledged)| acknowledged)
    598                 .unwrap_or_default(),
    599             error
    600                 .publish_rejection_details()
    601                 .map(ToOwned::to_owned)
    602                 .unwrap_or_else(|| error.to_string()),
    603         );
    604         if let (
    605             Some(delivery_policy),
    606             Some(required_acknowledged_relay_count),
    607             Some(attempt_count),
    608         ) = (
    609             error.publish_delivery_policy(),
    610             error.publish_required_acknowledged_relay_count(),
    611             error.publish_attempt_count(),
    612         ) {
    613             record = record.with_delivery_details(
    614                 delivery_policy,
    615                 required_acknowledged_relay_count,
    616                 attempt_count,
    617             );
    618         }
    619         self.handler.signer.record_operation_audit(&record);
    620     }
    621 
    622     fn record_listener_publish_post_publish_failure(
    623         &self,
    624         connection_id: Option<&RadrootsNostrSignerConnectionId>,
    625         request_id: &str,
    626         publish_outcome: &crate::transport::MycPublishOutcome,
    627         summary: impl Into<String>,
    628     ) {
    629         self.handler.signer.record_operation_audit(
    630             &MycOperationAuditRecord::new(
    631                 MycOperationAuditKind::ListenerResponsePublish,
    632                 MycOperationAuditOutcome::Rejected,
    633                 connection_id,
    634                 Some(request_id),
    635                 publish_outcome.relay_count,
    636                 publish_outcome.acknowledged_relay_count,
    637                 summary.into(),
    638             )
    639             .with_delivery_details(
    640                 publish_outcome.delivery_policy,
    641                 publish_outcome.required_acknowledged_relay_count,
    642                 publish_outcome.attempt_count,
    643             ),
    644         );
    645     }
    646 
    647     fn record_listener_publish_success(
    648         &self,
    649         connection_id: Option<&RadrootsNostrSignerConnectionId>,
    650         request_id: &str,
    651         publish_outcome: &crate::transport::MycPublishOutcome,
    652     ) {
    653         self.handler.signer.record_operation_audit(
    654             &MycOperationAuditRecord::new(
    655                 MycOperationAuditKind::ListenerResponsePublish,
    656                 MycOperationAuditOutcome::Succeeded,
    657                 connection_id,
    658                 Some(request_id),
    659                 publish_outcome.relay_count,
    660                 publish_outcome.acknowledged_relay_count,
    661                 publish_outcome.relay_outcome_summary.clone(),
    662             )
    663             .with_delivery_details(
    664                 publish_outcome.delivery_policy,
    665                 publish_outcome.required_acknowledged_relay_count,
    666                 publish_outcome.attempt_count,
    667             ),
    668         );
    669     }
    670 }
    671 
    672 #[cfg(test)]
    673 mod tests {
    674     use nostr::nips::nip04;
    675     use nostr::nips::nip44;
    676     use nostr::nips::nip44::Version;
    677     use nostr::{EventBuilder, Keys, PublicKey, SecretKey, Timestamp, UnsignedEvent};
    678     use radroots_nostr::prelude::{RadrootsNostrTag, radroots_nostr_kind};
    679     use radroots_nostr_connect::prelude::{
    680         RADROOTS_NOSTR_CONNECT_RPC_KIND, RadrootsNostrConnectMethod,
    681         RadrootsNostrConnectPermission, RadrootsNostrConnectRequest,
    682         RadrootsNostrConnectRequestMessage, RadrootsNostrConnectResponse,
    683         RadrootsNostrConnectResponseEnvelope,
    684     };
    685     use radroots_nostr_signer::prelude::{
    686         RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerHandledRequest,
    687     };
    688     use serde_json::json;
    689 
    690     use crate::app::MycRuntime;
    691     use crate::config::{MycConfig, MycConnectionApproval};
    692 
    693     use super::MycNip46Handler;
    694 
    695     fn write_identity(path: &std::path::Path, secret_key: &str) {
    696         let identity =
    697             radroots_identity::RadrootsIdentity::from_secret_key_str(secret_key).expect("identity");
    698         crate::identity_files::store_encrypted_identity(path, &identity).expect("save identity");
    699     }
    700 
    701     fn runtime() -> MycRuntime {
    702         runtime_with_config(MycConnectionApproval::NotRequired, |_| {})
    703     }
    704 
    705     fn runtime_with_config<F>(approval: MycConnectionApproval, configure: F) -> MycRuntime
    706     where
    707         F: FnOnce(&mut MycConfig),
    708     {
    709         let temp = tempfile::tempdir().expect("tempdir").keep();
    710         let mut config = MycConfig::default();
    711         config.paths.state_dir = temp.join("state");
    712         config.paths.signer_identity_path = temp.join("signer.json");
    713         config.paths.user_identity_path = temp.join("user.json");
    714         config.policy.connection_approval = approval;
    715         config.transport.enabled = true;
    716         config.transport.connect_timeout_secs = 15;
    717         config.transport.relays = vec!["wss://relay.example.com".to_owned()];
    718         configure(&mut config);
    719         write_identity(
    720             &config.paths.signer_identity_path,
    721             "1111111111111111111111111111111111111111111111111111111111111111",
    722         );
    723         write_identity(
    724             &config.paths.user_identity_path,
    725             "2222222222222222222222222222222222222222222222222222222222222222",
    726         );
    727         MycRuntime::bootstrap(config).expect("runtime")
    728     }
    729 
    730     fn runtime_with_explicit_approval() -> MycRuntime {
    731         runtime_with_config(MycConnectionApproval::ExplicitUser, |_| {})
    732     }
    733 
    734     fn handler(runtime: &MycRuntime) -> MycNip46Handler {
    735         MycNip46Handler::new(
    736             runtime.signer_context(),
    737             runtime.transport().expect("transport").relays().to_vec(),
    738         )
    739     }
    740 
    741     fn client_keys() -> Keys {
    742         client_keys_from_hex("3333333333333333333333333333333333333333333333333333333333333333")
    743     }
    744 
    745     fn client_keys_from_hex(secret_key: &str) -> Keys {
    746         let secret = SecretKey::from_hex(secret_key).expect("secret");
    747         Keys::new(secret)
    748     }
    749 
    750     fn request_event(
    751         handler: &MycNip46Handler,
    752         request: RadrootsNostrConnectRequestMessage,
    753     ) -> nostr::Event {
    754         request_event_with_client_keys(handler, request, &client_keys())
    755     }
    756 
    757     fn request_event_with_client_keys(
    758         handler: &MycNip46Handler,
    759         request: RadrootsNostrConnectRequestMessage,
    760         client_keys: &Keys,
    761     ) -> nostr::Event {
    762         let payload = serde_json::to_string(&request).expect("serialize request");
    763         let ciphertext = nip44::encrypt(
    764             client_keys.secret_key(),
    765             &PublicKey::parse(
    766                 handler
    767                     .signer
    768                     .signer_public_identity()
    769                     .public_key_hex
    770                     .as_str(),
    771             )
    772             .expect("signer pubkey"),
    773             payload,
    774             Version::V2,
    775         )
    776         .expect("encrypt");
    777         EventBuilder::new(
    778             radroots_nostr_kind(RADROOTS_NOSTR_CONNECT_RPC_KIND),
    779             ciphertext,
    780         )
    781         .tags(vec![RadrootsNostrTag::public_key(
    782             handler.signer.signer_identity().public_key(),
    783         )])
    784         .sign_with_keys(client_keys)
    785         .expect("sign request")
    786     }
    787 
    788     fn sign_event_permission(kind: u16) -> RadrootsNostrConnectPermission {
    789         RadrootsNostrConnectPermission::with_parameter(
    790             RadrootsNostrConnectMethod::SignEvent,
    791             format!("kind:{kind}"),
    792         )
    793     }
    794 
    795     fn unsigned_event(pubkey: PublicKey, kind: u16, content: &str) -> UnsignedEvent {
    796         serde_json::from_value(json!({
    797             "pubkey": pubkey.to_hex(),
    798             "created_at": Timestamp::from(1).as_secs(),
    799             "kind": kind,
    800             "tags": [],
    801             "content": content
    802         }))
    803         .expect("unsigned event")
    804     }
    805 
    806     fn connect_with_permissions(
    807         handler: &MycNip46Handler,
    808         runtime: &MycRuntime,
    809         requested_permissions: Vec<RadrootsNostrConnectPermission>,
    810     ) {
    811         handler
    812             .handle_request_response(
    813                 client_keys().public_key(),
    814                 RadrootsNostrConnectRequestMessage::new(
    815                     "req-connect",
    816                     RadrootsNostrConnectRequest::Connect {
    817                         remote_signer_public_key: runtime.signer_identity().public_key(),
    818                         secret: None,
    819                         requested_permissions: requested_permissions.into(),
    820                     },
    821                 ),
    822             )
    823             .expect("connect");
    824     }
    825 
    826     fn connection_for(
    827         runtime: &MycRuntime,
    828         client_public_key: PublicKey,
    829     ) -> RadrootsNostrSignerConnectionRecord {
    830         runtime
    831             .signer_manager()
    832             .expect("manager")
    833             .find_connections_by_client_public_key(&client_public_key)
    834             .expect("connections")
    835             .into_iter()
    836             .next()
    837             .expect("connection")
    838     }
    839 
    840     #[test]
    841     fn parse_and_build_nip46_envelopes_roundtrip() {
    842         let runtime = runtime();
    843         let handler = handler(&runtime);
    844         let request =
    845             RadrootsNostrConnectRequestMessage::new("req-1", RadrootsNostrConnectRequest::Ping);
    846         let event = request_event(&handler, request.clone());
    847 
    848         let parsed = handler.parse_request_event(&event).expect("parse request");
    849         assert_eq!(parsed, request);
    850 
    851         let response_builder = handler
    852             .build_response_event(event.pubkey, "req-1", RadrootsNostrConnectResponse::Pong)
    853             .expect("response builder");
    854         let response_event = runtime
    855             .signer_identity()
    856             .sign_event_builder(response_builder, "test response")
    857             .expect("sign response");
    858         let decrypted = nip44::decrypt(
    859             client_keys().secret_key(),
    860             &runtime.signer_identity().public_key(),
    861             &response_event.content,
    862         )
    863         .expect("decrypt response");
    864         let envelope: RadrootsNostrConnectResponseEnvelope =
    865             serde_json::from_str(&decrypted).expect("parse envelope");
    866         let parsed = RadrootsNostrConnectResponse::from_envelope(
    867             &RadrootsNostrConnectRequest::Ping.method(),
    868             envelope,
    869         )
    870         .expect("parse response");
    871         assert_eq!(parsed, RadrootsNostrConnectResponse::Pong);
    872     }
    873 
    874     #[test]
    875     fn connect_registers_client_and_echoes_secret() {
    876         let runtime = runtime();
    877         let handler = handler(&runtime);
    878         let response = handler
    879             .handle_request_response(
    880                 client_keys().public_key(),
    881                 RadrootsNostrConnectRequestMessage::new(
    882                     "req-connect",
    883                     RadrootsNostrConnectRequest::Connect {
    884                         remote_signer_public_key: runtime.signer_identity().public_key(),
    885                         secret: Some("s3cr3t".to_owned()),
    886                         requested_permissions: Default::default(),
    887                     },
    888                 ),
    889             )
    890             .expect("connect response");
    891 
    892         assert_eq!(
    893             response,
    894             RadrootsNostrConnectResponse::ConnectSecretEcho("s3cr3t".to_owned())
    895         );
    896         let connections = runtime
    897             .signer_manager()
    898             .expect("manager")
    899             .list_connections()
    900             .expect("connections");
    901         assert_eq!(connections.len(), 1);
    902         assert_eq!(
    903             connections[0].user_identity.id.to_string(),
    904             runtime.user_public_identity().id.to_string()
    905         );
    906         assert_eq!(connections[0].relays.len(), 1);
    907     }
    908 
    909     #[test]
    910     fn denied_clients_are_rejected_without_registration() {
    911         let denied_client_keys = client_keys_from_hex(
    912             "4444444444444444444444444444444444444444444444444444444444444444",
    913         );
    914         let runtime = runtime_with_config(MycConnectionApproval::ExplicitUser, |config| {
    915             config.policy.denied_client_pubkeys = vec![denied_client_keys.public_key().to_hex()];
    916         });
    917         let handler = handler(&runtime);
    918 
    919         let response = handler
    920             .handle_request_response(
    921                 denied_client_keys.public_key(),
    922                 RadrootsNostrConnectRequestMessage::new(
    923                     "req-connect",
    924                     RadrootsNostrConnectRequest::Connect {
    925                         remote_signer_public_key: runtime.signer_identity().public_key(),
    926                         secret: None,
    927                         requested_permissions: Default::default(),
    928                     },
    929                 ),
    930             )
    931             .expect("connect response");
    932 
    933         assert_eq!(
    934             response,
    935             RadrootsNostrConnectResponse::Error {
    936                 result: None,
    937                 error: "client public key denied by policy".to_owned(),
    938             }
    939         );
    940         assert!(
    941             runtime
    942                 .signer_manager()
    943                 .expect("manager")
    944                 .list_connections()
    945                 .expect("connections")
    946                 .is_empty()
    947         );
    948     }
    949 
    950     #[test]
    951     fn existing_unconsumed_connect_secret_can_still_retry_after_failed_publish() {
    952         let runtime = runtime();
    953         let handler = handler(&runtime);
    954 
    955         let first = handler
    956             .handle_request_response(
    957                 client_keys().public_key(),
    958                 RadrootsNostrConnectRequestMessage::new(
    959                     "req-connect-1",
    960                     RadrootsNostrConnectRequest::Connect {
    961                         remote_signer_public_key: runtime.signer_identity().public_key(),
    962                         secret: Some("s3cr3t".to_owned()),
    963                         requested_permissions: Default::default(),
    964                     },
    965                 ),
    966             )
    967             .expect("first connect response");
    968         let second = handler
    969             .handle_request_response(
    970                 client_keys().public_key(),
    971                 RadrootsNostrConnectRequestMessage::new(
    972                     "req-connect-2",
    973                     RadrootsNostrConnectRequest::Connect {
    974                         remote_signer_public_key: runtime.signer_identity().public_key(),
    975                         secret: Some("s3cr3t".to_owned()),
    976                         requested_permissions: Default::default(),
    977                     },
    978                 ),
    979             )
    980             .expect("second connect response");
    981 
    982         assert_eq!(
    983             first,
    984             RadrootsNostrConnectResponse::ConnectSecretEcho("s3cr3t".to_owned())
    985         );
    986         assert_eq!(second, first);
    987     }
    988 
    989     #[test]
    990     fn consumed_connect_secret_is_ignored_on_reuse() {
    991         let runtime = runtime();
    992         let handler = handler(&runtime);
    993         let response = handler
    994             .handle_request_response(
    995                 client_keys().public_key(),
    996                 RadrootsNostrConnectRequestMessage::new(
    997                     "req-connect",
    998                     RadrootsNostrConnectRequest::Connect {
    999                         remote_signer_public_key: runtime.signer_identity().public_key(),
   1000                         secret: Some("s3cr3t".to_owned()),
   1001                         requested_permissions: Default::default(),
   1002                     },
   1003                 ),
   1004             )
   1005             .expect("connect response");
   1006         assert_eq!(
   1007             response,
   1008             RadrootsNostrConnectResponse::ConnectSecretEcho("s3cr3t".to_owned())
   1009         );
   1010 
   1011         let connection = runtime
   1012             .signer_manager()
   1013             .expect("manager")
   1014             .list_connections()
   1015             .expect("connections")
   1016             .into_iter()
   1017             .next()
   1018             .expect("connection");
   1019         runtime
   1020             .signer_manager()
   1021             .expect("manager")
   1022             .mark_connect_secret_consumed(&connection.connection_id)
   1023             .expect("consume connect secret");
   1024 
   1025         let ignored = handler
   1026             .handle_request(
   1027                 client_keys().public_key(),
   1028                 RadrootsNostrConnectRequestMessage::new(
   1029                     "req-connect-reused",
   1030                     RadrootsNostrConnectRequest::Connect {
   1031                         remote_signer_public_key: runtime.signer_identity().public_key(),
   1032                         secret: Some("s3cr3t".to_owned()),
   1033                         requested_permissions: Default::default(),
   1034                     },
   1035                 ),
   1036             )
   1037             .expect("ignored response");
   1038 
   1039         assert_eq!(
   1040             ignored.handled_request,
   1041             RadrootsNostrSignerHandledRequest::Ignore
   1042         );
   1043         let connections = runtime
   1044             .signer_manager()
   1045             .expect("manager")
   1046             .list_connections()
   1047             .expect("connections");
   1048         assert_eq!(connections.len(), 1);
   1049         assert!(connections[0].connect_secret_is_consumed());
   1050     }
   1051 
   1052     #[test]
   1053     fn connect_requests_are_throttled_after_configured_limit() {
   1054         let runtime = runtime_with_config(MycConnectionApproval::NotRequired, |config| {
   1055             config.policy.connect_rate_limit_window_secs = Some(1);
   1056             config.policy.connect_rate_limit_max_attempts = Some(1);
   1057         });
   1058         let handler = handler(&runtime);
   1059 
   1060         let first = handler
   1061             .handle_request_response(
   1062                 client_keys().public_key(),
   1063                 RadrootsNostrConnectRequestMessage::new(
   1064                     "req-connect-1",
   1065                     RadrootsNostrConnectRequest::Connect {
   1066                         remote_signer_public_key: runtime.signer_identity().public_key(),
   1067                         secret: None,
   1068                         requested_permissions: Default::default(),
   1069                     },
   1070                 ),
   1071             )
   1072             .expect("first connect response");
   1073         assert_eq!(first, RadrootsNostrConnectResponse::ConnectAcknowledged);
   1074 
   1075         let second = handler
   1076             .handle_request_response(
   1077                 client_keys().public_key(),
   1078                 RadrootsNostrConnectRequestMessage::new(
   1079                     "req-connect-2",
   1080                     RadrootsNostrConnectRequest::Connect {
   1081                         remote_signer_public_key: runtime.signer_identity().public_key(),
   1082                         secret: None,
   1083                         requested_permissions: Default::default(),
   1084                     },
   1085                 ),
   1086             )
   1087             .expect("second connect response");
   1088         assert!(matches!(
   1089             second,
   1090             RadrootsNostrConnectResponse::Error { error, .. }
   1091                 if error.contains("connect attempts throttled by policy")
   1092         ));
   1093 
   1094         let connection = connection_for(&runtime, client_keys().public_key());
   1095         runtime
   1096             .signer_manager()
   1097             .expect("manager")
   1098             .revoke_connection(&connection.connection_id, Some("test reset".to_owned()))
   1099             .expect("revoke connection");
   1100 
   1101         std::thread::sleep(std::time::Duration::from_secs(2));
   1102 
   1103         let third = handler
   1104             .handle_request_response(
   1105                 client_keys().public_key(),
   1106                 RadrootsNostrConnectRequestMessage::new(
   1107                     "req-connect-3",
   1108                     RadrootsNostrConnectRequest::Connect {
   1109                         remote_signer_public_key: runtime.signer_identity().public_key(),
   1110                         secret: None,
   1111                         requested_permissions: Default::default(),
   1112                     },
   1113                 ),
   1114             )
   1115             .expect("third connect response");
   1116         assert_eq!(third, RadrootsNostrConnectResponse::ConnectAcknowledged);
   1117     }
   1118 
   1119     #[test]
   1120     fn connect_preserves_pending_status_when_explicit_approval_is_required() {
   1121         let runtime = runtime_with_explicit_approval();
   1122         let handler = handler(&runtime);
   1123 
   1124         let response = handler
   1125             .handle_request_response(
   1126                 client_keys().public_key(),
   1127                 RadrootsNostrConnectRequestMessage::new(
   1128                     "req-connect",
   1129                     RadrootsNostrConnectRequest::Connect {
   1130                         remote_signer_public_key: runtime.signer_identity().public_key(),
   1131                         secret: None,
   1132                         requested_permissions: vec![sign_event_permission(1)].into(),
   1133                     },
   1134                 ),
   1135             )
   1136             .expect("connect response");
   1137 
   1138         assert_eq!(response, RadrootsNostrConnectResponse::ConnectAcknowledged);
   1139         let connection = runtime
   1140             .signer_manager()
   1141             .expect("manager")
   1142             .list_connections()
   1143             .expect("connections")
   1144             .into_iter()
   1145             .next()
   1146             .expect("connection");
   1147         assert_eq!(
   1148             connection.status,
   1149             radroots_nostr_signer::prelude::RadrootsNostrSignerConnectionStatus::Pending
   1150         );
   1151         assert_eq!(
   1152             connection.approval_state,
   1153             radroots_nostr_signer::prelude::RadrootsNostrSignerApprovalState::Pending
   1154         );
   1155         assert!(connection.granted_permissions().as_slice().is_empty());
   1156     }
   1157 
   1158     #[test]
   1159     fn trusted_clients_auto_grant_only_policy_allowed_permissions() {
   1160         let trusted_client_keys = client_keys_from_hex(
   1161             "4545454545454545454545454545454545454545454545454545454545454545",
   1162         );
   1163         let runtime = runtime_with_config(MycConnectionApproval::ExplicitUser, |config| {
   1164             config.policy.trusted_client_pubkeys = vec![trusted_client_keys.public_key().to_hex()];
   1165             config.policy.permission_ceiling = vec![
   1166                 RadrootsNostrConnectPermission::new(RadrootsNostrConnectMethod::Nip04Encrypt),
   1167                 sign_event_permission(1),
   1168             ]
   1169             .into();
   1170             config.policy.allowed_sign_event_kinds = vec![1];
   1171         });
   1172         let handler = handler(&runtime);
   1173 
   1174         let response = handler
   1175             .handle_request_response(
   1176                 trusted_client_keys.public_key(),
   1177                 RadrootsNostrConnectRequestMessage::new(
   1178                     "req-connect",
   1179                     RadrootsNostrConnectRequest::Connect {
   1180                         remote_signer_public_key: runtime.signer_identity().public_key(),
   1181                         secret: None,
   1182                         requested_permissions: vec![
   1183                             RadrootsNostrConnectPermission::new(
   1184                                 RadrootsNostrConnectMethod::Nip04Encrypt,
   1185                             ),
   1186                             RadrootsNostrConnectPermission::new(
   1187                                 RadrootsNostrConnectMethod::SignEvent,
   1188                             ),
   1189                             sign_event_permission(7),
   1190                         ]
   1191                         .into(),
   1192                     },
   1193                 ),
   1194             )
   1195             .expect("connect response");
   1196 
   1197         assert_eq!(response, RadrootsNostrConnectResponse::ConnectAcknowledged);
   1198         let connection = connection_for(&runtime, trusted_client_keys.public_key());
   1199         assert_eq!(
   1200             connection.granted_permissions().to_string(),
   1201             "sign_event:kind:1,nip04_encrypt"
   1202         );
   1203         assert_eq!(
   1204             connection.requested_permissions.to_string(),
   1205             "sign_event:kind:1,nip04_encrypt"
   1206         );
   1207     }
   1208 
   1209     #[test]
   1210     fn trusted_client_requires_auth_again_after_authorized_ttl() {
   1211         let trusted_client_keys = client_keys_from_hex(
   1212             "5656565656565656565656565656565656565656565656565656565656565656",
   1213         );
   1214         let runtime = runtime_with_config(MycConnectionApproval::ExplicitUser, |config| {
   1215             config.policy.trusted_client_pubkeys = vec![trusted_client_keys.public_key().to_hex()];
   1216             config.policy.permission_ceiling = vec![sign_event_permission(1)].into();
   1217             config.policy.allowed_sign_event_kinds = vec![1];
   1218             config.policy.auth_url = Some("https://auth.example/challenge".to_owned());
   1219             config.policy.auth_authorized_ttl_secs = Some(1);
   1220         });
   1221         let handler = handler(&runtime);
   1222 
   1223         let _ = handler
   1224             .handle_request_response(
   1225                 trusted_client_keys.public_key(),
   1226                 RadrootsNostrConnectRequestMessage::new(
   1227                     "req-connect",
   1228                     RadrootsNostrConnectRequest::Connect {
   1229                         remote_signer_public_key: runtime.signer_identity().public_key(),
   1230                         secret: None,
   1231                         requested_permissions: vec![sign_event_permission(1)].into(),
   1232                     },
   1233                 ),
   1234             )
   1235             .expect("connect");
   1236 
   1237         let first = handler
   1238             .handle_request_response(
   1239                 trusted_client_keys.public_key(),
   1240                 RadrootsNostrConnectRequestMessage::new(
   1241                     "req-sign-1",
   1242                     RadrootsNostrConnectRequest::SignEvent(unsigned_event(
   1243                         runtime.user_identity().public_key(),
   1244                         1,
   1245                         "first",
   1246                     )),
   1247                 ),
   1248             )
   1249             .expect("first sign request");
   1250         assert_eq!(
   1251             first,
   1252             RadrootsNostrConnectResponse::AuthUrl("https://auth.example/challenge".to_owned())
   1253         );
   1254 
   1255         let connection = connection_for(&runtime, trusted_client_keys.public_key());
   1256         runtime
   1257             .signer_manager()
   1258             .expect("manager")
   1259             .authorize_auth_challenge(&connection.connection_id)
   1260             .expect("authorize auth challenge");
   1261 
   1262         let second = handler
   1263             .handle_request_response(
   1264                 trusted_client_keys.public_key(),
   1265                 RadrootsNostrConnectRequestMessage::new(
   1266                     "req-sign-2",
   1267                     RadrootsNostrConnectRequest::SignEvent(unsigned_event(
   1268                         runtime.user_identity().public_key(),
   1269                         1,
   1270                         "second",
   1271                     )),
   1272                 ),
   1273             )
   1274             .expect("second sign request");
   1275         assert!(matches!(
   1276             second,
   1277             RadrootsNostrConnectResponse::SignedEvent(_)
   1278         ));
   1279 
   1280         std::thread::sleep(std::time::Duration::from_secs(2));
   1281 
   1282         let third = handler
   1283             .handle_request_response(
   1284                 trusted_client_keys.public_key(),
   1285                 RadrootsNostrConnectRequestMessage::new(
   1286                     "req-sign-3",
   1287                     RadrootsNostrConnectRequest::SignEvent(unsigned_event(
   1288                         runtime.user_identity().public_key(),
   1289                         1,
   1290                         "third",
   1291                     )),
   1292                 ),
   1293             )
   1294             .expect("third sign request");
   1295         assert_eq!(
   1296             third,
   1297             RadrootsNostrConnectResponse::AuthUrl("https://auth.example/challenge".to_owned())
   1298         );
   1299     }
   1300 
   1301     #[test]
   1302     fn trusted_client_requires_auth_again_after_inactivity() {
   1303         let trusted_client_keys = client_keys_from_hex(
   1304             "5757575757575757575757575757575757575757575757575757575757575757",
   1305         );
   1306         let runtime = runtime_with_config(MycConnectionApproval::ExplicitUser, |config| {
   1307             config.policy.trusted_client_pubkeys = vec![trusted_client_keys.public_key().to_hex()];
   1308             config.policy.permission_ceiling = vec![sign_event_permission(1)].into();
   1309             config.policy.allowed_sign_event_kinds = vec![1];
   1310             config.policy.auth_url = Some("https://auth.example/challenge".to_owned());
   1311             config.policy.reauth_after_inactivity_secs = Some(1);
   1312         });
   1313         let handler = handler(&runtime);
   1314 
   1315         let _ = handler
   1316             .handle_request_response(
   1317                 trusted_client_keys.public_key(),
   1318                 RadrootsNostrConnectRequestMessage::new(
   1319                     "req-connect",
   1320                     RadrootsNostrConnectRequest::Connect {
   1321                         remote_signer_public_key: runtime.signer_identity().public_key(),
   1322                         secret: None,
   1323                         requested_permissions: vec![sign_event_permission(1)].into(),
   1324                     },
   1325                 ),
   1326             )
   1327             .expect("connect");
   1328 
   1329         let first = handler
   1330             .handle_request_response(
   1331                 trusted_client_keys.public_key(),
   1332                 RadrootsNostrConnectRequestMessage::new(
   1333                     "req-sign-1",
   1334                     RadrootsNostrConnectRequest::SignEvent(unsigned_event(
   1335                         runtime.user_identity().public_key(),
   1336                         1,
   1337                         "first",
   1338                     )),
   1339                 ),
   1340             )
   1341             .expect("first sign request");
   1342         assert_eq!(
   1343             first,
   1344             RadrootsNostrConnectResponse::AuthUrl("https://auth.example/challenge".to_owned())
   1345         );
   1346 
   1347         let connection = connection_for(&runtime, trusted_client_keys.public_key());
   1348         runtime
   1349             .signer_manager()
   1350             .expect("manager")
   1351             .authorize_auth_challenge(&connection.connection_id)
   1352             .expect("authorize auth challenge");
   1353 
   1354         std::thread::sleep(std::time::Duration::from_secs(2));
   1355 
   1356         let second = handler
   1357             .handle_request_response(
   1358                 trusted_client_keys.public_key(),
   1359                 RadrootsNostrConnectRequestMessage::new(
   1360                     "req-sign-2",
   1361                     RadrootsNostrConnectRequest::SignEvent(unsigned_event(
   1362                         runtime.user_identity().public_key(),
   1363                         1,
   1364                         "second",
   1365                     )),
   1366                 ),
   1367             )
   1368             .expect("second sign request");
   1369         assert_eq!(
   1370             second,
   1371             RadrootsNostrConnectResponse::AuthUrl("https://auth.example/challenge".to_owned())
   1372         );
   1373     }
   1374 
   1375     #[test]
   1376     fn trusted_client_auth_challenge_reissue_is_throttled() {
   1377         let trusted_client_keys = client_keys_from_hex(
   1378             "5858585858585858585858585858585858585858585858585858585858585858",
   1379         );
   1380         let runtime = runtime_with_config(MycConnectionApproval::ExplicitUser, |config| {
   1381             config.policy.trusted_client_pubkeys = vec![trusted_client_keys.public_key().to_hex()];
   1382             config.policy.permission_ceiling = vec![sign_event_permission(1)].into();
   1383             config.policy.allowed_sign_event_kinds = vec![1];
   1384             config.policy.auth_url = Some("https://auth.example/challenge".to_owned());
   1385             config.policy.auth_pending_ttl_secs = 1;
   1386             config.policy.auth_challenge_rate_limit_window_secs = Some(60);
   1387             config.policy.auth_challenge_rate_limit_max_attempts = Some(1);
   1388         });
   1389         let handler = handler(&runtime);
   1390 
   1391         let _ = handler
   1392             .handle_request_response(
   1393                 trusted_client_keys.public_key(),
   1394                 RadrootsNostrConnectRequestMessage::new(
   1395                     "req-connect",
   1396                     RadrootsNostrConnectRequest::Connect {
   1397                         remote_signer_public_key: runtime.signer_identity().public_key(),
   1398                         secret: None,
   1399                         requested_permissions: vec![sign_event_permission(1)].into(),
   1400                     },
   1401                 ),
   1402             )
   1403             .expect("connect");
   1404 
   1405         let first = handler
   1406             .handle_request_response(
   1407                 trusted_client_keys.public_key(),
   1408                 RadrootsNostrConnectRequestMessage::new(
   1409                     "req-sign-1",
   1410                     RadrootsNostrConnectRequest::SignEvent(unsigned_event(
   1411                         runtime.user_identity().public_key(),
   1412                         1,
   1413                         "first",
   1414                     )),
   1415                 ),
   1416             )
   1417             .expect("first sign request");
   1418         assert_eq!(
   1419             first,
   1420             RadrootsNostrConnectResponse::AuthUrl("https://auth.example/challenge".to_owned())
   1421         );
   1422 
   1423         std::thread::sleep(std::time::Duration::from_secs(2));
   1424 
   1425         let second = handler
   1426             .handle_request_response(
   1427                 trusted_client_keys.public_key(),
   1428                 RadrootsNostrConnectRequestMessage::new(
   1429                     "req-sign-2",
   1430                     RadrootsNostrConnectRequest::SignEvent(unsigned_event(
   1431                         runtime.user_identity().public_key(),
   1432                         1,
   1433                         "second",
   1434                     )),
   1435                 ),
   1436             )
   1437             .expect("second sign request");
   1438         assert!(matches!(
   1439             second,
   1440             RadrootsNostrConnectResponse::Error { error, .. }
   1441                 if error.contains("auth challenge issuance throttled by policy")
   1442         ));
   1443     }
   1444 
   1445     #[test]
   1446     fn base_methods_return_spec_results_after_connect() {
   1447         let runtime = runtime();
   1448         let handler = handler(&runtime);
   1449         handler
   1450             .handle_request_response(
   1451                 client_keys().public_key(),
   1452                 RadrootsNostrConnectRequestMessage::new(
   1453                     "req-connect",
   1454                     RadrootsNostrConnectRequest::Connect {
   1455                         remote_signer_public_key: runtime.signer_identity().public_key(),
   1456                         secret: None,
   1457                         requested_permissions: vec![RadrootsNostrConnectPermission::new(
   1458                             RadrootsNostrConnectMethod::SwitchRelays,
   1459                         )]
   1460                         .into(),
   1461                     },
   1462                 ),
   1463             )
   1464             .expect("connect");
   1465 
   1466         let public_key = handler
   1467             .handle_request_response(
   1468                 client_keys().public_key(),
   1469                 RadrootsNostrConnectRequestMessage::new(
   1470                     "req-pubkey",
   1471                     RadrootsNostrConnectRequest::GetPublicKey,
   1472                 ),
   1473             )
   1474             .expect("get public key");
   1475         assert_eq!(
   1476             public_key,
   1477             RadrootsNostrConnectResponse::UserPublicKey(runtime.user_identity().public_key())
   1478         );
   1479 
   1480         let pong = handler
   1481             .handle_request_response(
   1482                 client_keys().public_key(),
   1483                 RadrootsNostrConnectRequestMessage::new(
   1484                     "req-ping",
   1485                     RadrootsNostrConnectRequest::Ping,
   1486                 ),
   1487             )
   1488             .expect("ping");
   1489         assert_eq!(pong, RadrootsNostrConnectResponse::Pong);
   1490 
   1491         let relays = handler
   1492             .handle_request_response(
   1493                 client_keys().public_key(),
   1494                 RadrootsNostrConnectRequestMessage::new(
   1495                     "req-switch",
   1496                     RadrootsNostrConnectRequest::SwitchRelays,
   1497                 ),
   1498             )
   1499             .expect("switch relays");
   1500         assert_eq!(
   1501             relays,
   1502             RadrootsNostrConnectResponse::RelayList(
   1503                 runtime.transport().expect("transport").relays().to_vec()
   1504             )
   1505         );
   1506 
   1507         let capability = handler
   1508             .handle_request_response(
   1509                 client_keys().public_key(),
   1510                 RadrootsNostrConnectRequestMessage::new(
   1511                     "req-capability",
   1512                     RadrootsNostrConnectRequest::GetSessionCapability,
   1513                 ),
   1514             )
   1515             .expect("get session capability");
   1516         assert_eq!(
   1517             capability,
   1518             RadrootsNostrConnectResponse::RemoteSessionCapability(
   1519                 radroots_nostr_connect::prelude::RadrootsNostrConnectRemoteSessionCapability {
   1520                     user_public_key: runtime.user_identity().public_key(),
   1521                     relays: runtime.transport().expect("transport").relays().to_vec(),
   1522                     permissions: vec![RadrootsNostrConnectPermission::new(
   1523                         RadrootsNostrConnectMethod::SwitchRelays,
   1524                     )]
   1525                     .into(),
   1526                 },
   1527             )
   1528         );
   1529     }
   1530 
   1531     #[test]
   1532     fn new_connections_preserve_requested_permissions_without_expansion() {
   1533         let runtime = runtime();
   1534         let handler = handler(&runtime);
   1535         handler
   1536             .handle_request_response(
   1537                 client_keys().public_key(),
   1538                 RadrootsNostrConnectRequestMessage::new(
   1539                     "req-connect",
   1540                     RadrootsNostrConnectRequest::Connect {
   1541                         remote_signer_public_key: runtime.signer_identity().public_key(),
   1542                         secret: None,
   1543                         requested_permissions: vec![sign_event_permission(1)].into(),
   1544                     },
   1545                 ),
   1546             )
   1547             .expect("connect");
   1548 
   1549         let connection = runtime
   1550             .signer_manager()
   1551             .expect("manager")
   1552             .list_connections()
   1553             .expect("connections")
   1554             .into_iter()
   1555             .next()
   1556             .expect("connection");
   1557         assert_eq!(
   1558             connection.granted_permissions().as_slice(),
   1559             &[sign_event_permission(1)]
   1560         );
   1561     }
   1562 
   1563     #[test]
   1564     fn sign_event_returns_signed_event_for_managed_user_key() {
   1565         let runtime = runtime();
   1566         let handler = handler(&runtime);
   1567         connect_with_permissions(&handler, &runtime, vec![sign_event_permission(1)]);
   1568 
   1569         let response = handler
   1570             .handle_request_response(
   1571                 client_keys().public_key(),
   1572                 RadrootsNostrConnectRequestMessage::new(
   1573                     "req-sign",
   1574                     RadrootsNostrConnectRequest::SignEvent(unsigned_event(
   1575                         runtime.user_identity().public_key(),
   1576                         1,
   1577                         "hello world",
   1578                     )),
   1579                 ),
   1580             )
   1581             .expect("sign event");
   1582 
   1583         let RadrootsNostrConnectResponse::SignedEvent(event) = response else {
   1584             panic!("unexpected sign_event response");
   1585         };
   1586         assert_eq!(event.pubkey, runtime.user_identity().public_key());
   1587         assert_eq!(event.kind.as_u16(), 1);
   1588         assert_eq!(event.content, "hello world");
   1589         assert!(event.verify_signature());
   1590     }
   1591 
   1592     #[test]
   1593     fn sign_event_is_denied_without_permission() {
   1594         let runtime = runtime();
   1595         let handler = handler(&runtime);
   1596         connect_with_permissions(&handler, &runtime, Vec::new());
   1597 
   1598         let response = handler
   1599             .handle_request_response(
   1600                 client_keys().public_key(),
   1601                 RadrootsNostrConnectRequestMessage::new(
   1602                     "req-sign",
   1603                     RadrootsNostrConnectRequest::SignEvent(unsigned_event(
   1604                         runtime.user_identity().public_key(),
   1605                         1,
   1606                         "hello world",
   1607                     )),
   1608                 ),
   1609             )
   1610             .expect("sign event");
   1611 
   1612         assert_eq!(
   1613             response,
   1614             RadrootsNostrConnectResponse::Error {
   1615                 result: None,
   1616                 error: "unauthorized sign_event".to_owned(),
   1617             }
   1618         );
   1619     }
   1620 
   1621     #[test]
   1622     fn sign_event_rejects_pubkey_mismatch() {
   1623         let runtime = runtime();
   1624         let handler = handler(&runtime);
   1625         connect_with_permissions(&handler, &runtime, vec![sign_event_permission(1)]);
   1626 
   1627         let response = handler
   1628             .handle_request_response(
   1629                 client_keys().public_key(),
   1630                 RadrootsNostrConnectRequestMessage::new(
   1631                     "req-sign",
   1632                     RadrootsNostrConnectRequest::SignEvent(unsigned_event(
   1633                         client_keys().public_key(),
   1634                         1,
   1635                         "hello world",
   1636                     )),
   1637                 ),
   1638             )
   1639             .expect("sign event");
   1640 
   1641         assert_eq!(
   1642             response,
   1643             RadrootsNostrConnectResponse::Error {
   1644                 result: None,
   1645                 error: "sign_event pubkey does not match the managed user identity".to_owned(),
   1646             }
   1647         );
   1648     }
   1649 
   1650     #[test]
   1651     fn nip04_encrypt_and_decrypt_roundtrip_on_managed_user_identity() {
   1652         let runtime = runtime();
   1653         let handler = handler(&runtime);
   1654         connect_with_permissions(
   1655             &handler,
   1656             &runtime,
   1657             vec![
   1658                 RadrootsNostrConnectPermission::new(RadrootsNostrConnectMethod::Nip04Encrypt),
   1659                 RadrootsNostrConnectPermission::new(RadrootsNostrConnectMethod::Nip04Decrypt),
   1660             ],
   1661         );
   1662 
   1663         let encrypt_response = handler
   1664             .handle_request_response(
   1665                 client_keys().public_key(),
   1666                 RadrootsNostrConnectRequestMessage::new(
   1667                     "req-nip04-encrypt",
   1668                     RadrootsNostrConnectRequest::Nip04Encrypt {
   1669                         public_key: client_keys().public_key(),
   1670                         plaintext: "hello from myc".to_owned(),
   1671                     },
   1672                 ),
   1673             )
   1674             .expect("nip04 encrypt");
   1675         let RadrootsNostrConnectResponse::Nip04Encrypt(ciphertext) = encrypt_response else {
   1676             panic!("unexpected nip04 encrypt response");
   1677         };
   1678         assert_eq!(
   1679             nip04::decrypt(
   1680                 client_keys().secret_key(),
   1681                 &runtime.user_identity().public_key(),
   1682                 ciphertext.clone(),
   1683             )
   1684             .expect("client decrypt"),
   1685             "hello from myc"
   1686         );
   1687 
   1688         let client_ciphertext = nip04::encrypt(
   1689             client_keys().secret_key(),
   1690             &runtime.user_identity().public_key(),
   1691             "hello to myc",
   1692         )
   1693         .expect("client encrypt");
   1694         let decrypt_response = handler
   1695             .handle_request_response(
   1696                 client_keys().public_key(),
   1697                 RadrootsNostrConnectRequestMessage::new(
   1698                     "req-nip04-decrypt",
   1699                     RadrootsNostrConnectRequest::Nip04Decrypt {
   1700                         public_key: client_keys().public_key(),
   1701                         ciphertext: client_ciphertext,
   1702                     },
   1703                 ),
   1704             )
   1705             .expect("nip04 decrypt");
   1706         assert_eq!(
   1707             decrypt_response,
   1708             RadrootsNostrConnectResponse::Nip04Decrypt("hello to myc".to_owned())
   1709         );
   1710     }
   1711 
   1712     #[test]
   1713     fn nip44_encrypt_and_decrypt_roundtrip_on_managed_user_identity() {
   1714         let runtime = runtime();
   1715         let handler = handler(&runtime);
   1716         connect_with_permissions(
   1717             &handler,
   1718             &runtime,
   1719             vec![
   1720                 RadrootsNostrConnectPermission::new(RadrootsNostrConnectMethod::Nip44Encrypt),
   1721                 RadrootsNostrConnectPermission::new(RadrootsNostrConnectMethod::Nip44Decrypt),
   1722             ],
   1723         );
   1724 
   1725         let encrypt_response = handler
   1726             .handle_request_response(
   1727                 client_keys().public_key(),
   1728                 RadrootsNostrConnectRequestMessage::new(
   1729                     "req-nip44-encrypt",
   1730                     RadrootsNostrConnectRequest::Nip44Encrypt {
   1731                         public_key: client_keys().public_key(),
   1732                         plaintext: "hello from myc".to_owned(),
   1733                     },
   1734                 ),
   1735             )
   1736             .expect("nip44 encrypt");
   1737         let RadrootsNostrConnectResponse::Nip44Encrypt(ciphertext) = encrypt_response else {
   1738             panic!("unexpected nip44 encrypt response");
   1739         };
   1740         assert_eq!(
   1741             nip44::decrypt(
   1742                 client_keys().secret_key(),
   1743                 &runtime.user_identity().public_key(),
   1744                 ciphertext.clone(),
   1745             )
   1746             .expect("client decrypt"),
   1747             "hello from myc"
   1748         );
   1749 
   1750         let client_ciphertext = nip44::encrypt(
   1751             client_keys().secret_key(),
   1752             &runtime.user_identity().public_key(),
   1753             "hello to myc",
   1754             Version::V2,
   1755         )
   1756         .expect("client encrypt");
   1757         let decrypt_response = handler
   1758             .handle_request_response(
   1759                 client_keys().public_key(),
   1760                 RadrootsNostrConnectRequestMessage::new(
   1761                     "req-nip44-decrypt",
   1762                     RadrootsNostrConnectRequest::Nip44Decrypt {
   1763                         public_key: client_keys().public_key(),
   1764                         ciphertext: client_ciphertext,
   1765                     },
   1766                 ),
   1767             )
   1768             .expect("nip44 decrypt");
   1769         assert_eq!(
   1770             decrypt_response,
   1771             RadrootsNostrConnectResponse::Nip44Decrypt("hello to myc".to_owned())
   1772         );
   1773     }
   1774 
   1775     #[test]
   1776     fn nip04_decrypt_is_denied_without_matching_permission() {
   1777         let runtime = runtime();
   1778         let handler = handler(&runtime);
   1779         connect_with_permissions(
   1780             &handler,
   1781             &runtime,
   1782             vec![RadrootsNostrConnectPermission::new(
   1783                 RadrootsNostrConnectMethod::Nip04Encrypt,
   1784             )],
   1785         );
   1786 
   1787         let response = handler
   1788             .handle_request_response(
   1789                 client_keys().public_key(),
   1790                 RadrootsNostrConnectRequestMessage::new(
   1791                     "req-nip04-decrypt",
   1792                     RadrootsNostrConnectRequest::Nip04Decrypt {
   1793                         public_key: client_keys().public_key(),
   1794                         ciphertext: "invalid".to_owned(),
   1795                     },
   1796                 ),
   1797             )
   1798             .expect("nip04 decrypt");
   1799 
   1800         assert_eq!(
   1801             response,
   1802             RadrootsNostrConnectResponse::Error {
   1803                 result: None,
   1804                 error: "unauthorized nip04_decrypt".to_owned(),
   1805             }
   1806         );
   1807     }
   1808 }