commit 22138442a8a167149da0455cb7044e451e454e0d parent 639cdae2fc15cab732ff507bb6640d1302d577ae Author: triesap <tyson@radroots.org> Date: Fri, 31 Jul 2026 14:38:06 +0000 nostr: migrate workspace consumers - Replace legacy Nostr alias imports with explicit protocol modules across canonical consumers. - Normalize transport client keys, relay URLs, and subscription IDs behind owned boundaries. - Enforce consumer source restrictions and migrate affected manifests and tests. - Verify feature-complete consumers, contracts, API boundaries, and architecture. Diffstat:
25 files changed, 220 insertions(+), 83 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock @@ -4682,6 +4682,7 @@ dependencies = [ "directories", "futures", "hex", + "nostr", "radroots_event", "radroots_event_codec", "radroots_identity", @@ -4802,6 +4803,7 @@ name = "radroots_outbox" version = "0.1.0-alpha" dependencies = [ "hex", + "nostr", "radroots_event", "radroots_event_store", "radroots_identity", diff --git a/crates/net/Cargo.toml b/crates/net/Cargo.toml @@ -27,6 +27,7 @@ nostr-client = [ "dep:tempfile", "dep:serde_json", "dep:radroots_nostr", + "dep:nostr", "dep:radroots_transport_nostr", ] directories = ["std", "dep:directories"] @@ -45,8 +46,8 @@ radroots_event_codec = { workspace = true, optional = true, default-features = t ] } radroots_nostr = { workspace = true, optional = true, default-features = true, features = [ "events", - "codec", ] } +nostr = { workspace = true, optional = true, features = ["std"] } radroots_transport_nostr = { workspace = true, optional = true, default-features = false, features = [ "client", ] } diff --git a/crates/net/src/keys.rs b/crates/net/src/keys.rs @@ -3,10 +3,9 @@ use crate::config::{KeyFormat, KeyPersistenceConfig}; #[cfg(feature = "nostr-client")] use crate::error::{NetError, Result}; #[cfg(feature = "nostr-client")] -use radroots_nostr::types::RadrootsNostrKeys; -use radroots_nostr::types::RadrootsNostrSecp256k1SecretKey; -use radroots_nostr::types::RadrootsNostrSecretKey; -use radroots_nostr::types::RadrootsNostrToBech32; +use nostr::nips::nip19::ToBech32 as RadrootsNostrToBech32; +use nostr::secp256k1::SecretKey as RadrootsNostrSecp256k1SecretKey; +use nostr::{Keys as RadrootsNostrKeys, SecretKey as RadrootsNostrSecretKey}; #[cfg(feature = "nostr-client")] use serde::Deserialize; #[cfg(feature = "nostr-client")] diff --git a/crates/net/src/net.rs b/crates/net/src/net.rs @@ -145,7 +145,7 @@ impl Net { } #[cfg(feature = "nostr-client")] - pub fn selected_nostr_keys(&self) -> Option<radroots_nostr::types::RadrootsNostrKeys> { + pub fn selected_nostr_keys(&self) -> Option<nostr::Keys> { let signer = self.selected_nostr_signer()?; self.accounts .resolve_signing_keys_for_signer(&signer) @@ -171,9 +171,9 @@ impl NetHandle { mod tests { use crate::builder::NetBuilder; #[cfg(feature = "nostr-client")] - use radroots_identity::{PublicIdentity, PublicKey}; + use nostr::Keys as RadrootsNostrKeys; #[cfg(feature = "nostr-client")] - use radroots_nostr::types::RadrootsNostrKeys; + use radroots_identity::{PublicIdentity, PublicKey}; #[cfg(feature = "nostr-client")] use radroots_nostr_signer::prelude::{ RadrootsNostrRemoteSessionSignerCapability, RadrootsNostrSignerCapability, diff --git a/crates/net/src/nostr_client/events/profile.rs b/crates/net/src/nostr_client/events/profile.rs @@ -1,9 +1,9 @@ use crate::error::{NetError, Result}; +use nostr::PublicKey as RadrootsNostrPublicKey; use radroots_event_codec::parsed::RadrootsParsedData; use radroots_event_codec::profile::RadrootsProfileData; use radroots_nostr::event::Kind as RadrootsNostrKind; use radroots_nostr::filter::Filter as RadrootsNostrFilter; -use radroots_nostr::types::RadrootsNostrPublicKey; use crate::nostr_client::manager::NostrClientManager; diff --git a/crates/net/src/nostr_client/inner.rs b/crates/net/src/nostr_client/inner.rs @@ -1,11 +1,11 @@ use std::collections::HashMap; use std::sync::{Arc, Mutex}; +use nostr::Keys as RadrootsNostrKeys; use radroots_event_codec::{parsed::RadrootsParsedData, post::decode::LegacyPost}; -use radroots_nostr::types::RadrootsNostrKeys; -use radroots_nostr::types::RadrootsNostrRelayUrl; use radroots_transport_nostr::{ - RadrootsNostrClient, RadrootsNostrMonitor, RadrootsNostrRelayStatus, + RadrootsNostrClient, RadrootsNostrClientKey, RadrootsNostrMonitor, RadrootsNostrRelayStatus, + RelayUrl, }; use tokio::runtime::Handle; use tokio::sync::broadcast; @@ -15,7 +15,7 @@ pub(super) struct Inner { pub client: RadrootsNostrClient, pub keys: RadrootsNostrKeys, pub relays: Arc<Mutex<Vec<String>>>, - pub statuses: Arc<Mutex<HashMap<RadrootsNostrRelayUrl, RadrootsNostrRelayStatus>>>, + pub statuses: Arc<Mutex<HashMap<RelayUrl, RadrootsNostrRelayStatus>>>, pub last_error: Arc<Mutex<Option<String>>>, pub rt: Handle, pub post_events_tx: broadcast::Sender<RadrootsParsedData<LegacyPost>>, @@ -25,7 +25,10 @@ pub(super) struct Inner { impl Inner { pub fn new(keys: RadrootsNostrKeys, rt: Handle) -> Arc<Self> { let monitor = RadrootsNostrMonitor::new(2048); - let client = RadrootsNostrClient::new_with_monitor(keys.clone(), monitor); + let client_key = + RadrootsNostrClientKey::from_secret_key_bytes(keys.secret_key().to_secret_bytes()) + .expect("an existing Nostr key remains valid at the transport boundary"); + let client = RadrootsNostrClient::new_with_monitor(client_key, monitor); let (tx, _) = broadcast::channel(2048); Arc::new(Self { diff --git a/crates/net/src/nostr_client/manager.rs b/crates/net/src/nostr_client/manager.rs @@ -2,9 +2,9 @@ use std::sync::Arc; use tokio::runtime::Handle; use super::inner::Inner; +use nostr::Keys as RadrootsNostrKeys; use radroots_nostr::event::Timestamp as RadrootsNostrTimestamp; use radroots_nostr::events::post::radroots_nostr_post_events_filter; -use radroots_nostr::types::RadrootsNostrKeys; #[derive(Clone)] pub struct NostrClientManager { diff --git a/crates/nostr/src/event.rs b/crates/nostr/src/event.rs @@ -10,6 +10,9 @@ use radroots_event::id::Nip01Coordinate; use crate::Error; +#[cfg(feature = "events")] +pub use crate::types::{RadrootsNostrExternalSigningRequest, RadrootsNostrGenericEventBuilder}; + /// Upstream Nostr coordinate used only at the explicit protocol boundary. pub type Coordinate = nostr::nips::nip01::Coordinate; /// Upstream Nostr event used only at the explicit protocol boundary. diff --git a/crates/nostr/tests/package_boundary.rs b/crates/nostr/tests/package_boundary.rs @@ -480,6 +480,34 @@ fn production_api_declares_no_traits_or_host_runtime_implementations() { ); } +#[test] +fn workspace_consumers_do_not_use_superseded_nostr_alias_paths() { + let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let crates_dir = manifest_dir + .parent() + .expect("Nostr crate must have a crates directory") + .to_path_buf(); + + for source_path in rust_sources(&crates_dir) { + if source_path.starts_with(&manifest_dir) { + continue; + } + let source = fs::read_to_string(&source_path) + .unwrap_or_else(|error| panic!("failed to read {}: {error}", source_path.display())); + for forbidden in [ + "radroots_nostr::error::", + "radroots_nostr::prelude", + "radroots_nostr::types::", + ] { + assert!( + !source.contains(forbidden), + "{} still imports superseded Nostr path `{forbidden}`", + source_path.display() + ); + } + } +} + fn rust_sources(root: &Path) -> Vec<PathBuf> { let mut pending = vec![root.to_path_buf()]; let mut sources = Vec::new(); diff --git a/crates/nostr_runtime/Cargo.toml b/crates/nostr_runtime/Cargo.toml @@ -15,7 +15,7 @@ readme = "README" default = ["std", "nostr-client", "rt"] std = [] rt = ["std", "dep:futures", "dep:tokio"] -nostr-client = ["std", "dep:radroots_nostr", "dep:radroots_transport_nostr"] +nostr-client = ["std", "dep:nostr", "dep:radroots_nostr", "dep:radroots_transport_nostr"] nostrdb = ["nostr-client"] [dependencies] @@ -23,10 +23,10 @@ radroots_nostr = { workspace = true, optional = true, default-features = true } radroots_transport_nostr = { workspace = true, optional = true, default-features = false, features = [ "client", ] } +nostr = { workspace = true, optional = true, features = ["std"] } futures = { workspace = true, optional = true } thiserror = { workspace = true } tokio = { workspace = true, optional = true, features = ["rt", "sync", "time"] } [dev-dependencies] -nostr = { workspace = true, features = ["std"] } tokio = { workspace = true, features = ["macros", "rt", "sync", "time"] } diff --git a/crates/nostr_runtime/src/runtime.rs b/crates/nostr_runtime/src/runtime.rs @@ -11,20 +11,17 @@ use core::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use core::time::Duration; use futures::StreamExt; use radroots_nostr::event::Timestamp as RadrootsNostrTimestamp; -use radroots_nostr::types::RadrootsNostrKeys; -use radroots_nostr::types::RadrootsNostrRelayUrl; use radroots_transport_nostr::{ - RadrootsNostrClient, RadrootsNostrMonitor, RadrootsNostrMonitorNotification, - RadrootsNostrRelayStatus, + RadrootsNostrClient, RadrootsNostrClientKey, RadrootsNostrMonitor, + RadrootsNostrMonitorNotification, RadrootsNostrRelayStatus, RelayUrl, }; use std::collections::HashMap; use std::sync::Mutex; use tokio::sync::mpsc; use tokio::task::JoinHandle; -#[derive(Clone)] pub struct RadrootsNostrRuntimeBuilder { - keys: Option<RadrootsNostrKeys>, + keys: Option<RadrootsNostrClientKey>, relays: Vec<String>, queue_capacity: usize, monitor_capacity: usize, @@ -45,7 +42,7 @@ impl RadrootsNostrRuntimeBuilder { } } - pub fn keys(mut self, keys: RadrootsNostrKeys) -> Self { + pub fn keys(mut self, keys: RadrootsNostrClientKey) -> Self { self.keys = Some(keys); self } @@ -128,7 +125,7 @@ struct RadrootsNostrRuntimeInner { relays: Mutex<Vec<String>>, queue_tx: mpsc::Sender<RadrootsNostrRuntimeEvent>, queue_rx: Mutex<mpsc::Receiver<RadrootsNostrRuntimeEvent>>, - statuses: Mutex<HashMap<RadrootsNostrRelayUrl, RadrootsNostrRelayStatus>>, + statuses: Mutex<HashMap<RelayUrl, RadrootsNostrRelayStatus>>, last_error: Mutex<Option<String>>, monitor_task: Mutex<Option<JoinHandle<()>>>, subscription_tasks: Mutex<HashMap<String, JoinHandle<()>>>, @@ -467,7 +464,7 @@ mod tests { fn sample_runtime() -> RadrootsNostrRuntime { RadrootsNostrRuntimeBuilder::new() - .keys(RadrootsNostrKeys::generate()) + .keys(RadrootsNostrClientKey::generate()) .add_relay("wss://relay.example.com") .build() .expect("runtime should build") @@ -487,7 +484,7 @@ mod tests { #[test] fn build_requires_relays() { let result = RadrootsNostrRuntimeBuilder::new() - .keys(RadrootsNostrKeys::generate()) + .keys(RadrootsNostrClientKey::generate()) .build(); assert!(matches!( result, @@ -498,7 +495,7 @@ mod tests { #[test] fn queue_capacity_must_be_positive() { let result = RadrootsNostrRuntimeBuilder::new() - .keys(RadrootsNostrKeys::generate()) + .keys(RadrootsNostrClientKey::generate()) .add_relay("wss://relay.example.com") .queue_capacity(0) .build(); @@ -511,7 +508,7 @@ mod tests { #[test] fn monitor_capacity_must_be_positive() { let result = RadrootsNostrRuntimeBuilder::new() - .keys(RadrootsNostrKeys::generate()) + .keys(RadrootsNostrClientKey::generate()) .add_relay("wss://relay.example.com") .monitor_capacity(0) .build(); @@ -525,7 +522,7 @@ mod tests { fn build_accepts_event_sink() { let sink = Arc::new(RadrootsNostrInMemoryEventSink::new()); let result = RadrootsNostrRuntimeBuilder::new() - .keys(RadrootsNostrKeys::generate()) + .keys(RadrootsNostrClientKey::generate()) .add_relay("wss://relay.example.com") .event_sink(sink) .build(); diff --git a/crates/nostr_runtime/src/sink.rs b/crates/nostr_runtime/src/sink.rs @@ -48,7 +48,7 @@ impl RadrootsNostrEventSink for RadrootsNostrInMemoryEventSink { mod tests { use super::*; use nostr::EventBuilder; - use radroots_nostr::types::RadrootsNostrKeys; + use nostr::Keys as RadrootsNostrKeys; #[test] fn in_memory_sink_tracks_events() { diff --git a/crates/nostr_runtime/src/types.rs b/crates/nostr_runtime/src/types.rs @@ -1,10 +1,10 @@ use alloc::string::String; +use nostr::PublicKey as RadrootsNostrPublicKey; use radroots_nostr::event::Timestamp as RadrootsNostrTimestamp; use radroots_nostr::events::post::radroots_nostr_post_events_filter; #[cfg(feature = "nostr-client")] use radroots_nostr::filter::Filter as RadrootsNostrFilter; use radroots_nostr::filter::radroots_nostr_kind; -use radroots_nostr::types::RadrootsNostrPublicKey; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum RadrootsNostrSubscriptionPolicy { @@ -105,7 +105,7 @@ impl RadrootsNostrSubscriptionSpec { mod tests { use super::*; #[cfg(feature = "nostr-client")] - use radroots_nostr::types::RadrootsNostrKeys; + use nostr::Keys as RadrootsNostrKeys; fn base_spec() -> RadrootsNostrSubscriptionSpec { RadrootsNostrSubscriptionSpec { diff --git a/crates/nostr_signer/src/error.rs b/crates/nostr_signer/src/error.rs @@ -76,8 +76,8 @@ impl From<nostr::event::Error> for RadrootsNostrSignerError { } } -impl From<radroots_nostr::error::RadrootsNostrError> for RadrootsNostrSignerError { - fn from(value: radroots_nostr::error::RadrootsNostrError) -> Self { +impl From<radroots_nostr::Error> for RadrootsNostrSignerError { + fn from(value: radroots_nostr::Error) -> Self { Self::InvalidState(value.to_string()) } } @@ -125,7 +125,7 @@ mod tests { #[test] fn converts_nostr_filter_error() { let converted: RadrootsNostrSignerError = - radroots_nostr::error::RadrootsNostrError::FilterTagError("bad tag".to_string()).into(); + radroots_nostr::Error::FilterTagError("bad tag".to_string()).into(); assert!(converted.to_string().starts_with("invalid signer state:")); } diff --git a/crates/nostr_signer/src/nip46.rs b/crates/nostr_signer/src/nip46.rs @@ -1,3 +1,4 @@ +use nostr::{PublicKey as RadrootsNostrPublicKey, RelayUrl as RadrootsNostrRelayUrl}; use nostr::{ UnsignedEvent, filter::{Alphabet, SingleLetterTag}, @@ -5,12 +6,10 @@ use nostr::{ use radroots_identity::PublicIdentity; use radroots_nostr::event::Event as RadrootsNostrEvent; use radroots_nostr::event::Kind as RadrootsNostrKind; +use radroots_nostr::event::RadrootsNostrGenericEventBuilder; use radroots_nostr::event::Timestamp as RadrootsNostrTimestamp; use radroots_nostr::filter::Filter as RadrootsNostrFilter; use radroots_nostr::tag::Tag as RadrootsNostrTag; -use radroots_nostr::types::RadrootsNostrGenericEventBuilder; -use radroots_nostr::types::RadrootsNostrPublicKey; -use radroots_nostr::types::RadrootsNostrRelayUrl; use radroots_nostr_connect::prelude::{ RADROOTS_NOSTR_CONNECT_RPC_KIND, RadrootsNostrConnectError, RadrootsNostrConnectPermissions, RadrootsNostrConnectRequest, RadrootsNostrConnectRequestMessage, RadrootsNostrConnectResponse, @@ -857,13 +856,13 @@ mod tests { }; use crate::store::RadrootsNostrSignerStore; use crate::test_support::{fixture_alice_identity, fixture_carol_public_key, primary_relay}; + use nostr::PublicKey as RadrootsNostrPublicKey; use nostr::{Keys, SecretKey, Timestamp, UnsignedEvent}; use radroots_identity::{PublicIdentity, PublicKey as IdentityPublicKey}; use radroots_nostr::event::Event as RadrootsNostrEvent; use radroots_nostr::event::Kind as RadrootsNostrKind; + use radroots_nostr::event::RadrootsNostrGenericEventBuilder; use radroots_nostr::tag::TagKind as RadrootsNostrTagKind; - use radroots_nostr::types::RadrootsNostrGenericEventBuilder; - use radroots_nostr::types::RadrootsNostrPublicKey; use radroots_nostr_connect::prelude::{ RADROOTS_NOSTR_CONNECT_RPC_KIND, RadrootsNostrConnectMethod, RadrootsNostrConnectPermission, RadrootsNostrConnectPermissions, diff --git a/crates/nostrdb/src/nostrdb.rs b/crates/nostrdb/src/nostrdb.rs @@ -358,8 +358,8 @@ mod tests { use crate::test_fixtures::{FIXTURE_ALICE_EMAIL, FIXTURE_ALICE_USERNAME}; use futures::StreamExt; use nostr::EventBuilder; + use nostr::Keys as RadrootsNostrKeys; use radroots_nostr::event::Metadata as RadrootsNostrMetadata; - use radroots_nostr::types::RadrootsNostrKeys; use std::sync::atomic::Ordering; use std::sync::{Mutex, OnceLock}; use std::time::Duration; diff --git a/crates/nostrdb/src/runtime_adapter.rs b/crates/nostrdb/src/runtime_adapter.rs @@ -52,7 +52,7 @@ mod tests { use super::*; use crate::config::RadrootsNostrdbConfig; use nostr::EventBuilder; - use radroots_nostr::types::RadrootsNostrKeys; + use nostr::Keys as RadrootsNostrKeys; use tempfile::TempDir; #[test] diff --git a/crates/outbox/Cargo.toml b/crates/outbox/Cargo.toml @@ -35,6 +35,7 @@ sqlx = { workspace = true, optional = true, features = ["derive"] } thiserror = { workspace = true } [dev-dependencies] +nostr = { workspace = true, features = ["std"] } radroots_identity = { workspace = true, default-features = false, features = [ "std", ] } diff --git a/crates/outbox/src/store.rs b/crates/outbox/src/store.rs @@ -3393,6 +3393,7 @@ fn u32_from_i64(field: &'static str, value: i64) -> Result<u32, RadrootsOutboxEr #[cfg_attr(coverage_nightly, coverage(off))] mod tests { use super::*; + use nostr::{Keys as RadrootsNostrKeys, SecretKey as RadrootsNostrSecretKey}; use radroots_event::envelope::kind::{ KIND_CLASSIFIED_LISTING, KIND_FOLLOW, KIND_GEOCHAT, KIND_HTTP_AUTH, KIND_RELAY_AUTH, }; @@ -3406,8 +3407,6 @@ mod tests { }; use radroots_identity::PublicKey; use radroots_nostr::draft_signing::radroots_nostr_sign_frozen_draft; - use radroots_nostr::types::RadrootsNostrKeys; - use radroots_nostr::types::RadrootsNostrSecretKey; const FIXTURE_ALICE_SECRET_KEY_HEX: &str = "10c5304d6c9ae3a1a16f7860f1cc8f5e3a76225a2663b3a989a0d775919b7df5"; diff --git a/crates/transport_nostr/src/client.rs b/crates/transport_nostr/src/client.rs @@ -13,14 +13,11 @@ use std::net::SocketAddr; use futures::Stream; use nostr_sdk::{Client, ClientBuilder, ClientOptions}; -use crate::RadrootsRelayTransportError; +use crate::{RadrootsRelayTransportError, RelayUrl}; +use nostr::{Keys, SecretKey, SubscriptionId}; use radroots_nostr::event::Event as RadrootsNostrEvent; use radroots_nostr::event::EventId as RadrootsNostrEventId; use radroots_nostr::filter::Filter as RadrootsNostrFilter; -use radroots_nostr::types::RadrootsNostrKeys; -use radroots_nostr::types::RadrootsNostrPublicKey; -use radroots_nostr::types::RadrootsNostrRelayUrl; -use radroots_nostr::types::RadrootsNostrSubscriptionId; #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum RadrootsNostrRelayStatus { @@ -50,7 +47,7 @@ fn normalize_relay_status(value: nostr_sdk::RelayStatus) -> RadrootsNostrRelaySt #[derive(Debug, Clone, PartialEq, Eq)] pub enum RadrootsNostrMonitorNotification { StatusChanged { - relay_url: RadrootsNostrRelayUrl, + relay_url: RelayUrl, status: RadrootsNostrRelayStatus, }, } @@ -109,7 +106,7 @@ fn normalize_monitor_notification( match notification { nostr_sdk::prelude::MonitorNotification::StatusChanged { relay_url, status } => { RadrootsNostrMonitorNotification::StatusChanged { - relay_url, + relay_url: RelayUrl::from_normalized_transport(relay_url.to_string()), status: normalize_relay_status(status), } } @@ -119,8 +116,8 @@ fn normalize_monitor_notification( #[derive(Debug, Clone)] pub struct RadrootsNostrOutput<T> { pub val: T, - pub success: HashSet<RadrootsNostrRelayUrl>, - pub failed: HashMap<RadrootsNostrRelayUrl, String>, + pub success: HashSet<RelayUrl>, + pub failed: HashMap<RelayUrl, String>, } fn normalize_output<T>(output: nostr_sdk::prelude::Output<T>) -> RadrootsNostrOutput<T> @@ -129,11 +126,93 @@ where { RadrootsNostrOutput { val: output.val, + success: output + .success + .into_iter() + .map(|url| RelayUrl::from_normalized_transport(url.to_string())) + .collect(), + failed: output + .failed + .into_iter() + .map(|(url, error)| (RelayUrl::from_normalized_transport(url.to_string()), error)) + .collect(), + } +} + +fn normalize_subscription_output( + output: nostr_sdk::prelude::Output<SubscriptionId>, +) -> RadrootsNostrOutput<RadrootsNostrSubscriptionId> { + let output = normalize_output(output); + RadrootsNostrOutput { + val: RadrootsNostrSubscriptionId(output.val.to_string()), success: output.success, failed: output.failed, } } +/// An opaque local credential for the compatibility Nostr transport client. +/// +/// Secret material cannot be cloned, formatted, or serialized through this +/// boundary. Hosts should retain their authoritative credential and create a +/// short-lived transport credential only when constructing a client. +/// +/// ```compile_fail +/// use radroots_transport_nostr::RadrootsNostrClientKey; +/// +/// let key = RadrootsNostrClientKey::generate(); +/// let duplicated = key.clone(); +/// ``` +pub struct RadrootsNostrClientKey { + inner: Keys, +} + +impl RadrootsNostrClientKey { + pub fn generate() -> Self { + Self { + inner: Keys::generate(), + } + } + + pub fn from_secret_key_bytes( + secret_key: [u8; 32], + ) -> Result<Self, RadrootsRelayTransportError> { + let secret_key = SecretKey::from_slice(&secret_key) + .map_err(|error| RadrootsRelayTransportError::ClientConfig(error.to_string()))?; + Ok(Self { + inner: Keys::new(secret_key), + }) + } + + pub fn public_key_hex(&self) -> String { + self.inner.public_key().to_hex() + } + + fn into_inner(self) -> Keys { + self.inner + } +} + +impl Debug for RadrootsNostrClientKey { + fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + formatter.write_str("RadrootsNostrClientKey([REDACTED])") + } +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct RadrootsNostrSubscriptionId(String); + +impl RadrootsNostrSubscriptionId { + pub fn as_str(&self) -> &str { + self.0.as_str() + } +} + +impl core::fmt::Display for RadrootsNostrSubscriptionId { + fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + formatter.write_str(self.as_str()) + } +} + pub struct RadrootsNostrEventStream { inner: nostr_sdk::pool::stream::BoxedStream<RadrootsNostrEvent>, } @@ -149,6 +228,7 @@ impl Stream for RadrootsNostrEventStream { #[derive(Clone)] pub struct RadrootsNostrRelay { inner: nostr_sdk::Relay, + url: RelayUrl, } impl RadrootsNostrRelay { @@ -156,8 +236,8 @@ impl RadrootsNostrRelay { self.inner.is_connected() } - pub fn url(&self) -> &RadrootsNostrRelayUrl { - self.inner.url() + pub fn url(&self) -> &RelayUrl { + &self.url } } @@ -282,19 +362,19 @@ impl RadrootsNostrClient { } } - pub fn new(keys: RadrootsNostrKeys) -> Self { + pub fn new(keys: RadrootsNostrClientKey) -> Self { Self { - inner: Client::new(keys), + inner: Client::new(keys.into_inner()), monitor: None, } } pub fn from_keys_with_options( - keys: RadrootsNostrKeys, + keys: RadrootsNostrClientKey, options: RadrootsNostrClientOptions, ) -> Self { let inner = ClientBuilder::new() - .signer(keys) + .signer(keys.into_inner()) .opts(options.to_client_options()) .build(); Self { @@ -303,9 +383,9 @@ impl RadrootsNostrClient { } } - pub fn new_with_monitor(keys: RadrootsNostrKeys, monitor: RadrootsNostrMonitor) -> Self { + pub fn new_with_monitor(keys: RadrootsNostrClientKey, monitor: RadrootsNostrMonitor) -> Self { let inner = Client::builder() - .signer(keys) + .signer(keys.into_inner()) .monitor(monitor.inner.clone()) .build(); Self { @@ -318,10 +398,11 @@ impl RadrootsNostrClient { self.inner.has_signer().await } - pub async fn public_key(&self) -> Result<RadrootsNostrPublicKey, RadrootsRelayTransportError> { + pub async fn public_key_hex(&self) -> Result<String, RadrootsRelayTransportError> { self.inner .public_key() .await + .map(|public_key| public_key.to_hex()) .map_err(|error| RadrootsRelayTransportError::Client(error.to_string())) } @@ -369,12 +450,15 @@ impl RadrootsNostrClient { .map_err(|error| RadrootsRelayTransportError::Client(error.to_string())) } - pub async fn relays(&self) -> HashMap<RadrootsNostrRelayUrl, RadrootsNostrRelay> { + pub async fn relays(&self) -> HashMap<RelayUrl, RadrootsNostrRelay> { self.inner .relays() .await .into_iter() - .map(|(url, inner)| (url, RadrootsNostrRelay { inner })) + .map(|(url, inner)| { + let url = RelayUrl::from_normalized_transport(url.to_string()); + (url.clone(), RadrootsNostrRelay { inner, url }) + }) .collect() } @@ -425,29 +509,31 @@ impl RadrootsNostrClient { options.map(RadrootsNostrSubscribeAutoCloseOptions::into_sdk), ) .await - .map(normalize_output) + .map(normalize_subscription_output) .map_err(|error| RadrootsRelayTransportError::Client(error.to_string())) } pub async fn subscribe_to_relays( &self, - relays: &[RadrootsNostrRelayUrl], + relays: &[RelayUrl], filter: RadrootsNostrFilter, options: Option<RadrootsNostrSubscribeAutoCloseOptions>, ) -> Result<RadrootsNostrOutput<RadrootsNostrSubscriptionId>, RadrootsRelayTransportError> { self.inner .subscribe_to( - relays.iter().cloned(), + relays.iter().map(RelayUrl::as_str), filter, options.map(RadrootsNostrSubscribeAutoCloseOptions::into_sdk), ) .await - .map(normalize_output) + .map(normalize_subscription_output) .map_err(|error| RadrootsRelayTransportError::Client(error.to_string())) } pub async fn unsubscribe(&self, subscription_id: &RadrootsNostrSubscriptionId) { - self.inner.unsubscribe(subscription_id).await; + self.inner + .unsubscribe(&SubscriptionId::new(subscription_id.as_str())) + .await; } /// Relays a caller-supplied signed event. @@ -467,11 +553,11 @@ impl RadrootsNostrClient { pub async fn send_event_to_relays( &self, - relays: &[RadrootsNostrRelayUrl], + relays: &[RelayUrl], event: &RadrootsNostrEvent, ) -> Result<RadrootsNostrOutput<RadrootsNostrEventId>, RadrootsRelayTransportError> { self.inner - .send_event_to(relays.iter().cloned(), event) + .send_event_to(relays.iter().map(RelayUrl::as_str), event) .await .map(normalize_output) .map_err(|error| RadrootsRelayTransportError::Client(error.to_string())) @@ -503,3 +589,21 @@ pub async fn radroots_nostr_fetch_event_by_id( .ok_or_else(|| RadrootsRelayTransportError::EventNotFound(event_id.to_hex()))?; Ok(event.clone()) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn client_key_debug_output_is_redacted() { + let key = RadrootsNostrClientKey::generate(); + + assert_eq!(format!("{key:?}"), "RadrootsNostrClientKey([REDACTED])"); + assert!(!format!("{key:?}").contains(&key.public_key_hex())); + } + + #[test] + fn client_key_rejects_invalid_secret_scalar() { + assert!(RadrootsNostrClientKey::from_secret_key_bytes([0; 32]).is_err()); + } +} diff --git a/crates/transport_nostr/src/fetch.rs b/crates/transport_nostr/src/fetch.rs @@ -1373,11 +1373,10 @@ mod tests { unproven_relay_stream_completion, }; use nostr::JsonUtil; + use nostr::{Keys as RadrootsNostrKeys, SecretKey as RadrootsNostrSecretKey}; use radroots_event_store::{RadrootsEventVisibility, RadrootsNip09SuppressionReason}; use radroots_nostr::event::Kind as RadrootsNostrKind; use radroots_nostr::filter::Filter as RadrootsNostrFilter; - use radroots_nostr::types::RadrootsNostrKeys; - use radroots_nostr::types::RadrootsNostrSecretKey; use std::collections::HashMap; const FIXTURE_ALICE_SECRET_KEY_HEX: &str = diff --git a/crates/transport_nostr/src/lib.rs b/crates/transport_nostr/src/lib.rs @@ -18,11 +18,11 @@ mod relays; #[cfg(feature = "client")] pub use client::{ - RadrootsNostrClient, RadrootsNostrClientOptions, RadrootsNostrEventStream, - RadrootsNostrMonitor, RadrootsNostrMonitorNotification, RadrootsNostrMonitorReceiveError, - RadrootsNostrMonitorReceiver, RadrootsNostrOutput, RadrootsNostrRelay, - RadrootsNostrRelayStatus, RadrootsNostrSubscribeAutoCloseOptions, - radroots_nostr_fetch_event_by_id, + RadrootsNostrClient, RadrootsNostrClientKey, RadrootsNostrClientOptions, + RadrootsNostrEventStream, RadrootsNostrMonitor, RadrootsNostrMonitorNotification, + RadrootsNostrMonitorReceiveError, RadrootsNostrMonitorReceiver, RadrootsNostrOutput, + RadrootsNostrRelay, RadrootsNostrRelayStatus, RadrootsNostrSubscribeAutoCloseOptions, + RadrootsNostrSubscriptionId, radroots_nostr_fetch_event_by_id, }; pub use error::RadrootsRelayTransportError; #[cfg(all(feature = "storage", feature = "runtime-tokio"))] diff --git a/crates/transport_nostr/src/publish.rs b/crates/transport_nostr/src/publish.rs @@ -1046,12 +1046,11 @@ fn ensure_raw_event_matches_signed_event( mod tests { use super::{RadrootsNostrEvent, ensure_raw_event_matches_signed_event}; use nostr::JsonUtil; + use nostr::{Keys as RadrootsNostrKeys, SecretKey as RadrootsNostrSecretKey}; use radroots_event::draft::{EventDraft, SignedEvent}; use radroots_event::envelope::kind::KIND_GEOCHAT; use radroots_event::wire::Nip01EventWire; use radroots_nostr::draft_signing::radroots_nostr_sign_frozen_draft; - use radroots_nostr::types::RadrootsNostrKeys; - use radroots_nostr::types::RadrootsNostrSecretKey; const FIXTURE_ALICE_SECRET_KEY_HEX: &str = "10c5304d6c9ae3a1a16f7860f1cc8f5e3a76225a2663b3a989a0d775919b7df5"; diff --git a/crates/transport_nostr/src/relay.rs b/crates/transport_nostr/src/relay.rs @@ -29,6 +29,10 @@ impl RadrootsRelayUrlPolicy { pub struct RelayUrl(String); impl RelayUrl { + pub(crate) fn from_normalized_transport(value: impl Into<String>) -> Self { + Self(value.into()) + } + pub fn parse( value: impl AsRef<str>, policy: RadrootsRelayUrlPolicy, diff --git a/crates/transport_nostr/tests/transport.rs b/crates/transport_nostr/tests/transport.rs @@ -1,5 +1,6 @@ use futures::future::BoxFuture; use nostr::{EventBuilder, JsonUtil}; +use nostr::{Keys as RadrootsNostrKeys, SecretKey as RadrootsNostrSecretKey}; use radroots_event::draft::{EventDraft, SignedEvent}; use radroots_event::envelope::kind::{ KIND_DELETION_REQUEST, KIND_FOLLOW, KIND_GEOCHAT, KIND_POST, KIND_PROFILE, @@ -15,8 +16,6 @@ use radroots_nostr::filter::Filter as RadrootsNostrFilter; use radroots_nostr::filter::radroots_nostr_filter_tag; use radroots_nostr::tag::Tag as RadrootsNostrTag; use radroots_nostr::tag::TagKind as RadrootsNostrTagKind; -use radroots_nostr::types::RadrootsNostrKeys; -use radroots_nostr::types::RadrootsNostrSecretKey; use radroots_outbox::{ RadrootsOutbox, RadrootsOutboxClaimedEvent, RadrootsOutboxDeliveryPlanInput, RadrootsOutboxDeliveryTargetStatus, RadrootsOutboxEventState, RadrootsOutboxOperationInput,