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(¶ms)?; 337 Ok(params) 338 } 339 340 pub fn from_parts( 341 method: Method, 342 params: Vec<String>, 343 ) -> Result<Self, RadrootsNostrConnectError> { 344 validate_params(¶ms)?; 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(¶ms[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, ¶ms, 0)?; 373 Ok(Self::GetPublicKey) 374 } 375 Method::GetSessionCapability => { 376 expect_param_count(&method, ¶ms, 0)?; 377 Ok(Self::GetSessionCapability) 378 } 379 Method::SignEvent => { 380 expect_param_count(&method, ¶ms, 1)?; 381 let unsigned_event = UnsignedEvent::from_json(¶ms[0])?; 382 Ok(Self::SignEvent(unsigned_event)) 383 } 384 Method::Nip04Encrypt => { 385 expect_param_count(&method, ¶ms, 2)?; 386 Ok(Self::Nip04Encrypt { 387 public_key: parse_public_key(¶ms[0])?, 388 plaintext: params[1].clone(), 389 }) 390 } 391 Method::Nip04Decrypt => { 392 expect_param_count(&method, ¶ms, 2)?; 393 Ok(Self::Nip04Decrypt { 394 public_key: parse_public_key(¶ms[0])?, 395 ciphertext: params[1].clone(), 396 }) 397 } 398 Method::Nip44Encrypt => { 399 expect_param_count(&method, ¶ms, 2)?; 400 Ok(Self::Nip44Encrypt { 401 public_key: parse_public_key(¶ms[0])?, 402 plaintext: params[1].clone(), 403 }) 404 } 405 Method::Nip44Decrypt => { 406 expect_param_count(&method, ¶ms, 2)?; 407 Ok(Self::Nip44Decrypt { 408 public_key: parse_public_key(¶ms[0])?, 409 ciphertext: params[1].clone(), 410 }) 411 } 412 Method::Ping => { 413 expect_param_count(&method, ¶ms, 0)?; 414 Ok(Self::Ping) 415 } 416 Method::SwitchRelays => { 417 expect_param_count(&method, ¶ms, 0)?; 418 Ok(Self::SwitchRelays) 419 } 420 Method::Logout => { 421 expect_param_count(&method, ¶ms, 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 }