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:
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}");