myc

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

nip46_work.rs (47382B)


      1 //! Transaction-free NIP-46 decryption, method admission, and provider work.
      2 
      3 use core::fmt;
      4 use std::error::Error;
      5 
      6 use nostr::UnsignedEvent as NostrUnsignedEvent;
      7 use radroots_nostr_connect::{
      8     message::{Request, RequestMessage, UnsignedEvent as ConnectUnsignedEvent},
      9     server::required_permission,
     10 };
     11 use sha2::{Digest, Sha256};
     12 
     13 use crate::{
     14     MycConnectionAdmissionPolicy, MycConnectionAdmissionRequest, MycConnectionNonce,
     15     MycConnectionPermission, MycConnectionPermissionSet, MycConnectionPolicyGeneration,
     16     MycConnectionRecord, MycConnectionStatus, MycConnectionTimeUnixMs, MycNip46AdmissionLimits,
     17     MycNip46EncryptionContext, MycProviderBinding, MycProviderContract, MycProviderCorrelationId,
     18     MycProviderDeadlineUnixMs, MycProviderNip44Version, MycProviderOperation,
     19     MycProviderOperationId, MycProviderOperationInput, MycProviderPublicIdentity, MycProviderRole,
     20     MycRateRelayId, MycReplayBoundNip46Request, MycRequestReceivedAtUnixMs,
     21     MycSignerOperationNonce, MycSignerRequest, MycSignerRequestMethod, MycSignerRequestRecord,
     22     MycVerifiedNip46Event, MycVerifiedProviderResponse, admit_myc_nip46_request,
     23     bind_myc_nip46_replay, verify_myc_nip46_request,
     24 };
     25 
     26 const DECRYPT_OPERATION_ID_DOMAIN: &[u8] = b"radroots.myc.nip46.decrypt.operation.v1\0";
     27 const DECRYPT_CORRELATION_ID_DOMAIN: &[u8] = b"radroots.myc.nip46.decrypt.correlation.v1\0";
     28 
     29 /// Stable source-free Step 145 failure classification.
     30 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
     31 pub enum MycNip46WorkErrorKind {
     32     InvalidBinding,
     33     DecryptionRejected,
     34     UnsupportedMethod,
     35     PermissionDenied,
     36     ConnectionExpired,
     37     ProviderWorkRejected,
     38 }
     39 
     40 impl MycNip46WorkErrorKind {
     41     /// Returns the stable machine-facing safe code.
     42     #[must_use]
     43     pub const fn code(self) -> &'static str {
     44         match self {
     45             Self::InvalidBinding => "nip46_work_binding_invalid",
     46             Self::DecryptionRejected => "nip46_decryption_rejected",
     47             Self::UnsupportedMethod => "nip46_method_unsupported",
     48             Self::PermissionDenied => "nip46_permission_denied",
     49             Self::ConnectionExpired => "nip46_connection_inactive",
     50             Self::ProviderWorkRejected => "nip46_provider_work_rejected",
     51         }
     52     }
     53 
     54     const fn message(self) -> &'static str {
     55         match self {
     56             Self::InvalidBinding => "NIP-46 work binding is invalid",
     57             Self::DecryptionRejected => "NIP-46 decrypted request was rejected",
     58             Self::UnsupportedMethod => "NIP-46 method is unsupported",
     59             Self::PermissionDenied => "NIP-46 permission was denied",
     60             Self::ConnectionExpired => "NIP-46 connection is not active",
     61             Self::ProviderWorkRejected => "NIP-46 provider work was rejected",
     62         }
     63     }
     64 }
     65 
     66 /// One redacted source-free Step 145 failure.
     67 #[derive(Clone, Copy, PartialEq, Eq)]
     68 pub struct MycNip46WorkError {
     69     kind: MycNip46WorkErrorKind,
     70 }
     71 
     72 impl MycNip46WorkError {
     73     /// Returns the stable failure classification.
     74     #[must_use]
     75     pub const fn kind(self) -> MycNip46WorkErrorKind {
     76         self.kind
     77     }
     78 
     79     /// Returns the stable machine-facing safe code.
     80     #[must_use]
     81     pub const fn code(self) -> &'static str {
     82         self.kind.code()
     83     }
     84 }
     85 
     86 impl fmt::Debug for MycNip46WorkError {
     87     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
     88         formatter
     89             .debug_struct("MycNip46WorkError")
     90             .field("kind", &self.kind)
     91             .finish()
     92     }
     93 }
     94 
     95 impl fmt::Display for MycNip46WorkError {
     96     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
     97         formatter.write_str(self.kind.message())
     98     }
     99 }
    100 
    101 impl Error for MycNip46WorkError {}
    102 
    103 const fn work_error(kind: MycNip46WorkErrorKind) -> MycNip46WorkError {
    104     MycNip46WorkError { kind }
    105 }
    106 
    107 /// Provider-bound work that must complete before plaintext request admission.
    108 pub struct MycNip46DecryptWork {
    109     event: MycVerifiedNip46Event,
    110     operation: MycProviderOperation,
    111 }
    112 
    113 impl MycNip46DecryptWork {
    114     /// Returns the exact transport-provider operation for external execution.
    115     #[must_use]
    116     pub const fn operation(&self) -> &MycProviderOperation {
    117         &self.operation
    118     }
    119 
    120     /// Binds an independently verified provider result to the exact event.
    121     pub fn complete(
    122         self,
    123         response: MycVerifiedProviderResponse,
    124         limits: MycNip46AdmissionLimits,
    125     ) -> Result<MycDecryptedNip46Request, MycNip46WorkError> {
    126         let expected_capability = self.operation.input().capability();
    127         if response.operation_id() != self.operation.operation_id()
    128             || response.correlation_id() != self.operation.correlation_id()
    129             || response.instance() != self.operation.instance()
    130             || response.role() != MycProviderRole::Transport
    131             || response.capability() != expected_capability
    132             || response.nip44_version() != self.operation.input().nip44_version()
    133             || !response.matches_operation(&self.operation)
    134         {
    135             return Err(work_error(MycNip46WorkErrorKind::InvalidBinding));
    136         }
    137         let plaintext = response
    138             .protected_payload()
    139             .ok_or_else(|| work_error(MycNip46WorkErrorKind::DecryptionRejected))?;
    140         let bounded = admit_myc_nip46_request(limits, plaintext)
    141             .map_err(|_| work_error(MycNip46WorkErrorKind::DecryptionRejected))?;
    142         let request = verify_myc_nip46_request(bounded)
    143             .map_err(|_| work_error(MycNip46WorkErrorKind::DecryptionRejected))?;
    144         let replay = bind_myc_nip46_replay(self.event, request)
    145             .map_err(|_| work_error(MycNip46WorkErrorKind::DecryptionRejected))?;
    146         Ok(MycDecryptedNip46Request { replay })
    147     }
    148 }
    149 
    150 impl fmt::Debug for MycNip46DecryptWork {
    151     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    152         formatter
    153             .debug_struct("MycNip46DecryptWork")
    154             .field("event", &"[redacted]")
    155             .field("operation", &self.operation)
    156             .finish()
    157     }
    158 }
    159 
    160 /// Constructs the exact transport-provider decryption work for one verified event.
    161 pub fn prepare_myc_nip46_decrypt_work(
    162     event: MycVerifiedNip46Event,
    163     transport: &MycProviderBinding,
    164     deadline: MycProviderDeadlineUnixMs,
    165 ) -> Result<MycNip46DecryptWork, MycNip46WorkError> {
    166     if transport.role() != MycProviderRole::Transport
    167         || transport.expected_identity() != event.receiver_public_key()
    168     {
    169         return Err(work_error(MycNip46WorkErrorKind::InvalidBinding));
    170     }
    171     let operation_id = MycProviderOperationId::from_bytes(derive_decrypt_identity(
    172         DECRYPT_OPERATION_ID_DOMAIN,
    173         event.event_id().as_bytes(),
    174     ));
    175     let correlation_id = MycProviderCorrelationId::from_bytes(derive_decrypt_identity(
    176         DECRYPT_CORRELATION_ID_DOMAIN,
    177         event.event_id().as_bytes(),
    178     ));
    179     let peer = MycProviderPublicIdentity::new(event.client_public_key().as_hex())
    180         .map_err(|_| work_error(MycNip46WorkErrorKind::InvalidBinding))?;
    181     let input = match event.encryption_context() {
    182         MycNip46EncryptionContext::Nip04 => {
    183             MycProviderOperationInput::nip04_decrypt(peer, event.encrypted_content().as_bytes())
    184         }
    185         MycNip46EncryptionContext::Nip44V2 => MycProviderOperationInput::nip44_decrypt(
    186             peer,
    187             MycProviderNip44Version::V2,
    188             event.encrypted_content().as_bytes(),
    189         ),
    190     }
    191     .map_err(|_| work_error(MycNip46WorkErrorKind::ProviderWorkRejected))?;
    192     let operation =
    193         MycProviderOperation::new(transport, operation_id, correlation_id, deadline, input)
    194             .map_err(|_| work_error(MycNip46WorkErrorKind::ProviderWorkRejected))?;
    195     Ok(MycNip46DecryptWork { event, operation })
    196 }
    197 
    198 fn derive_decrypt_identity(domain: &[u8], event_id: &[u8; 32]) -> [u8; 32] {
    199     let mut hasher = Sha256::new();
    200     hasher.update(domain);
    201     hasher.update(event_id);
    202     hasher.finalize().into()
    203 }
    204 
    205 /// Non-forgeable proof that the typed request came from the exact decrypt work.
    206 pub struct MycDecryptedNip46Request {
    207     replay: MycReplayBoundNip46Request,
    208 }
    209 
    210 impl fmt::Debug for MycDecryptedNip46Request {
    211     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    212         formatter.write_str("MycDecryptedNip46Request([redacted])")
    213     }
    214 }
    215 
    216 /// Exact request ready for durable admission but not provider execution.
    217 pub struct MycPreparedNip46Request {
    218     signer_request: MycSignerRequest,
    219     request: Request,
    220     encryption_context: MycNip46EncryptionContext,
    221 }
    222 
    223 impl MycPreparedNip46Request {
    224     /// Returns the exact input for the short durable request-admission transaction.
    225     #[must_use]
    226     pub const fn signer_request(&self) -> &MycSignerRequest {
    227         &self.signer_request
    228     }
    229 
    230     /// Returns the exact closed method admitted before persistence.
    231     #[must_use]
    232     pub const fn method(&self) -> MycSignerRequestMethod {
    233         self.signer_request.method()
    234     }
    235 }
    236 
    237 impl fmt::Debug for MycPreparedNip46Request {
    238     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    239         formatter
    240             .debug_struct("MycPreparedNip46Request")
    241             .field("method", &self.method())
    242             .field("request", &"[redacted]")
    243             .finish()
    244     }
    245 }
    246 
    247 /// Converts one proven decrypted request into exact durable-admission input.
    248 pub fn prepare_myc_nip46_request(
    249     decrypted: MycDecryptedNip46Request,
    250     nonce: MycSignerOperationNonce,
    251     received_at: MycRequestReceivedAtUnixMs,
    252 ) -> Result<MycPreparedNip46Request, MycNip46WorkError> {
    253     let encryption_context = decrypted.replay.encryption_context();
    254     let method = MycSignerRequestMethod::parse(decrypted.replay.method())
    255         .ok_or_else(|| work_error(MycNip46WorkErrorKind::UnsupportedMethod))?;
    256     let message: RequestMessage = serde_json::from_slice(decrypted.replay.canonical_request())
    257         .map_err(|_| work_error(MycNip46WorkErrorKind::InvalidBinding))?;
    258     if message.payload().method().as_str() != method.as_str()
    259         || message
    260             .request_id()
    261             .map(|request_id| request_id.as_str() != decrypted.replay.request_id().as_str())
    262             .unwrap_or(true)
    263     {
    264         return Err(work_error(MycNip46WorkErrorKind::InvalidBinding));
    265     }
    266     let signer_request = MycSignerRequest::new(
    267         decrypted.replay.client_public_key().clone(),
    268         decrypted.replay.request_id().clone(),
    269         decrypted.replay.event_id(),
    270         method,
    271         decrypted.replay.request_digest(),
    272         nonce,
    273         received_at,
    274     );
    275     Ok(MycPreparedNip46Request {
    276         signer_request,
    277         request: message.request,
    278         encryption_context,
    279     })
    280 }
    281 
    282 /// Exact Step 145 work classification.
    283 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
    284 pub enum MycNip46WorkKind {
    285     Connect,
    286     Local,
    287     Provider,
    288 }
    289 
    290 enum Nip46WorkPayload {
    291     Connect {
    292         client: crate::MycNip46ClientPublicKey,
    293         permissions: MycConnectionPermissionSet,
    294     },
    295     Local,
    296     Provider(MycProviderOperation),
    297 }
    298 
    299 /// Sealed request work constructed only after durable request admission.
    300 pub struct MycNip46Work {
    301     request: MycSignerRequestRecord,
    302     connection: Option<MycConnectionRecord>,
    303     method: MycSignerRequestMethod,
    304     encryption_context: MycNip46EncryptionContext,
    305     payload: Nip46WorkPayload,
    306 }
    307 
    308 impl MycNip46Work {
    309     #[cfg(all(test, any(target_os = "linux", target_os = "macos")))]
    310     pub(crate) fn local_for_test(
    311         request: MycSignerRequestRecord,
    312         connection: MycConnectionRecord,
    313         method: MycSignerRequestMethod,
    314     ) -> Self {
    315         assert!(matches!(
    316             method,
    317             MycSignerRequestMethod::GetPublicKey
    318                 | MycSignerRequestMethod::GetSessionCapability
    319                 | MycSignerRequestMethod::Ping
    320                 | MycSignerRequestMethod::SwitchRelays
    321                 | MycSignerRequestMethod::Logout
    322         ));
    323         Self {
    324             request,
    325             connection: Some(connection),
    326             method,
    327             encryption_context: MycNip46EncryptionContext::Nip44V2,
    328             payload: Nip46WorkPayload::Local,
    329         }
    330     }
    331 
    332     /// Returns the exact admitted durable request record.
    333     #[must_use]
    334     pub const fn request_record(&self) -> &MycSignerRequestRecord {
    335         &self.request
    336     }
    337 
    338     /// Returns the exact active connection used for non-connect work.
    339     #[must_use]
    340     pub const fn connection(&self) -> Option<&MycConnectionRecord> {
    341         self.connection.as_ref()
    342     }
    343 
    344     /// Returns the exact admitted request method.
    345     #[must_use]
    346     pub const fn method(&self) -> MycSignerRequestMethod {
    347         self.method
    348     }
    349 
    350     pub(crate) const fn encryption_context(&self) -> MycNip46EncryptionContext {
    351         self.encryption_context
    352     }
    353 
    354     /// Returns the work class without exposing protected parameters.
    355     #[must_use]
    356     pub const fn kind(&self) -> MycNip46WorkKind {
    357         match &self.payload {
    358             Nip46WorkPayload::Connect { .. } => MycNip46WorkKind::Connect,
    359             Nip46WorkPayload::Local => MycNip46WorkKind::Local,
    360             Nip46WorkPayload::Provider(_) => MycNip46WorkKind::Provider,
    361         }
    362     }
    363 
    364     /// Returns the provider operation only for provider-bearing work.
    365     #[must_use]
    366     pub const fn provider_operation(&self) -> Option<&MycProviderOperation> {
    367         match &self.payload {
    368             Nip46WorkPayload::Provider(operation) => Some(operation),
    369             Nip46WorkPayload::Connect { .. } | Nip46WorkPayload::Local => None,
    370         }
    371     }
    372 
    373     /// Constructs the exact Step 144 admission request for connect work.
    374     pub fn connection_admission_request(
    375         &self,
    376         policy_generation: MycConnectionPolicyGeneration,
    377         nonce: MycConnectionNonce,
    378         observed_at: MycConnectionTimeUnixMs,
    379         authorized_until: Option<MycConnectionTimeUnixMs>,
    380         policy: MycConnectionAdmissionPolicy,
    381         relay_id: MycRateRelayId,
    382     ) -> Result<MycConnectionAdmissionRequest, MycNip46WorkError> {
    383         let Nip46WorkPayload::Connect {
    384             client,
    385             permissions,
    386         } = &self.payload
    387         else {
    388             return Err(work_error(MycNip46WorkErrorKind::InvalidBinding));
    389         };
    390         MycConnectionAdmissionRequest::new(
    391             self.request.operation_id(),
    392             client.clone(),
    393             permissions.clone(),
    394             policy_generation,
    395             nonce,
    396             observed_at,
    397             authorized_until,
    398             policy,
    399             relay_id,
    400         )
    401         .map_err(|_| work_error(MycNip46WorkErrorKind::InvalidBinding))
    402     }
    403 }
    404 
    405 impl fmt::Debug for MycNip46Work {
    406     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    407         formatter
    408             .debug_struct("MycNip46Work")
    409             .field("method", &self.method)
    410             .field("kind", &self.kind())
    411             .field("request", &"[redacted]")
    412             .field(
    413                 "connection",
    414                 &self.connection.as_ref().map(|_| "[redacted]"),
    415             )
    416             .finish()
    417     }
    418 }
    419 
    420 /// Binds an admitted request and optional active connection to exact work.
    421 ///
    422 /// This function accepts no repository, host, transaction, provider executor,
    423 /// clock, or entropy source. Provider execution therefore starts only after
    424 /// the caller's durable request-admission transaction has ended.
    425 pub fn prepare_myc_nip46_work(
    426     prepared: MycPreparedNip46Request,
    427     record: MycSignerRequestRecord,
    428     connection: Option<MycConnectionRecord>,
    429     providers: &MycProviderContract,
    430     observed_at: MycConnectionTimeUnixMs,
    431     provider_deadline: Option<MycProviderDeadlineUnixMs>,
    432 ) -> Result<MycNip46Work, MycNip46WorkError> {
    433     let MycPreparedNip46Request {
    434         signer_request,
    435         request,
    436         encryption_context,
    437     } = prepared;
    438     if !record.matches_request(&signer_request) || observed_at.get() < record.received_at().get() {
    439         return Err(work_error(MycNip46WorkErrorKind::InvalidBinding));
    440     }
    441     let method = signer_request.method();
    442     let request = match request {
    443         Request::Connect {
    444             remote_signer_public_key,
    445             secret,
    446             requested_permissions,
    447             client_metadata: _,
    448         } => {
    449             if connection.is_some() || provider_deadline.is_some() || secret.is_some() {
    450                 return Err(work_error(MycNip46WorkErrorKind::InvalidBinding));
    451             }
    452             let transport = transport_binding(providers)?;
    453             if remote_signer_public_key.to_hex() != transport.expected_identity().as_hex() {
    454                 return Err(work_error(MycNip46WorkErrorKind::InvalidBinding));
    455             }
    456             let permissions = connect_permissions(requested_permissions.as_slice())?;
    457             return Ok(MycNip46Work {
    458                 request: record,
    459                 connection: None,
    460                 method,
    461                 encryption_context,
    462                 payload: Nip46WorkPayload::Connect {
    463                     client: signer_request.client_public_key().clone(),
    464                     permissions,
    465                 },
    466             });
    467         }
    468         Request::Custom { .. } => {
    469             return Err(work_error(MycNip46WorkErrorKind::UnsupportedMethod));
    470         }
    471         request => request,
    472     };
    473 
    474     let connection = connection.ok_or_else(|| work_error(MycNip46WorkErrorKind::InvalidBinding))?;
    475     if connection.client_public_key() != signer_request.client_public_key() {
    476         return Err(work_error(MycNip46WorkErrorKind::InvalidBinding));
    477     }
    478     if connection.status() != MycConnectionStatus::Active
    479         || connection
    480             .authorized_until()
    481             .is_some_and(|until| observed_at > until)
    482     {
    483         return Err(work_error(MycNip46WorkErrorKind::ConnectionExpired));
    484     }
    485     if let Some(permission) = required_permission(&request) {
    486         let permission = protocol_permission(&permission.to_string())?;
    487         if !connection.granted_permissions().contains(permission) {
    488             return Err(work_error(MycNip46WorkErrorKind::PermissionDenied));
    489         }
    490     }
    491 
    492     let payload = match request {
    493         Request::GetPublicKey
    494         | Request::GetSessionCapability
    495         | Request::Ping
    496         | Request::SwitchRelays
    497         | Request::Logout => {
    498             if provider_deadline.is_some() {
    499                 return Err(work_error(MycNip46WorkErrorKind::InvalidBinding));
    500             }
    501             Nip46WorkPayload::Local
    502         }
    503         Request::SignEvent(event) => {
    504             let input = sign_event_input(&event, providers)?;
    505             provider_work(
    506                 providers,
    507                 &record,
    508                 observed_at,
    509                 provider_deadline,
    510                 Ok(input),
    511             )?
    512         }
    513         Request::Nip04Encrypt {
    514             public_key,
    515             plaintext,
    516         } => provider_work(
    517             providers,
    518             &record,
    519             observed_at,
    520             provider_deadline,
    521             MycProviderOperationInput::nip04_encrypt(
    522                 provider_identity(&public_key.to_hex())?,
    523                 plaintext.as_bytes(),
    524             ),
    525         )?,
    526         Request::Nip04Decrypt {
    527             public_key,
    528             ciphertext,
    529         } => provider_work(
    530             providers,
    531             &record,
    532             observed_at,
    533             provider_deadline,
    534             MycProviderOperationInput::nip04_decrypt(
    535                 provider_identity(&public_key.to_hex())?,
    536                 ciphertext.as_bytes(),
    537             ),
    538         )?,
    539         Request::Nip44Encrypt {
    540             public_key,
    541             plaintext,
    542         } => provider_work(
    543             providers,
    544             &record,
    545             observed_at,
    546             provider_deadline,
    547             MycProviderOperationInput::nip44_encrypt(
    548                 provider_identity(&public_key.to_hex())?,
    549                 MycProviderNip44Version::V2,
    550                 plaintext.as_bytes(),
    551             ),
    552         )?,
    553         Request::Nip44Decrypt {
    554             public_key,
    555             ciphertext,
    556         } => provider_work(
    557             providers,
    558             &record,
    559             observed_at,
    560             provider_deadline,
    561             MycProviderOperationInput::nip44_decrypt(
    562                 provider_identity(&public_key.to_hex())?,
    563                 MycProviderNip44Version::V2,
    564                 ciphertext.as_bytes(),
    565             ),
    566         )?,
    567         Request::Connect { .. } | Request::Custom { .. } => {
    568             return Err(work_error(MycNip46WorkErrorKind::UnsupportedMethod));
    569         }
    570     };
    571 
    572     Ok(MycNip46Work {
    573         request: record,
    574         connection: Some(connection),
    575         method,
    576         encryption_context,
    577         payload,
    578     })
    579 }
    580 
    581 fn provider_work(
    582     providers: &MycProviderContract,
    583     record: &MycSignerRequestRecord,
    584     observed_at: MycConnectionTimeUnixMs,
    585     deadline: Option<MycProviderDeadlineUnixMs>,
    586     input: Result<MycProviderOperationInput, crate::MycProviderContractError>,
    587 ) -> Result<Nip46WorkPayload, MycNip46WorkError> {
    588     let deadline = deadline.ok_or_else(|| work_error(MycNip46WorkErrorKind::InvalidBinding))?;
    589     if deadline.get() <= observed_at.get() {
    590         return Err(work_error(MycNip46WorkErrorKind::ProviderWorkRejected));
    591     }
    592     let input = input.map_err(|_| work_error(MycNip46WorkErrorKind::ProviderWorkRejected))?;
    593     let operation = MycProviderOperation::new(
    594         user_binding(providers)?,
    595         MycProviderOperationId::from_bytes(*record.operation_id().as_bytes()),
    596         MycProviderCorrelationId::from_bytes(*record.correlation_id().as_bytes()),
    597         deadline,
    598         input,
    599     )
    600     .map_err(|_| work_error(MycNip46WorkErrorKind::ProviderWorkRejected))?;
    601     Ok(Nip46WorkPayload::Provider(operation))
    602 }
    603 
    604 fn user_binding(providers: &MycProviderContract) -> Result<&MycProviderBinding, MycNip46WorkError> {
    605     providers
    606         .binding(MycProviderRole::User)
    607         .ok_or_else(|| work_error(MycNip46WorkErrorKind::InvalidBinding))
    608 }
    609 
    610 fn transport_binding(
    611     providers: &MycProviderContract,
    612 ) -> Result<&MycProviderBinding, MycNip46WorkError> {
    613     providers
    614         .binding(MycProviderRole::Transport)
    615         .ok_or_else(|| work_error(MycNip46WorkErrorKind::InvalidBinding))
    616 }
    617 
    618 fn sign_event_input(
    619     event: &ConnectUnsignedEvent,
    620     providers: &MycProviderContract,
    621 ) -> Result<MycProviderOperationInput, MycNip46WorkError> {
    622     let canonical = event.as_json();
    623     let unsigned: NostrUnsignedEvent = serde_json::from_str(&canonical)
    624         .map_err(|_| work_error(MycNip46WorkErrorKind::ProviderWorkRejected))?;
    625     let reencoded = serde_json::to_vec(&unsigned)
    626         .map_err(|_| work_error(MycNip46WorkErrorKind::ProviderWorkRejected))?;
    627     if reencoded.as_slice() != canonical.as_bytes()
    628         || unsigned.verify_id().is_err()
    629         || unsigned.pubkey.to_hex() != user_binding(providers)?.expected_identity().as_hex()
    630     {
    631         return Err(work_error(MycNip46WorkErrorKind::ProviderWorkRejected));
    632     }
    633     MycProviderOperationInput::sign_event(canonical.as_bytes())
    634         .map_err(|_| work_error(MycNip46WorkErrorKind::ProviderWorkRejected))
    635 }
    636 
    637 fn provider_identity(value: &str) -> Result<MycProviderPublicIdentity, MycNip46WorkError> {
    638     MycProviderPublicIdentity::new(value)
    639         .map_err(|_| work_error(MycNip46WorkErrorKind::ProviderWorkRejected))
    640 }
    641 
    642 fn protocol_permission(value: &str) -> Result<MycConnectionPermission, MycNip46WorkError> {
    643     MycConnectionPermission::parse(value)
    644         .ok_or_else(|| work_error(MycNip46WorkErrorKind::UnsupportedMethod))
    645 }
    646 
    647 fn connect_permissions(
    648     permissions: &[radroots_nostr_connect::permission::Permission],
    649 ) -> Result<MycConnectionPermissionSet, MycNip46WorkError> {
    650     let permissions = permissions
    651         .iter()
    652         .map(connect_permission)
    653         .collect::<Result<Vec<_>, _>>()?;
    654     if permissions.iter().any(|permission| {
    655         !matches!(
    656             permission,
    657             MycConnectionPermission::SignEvent(_)
    658                 | MycConnectionPermission::Nip04Encrypt
    659                 | MycConnectionPermission::Nip04Decrypt
    660                 | MycConnectionPermission::Nip44Encrypt
    661                 | MycConnectionPermission::Nip44Decrypt
    662                 | MycConnectionPermission::SwitchRelays
    663         )
    664     }) {
    665         return Err(work_error(MycNip46WorkErrorKind::PermissionDenied));
    666     }
    667     MycConnectionPermissionSet::new(&permissions)
    668         .map_err(|_| work_error(MycNip46WorkErrorKind::PermissionDenied))
    669 }
    670 
    671 fn connect_permission(
    672     permission: &radroots_nostr_connect::permission::Permission,
    673 ) -> Result<MycConnectionPermission, MycNip46WorkError> {
    674     if permission.method() == &radroots_nostr_connect::Method::SignEvent {
    675         let parameter = permission
    676             .parameter()
    677             .ok_or_else(|| work_error(MycNip46WorkErrorKind::PermissionDenied))?;
    678         let kind = parameter.strip_prefix("kind:").unwrap_or(parameter);
    679         let parsed = kind
    680             .parse::<u32>()
    681             .ok()
    682             .filter(|parsed| kind == parsed.to_string())
    683             .ok_or_else(|| work_error(MycNip46WorkErrorKind::PermissionDenied))?;
    684         return Ok(MycConnectionPermission::SignEvent(parsed));
    685     }
    686     if permission.parameter().is_some() {
    687         return Err(work_error(MycNip46WorkErrorKind::PermissionDenied));
    688     }
    689     protocol_permission(permission.method().as_str())
    690 }
    691 
    692 #[cfg(test)]
    693 mod tests {
    694     use std::error::Error as _;
    695 
    696     use nostr::{
    697         EventBuilder, JsonUtil as _, Keys, Kind, Tag, Timestamp,
    698         UnsignedEvent as NostrUnsignedEvent,
    699         nips::{nip04, nip44},
    700     };
    701     use radroots_nostr_connect::{
    702         Method,
    703         message::{Request, RequestMessage, UnsignedEvent as ConnectUnsignedEvent},
    704     };
    705 
    706     use crate::{
    707         MycConfigDocumentV1, MycConfigProfile, MycNip46AuthoredTimePolicy,
    708         MycNip46ObservedAtUnixSeconds, MycProviderCapability, MycProviderResponseObservedAtUnixMs,
    709         admit_myc_nip46_event, parse_myc_config_v1,
    710         provider_verification::verify_protected_response_for_test, verify_myc_nip46_event,
    711     };
    712 
    713     use super::*;
    714 
    715     const CONFIG: &str = include_str!("../contracts/services_hardening/config.v1.example.toml");
    716     const OBSERVED_AT_SECONDS: u64 = 1_725_000_000;
    717     const RECEIVED_AT_MS: u64 = 1_725_000_000_000;
    718     const PROVIDER_DEADLINE_MS: u64 = RECEIVED_AT_MS + 10_000;
    719 
    720     fn keys(seed: u8) -> Keys {
    721         Keys::parse(&format!("{seed:02x}{}", "00".repeat(31))).expect("test keys")
    722     }
    723 
    724     fn configuration() -> MycConfigDocumentV1 {
    725         let source = CONFIG
    726             .replacen(
    727                 "4444444444444444444444444444444444444444444444444444444444444444",
    728                 &keys(2).public_key().to_hex(),
    729                 1,
    730             )
    731             .replacen(
    732                 "2222222222222222222222222222222222222222222222222222222222222222",
    733                 &keys(3).public_key().to_hex(),
    734                 1,
    735             );
    736         parse_myc_config_v1(source.as_bytes(), MycConfigProfile::RepoLocal)
    737             .expect("test configuration")
    738     }
    739 
    740     fn verified_event(
    741         request: &Request,
    742         nip44_v2: bool,
    743         created_at: u64,
    744     ) -> (MycVerifiedNip46Event, Vec<u8>) {
    745         let client = keys(1);
    746         let transport = keys(2);
    747         let message = RequestMessage::try_new(format!("request-{created_at}"), request.clone())
    748             .expect("request message");
    749         let plaintext = serde_json::to_vec(&message).expect("canonical request");
    750         let content = if nip44_v2 {
    751             nip44::encrypt(
    752                 client.secret_key(),
    753                 &transport.public_key(),
    754                 &plaintext,
    755                 nip44::Version::V2,
    756             )
    757             .expect("NIP-44 encryption")
    758         } else {
    759             nip04::encrypt(client.secret_key(), &transport.public_key(), &plaintext)
    760                 .expect("NIP-04 encryption")
    761         };
    762         let event = EventBuilder::new(Kind::Custom(24_133), content)
    763             .tag(Tag::public_key(transport.public_key()))
    764             .custom_created_at(Timestamp::from_secs(created_at))
    765             .sign_with_keys(&client)
    766             .expect("signed request event");
    767         let config = configuration();
    768         let limits = MycNip46AdmissionLimits::from_config(&config).expect("admission limits");
    769         let wire = serde_json::to_vec(&event).expect("event wire");
    770         let event = verify_myc_nip46_event(
    771             admit_myc_nip46_event(limits, &wire).expect("bounded event"),
    772             config
    773                 .provider_contract()
    774                 .binding(MycProviderRole::Transport)
    775                 .expect("transport binding"),
    776             MycNip46ObservedAtUnixSeconds::new(OBSERVED_AT_SECONDS).expect("observation"),
    777             MycNip46AuthoredTimePolicy::new(120, 120).expect("authored-time policy"),
    778         )
    779         .expect("verified event");
    780         (event, plaintext)
    781     }
    782 
    783     fn decrypted(request: &Request, nip44_v2: bool, created_at: u64) -> MycDecryptedNip46Request {
    784         let (event, plaintext) = verified_event(request, nip44_v2, created_at);
    785         let config = configuration();
    786         let transport = config
    787             .provider_contract()
    788             .binding(MycProviderRole::Transport)
    789             .expect("transport binding");
    790         let work = prepare_myc_nip46_decrypt_work(
    791             event,
    792             transport,
    793             MycProviderDeadlineUnixMs::new(PROVIDER_DEADLINE_MS).expect("deadline"),
    794         )
    795         .expect("decrypt work");
    796         let response = verify_protected_response_for_test(transport, work.operation(), &plaintext)
    797             .expect("verified provider response");
    798         work.complete(
    799             response,
    800             MycNip46AdmissionLimits::from_config(&config).expect("admission limits"),
    801         )
    802         .expect("decrypted request")
    803     }
    804 
    805     fn prepared_request(request: Request, seed: u8) -> MycPreparedNip46Request {
    806         prepare_myc_nip46_request(
    807             decrypted(&request, true, OBSERVED_AT_SECONDS + u64::from(seed)),
    808             MycSignerOperationNonce::from_injected_entropy([seed; 32]),
    809             MycRequestReceivedAtUnixMs::new(RECEIVED_AT_MS + u64::from(seed))
    810                 .expect("received time"),
    811         )
    812         .expect("prepared request")
    813     }
    814 
    815     fn active_connection(
    816         prepared: &MycPreparedNip46Request,
    817         permissions: &[MycConnectionPermission],
    818         authorized_until: Option<u64>,
    819     ) -> MycConnectionRecord {
    820         MycConnectionRecord::active_for_test(
    821             prepared.signer_request.client_public_key().clone(),
    822             MycConnectionPermissionSet::new(permissions).expect("permissions"),
    823             authorized_until
    824                 .map(|value| MycConnectionTimeUnixMs::new(value).expect("authorization deadline")),
    825         )
    826     }
    827 
    828     fn unsigned_sign_event() -> ConnectUnsignedEvent {
    829         unsigned_sign_event_for(3)
    830     }
    831 
    832     fn unsigned_sign_event_for(author_seed: u8) -> ConnectUnsignedEvent {
    833         let event = NostrUnsignedEvent::new(
    834             keys(author_seed).public_key(),
    835             Timestamp::from_secs(OBSERVED_AT_SECONDS),
    836             Kind::TextNote,
    837             Vec::<Tag>::new(),
    838             "bounded test event",
    839         );
    840         ConnectUnsignedEvent::from_json(&event.as_json()).expect("connect unsigned event")
    841     }
    842 
    843     fn protocol_request(method: Method, params: Vec<String>) -> Request {
    844         Request::from_parts(method, params).expect("protocol request")
    845     }
    846 
    847     #[test]
    848     fn verified_decryption_is_bound_to_the_exact_event_and_context() {
    849         let request = Request::Ping;
    850         for (nip44_v2, expected_capability, expected_version) in [
    851             (false, MycProviderCapability::Nip04Decrypt, None),
    852             (
    853                 true,
    854                 MycProviderCapability::Nip44Decrypt,
    855                 Some(MycProviderNip44Version::V2),
    856             ),
    857         ] {
    858             let (event, plaintext) = verified_event(&request, nip44_v2, OBSERVED_AT_SECONDS);
    859             let config = configuration();
    860             let transport = config
    861                 .provider_contract()
    862                 .binding(MycProviderRole::Transport)
    863                 .expect("transport binding");
    864             let work = prepare_myc_nip46_decrypt_work(
    865                 event,
    866                 transport,
    867                 MycProviderDeadlineUnixMs::new(PROVIDER_DEADLINE_MS).expect("deadline"),
    868             )
    869             .expect("decrypt work");
    870             assert_eq!(work.operation().role(), MycProviderRole::Transport);
    871             assert_eq!(work.operation().input().capability(), expected_capability);
    872             assert_eq!(work.operation().input().nip44_version(), expected_version);
    873             let response =
    874                 verify_protected_response_for_test(transport, work.operation(), &plaintext)
    875                     .expect("verified response");
    876             work.complete(
    877                 response,
    878                 MycNip46AdmissionLimits::from_config(&config).expect("limits"),
    879             )
    880             .expect("exact event completion");
    881         }
    882 
    883         let (first_event, plaintext) = verified_event(&request, true, OBSERVED_AT_SECONDS + 1);
    884         let (second_event, _) = verified_event(&request, true, OBSERVED_AT_SECONDS + 2);
    885         let config = configuration();
    886         let transport = config
    887             .provider_contract()
    888             .binding(MycProviderRole::Transport)
    889             .expect("transport binding");
    890         let first = prepare_myc_nip46_decrypt_work(
    891             first_event,
    892             transport,
    893             MycProviderDeadlineUnixMs::new(PROVIDER_DEADLINE_MS).expect("deadline"),
    894         )
    895         .expect("first work");
    896         let second = prepare_myc_nip46_decrypt_work(
    897             second_event,
    898             transport,
    899             MycProviderDeadlineUnixMs::new(PROVIDER_DEADLINE_MS).expect("deadline"),
    900         )
    901         .expect("second work");
    902         let response = verify_protected_response_for_test(transport, first.operation(), &plaintext)
    903             .expect("first response");
    904         assert_ne!(
    905             first.operation().operation_id(),
    906             second.operation().operation_id()
    907         );
    908         assert_eq!(
    909             second
    910                 .complete(
    911                     response,
    912                     MycNip46AdmissionLimits::from_config(&config).expect("limits"),
    913                 )
    914                 .expect_err("cross-event response"),
    915             work_error(MycNip46WorkErrorKind::InvalidBinding)
    916         );
    917     }
    918 
    919     #[test]
    920     fn exact_method_inventory_maps_to_local_and_user_provider_work() {
    921         let peer = keys(4).public_key().to_hex();
    922         let cases = vec![
    923             (Request::GetPublicKey, None, MycNip46WorkKind::Local, None),
    924             (
    925                 Request::GetSessionCapability,
    926                 None,
    927                 MycNip46WorkKind::Local,
    928                 None,
    929             ),
    930             (Request::Ping, None, MycNip46WorkKind::Local, None),
    931             (Request::Logout, None, MycNip46WorkKind::Local, None),
    932             (
    933                 Request::SwitchRelays,
    934                 Some(MycConnectionPermission::SwitchRelays),
    935                 MycNip46WorkKind::Local,
    936                 None,
    937             ),
    938             (
    939                 Request::SignEvent(unsigned_sign_event()),
    940                 Some(MycConnectionPermission::SignEvent(1)),
    941                 MycNip46WorkKind::Provider,
    942                 Some(MycProviderCapability::SignEvent),
    943             ),
    944             (
    945                 protocol_request(Method::Nip04Encrypt, vec![peer.clone(), "bounded".into()]),
    946                 Some(MycConnectionPermission::Nip04Encrypt),
    947                 MycNip46WorkKind::Provider,
    948                 Some(MycProviderCapability::Nip04Encrypt),
    949             ),
    950             (
    951                 protocol_request(
    952                     Method::Nip04Decrypt,
    953                     vec![
    954                         peer.clone(),
    955                         "QUFBQUFBQUFBQUFBQUFBQQ==?iv=QUFBQUFBQUFBQUFBQUFBQQ==".into(),
    956                     ],
    957                 ),
    958                 Some(MycConnectionPermission::Nip04Decrypt),
    959                 MycNip46WorkKind::Provider,
    960                 Some(MycProviderCapability::Nip04Decrypt),
    961             ),
    962             (
    963                 protocol_request(Method::Nip44Encrypt, vec![peer.clone(), "bounded".into()]),
    964                 Some(MycConnectionPermission::Nip44Encrypt),
    965                 MycNip46WorkKind::Provider,
    966                 Some(MycProviderCapability::Nip44Encrypt),
    967             ),
    968             (
    969                 protocol_request(
    970                     Method::Nip44Decrypt,
    971                     vec![
    972                         peer,
    973                         "AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".into(),
    974                     ],
    975                 ),
    976                 Some(MycConnectionPermission::Nip44Decrypt),
    977                 MycNip46WorkKind::Provider,
    978                 Some(MycProviderCapability::Nip44Decrypt),
    979             ),
    980         ];
    981         for (index, (request, permission, expected_kind, expected_capability)) in
    982             cases.into_iter().enumerate()
    983         {
    984             let prepared = prepared_request(request, u8::try_from(index + 1).expect("seed"));
    985             let record = prepared.signer_request.admitted_record_for_test();
    986             let permissions = permission.into_iter().collect::<Vec<_>>();
    987             let connection =
    988                 active_connection(&prepared, &permissions, Some(RECEIVED_AT_MS + 100_000));
    989             let provider_deadline = (expected_kind == MycNip46WorkKind::Provider)
    990                 .then(|| MycProviderDeadlineUnixMs::new(PROVIDER_DEADLINE_MS).expect("deadline"));
    991             let work = prepare_myc_nip46_work(
    992                 prepared,
    993                 record,
    994                 Some(connection),
    995                 configuration().provider_contract(),
    996                 MycConnectionTimeUnixMs::new(RECEIVED_AT_MS + 1_000).expect("observation"),
    997                 provider_deadline,
    998             )
    999             .expect("governed work");
   1000             assert_eq!(work.kind(), expected_kind);
   1001             assert_eq!(
   1002                 work.provider_operation()
   1003                     .map(|operation| operation.input().capability()),
   1004                 expected_capability
   1005             );
   1006             if let Some(operation) = work.provider_operation() {
   1007                 assert_eq!(operation.role(), MycProviderRole::User);
   1008                 assert_eq!(
   1009                     operation.operation_id().as_bytes(),
   1010                     work.request_record().operation_id().as_bytes()
   1011                 );
   1012             }
   1013         }
   1014     }
   1015 
   1016     #[test]
   1017     fn connect_and_nonconnect_authorization_fail_closed() {
   1018         let connect = protocol_request(
   1019             Method::Connect,
   1020             vec![
   1021                 keys(2).public_key().to_hex(),
   1022                 String::new(),
   1023                 "nip44_encrypt,sign_event:1".into(),
   1024             ],
   1025         );
   1026         let prepared = prepared_request(connect, 20);
   1027         let record = prepared.signer_request.admitted_record_for_test();
   1028         let work = prepare_myc_nip46_work(
   1029             prepared,
   1030             record,
   1031             None,
   1032             configuration().provider_contract(),
   1033             MycConnectionTimeUnixMs::new(RECEIVED_AT_MS + 1_000).expect("observation"),
   1034             None,
   1035         )
   1036         .expect("connect work");
   1037         assert_eq!(work.kind(), MycNip46WorkKind::Connect);
   1038         work.connection_admission_request(
   1039             MycConnectionPolicyGeneration::new(1).expect("generation"),
   1040             MycConnectionNonce::from_injected_entropy([0x44; 32]),
   1041             MycConnectionTimeUnixMs::new(RECEIVED_AT_MS + 1_000).expect("observation"),
   1042             None,
   1043             MycConnectionAdmissionPolicy::ExplicitApproval,
   1044             MycRateRelayId::new("primary").expect("relay"),
   1045         )
   1046         .expect("admission request");
   1047 
   1048         for (request_seed, remote_signer_seed) in [(21, 3), (22, 4)] {
   1049             let wrong_identity_connect = protocol_request(
   1050                 Method::Connect,
   1051                 vec![
   1052                     keys(remote_signer_seed).public_key().to_hex(),
   1053                     String::new(),
   1054                 ],
   1055             );
   1056             let prepared = prepared_request(wrong_identity_connect, request_seed);
   1057             let record = prepared.signer_request.admitted_record_for_test();
   1058             assert_eq!(
   1059                 prepare_myc_nip46_work(
   1060                     prepared,
   1061                     record,
   1062                     None,
   1063                     configuration().provider_contract(),
   1064                     MycConnectionTimeUnixMs::new(RECEIVED_AT_MS + 1_000).expect("observation"),
   1065                     None,
   1066                 )
   1067                 .expect_err("connect remote signer identity mismatch"),
   1068                 work_error(MycNip46WorkErrorKind::InvalidBinding)
   1069             );
   1070         }
   1071 
   1072         let secret_connect = protocol_request(
   1073             Method::Connect,
   1074             vec![keys(3).public_key().to_hex(), "forbidden-secret".into()],
   1075         );
   1076         let prepared = prepared_request(secret_connect, 21);
   1077         let record = prepared.signer_request.admitted_record_for_test();
   1078         assert_eq!(
   1079             prepare_myc_nip46_work(
   1080                 prepared,
   1081                 record,
   1082                 None,
   1083                 configuration().provider_contract(),
   1084                 MycConnectionTimeUnixMs::new(RECEIVED_AT_MS + 1_000).expect("observation"),
   1085                 None,
   1086             )
   1087             .expect_err("connect secret"),
   1088             work_error(MycNip46WorkErrorKind::InvalidBinding)
   1089         );
   1090 
   1091         let prepared = prepared_request(
   1092             protocol_request(
   1093                 Method::Nip44Encrypt,
   1094                 vec![keys(4).public_key().to_hex(), "protected-marker".into()],
   1095             ),
   1096             22,
   1097         );
   1098         let record = prepared.signer_request.admitted_record_for_test();
   1099         let denied = active_connection(&prepared, &[], Some(RECEIVED_AT_MS + 100_000));
   1100         assert_eq!(
   1101             prepare_myc_nip46_work(
   1102                 prepared,
   1103                 record,
   1104                 Some(denied),
   1105                 configuration().provider_contract(),
   1106                 MycConnectionTimeUnixMs::new(RECEIVED_AT_MS + 1_000).expect("observation"),
   1107                 Some(MycProviderDeadlineUnixMs::new(PROVIDER_DEADLINE_MS).expect("deadline")),
   1108             )
   1109             .expect_err("missing permission"),
   1110             work_error(MycNip46WorkErrorKind::PermissionDenied)
   1111         );
   1112 
   1113         let prepared = prepared_request(Request::Ping, 24);
   1114         let record = prepared.signer_request.admitted_record_for_test();
   1115         let expired = active_connection(&prepared, &[], Some(RECEIVED_AT_MS + 500));
   1116         assert_eq!(
   1117             prepare_myc_nip46_work(
   1118                 prepared,
   1119                 record,
   1120                 Some(expired),
   1121                 configuration().provider_contract(),
   1122                 MycConnectionTimeUnixMs::new(RECEIVED_AT_MS + 1_000).expect("observation"),
   1123                 None,
   1124             )
   1125             .expect_err("expired connection"),
   1126             work_error(MycNip46WorkErrorKind::ConnectionExpired)
   1127         );
   1128 
   1129         let prepared = prepared_request(
   1130             protocol_request(
   1131                 Method::Nip04Encrypt,
   1132                 vec![keys(4).public_key().to_hex(), "bounded".into()],
   1133             ),
   1134             25,
   1135         );
   1136         let record = prepared.signer_request.admitted_record_for_test();
   1137         let connection = active_connection(
   1138             &prepared,
   1139             &[MycConnectionPermission::Nip04Encrypt],
   1140             Some(RECEIVED_AT_MS + 100_000),
   1141         );
   1142         assert_eq!(
   1143             prepare_myc_nip46_work(
   1144                 prepared,
   1145                 record,
   1146                 Some(connection),
   1147                 configuration().provider_contract(),
   1148                 MycConnectionTimeUnixMs::new(PROVIDER_DEADLINE_MS).expect("observation"),
   1149                 Some(MycProviderDeadlineUnixMs::new(PROVIDER_DEADLINE_MS).expect("deadline")),
   1150             )
   1151             .expect_err("nonfuture provider deadline"),
   1152             work_error(MycNip46WorkErrorKind::ProviderWorkRejected)
   1153         );
   1154 
   1155         let prepared = prepared_request(Request::SignEvent(unsigned_sign_event_for(4)), 28);
   1156         let record = prepared.signer_request.admitted_record_for_test();
   1157         let connection = active_connection(
   1158             &prepared,
   1159             &[MycConnectionPermission::SignEvent(1)],
   1160             Some(RECEIVED_AT_MS + 100_000),
   1161         );
   1162         assert_eq!(
   1163             prepare_myc_nip46_work(
   1164                 prepared,
   1165                 record,
   1166                 Some(connection),
   1167                 configuration().provider_contract(),
   1168                 MycConnectionTimeUnixMs::new(RECEIVED_AT_MS + 1_000).expect("observation"),
   1169                 Some(MycProviderDeadlineUnixMs::new(PROVIDER_DEADLINE_MS).expect("deadline")),
   1170             )
   1171             .expect_err("wrong signing author"),
   1172             work_error(MycNip46WorkErrorKind::ProviderWorkRejected)
   1173         );
   1174 
   1175         let prepared = prepared_request(Request::Ping, 26);
   1176         let mismatched = prepared_request(Request::Ping, 27)
   1177             .signer_request
   1178             .admitted_record_for_test();
   1179         let connection = active_connection(&prepared, &[], Some(RECEIVED_AT_MS + 100_000));
   1180         assert_eq!(
   1181             prepare_myc_nip46_work(
   1182                 prepared,
   1183                 mismatched,
   1184                 Some(connection),
   1185                 configuration().provider_contract(),
   1186                 MycConnectionTimeUnixMs::new(RECEIVED_AT_MS + 1_000).expect("observation"),
   1187                 None,
   1188             )
   1189             .expect_err("mismatched durable request"),
   1190             work_error(MycNip46WorkErrorKind::InvalidBinding)
   1191         );
   1192 
   1193         let custom = Request::Custom {
   1194             method: Method::custom("vendor_method").expect("custom method"),
   1195             params: vec!["protected-marker".to_owned()],
   1196         };
   1197         assert_eq!(
   1198             prepare_myc_nip46_request(
   1199                 decrypted(&custom, true, OBSERVED_AT_SECONDS + 23),
   1200                 MycSignerOperationNonce::from_injected_entropy([23; 32]),
   1201                 MycRequestReceivedAtUnixMs::new(RECEIVED_AT_MS).expect("received"),
   1202             )
   1203             .expect_err("custom method"),
   1204             work_error(MycNip46WorkErrorKind::UnsupportedMethod)
   1205         );
   1206     }
   1207 
   1208     #[test]
   1209     fn errors_and_work_debug_are_source_free_and_redacted() {
   1210         for kind in [
   1211             MycNip46WorkErrorKind::InvalidBinding,
   1212             MycNip46WorkErrorKind::DecryptionRejected,
   1213             MycNip46WorkErrorKind::UnsupportedMethod,
   1214             MycNip46WorkErrorKind::PermissionDenied,
   1215             MycNip46WorkErrorKind::ConnectionExpired,
   1216             MycNip46WorkErrorKind::ProviderWorkRejected,
   1217         ] {
   1218             let error = work_error(kind);
   1219             assert_eq!(error.kind(), kind);
   1220             assert!(!error.code().is_empty());
   1221             assert!(!error.to_string().is_empty());
   1222             assert!(error.source().is_none());
   1223         }
   1224 
   1225         let prepared = prepared_request(Request::Ping, 30);
   1226         assert!(!format!("{prepared:?}").contains("request-"));
   1227         assert!(!format!("{prepared:?}").contains("protected-marker"));
   1228         assert!(MycProviderResponseObservedAtUnixMs::new(PROVIDER_DEADLINE_MS).is_ok());
   1229     }
   1230 }