lib

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

message.rs (38509B)


      1 //! Bounded NIP-46 request, response, and correlation models.
      2 
      3 use crate::error::RadrootsNostrConnectError;
      4 use crate::method::Method;
      5 use crate::permission::Permissions;
      6 use crate::uri::{ClientMetadata, RelayUrl};
      7 use nostr::JsonUtil;
      8 use radroots_identity::PublicKey;
      9 use serde::{Deserialize, Deserializer, Serialize, Serializer};
     10 use serde_json::{Value, json};
     11 use std::collections::BTreeSet;
     12 use std::fmt;
     13 use std::str::FromStr;
     14 use url::Url;
     15 
     16 pub const RPC_KIND: u16 = 24_133;
     17 pub const REQUEST_ID_MAX_BYTES: usize = 128;
     18 pub const REQUEST_PARAM_COUNT_MAX: usize = 64;
     19 pub const REQUEST_PARAM_MAX_BYTES: usize = 65_536;
     20 pub const REQUEST_PARAMS_MAX_BYTES: usize = 262_144;
     21 pub const RESPONSE_ERROR_MAX_BYTES: usize = 4_096;
     22 pub const RESPONSE_RESULT_MAX_BYTES: usize = 262_144;
     23 pub const REMOTE_CAPABILITY_RELAY_COUNT_MAX: usize = 32;
     24 
     25 /// A validated NIP-46 unsigned-event payload with package-owned representation.
     26 #[derive(Clone, PartialEq, Eq)]
     27 pub struct UnsignedEvent(nostr::UnsignedEvent);
     28 
     29 impl fmt::Debug for UnsignedEvent {
     30     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
     31         formatter.write_str("UnsignedEvent(<redacted>)")
     32     }
     33 }
     34 
     35 impl UnsignedEvent {
     36     pub fn from_json(value: &str) -> Result<Self, RadrootsNostrConnectError> {
     37         serde_json::from_str(value).map(Self).map_err(|error| {
     38             RadrootsNostrConnectError::InvalidRequestPayload {
     39                 method: Method::SignEvent.to_string(),
     40                 reason: error.to_string(),
     41             }
     42         })
     43     }
     44 
     45     #[must_use]
     46     pub fn as_json(&self) -> String {
     47         self.0.as_json()
     48     }
     49 
     50     #[must_use]
     51     pub fn kind(&self) -> u16 {
     52         self.0.kind.as_u16()
     53     }
     54 }
     55 
     56 /// A validated NIP-46 signed-event payload with package-owned representation.
     57 #[derive(Clone, PartialEq, Eq)]
     58 pub struct SignedEvent(nostr::Event);
     59 
     60 impl fmt::Debug for SignedEvent {
     61     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
     62         formatter.write_str("SignedEvent(<redacted>)")
     63     }
     64 }
     65 
     66 impl SignedEvent {
     67     pub fn from_json(value: &str) -> Result<Self, RadrootsNostrConnectError> {
     68         serde_json::from_str(value).map(Self).map_err(|error| {
     69             RadrootsNostrConnectError::InvalidResponsePayload {
     70                 method: Method::SignEvent.to_string(),
     71                 reason: error.to_string(),
     72             }
     73         })
     74     }
     75 
     76     #[must_use]
     77     pub fn as_json(&self) -> String {
     78         self.0.as_json()
     79     }
     80 }
     81 
     82 /// A bounded correlation identifier carried by a NIP-46 request and response.
     83 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
     84 pub struct RequestId(String);
     85 
     86 impl RequestId {
     87     pub fn parse(value: impl Into<String>) -> Result<Self, RadrootsNostrConnectError> {
     88         let value = value.into();
     89         validate_request_id(&value)?;
     90         Ok(Self(value))
     91     }
     92 
     93     #[must_use]
     94     pub fn as_str(&self) -> &str {
     95         &self.0
     96     }
     97 }
     98 
     99 impl fmt::Display for RequestId {
    100     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    101         formatter.write_str(&self.0)
    102     }
    103 }
    104 
    105 impl FromStr for RequestId {
    106     type Err = RadrootsNostrConnectError;
    107 
    108     fn from_str(value: &str) -> Result<Self, Self::Err> {
    109         Self::parse(value)
    110     }
    111 }
    112 
    113 impl Serialize for RequestId {
    114     fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    115     where
    116         S: Serializer,
    117     {
    118         serializer.serialize_str(self.as_str())
    119     }
    120 }
    121 
    122 impl<'de> Deserialize<'de> for RequestId {
    123     fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    124     where
    125         D: Deserializer<'de>,
    126     {
    127         let value = String::deserialize(deserializer)?;
    128         Self::parse(value).map_err(serde::de::Error::custom)
    129     }
    130 }
    131 
    132 #[derive(Debug, Clone, PartialEq, Eq)]
    133 pub struct RemoteSessionCapability {
    134     #[doc(hidden)]
    135     pub user_public_key: PublicKey,
    136     #[doc(hidden)]
    137     pub relays: Vec<RelayUrl>,
    138     #[doc(hidden)]
    139     pub permissions: Permissions,
    140 }
    141 
    142 impl RemoteSessionCapability {
    143     pub fn try_new(
    144         user_public_key: PublicKey,
    145         relays: Vec<RelayUrl>,
    146         permissions: Permissions,
    147     ) -> Result<Self, RadrootsNostrConnectError> {
    148         let capability = Self {
    149             user_public_key,
    150             relays,
    151             permissions,
    152         };
    153         capability.validate()?;
    154         Ok(capability)
    155     }
    156 
    157     #[must_use]
    158     pub const fn user_public_key(&self) -> PublicKey {
    159         self.user_public_key
    160     }
    161 
    162     #[must_use]
    163     pub fn relays(&self) -> &[RelayUrl] {
    164         &self.relays
    165     }
    166 
    167     #[must_use]
    168     pub fn permissions(&self) -> &Permissions {
    169         &self.permissions
    170     }
    171 
    172     fn validate(&self) -> Result<(), RadrootsNostrConnectError> {
    173         if self.relays.len() > REMOTE_CAPABILITY_RELAY_COUNT_MAX {
    174             return Err(RadrootsNostrConnectError::InvalidResponsePayload {
    175                 method: Method::GetSessionCapability.to_string(),
    176                 reason: "remote capability relay count exceeds its limit".to_owned(),
    177             });
    178         }
    179         self.permissions
    180             .to_string()
    181             .parse::<Permissions>()
    182             .map(|_| ())
    183     }
    184 }
    185 
    186 #[derive(Serialize, Deserialize)]
    187 #[serde(deny_unknown_fields)]
    188 struct RemoteSessionCapabilitySerde {
    189     user_public_key: String,
    190     relays: Vec<RelayUrl>,
    191     permissions: Permissions,
    192 }
    193 
    194 impl Serialize for RemoteSessionCapability {
    195     fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    196     where
    197         S: Serializer,
    198     {
    199         self.validate().map_err(serde::ser::Error::custom)?;
    200         RemoteSessionCapabilitySerde {
    201             user_public_key: self.user_public_key.to_hex(),
    202             relays: self.relays.clone(),
    203             permissions: self.permissions.clone(),
    204         }
    205         .serialize(serializer)
    206     }
    207 }
    208 
    209 impl<'de> Deserialize<'de> for RemoteSessionCapability {
    210     fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    211     where
    212         D: Deserializer<'de>,
    213     {
    214         let raw = RemoteSessionCapabilitySerde::deserialize(deserializer)?;
    215         let user_public_key =
    216             parse_public_key(&raw.user_public_key).map_err(serde::de::Error::custom)?;
    217         Self::try_new(user_public_key, raw.relays, raw.permissions)
    218             .map_err(serde::de::Error::custom)
    219     }
    220 }
    221 
    222 #[derive(Clone, PartialEq, Eq)]
    223 pub enum Request {
    224     Connect {
    225         remote_signer_public_key: PublicKey,
    226         secret: Option<String>,
    227         requested_permissions: Permissions,
    228         client_metadata: Option<ClientMetadata>,
    229     },
    230     GetPublicKey,
    231     GetSessionCapability,
    232     SignEvent(UnsignedEvent),
    233     Nip04Encrypt {
    234         public_key: PublicKey,
    235         plaintext: String,
    236     },
    237     Nip04Decrypt {
    238         public_key: PublicKey,
    239         ciphertext: String,
    240     },
    241     Nip44Encrypt {
    242         public_key: PublicKey,
    243         plaintext: String,
    244     },
    245     Nip44Decrypt {
    246         public_key: PublicKey,
    247         ciphertext: String,
    248     },
    249     Ping,
    250     SwitchRelays,
    251     Logout,
    252     Custom {
    253         method: Method,
    254         params: Vec<String>,
    255     },
    256 }
    257 
    258 impl fmt::Debug for Request {
    259     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    260         formatter
    261             .debug_struct("Request")
    262             .field("method", &self.method())
    263             .field("payload", &"<redacted>")
    264             .finish()
    265     }
    266 }
    267 
    268 impl Request {
    269     /// Returns the canonical NIP-46 method represented by this payload.
    270     #[must_use]
    271     pub fn method(&self) -> Method {
    272         match self {
    273             Self::Connect { .. } => Method::Connect,
    274             Self::GetPublicKey => Method::GetPublicKey,
    275             Self::GetSessionCapability => Method::GetSessionCapability,
    276             Self::SignEvent(_) => Method::SignEvent,
    277             Self::Nip04Encrypt { .. } => Method::Nip04Encrypt,
    278             Self::Nip04Decrypt { .. } => Method::Nip04Decrypt,
    279             Self::Nip44Encrypt { .. } => Method::Nip44Encrypt,
    280             Self::Nip44Decrypt { .. } => Method::Nip44Decrypt,
    281             Self::Ping => Method::Ping,
    282             Self::SwitchRelays => Method::SwitchRelays,
    283             Self::Logout => Method::Logout,
    284             Self::Custom { method, .. } => method.clone(),
    285         }
    286     }
    287 
    288     pub fn to_params(&self) -> Result<Vec<String>, RadrootsNostrConnectError> {
    289         let params = match self {
    290             Self::Connect {
    291                 remote_signer_public_key,
    292                 secret,
    293                 requested_permissions,
    294                 client_metadata,
    295             } => {
    296                 let mut params = vec![remote_signer_public_key.to_hex()];
    297                 let normalized_secret = secret.as_ref().filter(|value| !value.is_empty()).cloned();
    298                 if normalized_secret.is_some()
    299                     || !requested_permissions.is_empty()
    300                     || client_metadata.is_some()
    301                 {
    302                     params.push(normalized_secret.unwrap_or_default());
    303                 }
    304                 if !requested_permissions.is_empty() || client_metadata.is_some() {
    305                     params.push(requested_permissions.to_string());
    306                 }
    307                 if let Some(client_metadata) = client_metadata {
    308                     params.push(client_metadata.to_connect_param()?);
    309                 }
    310                 params
    311             }
    312             Self::GetPublicKey
    313             | Self::GetSessionCapability
    314             | Self::Ping
    315             | Self::SwitchRelays
    316             | Self::Logout => Vec::new(),
    317             Self::SignEvent(unsigned_event) => vec![unsigned_event.as_json()],
    318             Self::Nip04Encrypt {
    319                 public_key,
    320                 plaintext,
    321             }
    322             | Self::Nip44Encrypt {
    323                 public_key,
    324                 plaintext,
    325             } => vec![public_key.to_hex(), plaintext.clone()],
    326             Self::Nip04Decrypt {
    327                 public_key,
    328                 ciphertext,
    329             }
    330             | Self::Nip44Decrypt {
    331                 public_key,
    332                 ciphertext,
    333             } => vec![public_key.to_hex(), ciphertext.clone()],
    334             Self::Custom { params, .. } => params.clone(),
    335         };
    336         validate_params(&params)?;
    337         Ok(params)
    338     }
    339 
    340     pub fn from_parts(
    341         method: Method,
    342         params: Vec<String>,
    343     ) -> Result<Self, RadrootsNostrConnectError> {
    344         validate_params(&params)?;
    345         match method {
    346             Method::Connect => {
    347                 if params.is_empty() || params.len() > 4 {
    348                     return Err(RadrootsNostrConnectError::InvalidParams {
    349                         method: method.to_string(),
    350                         expected: "1 to 4 params",
    351                         received: params.len(),
    352                     });
    353                 }
    354                 let remote_signer_public_key = parse_public_key(&params[0])?;
    355                 let secret = params.get(1).cloned().filter(|value| !value.is_empty());
    356                 let requested_permissions = match params.get(2) {
    357                     Some(value) => Permissions::from_str(value)?,
    358                     None => Permissions::default(),
    359                 };
    360                 let client_metadata = params
    361                     .get(3)
    362                     .map(|value| ClientMetadata::from_connect_param(value))
    363                     .transpose()?;
    364                 Ok(Self::Connect {
    365                     remote_signer_public_key,
    366                     secret,
    367                     requested_permissions,
    368                     client_metadata,
    369                 })
    370             }
    371             Method::GetPublicKey => {
    372                 expect_param_count(&method, &params, 0)?;
    373                 Ok(Self::GetPublicKey)
    374             }
    375             Method::GetSessionCapability => {
    376                 expect_param_count(&method, &params, 0)?;
    377                 Ok(Self::GetSessionCapability)
    378             }
    379             Method::SignEvent => {
    380                 expect_param_count(&method, &params, 1)?;
    381                 let unsigned_event = UnsignedEvent::from_json(&params[0])?;
    382                 Ok(Self::SignEvent(unsigned_event))
    383             }
    384             Method::Nip04Encrypt => {
    385                 expect_param_count(&method, &params, 2)?;
    386                 Ok(Self::Nip04Encrypt {
    387                     public_key: parse_public_key(&params[0])?,
    388                     plaintext: params[1].clone(),
    389                 })
    390             }
    391             Method::Nip04Decrypt => {
    392                 expect_param_count(&method, &params, 2)?;
    393                 Ok(Self::Nip04Decrypt {
    394                     public_key: parse_public_key(&params[0])?,
    395                     ciphertext: params[1].clone(),
    396                 })
    397             }
    398             Method::Nip44Encrypt => {
    399                 expect_param_count(&method, &params, 2)?;
    400                 Ok(Self::Nip44Encrypt {
    401                     public_key: parse_public_key(&params[0])?,
    402                     plaintext: params[1].clone(),
    403                 })
    404             }
    405             Method::Nip44Decrypt => {
    406                 expect_param_count(&method, &params, 2)?;
    407                 Ok(Self::Nip44Decrypt {
    408                     public_key: parse_public_key(&params[0])?,
    409                     ciphertext: params[1].clone(),
    410                 })
    411             }
    412             Method::Ping => {
    413                 expect_param_count(&method, &params, 0)?;
    414                 Ok(Self::Ping)
    415             }
    416             Method::SwitchRelays => {
    417                 expect_param_count(&method, &params, 0)?;
    418                 Ok(Self::SwitchRelays)
    419             }
    420             Method::Logout => {
    421                 expect_param_count(&method, &params, 0)?;
    422                 Ok(Self::Logout)
    423             }
    424             custom => Ok(Self::Custom {
    425                 method: custom,
    426                 params,
    427             }),
    428         }
    429     }
    430 }
    431 
    432 #[derive(Debug, Clone, PartialEq, Eq)]
    433 pub struct RequestMessage {
    434     #[doc(hidden)]
    435     pub id: String,
    436     #[doc(hidden)]
    437     pub request: Request,
    438 }
    439 
    440 impl RequestMessage {
    441     /// Creates and validates a serialized request envelope.
    442     pub fn try_new(
    443         id: impl Into<String>,
    444         request: Request,
    445     ) -> Result<Self, RadrootsNostrConnectError> {
    446         let id = id.into();
    447         validate_request_id(&id)?;
    448         request.to_params()?;
    449         Ok(Self { id, request })
    450     }
    451 
    452     /// Compatibility constructor retained until the Step 141 consumer cutover.
    453     #[doc(hidden)]
    454     #[must_use]
    455     pub fn new(id: impl Into<String>, request: Request) -> Self {
    456         Self {
    457             id: id.into(),
    458             request,
    459         }
    460     }
    461 
    462     pub fn request_id(&self) -> Result<RequestId, RadrootsNostrConnectError> {
    463         RequestId::parse(self.id.clone())
    464     }
    465 
    466     #[must_use]
    467     pub fn payload(&self) -> &Request {
    468         &self.request
    469     }
    470 
    471     /// Correlates and decodes a response using this request's method.
    472     pub fn correlate(
    473         &self,
    474         envelope: ResponseEnvelope,
    475     ) -> Result<Response, RadrootsNostrConnectError> {
    476         envelope.validate()?;
    477         if envelope.id != self.id {
    478             return Err(RadrootsNostrConnectError::WrongRequestId);
    479         }
    480         Response::from_envelope(&self.request.method(), envelope)
    481     }
    482 
    483     fn into_raw(self) -> Result<RawRequestMessage, RadrootsNostrConnectError> {
    484         validate_request_id(&self.id)?;
    485         Ok(RawRequestMessage {
    486             id: self.id,
    487             method: self.request.method(),
    488             params: self.request.to_params()?,
    489         })
    490     }
    491 
    492     fn from_raw(raw: RawRequestMessage) -> Result<Self, RadrootsNostrConnectError> {
    493         let request = Request::from_parts(raw.method, raw.params)?;
    494         Self::try_new(raw.id, request)
    495     }
    496 }
    497 
    498 impl Serialize for RequestMessage {
    499     fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    500     where
    501         S: Serializer,
    502     {
    503         self.clone()
    504             .into_raw()
    505             .map_err(serde::ser::Error::custom)?
    506             .serialize(serializer)
    507     }
    508 }
    509 
    510 impl<'de> Deserialize<'de> for RequestMessage {
    511     fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    512     where
    513         D: Deserializer<'de>,
    514     {
    515         let raw = RawRequestMessage::deserialize(deserializer)?;
    516         Self::from_raw(raw).map_err(serde::de::Error::custom)
    517     }
    518 }
    519 
    520 #[derive(Clone, PartialEq, Eq)]
    521 pub struct ResponseEnvelope {
    522     #[doc(hidden)]
    523     pub id: String,
    524     #[doc(hidden)]
    525     pub result: Option<Value>,
    526     #[doc(hidden)]
    527     pub error: Option<String>,
    528 }
    529 
    530 impl fmt::Debug for ResponseEnvelope {
    531     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    532         formatter
    533             .debug_struct("ResponseEnvelope")
    534             .field("id", &self.id)
    535             .field("has_result", &self.result.is_some())
    536             .field("has_error", &self.error.is_some())
    537             .finish()
    538     }
    539 }
    540 
    541 impl ResponseEnvelope {
    542     pub fn try_new(
    543         id: impl Into<String>,
    544         result: Option<Value>,
    545         error: Option<String>,
    546     ) -> Result<Self, RadrootsNostrConnectError> {
    547         let envelope = Self {
    548             id: id.into(),
    549             result,
    550             error,
    551         };
    552         envelope.validate()?;
    553         Ok(envelope)
    554     }
    555 
    556     pub fn request_id(&self) -> Result<RequestId, RadrootsNostrConnectError> {
    557         RequestId::parse(self.id.clone())
    558     }
    559 
    560     #[must_use]
    561     pub fn result(&self) -> Option<&Value> {
    562         self.result.as_ref()
    563     }
    564 
    565     #[must_use]
    566     pub fn error(&self) -> Option<&str> {
    567         self.error.as_deref()
    568     }
    569 
    570     pub fn validate(&self) -> Result<(), RadrootsNostrConnectError> {
    571         validate_request_id(&self.id)?;
    572         if let Some(error) = self.error.as_deref()
    573             && (error.is_empty()
    574                 || error.len() > RESPONSE_ERROR_MAX_BYTES
    575                 || error.chars().any(char::is_control))
    576         {
    577             return Err(RadrootsNostrConnectError::InvalidResponseEnvelope {
    578                 reason: "error must be non-empty, bounded, and control-free",
    579             });
    580         }
    581         if let Some(result) = self.result.as_ref()
    582             && serde_json::to_vec(result)
    583                 .map_err(RadrootsNostrConnectError::from)?
    584                 .len()
    585                 > RESPONSE_RESULT_MAX_BYTES
    586         {
    587             return Err(RadrootsNostrConnectError::InvalidResponseEnvelope {
    588                 reason: "result exceeds its byte limit",
    589             });
    590         }
    591         Ok(())
    592     }
    593 }
    594 
    595 #[derive(Serialize, Deserialize)]
    596 #[serde(deny_unknown_fields)]
    597 struct ResponseEnvelopeSerde {
    598     id: String,
    599     #[serde(default, skip_serializing_if = "Option::is_none")]
    600     result: Option<Value>,
    601     #[serde(default, skip_serializing_if = "Option::is_none")]
    602     error: Option<String>,
    603 }
    604 
    605 impl Serialize for ResponseEnvelope {
    606     fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    607     where
    608         S: Serializer,
    609     {
    610         self.validate().map_err(serde::ser::Error::custom)?;
    611         ResponseEnvelopeSerde {
    612             id: self.id.clone(),
    613             result: self.result.clone(),
    614             error: self.error.clone(),
    615         }
    616         .serialize(serializer)
    617     }
    618 }
    619 
    620 impl<'de> Deserialize<'de> for ResponseEnvelope {
    621     fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    622     where
    623         D: Deserializer<'de>,
    624     {
    625         let raw = ResponseEnvelopeSerde::deserialize(deserializer)?;
    626         Self::try_new(raw.id, raw.result, raw.error).map_err(serde::de::Error::custom)
    627     }
    628 }
    629 
    630 /// Correlation state supplied by a caller that owns response-event identity.
    631 #[derive(Debug)]
    632 pub struct ResponseValidator {
    633     request_id: RequestId,
    634     expected_signer: radroots_identity::PublicKey,
    635     seen_fingerprints: BTreeSet<String>,
    636 }
    637 
    638 impl ResponseValidator {
    639     #[must_use]
    640     pub fn new(request_id: RequestId, expected_signer: radroots_identity::PublicKey) -> Self {
    641         Self {
    642             request_id,
    643             expected_signer,
    644             seen_fingerprints: BTreeSet::new(),
    645         }
    646     }
    647 
    648     /// Validates signer and request correlation and rejects a repeated event fingerprint.
    649     pub fn validate(
    650         &mut self,
    651         signer: radroots_identity::PublicKey,
    652         response_fingerprint: impl Into<String>,
    653         envelope: &ResponseEnvelope,
    654     ) -> Result<(), RadrootsNostrConnectError> {
    655         if signer != self.expected_signer {
    656             return Err(RadrootsNostrConnectError::WrongResponseSigner);
    657         }
    658         envelope.validate()?;
    659         if envelope.id != self.request_id.as_str() {
    660             return Err(RadrootsNostrConnectError::WrongRequestId);
    661         }
    662         let fingerprint = response_fingerprint.into();
    663         validate_response_fingerprint(&fingerprint)?;
    664         if !self.seen_fingerprints.insert(fingerprint) {
    665             return Err(RadrootsNostrConnectError::ReplayedResponse);
    666         }
    667         Ok(())
    668     }
    669 }
    670 
    671 pub const PENDING_CONNECTION_ERROR: &str = "connection is pending";
    672 
    673 #[derive(Debug, Clone, PartialEq, Eq)]
    674 pub enum PendingConnectionOutcome {
    675     PendingApproval,
    676     Approved(PublicKey),
    677     ApprovedCapability(RemoteSessionCapability),
    678     Rejected { message: String },
    679     AuthChallenge { url: String },
    680     UnexpectedResponse { response: String },
    681 }
    682 
    683 #[derive(Clone, PartialEq, Eq)]
    684 pub enum Response {
    685     ConnectAcknowledged,
    686     ConnectSecretEcho(String),
    687     LogoutAcknowledged,
    688     PendingConnection,
    689     UserPublicKey(PublicKey),
    690     RemoteSessionCapability(RemoteSessionCapability),
    691     SignedEvent(SignedEvent),
    692     Pong,
    693     Nip04Encrypt(String),
    694     Nip04Decrypt(String),
    695     Nip44Encrypt(String),
    696     Nip44Decrypt(String),
    697     RelayList(Vec<RelayUrl>),
    698     RelayListUnchanged,
    699     AuthUrl(String),
    700     Error {
    701         result: Option<Value>,
    702         error: String,
    703     },
    704     Custom {
    705         result: Option<Value>,
    706         error: Option<String>,
    707     },
    708 }
    709 
    710 impl fmt::Debug for Response {
    711     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    712         formatter
    713             .debug_struct("Response")
    714             .field("kind", &self.kind_name())
    715             .field("payload", &"<redacted>")
    716             .finish()
    717     }
    718 }
    719 
    720 impl Response {
    721     const fn kind_name(&self) -> &'static str {
    722         match self {
    723             Self::ConnectAcknowledged => "connect_acknowledged",
    724             Self::ConnectSecretEcho(_) => "connect_secret_echo",
    725             Self::LogoutAcknowledged => "logout_acknowledged",
    726             Self::PendingConnection => "pending_connection",
    727             Self::UserPublicKey(_) => "user_public_key",
    728             Self::RemoteSessionCapability(_) => "remote_session_capability",
    729             Self::SignedEvent(_) => "signed_event",
    730             Self::Pong => "pong",
    731             Self::Nip04Encrypt(_) => "nip04_encrypt",
    732             Self::Nip04Decrypt(_) => "nip04_decrypt",
    733             Self::Nip44Encrypt(_) => "nip44_encrypt",
    734             Self::Nip44Decrypt(_) => "nip44_decrypt",
    735             Self::RelayList(_) => "relay_list",
    736             Self::RelayListUnchanged => "relay_list_unchanged",
    737             Self::AuthUrl(_) => "auth_url",
    738             Self::Error { .. } => "error",
    739             Self::Custom { .. } => "custom",
    740         }
    741     }
    742 
    743     pub fn into_pending_connection_poll_outcome(self) -> PendingConnectionOutcome {
    744         match self {
    745             Self::PendingConnection => PendingConnectionOutcome::PendingApproval,
    746             Self::UserPublicKey(public_key) => PendingConnectionOutcome::Approved(public_key),
    747             Self::RemoteSessionCapability(capability) => {
    748                 PendingConnectionOutcome::ApprovedCapability(capability)
    749             }
    750             Self::Error { error, .. } if error == PENDING_CONNECTION_ERROR => {
    751                 PendingConnectionOutcome::PendingApproval
    752             }
    753             Self::Error { error, .. } => PendingConnectionOutcome::Rejected { message: error },
    754             Self::AuthUrl(url) => PendingConnectionOutcome::AuthChallenge { url },
    755             other => PendingConnectionOutcome::UnexpectedResponse {
    756                 response: other.kind_name().to_owned(),
    757             },
    758         }
    759     }
    760 
    761     pub fn into_envelope(
    762         self,
    763         id: impl Into<String>,
    764     ) -> Result<ResponseEnvelope, RadrootsNostrConnectError> {
    765         let id = id.into();
    766         let envelope = match self {
    767             Self::ConnectAcknowledged | Self::LogoutAcknowledged => ResponseEnvelope {
    768                 id,
    769                 result: Some(Value::String("ack".to_owned())),
    770                 error: None,
    771             },
    772             Self::ConnectSecretEcho(secret) => ResponseEnvelope {
    773                 id,
    774                 result: Some(Value::String(secret)),
    775                 error: None,
    776             },
    777             Self::PendingConnection => ResponseEnvelope {
    778                 id,
    779                 result: None,
    780                 error: Some(PENDING_CONNECTION_ERROR.to_owned()),
    781             },
    782             Self::UserPublicKey(public_key) => ResponseEnvelope {
    783                 id,
    784                 result: Some(Value::String(public_key.to_hex())),
    785                 error: None,
    786             },
    787             Self::RemoteSessionCapability(capability) => ResponseEnvelope {
    788                 id,
    789                 result: Some(remote_session_capability_value(capability)),
    790                 error: None,
    791             },
    792             Self::SignedEvent(event) => ResponseEnvelope {
    793                 id,
    794                 result: Some(Value::String(event.as_json())),
    795                 error: None,
    796             },
    797             Self::Pong => ResponseEnvelope {
    798                 id,
    799                 result: Some(Value::String("pong".to_owned())),
    800                 error: None,
    801             },
    802             Self::Nip04Encrypt(text)
    803             | Self::Nip04Decrypt(text)
    804             | Self::Nip44Encrypt(text)
    805             | Self::Nip44Decrypt(text) => ResponseEnvelope {
    806                 id,
    807                 result: Some(Value::String(text)),
    808                 error: None,
    809             },
    810             Self::RelayList(relays) => {
    811                 let relays = relays
    812                     .into_iter()
    813                     .map(|relay| relay.to_string())
    814                     .collect::<Vec<_>>();
    815                 ResponseEnvelope {
    816                     id,
    817                     result: Some(Value::Array(
    818                         relays.into_iter().map(Value::String).collect(),
    819                     )),
    820                     error: None,
    821                 }
    822             }
    823             Self::RelayListUnchanged => ResponseEnvelope {
    824                 id,
    825                 result: Some(Value::Null),
    826                 error: None,
    827             },
    828             Self::AuthUrl(url) => {
    829                 let normalized = validate_url(&url)?;
    830                 ResponseEnvelope {
    831                     id,
    832                     result: Some(Value::String("auth_url".to_owned())),
    833                     error: Some(normalized),
    834                 }
    835             }
    836             Self::Error { result, error } => ResponseEnvelope {
    837                 id,
    838                 result,
    839                 error: Some(error),
    840             },
    841             Self::Custom { result, error } => ResponseEnvelope { id, result, error },
    842         };
    843         envelope.validate()?;
    844         Ok(envelope)
    845     }
    846 
    847     pub fn from_envelope(
    848         method: &Method,
    849         envelope: ResponseEnvelope,
    850     ) -> Result<Self, RadrootsNostrConnectError> {
    851         envelope.validate()?;
    852         if let (Some(Value::String(result)), Some(url)) = (&envelope.result, &envelope.error)
    853             && result == "auth_url"
    854         {
    855             return Ok(Self::AuthUrl(validate_url(url)?));
    856         }
    857 
    858         if let Some(error) = envelope.error {
    859             if matches!(
    860                 method,
    861                 Method::Connect | Method::GetPublicKey | Method::GetSessionCapability
    862             ) && envelope.result.is_none()
    863                 && error == PENDING_CONNECTION_ERROR
    864             {
    865                 return Ok(Self::PendingConnection);
    866             }
    867             if let Method::Custom(_) = method {
    868                 return Ok(Self::Custom {
    869                     result: envelope.result,
    870                     error: Some(error),
    871                 });
    872             }
    873             return Ok(Self::Error {
    874                 result: envelope.result,
    875                 error,
    876             });
    877         }
    878 
    879         match method {
    880             Method::Connect => {
    881                 let result = expect_string_result(method, envelope.result)?;
    882                 if result == "ack" {
    883                     Ok(Self::ConnectAcknowledged)
    884                 } else {
    885                     Ok(Self::ConnectSecretEcho(result))
    886                 }
    887             }
    888             Method::GetPublicKey => {
    889                 let result = expect_string_result(method, envelope.result)?;
    890                 Ok(Self::UserPublicKey(parse_public_key(&result)?))
    891             }
    892             Method::GetSessionCapability => {
    893                 let capability = parse_json_string_result(method, envelope.result)?;
    894                 Ok(Self::RemoteSessionCapability(capability))
    895             }
    896             Method::SignEvent => {
    897                 let value = expect_json_string_or_value(method, envelope.result)?;
    898                 let event = SignedEvent::from_json(&value)?;
    899                 Ok(Self::SignedEvent(event))
    900             }
    901             Method::Ping => {
    902                 let result = expect_string_result(method, envelope.result)?;
    903                 if result != "pong" {
    904                     return Err(RadrootsNostrConnectError::InvalidResponsePayload {
    905                         method: method.to_string(),
    906                         reason: "expected canonical `pong` result".to_owned(),
    907                     });
    908                 }
    909                 Ok(Self::Pong)
    910             }
    911             Method::Nip04Encrypt => Ok(Self::Nip04Encrypt(expect_string_result(
    912                 method,
    913                 envelope.result,
    914             )?)),
    915             Method::Nip04Decrypt => Ok(Self::Nip04Decrypt(expect_string_result(
    916                 method,
    917                 envelope.result,
    918             )?)),
    919             Method::Nip44Encrypt => Ok(Self::Nip44Encrypt(expect_string_result(
    920                 method,
    921                 envelope.result,
    922             )?)),
    923             Method::Nip44Decrypt => Ok(Self::Nip44Decrypt(expect_string_result(
    924                 method,
    925                 envelope.result,
    926             )?)),
    927             Method::SwitchRelays => parse_switch_relays_response(envelope.result),
    928             Method::Logout => {
    929                 let result = expect_string_result(method, envelope.result)?;
    930                 if result != "ack" {
    931                     return Err(RadrootsNostrConnectError::InvalidResponsePayload {
    932                         method: method.to_string(),
    933                         reason: "expected canonical `ack` result".to_owned(),
    934                     });
    935                 }
    936                 Ok(Self::LogoutAcknowledged)
    937             }
    938             Method::Custom(_) => Ok(Self::Custom {
    939                 result: envelope.result,
    940                 error: None,
    941             }),
    942         }
    943     }
    944 }
    945 
    946 fn remote_session_capability_value(capability: RemoteSessionCapability) -> Value {
    947     json!({
    948         "user_public_key": capability.user_public_key.to_hex(),
    949         "relays": capability.relays,
    950         "permissions": capability.permissions,
    951     })
    952 }
    953 
    954 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
    955 #[serde(deny_unknown_fields)]
    956 struct RawRequestMessage {
    957     id: String,
    958     method: Method,
    959     params: Vec<String>,
    960 }
    961 
    962 fn validate_request_id(value: &str) -> Result<(), RadrootsNostrConnectError> {
    963     if value.is_empty() {
    964         return Err(RadrootsNostrConnectError::InvalidRequestId {
    965             reason: "request id cannot be empty",
    966         });
    967     }
    968     if value.len() > REQUEST_ID_MAX_BYTES {
    969         return Err(RadrootsNostrConnectError::InvalidRequestId {
    970             reason: "request id exceeds its byte limit",
    971         });
    972     }
    973     if value.trim() != value || value.chars().any(char::is_control) {
    974         return Err(RadrootsNostrConnectError::InvalidRequestId {
    975             reason: "request id must be canonical and control-free",
    976         });
    977     }
    978     Ok(())
    979 }
    980 
    981 fn validate_params(params: &[String]) -> Result<(), RadrootsNostrConnectError> {
    982     if params.len() > REQUEST_PARAM_COUNT_MAX {
    983         return Err(RadrootsNostrConnectError::InvalidRequestPayload {
    984             method: "custom".to_owned(),
    985             reason: "parameter count exceeds its limit".to_owned(),
    986         });
    987     }
    988     if params
    989         .iter()
    990         .any(|param| param.len() > REQUEST_PARAM_MAX_BYTES)
    991     {
    992         return Err(RadrootsNostrConnectError::InvalidRequestPayload {
    993             method: "unknown".to_owned(),
    994             reason: "a parameter exceeds its byte limit".to_owned(),
    995         });
    996     }
    997     if params.iter().map(String::len).sum::<usize>() > REQUEST_PARAMS_MAX_BYTES {
    998         return Err(RadrootsNostrConnectError::InvalidRequestPayload {
    999             method: "unknown".to_owned(),
   1000             reason: "serialized parameters exceed their byte limit".to_owned(),
   1001         });
   1002     }
   1003     Ok(())
   1004 }
   1005 
   1006 fn validate_response_fingerprint(value: &str) -> Result<(), RadrootsNostrConnectError> {
   1007     if value.is_empty() || value.len() > REQUEST_ID_MAX_BYTES || value.chars().any(char::is_control)
   1008     {
   1009         return Err(RadrootsNostrConnectError::InvalidResponseEnvelope {
   1010             reason: "response fingerprint must be non-empty, bounded, and control-free",
   1011         });
   1012     }
   1013     Ok(())
   1014 }
   1015 
   1016 fn expect_param_count(
   1017     method: &Method,
   1018     params: &[String],
   1019     expected: usize,
   1020 ) -> Result<(), RadrootsNostrConnectError> {
   1021     if params.len() == expected {
   1022         return Ok(());
   1023     }
   1024 
   1025     Err(RadrootsNostrConnectError::InvalidParams {
   1026         method: method.to_string(),
   1027         expected: if expected == 0 {
   1028             "no params"
   1029         } else if expected == 1 {
   1030             "exactly 1 param"
   1031         } else {
   1032             "exactly 2 params"
   1033         },
   1034         received: params.len(),
   1035     })
   1036 }
   1037 
   1038 fn parse_public_key(value: &str) -> Result<PublicKey, RadrootsNostrConnectError> {
   1039     radroots_nostr::key::parse_public_key(value).map_err(|error| {
   1040         RadrootsNostrConnectError::InvalidPublicKey {
   1041             value: value.to_owned(),
   1042             reason: error.to_string(),
   1043         }
   1044     })
   1045 }
   1046 
   1047 fn expect_string_result(
   1048     method: &Method,
   1049     result: Option<Value>,
   1050 ) -> Result<String, RadrootsNostrConnectError> {
   1051     match result {
   1052         Some(Value::String(value)) => Ok(value),
   1053         Some(other) => Err(RadrootsNostrConnectError::InvalidResponsePayload {
   1054             method: method.to_string(),
   1055             reason: format!("expected string result, got {}", json_type(&other)),
   1056         }),
   1057         None => Err(RadrootsNostrConnectError::MissingResult),
   1058     }
   1059 }
   1060 
   1061 fn parse_json_string_result<T>(
   1062     method: &Method,
   1063     result: Option<Value>,
   1064 ) -> Result<T, RadrootsNostrConnectError>
   1065 where
   1066     T: for<'de> Deserialize<'de>,
   1067 {
   1068     match result {
   1069         Some(Value::String(value)) => serde_json::from_str(&value).map_err(|error| {
   1070             RadrootsNostrConnectError::InvalidResponsePayload {
   1071                 method: method.to_string(),
   1072                 reason: error.to_string(),
   1073             }
   1074         }),
   1075         Some(other) => serde_json::from_value(other).map_err(|error| {
   1076             RadrootsNostrConnectError::InvalidResponsePayload {
   1077                 method: method.to_string(),
   1078                 reason: error.to_string(),
   1079             }
   1080         }),
   1081         None => Err(RadrootsNostrConnectError::MissingResult),
   1082     }
   1083 }
   1084 
   1085 fn expect_json_string_or_value(
   1086     method: &Method,
   1087     result: Option<Value>,
   1088 ) -> Result<String, RadrootsNostrConnectError> {
   1089     match result {
   1090         Some(Value::String(value)) => Ok(value),
   1091         Some(value) => serde_json::to_string(&value).map_err(|error| {
   1092             RadrootsNostrConnectError::InvalidResponsePayload {
   1093                 method: method.to_string(),
   1094                 reason: error.to_string(),
   1095             }
   1096         }),
   1097         None => Err(RadrootsNostrConnectError::MissingResult),
   1098     }
   1099 }
   1100 
   1101 fn parse_switch_relays_response(
   1102     result: Option<Value>,
   1103 ) -> Result<Response, RadrootsNostrConnectError> {
   1104     let method = Method::SwitchRelays;
   1105     match result {
   1106         None | Some(Value::Null) => Ok(Response::RelayListUnchanged),
   1107         Some(Value::Array(values)) => {
   1108             let relays = parse_relay_values(values)?;
   1109             Ok(Response::RelayList(relays))
   1110         }
   1111         Some(Value::String(value)) if value == "null" => Ok(Response::RelayListUnchanged),
   1112         Some(Value::String(value)) => {
   1113             let parsed = serde_json::from_str::<Value>(&value).map_err(|error| {
   1114                 RadrootsNostrConnectError::InvalidResponsePayload {
   1115                     method: method.to_string(),
   1116                     reason: error.to_string(),
   1117                 }
   1118             })?;
   1119             parse_switch_relays_response(Some(parsed))
   1120         }
   1121         Some(other) => Err(RadrootsNostrConnectError::InvalidResponsePayload {
   1122             method: method.to_string(),
   1123             reason: format!("expected relay list or null, got {}", json_type(&other)),
   1124         }),
   1125     }
   1126 }
   1127 
   1128 fn parse_relay_values(values: Vec<Value>) -> Result<Vec<RelayUrl>, RadrootsNostrConnectError> {
   1129     values
   1130         .into_iter()
   1131         .map(|value| match value {
   1132             Value::String(value) => RelayUrl::parse(&value),
   1133             other => Err(RadrootsNostrConnectError::InvalidResponsePayload {
   1134                 method: Method::SwitchRelays.to_string(),
   1135                 reason: format!("expected relay string, got {}", json_type(&other)),
   1136             }),
   1137         })
   1138         .collect()
   1139 }
   1140 
   1141 fn validate_url(value: &str) -> Result<String, RadrootsNostrConnectError> {
   1142     if value.len() > crate::uri::CLIENT_URL_MAX_BYTES || value.chars().any(char::is_control) {
   1143         return Err(RadrootsNostrConnectError::InvalidUrl {
   1144             value: "[redacted auth URL]".to_owned(),
   1145             reason: "auth URL is oversized or contains control characters".to_owned(),
   1146         });
   1147     }
   1148     let url = Url::parse(value).map_err(|error| RadrootsNostrConnectError::InvalidUrl {
   1149         value: "[redacted auth URL]".to_owned(),
   1150         reason: error.to_string(),
   1151     })?;
   1152     if !matches!(url.scheme(), "http" | "https") {
   1153         return Err(RadrootsNostrConnectError::InvalidUrl {
   1154             value: "[redacted auth URL]".to_owned(),
   1155             reason: "auth URL scheme must be http or https".to_owned(),
   1156         });
   1157     }
   1158     Ok(url.to_string())
   1159 }
   1160 
   1161 fn json_type(value: &Value) -> &'static str {
   1162     match value {
   1163         Value::Null => "null",
   1164         Value::Bool(_) => "boolean",
   1165         Value::Number(_) => "number",
   1166         Value::String(_) => "string",
   1167         Value::Array(_) => "array",
   1168         Value::Object(_) => "object",
   1169     }
   1170 }