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