lib

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

commit fae39d7c7bd860858ef9115a215f9458c54d32fc
parent 4801bd8e6392fe036bc4cfe40621045d34279918
Author: triesap <tyson@radroots.org>
Date:   Thu,  2 Apr 2026 19:10:35 +0000

nostr-connect: add approved remote session capability

Diffstat:
Mcrates/nostr-connect/src/lib.rs | 3++-
Mcrates/nostr-connect/src/message.rs | 104++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mcrates/nostr-connect/src/method.rs | 3+++
Mcrates/nostr-signer/src/evaluation.rs | 41++++++++++++++++++++++++++++++++++++++++-
4 files changed, 148 insertions(+), 3 deletions(-)

diff --git a/crates/nostr-connect/src/lib.rs b/crates/nostr-connect/src/lib.rs @@ -10,7 +10,8 @@ pub mod prelude { pub use crate::error::RadrootsNostrConnectError; pub use crate::message::{ RADROOTS_NOSTR_CONNECT_PENDING_CONNECTION_ERROR, RADROOTS_NOSTR_CONNECT_RPC_KIND, - RadrootsNostrConnectPendingConnectionPollOutcome, RadrootsNostrConnectRequest, + RadrootsNostrConnectPendingConnectionPollOutcome, + RadrootsNostrConnectRemoteSessionCapability, RadrootsNostrConnectRequest, RadrootsNostrConnectRequestMessage, RadrootsNostrConnectResponse, RadrootsNostrConnectResponseEnvelope, }; diff --git a/crates/nostr-connect/src/message.rs b/crates/nostr-connect/src/message.rs @@ -9,6 +9,13 @@ use url::Url; pub const RADROOTS_NOSTR_CONNECT_RPC_KIND: u16 = 24_133; +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RadrootsNostrConnectRemoteSessionCapability { + pub user_public_key: PublicKey, + pub relays: Vec<RelayUrl>, + pub permissions: RadrootsNostrConnectPermissions, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub enum RadrootsNostrConnectRequest { Connect { @@ -17,6 +24,7 @@ pub enum RadrootsNostrConnectRequest { requested_permissions: RadrootsNostrConnectPermissions, }, GetPublicKey, + GetSessionCapability, SignEvent(UnsignedEvent), Nip04Encrypt { public_key: PublicKey, @@ -47,6 +55,7 @@ impl RadrootsNostrConnectRequest { match self { Self::Connect { .. } => RadrootsNostrConnectMethod::Connect, Self::GetPublicKey => RadrootsNostrConnectMethod::GetPublicKey, + Self::GetSessionCapability => RadrootsNostrConnectMethod::GetSessionCapability, Self::SignEvent(_) => RadrootsNostrConnectMethod::SignEvent, Self::Nip04Encrypt { .. } => RadrootsNostrConnectMethod::Nip04Encrypt, Self::Nip04Decrypt { .. } => RadrootsNostrConnectMethod::Nip04Decrypt, @@ -75,7 +84,9 @@ impl RadrootsNostrConnectRequest { } params } - Self::GetPublicKey | Self::Ping | Self::SwitchRelays => Vec::new(), + Self::GetPublicKey | Self::GetSessionCapability | Self::Ping | Self::SwitchRelays => { + Vec::new() + } Self::SignEvent(unsigned_event) => vec![unsigned_event.as_json()], Self::Nip04Encrypt { public_key, @@ -129,6 +140,10 @@ impl RadrootsNostrConnectRequest { expect_param_count(&method, &params, 0)?; Ok(Self::GetPublicKey) } + RadrootsNostrConnectMethod::GetSessionCapability => { + expect_param_count(&method, &params, 0)?; + Ok(Self::GetSessionCapability) + } RadrootsNostrConnectMethod::SignEvent => { expect_param_count(&method, &params, 1)?; let unsigned_event = serde_json::from_str(&params[0]).map_err(|error| { @@ -247,6 +262,7 @@ pub const RADROOTS_NOSTR_CONNECT_PENDING_CONNECTION_ERROR: &str = "connection is pub enum RadrootsNostrConnectPendingConnectionPollOutcome { PendingApproval, Approved(PublicKey), + ApprovedCapability(RadrootsNostrConnectRemoteSessionCapability), Rejected { message: String }, AuthChallenge { url: String }, UnexpectedResponse { response: String }, @@ -258,6 +274,7 @@ pub enum RadrootsNostrConnectResponse { ConnectSecretEcho(String), PendingConnection, UserPublicKey(PublicKey), + RemoteSessionCapability(RadrootsNostrConnectRemoteSessionCapability), SignedEvent(Event), Pong, Nip04Encrypt(String), @@ -288,6 +305,9 @@ impl RadrootsNostrConnectResponse { Self::UserPublicKey(public_key) => { RadrootsNostrConnectPendingConnectionPollOutcome::Approved(public_key) } + Self::RemoteSessionCapability(capability) => { + RadrootsNostrConnectPendingConnectionPollOutcome::ApprovedCapability(capability) + } Self::Error { error, .. } => { RadrootsNostrConnectPendingConnectionPollOutcome::Rejected { message: error } } @@ -326,6 +346,16 @@ impl RadrootsNostrConnectResponse { result: Some(Value::String(public_key.to_hex())), error: None, }, + Self::RemoteSessionCapability(capability) => RadrootsNostrConnectResponseEnvelope { + id, + result: Some(serde_json::to_value(capability).map_err(|error| { + RadrootsNostrConnectError::InvalidResponsePayload { + method: RadrootsNostrConnectMethod::GetSessionCapability.to_string(), + reason: error.to_string(), + } + })?), + error: None, + }, Self::SignedEvent(event) => RadrootsNostrConnectResponseEnvelope { id, result: Some(Value::String(event.as_json())), @@ -424,6 +454,9 @@ impl RadrootsNostrConnectResponse { let result = expect_string_result(method, envelope.result)?; Ok(Self::UserPublicKey(parse_public_key(&result)?)) } + RadrootsNostrConnectMethod::GetSessionCapability => Ok(Self::RemoteSessionCapability( + parse_json_string_result(method, envelope.result)?, + )), RadrootsNostrConnectMethod::SignEvent => { let event = parse_json_string_result::<Event>(method, envelope.result)?; Ok(Self::SignedEvent(event)) @@ -592,3 +625,72 @@ fn validate_url(value: &str) -> Result<String, RadrootsNostrConnectError> { reason: error.to_string(), }) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::method::RadrootsNostrConnectMethod; + use crate::permission::RadrootsNostrConnectPermission; + + fn relay(value: &str) -> RelayUrl { + RelayUrl::parse(value).expect("relay url") + } + + fn public_key() -> PublicKey { + PublicKey::from_hex("4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa") + .expect("public key") + } + + #[test] + fn get_session_capability_request_round_trips_without_params() { + let request = RadrootsNostrConnectRequest::GetSessionCapability; + let message = RadrootsNostrConnectRequestMessage::new("req-cap", request.clone()); + let encoded = serde_json::to_string(&message).expect("encode request"); + let decoded: RadrootsNostrConnectRequestMessage = + serde_json::from_str(&encoded).expect("decode request"); + + assert_eq!(decoded.request, request); + assert_eq!( + decoded.request.method(), + RadrootsNostrConnectMethod::GetSessionCapability + ); + } + + #[test] + fn get_session_capability_response_round_trips() { + let capability = RadrootsNostrConnectRemoteSessionCapability { + user_public_key: public_key(), + relays: vec![ + relay("wss://relay.example.com"), + relay("wss://relay2.example.com"), + ], + permissions: vec![ + RadrootsNostrConnectPermission::with_parameter( + RadrootsNostrConnectMethod::SignEvent, + "kind:1", + ), + RadrootsNostrConnectPermission::new(RadrootsNostrConnectMethod::SwitchRelays), + ] + .into(), + }; + let response = RadrootsNostrConnectResponse::RemoteSessionCapability(capability.clone()); + let envelope = response + .into_envelope("resp-cap") + .expect("encode response envelope"); + let decoded = RadrootsNostrConnectResponse::from_envelope( + &RadrootsNostrConnectMethod::GetSessionCapability, + envelope, + ) + .expect("decode capability response"); + + assert_eq!( + decoded, + RadrootsNostrConnectResponse::RemoteSessionCapability(capability.clone()) + ); + assert_eq!( + RadrootsNostrConnectResponse::RemoteSessionCapability(capability.clone()) + .into_pending_connection_poll_outcome(), + RadrootsNostrConnectPendingConnectionPollOutcome::ApprovedCapability(capability) + ); + } +} diff --git a/crates/nostr-connect/src/method.rs b/crates/nostr-connect/src/method.rs @@ -7,6 +7,7 @@ use std::str::FromStr; pub enum RadrootsNostrConnectMethod { Connect, GetPublicKey, + GetSessionCapability, SignEvent, Nip04Encrypt, Nip04Decrypt, @@ -22,6 +23,7 @@ impl RadrootsNostrConnectMethod { match self { Self::Connect => "connect", Self::GetPublicKey => "get_public_key", + Self::GetSessionCapability => "get_session_capability", Self::SignEvent => "sign_event", Self::Nip04Encrypt => "nip04_encrypt", Self::Nip04Decrypt => "nip04_decrypt", @@ -47,6 +49,7 @@ impl FromStr for RadrootsNostrConnectMethod { match value { "connect" => Ok(Self::Connect), "get_public_key" => Ok(Self::GetPublicKey), + "get_session_capability" => Ok(Self::GetSessionCapability), "sign_event" => Ok(Self::SignEvent), "nip04_encrypt" => Ok(Self::Nip04Encrypt), "nip04_decrypt" => Ok(Self::Nip04Decrypt), diff --git a/crates/nostr-signer/src/evaluation.rs b/crates/nostr-signer/src/evaluation.rs @@ -8,7 +8,7 @@ use nostr::{PublicKey, RelayUrl}; use radroots_identity::RadrootsIdentityPublic; use radroots_nostr_connect::prelude::{ RadrootsNostrConnectMethod, RadrootsNostrConnectPermission, RadrootsNostrConnectPermissions, - RadrootsNostrConnectRequest, + RadrootsNostrConnectRemoteSessionCapability, RadrootsNostrConnectRequest, }; #[derive(Debug, Clone)] @@ -36,6 +36,7 @@ pub enum RadrootsNostrSignerRequestResponseHint { None, Pong, UserPublicKey(PublicKey), + RemoteSessionCapability(RadrootsNostrConnectRemoteSessionCapability), RelayList(Vec<RelayUrl>), } @@ -103,6 +104,7 @@ pub(crate) fn required_permission_for_request( match request { RadrootsNostrConnectRequest::Connect { .. } | RadrootsNostrConnectRequest::GetPublicKey + | RadrootsNostrConnectRequest::GetSessionCapability | RadrootsNostrConnectRequest::Ping => None, RadrootsNostrConnectRequest::SignEvent(unsigned_event) => { Some(RadrootsNostrConnectPermission::with_parameter( @@ -155,6 +157,15 @@ pub(crate) fn response_hint_for_request( identity_public_key(&connection.user_identity)?, )) } + RadrootsNostrConnectRequest::GetSessionCapability => Ok( + RadrootsNostrSignerRequestResponseHint::RemoteSessionCapability( + RadrootsNostrConnectRemoteSessionCapability { + user_public_key: identity_public_key(&connection.user_identity)?, + relays: connection.relays.clone(), + permissions: connection.effective_permissions(), + }, + ), + ), RadrootsNostrConnectRequest::Ping => Ok(RadrootsNostrSignerRequestResponseHint::Pong), RadrootsNostrConnectRequest::SwitchRelays => Ok( RadrootsNostrSignerRequestResponseHint::RelayList(connection.relays.clone()), @@ -273,6 +284,27 @@ mod tests { } } + #[cfg_attr(coverage_nightly, coverage(off))] + fn assert_response_hint_remote_session_capability( + hint: RadrootsNostrSignerRequestResponseHint, + expected_permissions: RadrootsNostrConnectPermissions, + ) { + match hint { + RadrootsNostrSignerRequestResponseHint::RemoteSessionCapability(capability) => { + let expected_public_key = + PublicKey::parse(fixture_diego_identity().public_key_hex.as_str()) + .expect("user public key"); + assert_eq!( + capability.user_public_key.to_hex(), + expected_public_key.to_hex() + ); + assert_eq!(capability.relays, vec![primary_relay()]); + assert_eq!(capability.permissions, expected_permissions); + } + other => panic!("unexpected response hint: {other:?}"), + } + } + #[test] fn connect_proposal_builds_connection_draft() { let requested_permissions: RadrootsNostrConnectPermissions = @@ -423,6 +455,7 @@ mod tests { }; let ping = RadrootsNostrConnectRequest::Ping; let get_public_key = RadrootsNostrConnectRequest::GetPublicKey; + let get_session_capability = RadrootsNostrConnectRequest::GetSessionCapability; let switch_relays = RadrootsNostrConnectRequest::SwitchRelays; let sign_event = RadrootsNostrConnectRequest::SignEvent(unsigned_event(7)); let custom = RadrootsNostrConnectRequest::Custom { @@ -433,6 +466,7 @@ mod tests { assert!(required_permission_for_request(&connect).is_none()); assert!(required_permission_for_request(&ping).is_none()); assert!(required_permission_for_request(&get_public_key).is_none()); + assert!(required_permission_for_request(&get_session_capability).is_none()); assert_eq!( required_permission_for_request(&RadrootsNostrConnectRequest::Nip04Decrypt { public_key, @@ -495,6 +529,11 @@ mod tests { assert_response_hint_user_public_key( response_hint_for_request(&connection, &get_public_key).expect("pubkey hint"), ); + assert_response_hint_remote_session_capability( + response_hint_for_request(&connection, &get_session_capability) + .expect("capability hint"), + connection.effective_permissions(), + ); assert_eq!( response_hint_for_request(&connection, &switch_relays).expect("relay hint"), RadrootsNostrSignerRequestResponseHint::RelayList(vec![primary_relay()])