radrootsd

JSON-RPC bridge for Radroots event publishing
git clone https://radroots.dev/git/radrootsd.git
Log | Files | Refs | README | LICENSE

commit 4d65e37568b074d8de5ae3af14e3449c68198baa
parent b9f53411f42a6e1eb50f29d376bd9298c9ef97c0
Author: triesap <triesap@radroots.dev>
Date:   Wed,  7 Jan 2026 15:44:25 +0000

nip46: add session list endpoint

- add session store list helper that filters expired entries
- expose nip46.session.list JSON-RPC method
- return session metadata with remaining TTL
- add session store test for list filtering

Diffstat:
Msrc/core/nip46/session.rs | 28++++++++++++++++++++++++++++
Msrc/transport/jsonrpc/methods/nip46/mod.rs | 2++
Asrc/transport/jsonrpc/methods/nip46/session_list.rs | 55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 85 insertions(+), 0 deletions(-)

diff --git a/src/core/nip46/session.rs b/src/core/nip46/session.rs @@ -81,6 +81,14 @@ impl Nip46SessionStore { None => false, } } + + pub async fn list(&self) -> Vec<Nip46Session> { + let mut sessions = self.inner.lock().await; + sessions.retain(|_, session| !session.is_expired()); + let mut listed: Vec<Nip46Session> = sessions.values().cloned().collect(); + listed.sort_by(|left, right| left.id.cmp(&right.id)); + listed + } } impl Nip46Session { @@ -159,4 +167,24 @@ mod tests { let found = store.get("active").await; assert!(found.is_some()); } + + #[tokio::test] + async fn session_store_list_filters_expired() { + let store = Nip46SessionStore::new(); + store + .insert(build_session( + "expired", + Some(Instant::now() - Duration::from_secs(1)), + )) + .await; + store + .insert(build_session( + "active", + Some(Instant::now() + Duration::from_secs(10)), + )) + .await; + let listed = store.list().await; + assert_eq!(listed.len(), 1); + assert_eq!(listed[0].id, "active"); + } } diff --git a/src/transport/jsonrpc/methods/nip46/mod.rs b/src/transport/jsonrpc/methods/nip46/mod.rs @@ -12,6 +12,7 @@ pub mod nip44; pub mod ping; pub mod sign_event; pub mod session_close; +pub mod session_list; pub mod session_status; pub mod status; @@ -26,5 +27,6 @@ pub fn module(ctx: RpcContext, registry: MethodRegistry) -> Result<RpcModule<Rpc sign_event::register(&mut m, &registry)?; session_status::register(&mut m, &registry)?; session_close::register(&mut m, &registry)?; + session_list::register(&mut m, &registry)?; Ok(m) } diff --git a/src/transport/jsonrpc/methods/nip46/session_list.rs b/src/transport/jsonrpc/methods/nip46/session_list.rs @@ -0,0 +1,55 @@ +use std::time::{Instant}; + +use anyhow::Result; +use jsonrpsee::server::RpcModule; +use serde::Serialize; + +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)), + }) + .collect::<Vec<_>>(); + Ok::<Vec<Nip46SessionListEntry>, 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() + } +}