lib

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

commit 244be2aef850a3f1df20b1be7903ed8967a82315
parent 1c016b0015c42762abd61b770789527aa1316a98
Author: triesap <tyson@radroots.org>
Date:   Fri, 31 Jul 2026 19:47:03 +0000

nostr-connect: separate protocol transport from relay implementation

- allow client execution through a dyn-compatible host transport object
- prove publication and receive injection with package-owned event values
- reject relay-pool, nostr-sdk, Tokio-runtime, and global-session ownership
- enforce the separation with focused mock and dependency-boundary tests

Diffstat:
Mcrates/nostr_connect/src/client.rs | 2+-
Mcrates/nostr_connect/tests/client_state_machine.rs | 35+++++++++++++++++++++++++++++++++++
Mcrates/nostr_connect/tests/package_boundary.rs | 18++++++++++++++++++
3 files changed, 54 insertions(+), 1 deletion(-)

diff --git a/crates/nostr_connect/src/client.rs b/crates/nostr_connect/src/client.rs @@ -106,7 +106,7 @@ impl Client { mut on_progress: F, ) -> Result<Completion, RadrootsNostrConnectError> where - T: Transport, + T: Transport + ?Sized, F: FnMut(Progress) -> Result<(), RadrootsNostrConnectError>, { let mut operation = self.prepare(request_id, request)?; diff --git a/crates/nostr_connect/tests/client_state_machine.rs b/crates/nostr_connect/tests/client_state_machine.rs @@ -106,6 +106,41 @@ impl Transport for MockTransport { } } +async fn execute_through_dyn_transport( + client: &Client, + transport: &mut dyn Transport, + request_id: &str, +) -> Result<Completion, Error> { + client + .execute( + RequestId::parse(request_id)?, + Request::Ping, + transport, + &CancellationToken::new(), + |_| Ok(()), + ) + .await +} + +#[tokio::test] +async fn client_executes_through_dyn_transport_without_runtime_ownership() { + let remote_keys = keys(FIXTURE_BOB.secret_key_hex); + let client = client(&remote_keys); + let response = response_event( + &remote_keys, + client.public_key().expect("client public key"), + "request-dyn", + Response::Pong, + ); + let mut transport = MockTransport::new([Receive::event(response)]); + assert_eq!( + execute_through_dyn_transport(&client, &mut transport, "request-dyn") + .await + .expect("completion"), + Completion::response(Response::Pong) + ); +} + #[tokio::test] async fn client_completes_happy_path_after_one_publication() { let remote_keys = keys(FIXTURE_BOB.secret_key_hex); diff --git a/crates/nostr_connect/tests/package_boundary.rs b/crates/nostr_connect/tests/package_boundary.rs @@ -124,6 +124,24 @@ fn client_root_and_transport_use_package_owned_state_machine_types() { } } +#[test] +fn protocol_transport_boundary_has_no_relay_pool_or_runtime_owner() { + for forbidden in [ + "nostr-sdk", + "nostr_sdk", + "tokio::runtime", + "RelayPool", + "Client::start", + ] { + assert!( + !CLIENT.contains(forbidden) && !MANIFEST.contains(forbidden), + "protocol transport retains forbidden owner `{forbidden}`" + ); + } + assert!(CLIENT.contains("pub trait Transport: Send")); + assert!(CLIENT.contains("T: Transport + ?Sized")); +} + fn table_keys<'a>(source: &'a str, header: &str) -> BTreeSet<&'a str> { let Some((_, tail)) = source.split_once(header) else { panic!("manifest is missing {header}");