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:
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(