lib

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

commit 1f7200ab0b41c0dd14d30ac76d702ab819c1c55c
parent 487f7e3f389c4c7cd3ddbb715a610453b95f0e89
Author: triesap <tyson@radroots.org>
Date:   Mon, 13 Apr 2026 08:06:01 +0000

sdk: expose radrootsd signer session lifecycle

Diffstat:
Mcrates/sdk/src/adapters/radrootsd.rs | 170+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/sdk/src/client.rs | 344++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Mcrates/sdk/src/lib.rs | 7+++++--
Mcrates/sdk/tests/radrootsd.rs | 368++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
4 files changed, 871 insertions(+), 18 deletions(-)

diff --git a/crates/sdk/src/adapters/radrootsd.rs b/crates/sdk/src/adapters/radrootsd.rs @@ -42,6 +42,13 @@ pub enum SdkRadrootsdSignerSessionMode { Nostrconnect, } +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum SdkRadrootsdSignerSessionRole { + InboundLocalSigner, + OutboundRemoteSigner, +} + #[derive(Clone, PartialEq, Eq, Serialize)] pub struct SdkRadrootsdSignerSessionConnectRequest { pub url: String, @@ -142,6 +149,70 @@ pub(crate) struct SdkRadrootsdSignerSessionConnectResponse { pub relays: Vec<String>, } +#[derive(Clone, PartialEq, Eq, Deserialize)] +pub(crate) struct SdkRadrootsdSignerSessionViewResponse { + pub session_id: String, + pub role: SdkRadrootsdSignerSessionRole, + pub client_pubkey: String, + pub signer_pubkey: String, + #[serde(default)] + pub user_pubkey: Option<String>, + pub relays: Vec<String>, + pub permissions: Vec<String>, + #[serde(default)] + pub name: Option<String>, + #[serde(default)] + pub url: Option<String>, + #[serde(default)] + pub image: Option<String>, + pub auth_required: bool, + pub authorized: bool, + #[serde(default)] + pub auth_url: Option<String>, + #[serde(default)] + pub expires_in_secs: Option<u64>, + #[serde(default)] + pub signer_authority: Option<SdkRadrootsdSignerAuthority>, +} + +impl fmt::Debug for SdkRadrootsdSignerSessionViewResponse { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut debug = f.debug_struct("SdkRadrootsdSignerSessionViewResponse"); + debug.field("session_id", &"<redacted>"); + debug.field("role", &self.role); + debug.field("client_pubkey", &self.client_pubkey); + debug.field("signer_pubkey", &self.signer_pubkey); + debug.field("user_pubkey", &self.user_pubkey); + debug.field("relays", &self.relays); + debug.field("permissions", &self.permissions); + debug.field("name", &self.name); + debug.field("url", &self.url); + debug.field("image", &self.image); + debug.field("auth_required", &self.auth_required); + debug.field("authorized", &self.authorized); + debug.field("auth_url", &self.auth_url); + debug.field("expires_in_secs", &self.expires_in_secs); + debug.field("signer_authority", &self.signer_authority); + debug.finish() + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Deserialize)] +pub(crate) struct SdkRadrootsdSignerSessionAuthorizeResponse { + pub authorized: bool, + pub replayed: bool, +} + +#[derive(Clone, Debug, PartialEq, Eq, Deserialize)] +pub(crate) struct SdkRadrootsdSignerSessionRequireAuthResponse { + pub required: bool, +} + +#[derive(Clone, Debug, PartialEq, Eq, Deserialize)] +pub(crate) struct SdkRadrootsdSignerSessionCloseResponse { + pub closed: bool, +} + #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] pub struct SdkRadrootsdBridgePublishResponse { pub deduplicated: bool, @@ -224,6 +295,17 @@ struct JsonRpcError { message: String, } +#[derive(Debug, Serialize)] +struct SdkRadrootsdSignerSessionParams<'a> { + session_id: &'a str, +} + +#[derive(Debug, Serialize)] +struct SdkRadrootsdSignerSessionRequireAuthParams<'a> { + session_id: &'a str, + auth_url: &'a str, +} + pub async fn publish_listing( endpoint: &str, auth: &RadrootsdAuth, @@ -258,6 +340,94 @@ pub(crate) async fn connect_signer_session( .await } +pub(crate) async fn signer_session_status( + endpoint: &str, + auth: &RadrootsdAuth, + session_id: &str, + timeout: Duration, +) -> Result<SdkRadrootsdSignerSessionViewResponse, RadrootsdError> { + jsonrpc_call( + endpoint, + auth, + "radroots-sdk-nip46-session-status", + "nip46.session.status", + &SdkRadrootsdSignerSessionParams { session_id }, + timeout, + ) + .await +} + +pub(crate) async fn list_signer_sessions( + endpoint: &str, + auth: &RadrootsdAuth, + timeout: Duration, +) -> Result<Vec<SdkRadrootsdSignerSessionViewResponse>, RadrootsdError> { + jsonrpc_call( + endpoint, + auth, + "radroots-sdk-nip46-session-list", + "nip46.session.list", + &json!({}), + timeout, + ) + .await +} + +pub(crate) async fn authorize_signer_session( + endpoint: &str, + auth: &RadrootsdAuth, + session_id: &str, + timeout: Duration, +) -> Result<SdkRadrootsdSignerSessionAuthorizeResponse, RadrootsdError> { + jsonrpc_call( + endpoint, + auth, + "radroots-sdk-nip46-session-authorize", + "nip46.session.authorize", + &SdkRadrootsdSignerSessionParams { session_id }, + timeout, + ) + .await +} + +pub(crate) async fn require_signer_session_auth( + endpoint: &str, + auth: &RadrootsdAuth, + session_id: &str, + auth_url: &str, + timeout: Duration, +) -> Result<SdkRadrootsdSignerSessionRequireAuthResponse, RadrootsdError> { + jsonrpc_call( + endpoint, + auth, + "radroots-sdk-nip46-session-require-auth", + "nip46.session.require_auth", + &SdkRadrootsdSignerSessionRequireAuthParams { + session_id, + auth_url, + }, + timeout, + ) + .await +} + +pub(crate) async fn close_signer_session( + endpoint: &str, + auth: &RadrootsdAuth, + session_id: &str, + timeout: Duration, +) -> Result<SdkRadrootsdSignerSessionCloseResponse, RadrootsdError> { + jsonrpc_call( + endpoint, + auth, + "radroots-sdk-nip46-session-close", + "nip46.session.close", + &SdkRadrootsdSignerSessionParams { session_id }, + timeout, + ) + .await +} + fn auth_headers(auth: &RadrootsdAuth) -> Result<HeaderMap, RadrootsdError> { let mut headers = HeaderMap::new(); match auth { diff --git a/crates/sdk/src/client.rs b/crates/sdk/src/client.rs @@ -218,8 +218,30 @@ impl std::error::Error for SdkRadrootsdSessionError {} #[cfg(feature = "radrootsd-client")] #[derive(Clone, PartialEq, Eq)] -pub struct SdkRadrootsdSignerSessionHandle { +pub struct SdkRadrootsdSignerSessionRef { session_id: String, +} + +#[cfg(feature = "radrootsd-client")] +impl fmt::Debug for SdkRadrootsdSignerSessionRef { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("SdkRadrootsdSignerSessionRef") + .field("session_id", &"<redacted>") + .finish() + } +} + +#[cfg(feature = "radrootsd-client")] +impl SdkRadrootsdSignerSessionRef { + pub(crate) fn session_id(&self) -> &str { + self.session_id.as_str() + } +} + +#[cfg(feature = "radrootsd-client")] +#[derive(Clone, PartialEq, Eq)] +pub struct SdkRadrootsdSignerSessionHandle { + session: SdkRadrootsdSignerSessionRef, mode: radrootsd::SdkRadrootsdSignerSessionMode, remote_signer_pubkey: String, client_pubkey: String, @@ -227,10 +249,138 @@ pub struct SdkRadrootsdSignerSessionHandle { } #[cfg(feature = "radrootsd-client")] +#[derive(Clone, PartialEq, Eq)] +pub struct SdkRadrootsdSignerSessionView { + session: SdkRadrootsdSignerSessionRef, + pub role: radrootsd::SdkRadrootsdSignerSessionRole, + pub client_pubkey: String, + pub signer_pubkey: String, + pub user_pubkey: Option<String>, + pub relays: Vec<String>, + pub permissions: Vec<String>, + pub name: Option<String>, + pub url: Option<String>, + pub image: Option<String>, + pub auth_required: bool, + pub authorized: bool, + pub auth_url: Option<String>, + pub expires_in_secs: Option<u64>, + pub signer_authority: Option<radrootsd::SdkRadrootsdSignerAuthority>, +} + +#[cfg(feature = "radrootsd-client")] +impl fmt::Debug for SdkRadrootsdSignerSessionView { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut debug = f.debug_struct("SdkRadrootsdSignerSessionView"); + debug.field("session", &self.session); + debug.field("role", &self.role); + debug.field("client_pubkey", &self.client_pubkey); + debug.field("signer_pubkey", &self.signer_pubkey); + debug.field("user_pubkey", &self.user_pubkey); + debug.field("relays", &self.relays); + debug.field("permissions", &self.permissions); + debug.field("name", &self.name); + debug.field("url", &self.url); + debug.field("image", &self.image); + debug.field("auth_required", &self.auth_required); + debug.field("authorized", &self.authorized); + debug.field("auth_url", &self.auth_url); + debug.field("expires_in_secs", &self.expires_in_secs); + debug.field("signer_authority", &self.signer_authority); + debug.finish() + } +} + +#[cfg(feature = "radrootsd-client")] +impl SdkRadrootsdSignerSessionView { + pub fn session(&self) -> &SdkRadrootsdSignerSessionRef { + &self.session + } +} + +#[cfg(feature = "radrootsd-client")] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SdkRadrootsdSignerSessionAuthorizeResult { + pub authorized: bool, + pub replayed: bool, +} + +#[cfg(feature = "radrootsd-client")] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SdkRadrootsdSignerSessionRequireAuthResult { + pub required: bool, +} + +#[cfg(feature = "radrootsd-client")] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SdkRadrootsdSignerSessionCloseResult { + pub closed: bool, +} + +#[cfg(feature = "radrootsd-client")] +impl From<radrootsd::SdkRadrootsdSignerSessionViewResponse> for SdkRadrootsdSignerSessionView { + fn from(value: radrootsd::SdkRadrootsdSignerSessionViewResponse) -> Self { + Self { + session: SdkRadrootsdSignerSessionRef { + session_id: value.session_id, + }, + role: value.role, + client_pubkey: value.client_pubkey, + signer_pubkey: value.signer_pubkey, + user_pubkey: value.user_pubkey, + relays: value.relays, + permissions: value.permissions, + name: value.name, + url: value.url, + image: value.image, + auth_required: value.auth_required, + authorized: value.authorized, + auth_url: value.auth_url, + expires_in_secs: value.expires_in_secs, + signer_authority: value.signer_authority, + } + } +} + +#[cfg(feature = "radrootsd-client")] +impl From<radrootsd::SdkRadrootsdSignerSessionAuthorizeResponse> + for SdkRadrootsdSignerSessionAuthorizeResult +{ + fn from(value: radrootsd::SdkRadrootsdSignerSessionAuthorizeResponse) -> Self { + Self { + authorized: value.authorized, + replayed: value.replayed, + } + } +} + +#[cfg(feature = "radrootsd-client")] +impl From<radrootsd::SdkRadrootsdSignerSessionRequireAuthResponse> + for SdkRadrootsdSignerSessionRequireAuthResult +{ + fn from(value: radrootsd::SdkRadrootsdSignerSessionRequireAuthResponse) -> Self { + Self { + required: value.required, + } + } +} + +#[cfg(feature = "radrootsd-client")] +impl From<radrootsd::SdkRadrootsdSignerSessionCloseResponse> + for SdkRadrootsdSignerSessionCloseResult +{ + fn from(value: radrootsd::SdkRadrootsdSignerSessionCloseResponse) -> Self { + Self { + closed: value.closed, + } + } +} + +#[cfg(feature = "radrootsd-client")] impl fmt::Debug for SdkRadrootsdSignerSessionHandle { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut debug = f.debug_struct("SdkRadrootsdSignerSessionHandle"); - debug.field("session_id", &"<redacted>"); + debug.field("session", &self.session); debug.field("mode", &self.mode); debug.field("remote_signer_pubkey", &self.remote_signer_pubkey); debug.field("client_pubkey", &self.client_pubkey); @@ -241,6 +391,10 @@ impl fmt::Debug for SdkRadrootsdSignerSessionHandle { #[cfg(feature = "radrootsd-client")] impl SdkRadrootsdSignerSessionHandle { + pub fn session(&self) -> &SdkRadrootsdSignerSessionRef { + &self.session + } + pub fn mode(&self) -> radrootsd::SdkRadrootsdSignerSessionMode { self.mode } @@ -258,7 +412,7 @@ impl SdkRadrootsdSignerSessionHandle { } pub(crate) fn session_id(&self) -> &str { - self.session_id.as_str() + self.session.session_id() } } @@ -266,7 +420,9 @@ impl SdkRadrootsdSignerSessionHandle { impl From<radrootsd::SdkRadrootsdSignerSessionConnectResponse> for SdkRadrootsdSignerSessionHandle { fn from(value: radrootsd::SdkRadrootsdSignerSessionConnectResponse) -> Self { Self { - session_id: value.session_id, + session: SdkRadrootsdSignerSessionRef { + session_id: value.session_id, + }, mode: value.mode, remote_signer_pubkey: value.remote_signer_pubkey, client_pubkey: value.client_pubkey, @@ -485,15 +641,7 @@ impl RadrootsSdkClient { }); } - let endpoint = match &self.resolved_transport_target { - SdkResolvedTransportTarget::Radrootsd { endpoint } => endpoint.as_str(), - SdkResolvedTransportTarget::RelayDirect { .. } => { - return Err(SdkRadrootsdSessionError::UnsupportedTransport { - transport: self.transport(), - operation: "radrootsd.signer_sessions.connect", - }); - } - }; + let endpoint = self.require_radrootsd_endpoint("radrootsd.signer_sessions.connect")?; let response = radrootsd::connect_signer_session( endpoint, &self.config.radrootsd.auth, @@ -504,6 +652,137 @@ impl RadrootsSdkClient { .map_err(|err| SdkRadrootsdSessionError::Radrootsd(err.to_string()))?; Ok(response.into()) } + + #[cfg(feature = "radrootsd-client")] + fn require_radrootsd_endpoint( + &self, + operation: &'static str, + ) -> Result<&str, SdkRadrootsdSessionError> { + match &self.resolved_transport_target { + SdkResolvedTransportTarget::Radrootsd { endpoint } => Ok(endpoint.as_str()), + SdkResolvedTransportTarget::RelayDirect { .. } => { + Err(SdkRadrootsdSessionError::UnsupportedTransport { + transport: self.transport(), + operation, + }) + } + } + } + + #[cfg(feature = "radrootsd-client")] + async fn radrootsd_signer_session_status( + &self, + session: &SdkRadrootsdSignerSessionRef, + ) -> Result<SdkRadrootsdSignerSessionView, SdkRadrootsdSessionError> { + if self.transport() != SdkTransportMode::Radrootsd { + return Err(SdkRadrootsdSessionError::UnsupportedTransport { + transport: self.transport(), + operation: "radrootsd.signer_sessions.status", + }); + } + + let response = radrootsd::signer_session_status( + self.require_radrootsd_endpoint("radrootsd.signer_sessions.status")?, + &self.config.radrootsd.auth, + session.session_id(), + Duration::from_millis(self.config.network.timeout_ms), + ) + .await + .map_err(|err| SdkRadrootsdSessionError::Radrootsd(err.to_string()))?; + Ok(response.into()) + } + + #[cfg(feature = "radrootsd-client")] + async fn radrootsd_list_signer_sessions( + &self, + ) -> Result<Vec<SdkRadrootsdSignerSessionView>, SdkRadrootsdSessionError> { + if self.transport() != SdkTransportMode::Radrootsd { + return Err(SdkRadrootsdSessionError::UnsupportedTransport { + transport: self.transport(), + operation: "radrootsd.signer_sessions.list", + }); + } + + let response = radrootsd::list_signer_sessions( + self.require_radrootsd_endpoint("radrootsd.signer_sessions.list")?, + &self.config.radrootsd.auth, + Duration::from_millis(self.config.network.timeout_ms), + ) + .await + .map_err(|err| SdkRadrootsdSessionError::Radrootsd(err.to_string()))?; + Ok(response.into_iter().map(Into::into).collect()) + } + + #[cfg(feature = "radrootsd-client")] + async fn authorize_radrootsd_signer_session( + &self, + session: &SdkRadrootsdSignerSessionRef, + ) -> Result<SdkRadrootsdSignerSessionAuthorizeResult, SdkRadrootsdSessionError> { + if self.transport() != SdkTransportMode::Radrootsd { + return Err(SdkRadrootsdSessionError::UnsupportedTransport { + transport: self.transport(), + operation: "radrootsd.signer_sessions.authorize", + }); + } + + let response = radrootsd::authorize_signer_session( + self.require_radrootsd_endpoint("radrootsd.signer_sessions.authorize")?, + &self.config.radrootsd.auth, + session.session_id(), + Duration::from_millis(self.config.network.timeout_ms), + ) + .await + .map_err(|err| SdkRadrootsdSessionError::Radrootsd(err.to_string()))?; + Ok(response.into()) + } + + #[cfg(feature = "radrootsd-client")] + async fn require_radrootsd_signer_session_auth( + &self, + session: &SdkRadrootsdSignerSessionRef, + auth_url: &str, + ) -> Result<SdkRadrootsdSignerSessionRequireAuthResult, SdkRadrootsdSessionError> { + if self.transport() != SdkTransportMode::Radrootsd { + return Err(SdkRadrootsdSessionError::UnsupportedTransport { + transport: self.transport(), + operation: "radrootsd.signer_sessions.require_auth", + }); + } + + let response = radrootsd::require_signer_session_auth( + self.require_radrootsd_endpoint("radrootsd.signer_sessions.require_auth")?, + &self.config.radrootsd.auth, + session.session_id(), + auth_url, + Duration::from_millis(self.config.network.timeout_ms), + ) + .await + .map_err(|err| SdkRadrootsdSessionError::Radrootsd(err.to_string()))?; + Ok(response.into()) + } + + #[cfg(feature = "radrootsd-client")] + async fn close_radrootsd_signer_session( + &self, + session: &SdkRadrootsdSignerSessionRef, + ) -> Result<SdkRadrootsdSignerSessionCloseResult, SdkRadrootsdSessionError> { + if self.transport() != SdkTransportMode::Radrootsd { + return Err(SdkRadrootsdSessionError::UnsupportedTransport { + transport: self.transport(), + operation: "radrootsd.signer_sessions.close", + }); + } + + let response = radrootsd::close_signer_session( + self.require_radrootsd_endpoint("radrootsd.signer_sessions.close")?, + &self.config.radrootsd.auth, + session.session_id(), + Duration::from_millis(self.config.network.timeout_ms), + ) + .await + .map_err(|err| SdkRadrootsdSessionError::Radrootsd(err.to_string()))?; + Ok(response.into()) + } } #[cfg(feature = "radrootsd-client")] @@ -579,6 +858,45 @@ impl<'a> RadrootsdSignerSessionClient<'a> { ); self.connect(&request).await } + + pub async fn status( + &self, + session: &SdkRadrootsdSignerSessionRef, + ) -> Result<SdkRadrootsdSignerSessionView, SdkRadrootsdSessionError> { + self.client.radrootsd_signer_session_status(session).await + } + + pub async fn list( + &self, + ) -> Result<Vec<SdkRadrootsdSignerSessionView>, SdkRadrootsdSessionError> { + self.client.radrootsd_list_signer_sessions().await + } + + pub async fn authorize( + &self, + session: &SdkRadrootsdSignerSessionRef, + ) -> Result<SdkRadrootsdSignerSessionAuthorizeResult, SdkRadrootsdSessionError> { + self.client + .authorize_radrootsd_signer_session(session) + .await + } + + pub async fn require_auth( + &self, + session: &SdkRadrootsdSignerSessionRef, + auth_url: impl AsRef<str>, + ) -> Result<SdkRadrootsdSignerSessionRequireAuthResult, SdkRadrootsdSessionError> { + self.client + .require_radrootsd_signer_session_auth(session, auth_url.as_ref()) + .await + } + + pub async fn close( + &self, + session: &SdkRadrootsdSignerSessionRef, + ) -> Result<SdkRadrootsdSignerSessionCloseResult, SdkRadrootsdSessionError> { + self.client.close_radrootsd_signer_session(session).await + } } #[derive(Debug, Clone, Copy)] diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs @@ -28,7 +28,7 @@ pub mod trade; #[cfg(feature = "radrootsd-client")] pub use crate::adapters::radrootsd::{ SdkRadrootsdSignerAuthority, SdkRadrootsdSignerSessionConnectRequest, - SdkRadrootsdSignerSessionMode, + SdkRadrootsdSignerSessionMode, SdkRadrootsdSignerSessionRole, }; pub use crate::client::{ FarmClient, ListingClient, ProfileClient, RadrootsSdkClient, SdkPublishError, @@ -38,7 +38,10 @@ pub use crate::client::{ #[cfg(feature = "radrootsd-client")] pub use crate::client::{ RadrootsdClient, RadrootsdSignerSessionClient, SdkRadrootsdListingPublishOptions, - SdkRadrootsdSessionError, SdkRadrootsdSignerSessionHandle, + SdkRadrootsdSessionError, SdkRadrootsdSignerSessionAuthorizeResult, + SdkRadrootsdSignerSessionCloseResult, SdkRadrootsdSignerSessionHandle, + SdkRadrootsdSignerSessionRef, SdkRadrootsdSignerSessionRequireAuthResult, + SdkRadrootsdSignerSessionView, }; pub use crate::config::{ NetworkConfig, RADROOTS_SDK_LOCAL_RADROOTSD_ENDPOINT, RADROOTS_SDK_LOCAL_RELAY_URL, diff --git a/crates/sdk/tests/radrootsd.rs b/crates/sdk/tests/radrootsd.rs @@ -17,8 +17,10 @@ use radroots_sdk::listing::{ }; use radroots_sdk::{ RadrootsNostrEvent, RadrootsSdkClient, RadrootsSdkConfig, RadrootsdAuth, RadrootsdConfig, - SdkEnvironment, SdkPublishError, SdkRadrootsdListingPublishOptions, SdkRadrootsdPublishReceipt, - SdkRadrootsdSessionError, SdkTransportMode, SdkTransportReceipt, SignerConfig, + SdkConfigError, SdkEnvironment, SdkPublishError, SdkRadrootsdListingPublishOptions, + SdkRadrootsdPublishReceipt, SdkRadrootsdSessionError, SdkRadrootsdSignerSessionHandle, + SdkRadrootsdSignerSessionRole, SdkRadrootsdSignerSessionView, SdkTransportMode, + SdkTransportReceipt, SignerConfig, }; use serde_json::{Value, json}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; @@ -261,6 +263,41 @@ fn sdk_event( } } +fn radrootsd_test_client(endpoint: &str) -> Result<RadrootsSdkClient, SdkConfigError> { + let mut config = RadrootsSdkConfig::for_environment(SdkEnvironment::Production); + config.transport = SdkTransportMode::Radrootsd; + config.signer = SignerConfig::Nip46; + config.radrootsd = RadrootsdConfig { + endpoint: Some(endpoint.to_owned()), + auth: RadrootsdAuth::BearerToken("sdk-secret".to_owned()), + }; + RadrootsSdkClient::from_config(config) +} + +fn sample_session_view_json(session_id: &str) -> Value { + json!({ + "session_id": session_id, + "role": "outbound_remote_signer", + "client_pubkey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "signer_pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "user_pubkey": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "relays": ["wss://radroots.org"], + "permissions": ["sign_event:30402"], + "name": "Radroots Signer", + "url": "https://radroots.org/signers/demo", + "image": "https://radroots.org/signers/demo.png", + "auth_required": false, + "authorized": true, + "auth_url": null, + "expires_in_secs": 120, + "signer_authority": { + "provider_runtime_id": "runtime-1", + "account_identity_id": "identity-1", + "provider_signer_session_id": "provider-session-123" + } + }) +} + #[test] fn radrootsd_debug_redacts_signer_session_values() { let signer_authority = SdkRadrootsdSignerAuthority { @@ -373,7 +410,7 @@ async fn radrootsd_signer_session_connect_returns_opaque_handle() -> TestResult< "client-secret-key", ); - let handle = client + let handle: SdkRadrootsdSignerSessionHandle = client .radrootsd() .signer_sessions() .connect(&request) @@ -413,6 +450,331 @@ async fn radrootsd_signer_session_connect_returns_opaque_handle() -> TestResult< } #[tokio::test] +async fn radrootsd_signer_session_connect_bunker_supports_bunker_mode() -> TestResult<()> { + let (server, request_rx) = JsonRpcServer::spawn( + Some("Bearer sdk-secret"), + json!({ + "jsonrpc": "2.0", + "id": "radroots-sdk-nip46-connect", + "result": { + "session_id": "session-bunker", + "mode": "Bunker", + "remote_signer_pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "client_pubkey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "relays": ["wss://radroots.org"] + } + }), + ) + .await?; + + let client = radrootsd_test_client(server.endpoint())?; + let handle: SdkRadrootsdSignerSessionHandle = client + .radrootsd() + .signer_sessions() + .connect_bunker( + "bunker://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa?relay=wss%3A%2F%2Fradroots.org&secret=shared-secret", + ) + .await?; + let request_json = request_rx.await?; + + assert_eq!(request_json["method"], "nip46.connect"); + assert_eq!( + request_json["params"]["url"], + "bunker://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa?relay=wss%3A%2F%2Fradroots.org&secret=shared-secret" + ); + assert!(request_json["params"]["client_secret_key"].is_null()); + assert_eq!(handle.mode(), SdkRadrootsdSignerSessionMode::Bunker); + + Ok(()) +} + +#[tokio::test] +async fn radrootsd_signer_session_status_returns_typed_view() -> TestResult<()> { + let (connect_server, _) = JsonRpcServer::spawn( + Some("Bearer sdk-secret"), + json!({ + "jsonrpc": "2.0", + "id": "radroots-sdk-nip46-connect", + "result": { + "session_id": "session-123", + "mode": "Nostrconnect", + "remote_signer_pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "client_pubkey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "relays": ["wss://radroots.org"] + } + }), + ) + .await?; + let connect_client = radrootsd_test_client(connect_server.endpoint())?; + let handle: SdkRadrootsdSignerSessionHandle = connect_client + .radrootsd() + .signer_sessions() + .connect_nostrconnect( + "nostrconnect://bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb?relay=wss%3A%2F%2Fradroots.org&secret=shared-secret", + "client-secret-key", + ) + .await?; + + let (status_server, request_rx) = JsonRpcServer::spawn( + Some("Bearer sdk-secret"), + json!({ + "jsonrpc": "2.0", + "id": "radroots-sdk-nip46-session-status", + "result": sample_session_view_json("session-123") + }), + ) + .await?; + let status_client = radrootsd_test_client(status_server.endpoint())?; + let session: SdkRadrootsdSignerSessionView = status_client + .radrootsd() + .signer_sessions() + .status(handle.session()) + .await?; + let request_json = request_rx.await?; + + assert_eq!(request_json["method"], "nip46.session.status"); + assert_eq!(request_json["params"]["session_id"], "session-123"); + assert_eq!(session.session(), handle.session()); + assert_eq!( + session.role, + SdkRadrootsdSignerSessionRole::OutboundRemoteSigner + ); + assert_eq!( + session.client_pubkey, + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + ); + assert_eq!( + session.signer_pubkey, + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ); + assert_eq!( + session.user_pubkey.as_deref(), + Some("cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc") + ); + assert_eq!(session.relays, vec!["wss://radroots.org".to_owned()]); + assert_eq!(session.permissions, vec!["sign_event:30402".to_owned()]); + assert_eq!(session.name.as_deref(), Some("Radroots Signer")); + assert_eq!( + session.url.as_deref(), + Some("https://radroots.org/signers/demo") + ); + assert_eq!( + session.image.as_deref(), + Some("https://radroots.org/signers/demo.png") + ); + assert!(session.authorized); + assert!(!session.auth_required); + assert_eq!(session.expires_in_secs, Some(120)); + assert_eq!( + session + .signer_authority + .as_ref() + .map(|value| value.provider_runtime_id.as_str()), + Some("runtime-1") + ); + + let debug = format!("{session:?}"); + assert!(!debug.contains("session-123")); + assert!(debug.contains("<redacted>")); + + Ok(()) +} + +#[tokio::test] +async fn radrootsd_signer_session_list_returns_typed_views() -> TestResult<()> { + let (server, request_rx) = JsonRpcServer::spawn( + Some("Bearer sdk-secret"), + json!({ + "jsonrpc": "2.0", + "id": "radroots-sdk-nip46-session-list", + "result": [ + sample_session_view_json("session-123"), + sample_session_view_json("session-456") + ] + }), + ) + .await?; + let client = radrootsd_test_client(server.endpoint())?; + let sessions: Vec<SdkRadrootsdSignerSessionView> = + client.radrootsd().signer_sessions().list().await?; + let request_json = request_rx.await?; + + assert_eq!(request_json["method"], "nip46.session.list"); + assert_eq!(sessions.len(), 2); + assert_eq!( + sessions[0].role, + SdkRadrootsdSignerSessionRole::OutboundRemoteSigner + ); + let debug = format!("{:?}", sessions[0].session()); + assert!(!debug.contains("session-123")); + assert!(debug.contains("<redacted>")); + + Ok(()) +} + +#[tokio::test] +async fn radrootsd_signer_session_authorize_returns_typed_result() -> TestResult<()> { + let (connect_server, _) = JsonRpcServer::spawn( + Some("Bearer sdk-secret"), + json!({ + "jsonrpc": "2.0", + "id": "radroots-sdk-nip46-connect", + "result": { + "session_id": "session-123", + "mode": "Bunker", + "remote_signer_pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "client_pubkey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "relays": ["wss://radroots.org"] + } + }), + ) + .await?; + let connect_client = radrootsd_test_client(connect_server.endpoint())?; + let handle: SdkRadrootsdSignerSessionHandle = connect_client + .radrootsd() + .signer_sessions() + .connect_bunker( + "bunker://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa?relay=wss%3A%2F%2Fradroots.org&secret=shared-secret", + ) + .await?; + + let (server, request_rx) = JsonRpcServer::spawn( + Some("Bearer sdk-secret"), + json!({ + "jsonrpc": "2.0", + "id": "radroots-sdk-nip46-session-authorize", + "result": { + "authorized": true, + "replayed": true + } + }), + ) + .await?; + let client = radrootsd_test_client(server.endpoint())?; + let result = client + .radrootsd() + .signer_sessions() + .authorize(handle.session()) + .await?; + let request_json = request_rx.await?; + + assert_eq!(request_json["method"], "nip46.session.authorize"); + assert_eq!(request_json["params"]["session_id"], "session-123"); + assert!(result.authorized); + assert!(result.replayed); + + Ok(()) +} + +#[tokio::test] +async fn radrootsd_signer_session_require_auth_returns_typed_result() -> TestResult<()> { + let (connect_server, _) = JsonRpcServer::spawn( + Some("Bearer sdk-secret"), + json!({ + "jsonrpc": "2.0", + "id": "radroots-sdk-nip46-connect", + "result": { + "session_id": "session-123", + "mode": "Bunker", + "remote_signer_pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "client_pubkey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "relays": ["wss://radroots.org"] + } + }), + ) + .await?; + let connect_client = radrootsd_test_client(connect_server.endpoint())?; + let handle: SdkRadrootsdSignerSessionHandle = connect_client + .radrootsd() + .signer_sessions() + .connect_bunker( + "bunker://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa?relay=wss%3A%2F%2Fradroots.org&secret=shared-secret", + ) + .await?; + + let (server, request_rx) = JsonRpcServer::spawn( + Some("Bearer sdk-secret"), + json!({ + "jsonrpc": "2.0", + "id": "radroots-sdk-nip46-session-require-auth", + "result": { + "required": true + } + }), + ) + .await?; + let client = radrootsd_test_client(server.endpoint())?; + let result = client + .radrootsd() + .signer_sessions() + .require_auth(handle.session(), "https://radroots.org/auth") + .await?; + let request_json = request_rx.await?; + + assert_eq!(request_json["method"], "nip46.session.require_auth"); + assert_eq!(request_json["params"]["session_id"], "session-123"); + assert_eq!( + request_json["params"]["auth_url"], + "https://radroots.org/auth" + ); + assert!(result.required); + + Ok(()) +} + +#[tokio::test] +async fn radrootsd_signer_session_close_returns_typed_result() -> TestResult<()> { + let (connect_server, _) = JsonRpcServer::spawn( + Some("Bearer sdk-secret"), + json!({ + "jsonrpc": "2.0", + "id": "radroots-sdk-nip46-connect", + "result": { + "session_id": "session-123", + "mode": "Bunker", + "remote_signer_pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "client_pubkey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "relays": ["wss://radroots.org"] + } + }), + ) + .await?; + let connect_client = radrootsd_test_client(connect_server.endpoint())?; + let handle: SdkRadrootsdSignerSessionHandle = connect_client + .radrootsd() + .signer_sessions() + .connect_bunker( + "bunker://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa?relay=wss%3A%2F%2Fradroots.org&secret=shared-secret", + ) + .await?; + + let (server, request_rx) = JsonRpcServer::spawn( + Some("Bearer sdk-secret"), + json!({ + "jsonrpc": "2.0", + "id": "radroots-sdk-nip46-session-close", + "result": { + "closed": true + } + }), + ) + .await?; + let client = radrootsd_test_client(server.endpoint())?; + let result = client + .radrootsd() + .signer_sessions() + .close(handle.session()) + .await?; + let request_json = request_rx.await?; + + assert_eq!(request_json["method"], "nip46.session.close"); + assert_eq!(request_json["params"]["session_id"], "session-123"); + assert!(result.closed); + + Ok(()) +} + +#[tokio::test] async fn radrootsd_signer_session_connect_rejects_relay_transport_mode() -> TestResult<()> { let client = RadrootsSdkClient::from_config(RadrootsSdkConfig::production())?; let request = SdkRadrootsdSignerSessionConnectRequest::bunker(