commit 3a8a2ef73f2b99bbe9695d473f408194927206c2
parent dc19ff32ce0798a59024fa98eab911989ce9efe9
Author: triesap <tyson@radroots.org>
Date: Fri, 27 Mar 2026 19:15:49 +0000
api: stop exposing ad hoc nip46 session internals
Diffstat:
3 files changed, 117 insertions(+), 65 deletions(-)
diff --git a/src/core/nip46/session.rs b/src/core/nip46/session.rs
@@ -4,6 +4,7 @@ use std::collections::{HashMap, HashSet};
use std::time::{Duration, Instant};
use std::sync::Arc;
+use serde::Serialize;
use tokio::sync::Mutex;
use radroots_nostr::prelude::{
@@ -30,6 +31,31 @@ pub struct Nip46AuthorizeOutcome {
pub pending: Option<PendingNostrRequest>,
}
+#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
+#[serde(rename_all = "snake_case")]
+pub enum Nip46SessionRole {
+ InboundLocalSigner,
+ OutboundRemoteSigner,
+}
+
+#[derive(Clone, Debug, Serialize)]
+pub struct Nip46SessionView {
+ pub session_id: String,
+ pub role: Nip46SessionRole,
+ 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>,
+}
+
#[derive(Clone)]
pub struct Nip46Session {
pub id: String,
@@ -178,6 +204,41 @@ impl Nip46Session {
.map(|expires_at| expires_at <= Instant::now())
.unwrap_or(false)
}
+
+ pub fn role(&self) -> Nip46SessionRole {
+ if self.client_keys.public_key() == self.remote_signer_pubkey {
+ Nip46SessionRole::InboundLocalSigner
+ } else {
+ Nip46SessionRole::OutboundRemoteSigner
+ }
+ }
+
+ pub fn public_view(&self) -> Nip46SessionView {
+ Nip46SessionView {
+ session_id: self.id.clone(),
+ role: self.role(),
+ client_pubkey: self.client_pubkey.to_hex(),
+ signer_pubkey: self.remote_signer_pubkey.to_hex(),
+ user_pubkey: self.user_pubkey.as_ref().map(|pubkey| pubkey.to_hex()),
+ relays: self.relays.clone(),
+ permissions: self.perms.clone(),
+ name: self.name.clone(),
+ url: self.url.clone(),
+ image: self.image.clone(),
+ auth_required: self.auth_required,
+ authorized: self.authorized,
+ auth_url: self.auth_url.clone(),
+ expires_in_secs: self.expires_at.map(remaining_secs),
+ }
+ }
+}
+
+fn remaining_secs(expires_at: Instant) -> u64 {
+ if expires_at <= Instant::now() {
+ 0
+ } else {
+ expires_at.saturating_duration_since(Instant::now()).as_secs()
+ }
}
pub fn filter_perms(requested: &[String], allowed: &[String]) -> Vec<String> {
@@ -258,6 +319,56 @@ mod tests {
assert!(found_again.is_none());
}
+ #[test]
+ fn public_view_marks_inbound_local_signer_sessions() {
+ let session = build_session("inbound", None);
+
+ let view = session.public_view();
+
+ assert_eq!(view.session_id, "inbound");
+ assert_eq!(view.role, Nip46SessionRole::InboundLocalSigner);
+ assert_eq!(view.client_pubkey, session.client_pubkey.to_hex());
+ assert_eq!(view.signer_pubkey, session.remote_signer_pubkey.to_hex());
+ assert_eq!(view.permissions, session.perms);
+ }
+
+ #[test]
+ fn public_view_marks_outbound_remote_signer_sessions() {
+ let client_keys = RadrootsNostrKeys::generate();
+ let remote_signer_keys = RadrootsNostrKeys::generate();
+ let session = Nip46Session {
+ id: "outbound".to_string(),
+ client: RadrootsNostrClient::new(client_keys.clone()),
+ client_keys: client_keys.clone(),
+ client_pubkey: client_keys.public_key(),
+ remote_signer_pubkey: remote_signer_keys.public_key(),
+ user_pubkey: None,
+ relays: vec!["wss://relay.example.com".to_string()],
+ perms: vec!["sign_event".to_string()],
+ name: Some("remote signer".to_string()),
+ url: Some("https://signer.example.com".to_string()),
+ image: None,
+ expires_at: Some(Instant::now() + Duration::from_secs(30)),
+ auth_required: true,
+ authorized: false,
+ auth_url: Some("https://signer.example.com/auth".to_string()),
+ pending_request: None,
+ };
+
+ let view = session.public_view();
+
+ assert_eq!(view.session_id, "outbound");
+ assert_eq!(view.role, Nip46SessionRole::OutboundRemoteSigner);
+ assert_eq!(view.client_pubkey, session.client_pubkey.to_hex());
+ assert_eq!(view.signer_pubkey, session.remote_signer_pubkey.to_hex());
+ assert_eq!(view.relays, session.relays);
+ assert_eq!(view.permissions, session.perms);
+ assert!(view.auth_required);
+ assert!(!view.authorized);
+ assert_eq!(view.auth_url, session.auth_url);
+ assert!(view.expires_in_secs.is_some());
+ }
+
#[tokio::test]
async fn session_store_keeps_active() {
let store = Nip46SessionStore::new();
diff --git a/src/transport/jsonrpc/methods/nip46/session_list.rs b/src/transport/jsonrpc/methods/nip46/session_list.rs
@@ -1,55 +1,18 @@
-use std::time::{Instant};
-
use anyhow::Result;
use jsonrpsee::server::RpcModule;
-use serde::Serialize;
+use crate::core::nip46::session::Nip46SessionView;
use crate::transport::jsonrpc::{MethodRegistry, RpcContext};
-#[derive(Clone, Serialize)]
-struct Nip46SessionListEntry {
- session_id: String,
- client_pubkey: String,
- remote_signer_pubkey: String,
- user_pubkey: Option<String>,
- relays: Vec<String>,
- perms: Vec<String>,
- name: Option<String>,
- url: Option<String>,
- image: Option<String>,
- expires_in_secs: Option<u64>,
-}
-
pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
registry.track("nip46.session.list");
m.register_async_method("nip46.session.list", |_params, ctx, _| async move {
let sessions = ctx.state.nip46_sessions.list().await;
let entries = sessions
.into_iter()
- .map(|session| Nip46SessionListEntry {
- session_id: session.id,
- client_pubkey: session.client_pubkey.to_hex(),
- remote_signer_pubkey: session.remote_signer_pubkey.to_hex(),
- user_pubkey: session.user_pubkey.map(|pubkey| pubkey.to_hex()),
- relays: session.relays,
- perms: session.perms,
- name: session.name,
- url: session.url,
- image: session.image,
- expires_in_secs: session
- .expires_at
- .map(|expires_at| remaining_secs(expires_at)),
- })
+ .map(|session| session.public_view())
.collect::<Vec<_>>();
- Ok::<Vec<Nip46SessionListEntry>, crate::transport::jsonrpc::RpcError>(entries)
+ Ok::<Vec<Nip46SessionView>, crate::transport::jsonrpc::RpcError>(entries)
})?;
Ok(())
}
-
-fn remaining_secs(expires_at: Instant) -> u64 {
- if expires_at <= Instant::now() {
- 0
- } else {
- expires_at.saturating_duration_since(Instant::now()).as_secs()
- }
-}
diff --git a/src/transport/jsonrpc/methods/nip46/session_status.rs b/src/transport/jsonrpc/methods/nip46/session_status.rs
@@ -1,7 +1,8 @@
use anyhow::Result;
use jsonrpsee::server::RpcModule;
-use serde::{Deserialize, Serialize};
+use serde::Deserialize;
+use crate::core::nip46::session::Nip46SessionView;
use crate::transport::jsonrpc::nip46::session;
use crate::transport::jsonrpc::{MethodRegistry, RpcContext, RpcError};
@@ -10,19 +11,6 @@ struct Nip46SessionStatusParams {
session_id: String,
}
-#[derive(Clone, Debug, Serialize)]
-struct Nip46SessionStatusResponse {
- session_id: String,
- client_pubkey: String,
- remote_signer_pubkey: String,
- user_pubkey: Option<String>,
- relays: Vec<String>,
- perms: Vec<String>,
- name: Option<String>,
- url: Option<String>,
- image: Option<String>,
-}
-
pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
registry.track("nip46.session.status");
m.register_async_method("nip46.session.status", |params, ctx, _| async move {
@@ -30,17 +18,7 @@ pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Res
.parse()
.map_err(|e| RpcError::InvalidParams(e.to_string()))?;
let session = session::get_session(ctx.as_ref(), &session_id).await?;
- Ok::<Nip46SessionStatusResponse, RpcError>(Nip46SessionStatusResponse {
- session_id,
- client_pubkey: session.client_pubkey.to_hex(),
- remote_signer_pubkey: session.remote_signer_pubkey.to_hex(),
- user_pubkey: session.user_pubkey.map(|pubkey| pubkey.to_hex()),
- relays: session.relays,
- perms: session.perms,
- name: session.name,
- url: session.url,
- image: session.image,
- })
+ Ok::<Nip46SessionView, RpcError>(session.public_view())
})?;
Ok(())
}