lib

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

commit 339fd02086c0c1d6c2d3e82352b7612faf229a3a
parent ab41530c746bee279568f85bc7de28aa3ae1a810
Author: triesap <tyson@radroots.org>
Date:   Mon,  3 Aug 2026 21:02:29 +0000

nostr-connect: expose validated client URI construction

- add a canonical constructor for client-origin NIP-46 URI values
- normalize duplicate relays and reject empty relay or secret inputs
- revalidate client metadata at the public construction boundary
- cover construction invariants with package tests and strict Clippy

Diffstat:
Mcrates/nostr_connect/src/uri.rs | 33+++++++++++++++++++++++++++++++++
Mcrates/nostr_connect/tests/protocol.rs | 30+++++++++++++++++++++++++++++-
2 files changed, 62 insertions(+), 1 deletion(-)

diff --git a/crates/nostr_connect/src/uri.rs b/crates/nostr_connect/src/uri.rs @@ -291,6 +291,39 @@ pub struct ClientUri { } impl ClientUri { + /// Constructs a validated client-origin URI from canonical protocol values. + pub fn try_new( + client_public_key: PublicKey, + relays: impl IntoIterator<Item = RelayUrl>, + secret: impl Into<String>, + metadata: ClientMetadata, + ) -> Result<Self, RadrootsNostrConnectError> { + let mut normalized_relays = Vec::new(); + for relay in relays { + if normalized_relays.contains(&relay) { + continue; + } + if normalized_relays.len() == RELAY_COUNT_MAX { + return Err(RadrootsNostrConnectError::InvalidUri); + } + normalized_relays.push(relay); + } + if normalized_relays.is_empty() { + return Err(RadrootsNostrConnectError::MissingRelay); + } + let secret = secret.into(); + if secret.is_empty() { + return Err(RadrootsNostrConnectError::MissingSecret); + } + validate_secret(&secret)?; + Ok(Self { + client_public_key, + relays: normalized_relays, + secret, + metadata: metadata.normalized()?, + }) + } + #[must_use] pub const fn client_public_key(&self) -> PublicKey { self.client_public_key diff --git a/crates/nostr_connect/tests/protocol.rs b/crates/nostr_connect/tests/protocol.rs @@ -8,7 +8,7 @@ use radroots_nostr_connect::message::{ }; use radroots_nostr_connect::permission::Permissions; use radroots_nostr_connect::uri::{ - CLIENT_METADATA_JSON_MAX_BYTES, CLIENT_NAME_MAX_BYTES, ClientMetadata, + CLIENT_METADATA_JSON_MAX_BYTES, CLIENT_NAME_MAX_BYTES, ClientMetadata, ClientUri, RelayUrl as ConnectRelayUrl, Uri, }; use radroots_nostr_connect::{Error, Method, Permission, Request, Response}; @@ -22,6 +22,34 @@ fn test_public_key() -> PublicKey { PublicKey::parse(FIXTURE_ALICE.public_key_hex).expect("public key") } +#[test] +fn constructs_client_uri_from_validated_values() { + let relay = ConnectRelayUrl::parse(RELAY_PRIMARY_WSS).expect("relay"); + let metadata = ClientMetadata::new() + .with_name("Radroots") + .expect("metadata"); + let client = ClientUri::try_new( + test_identity_public_key(), + [relay.clone(), relay], + "shared-secret", + metadata, + ) + .expect("client URI"); + + assert_eq!(client.relays().len(), 1); + assert_eq!(client.secret(), "shared-secret"); + assert_eq!(client.metadata().name(), Some("Radroots")); + assert!( + ClientUri::try_new( + test_identity_public_key(), + Vec::<ConnectRelayUrl>::new(), + "shared-secret", + ClientMetadata::new(), + ) + .is_err() + ); +} + fn test_identity_public_key() -> radroots_identity::PublicKey { radroots_identity::PublicKey::from_hex(FIXTURE_ALICE.public_key_hex) .expect("identity public key")