lib

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

commit ec20c8477848b0d111f30b286c572cfc6b83ef7f
parent e1486d9ffb588ff4650bbb8bfef66feaee4f5d51
Author: triesap <tyson@radroots.org>
Date:   Sun, 16 Aug 2026 20:33:19 +0000

transport-nostr: require explicit relay endpoints

- Intent: Require every relay URL, destination policy, and access mode to be supplied explicitly.
- Why: Implicit profiles could inject or infer network authority outside the caller-owned configuration boundary.
- Verification: Transport, SDK, and mobile tests, warnings-denied Clippy and Rustdoc, full workspace check, and exact API regeneration passed through extbuild.
- Risk: This is an intentional breaking constructor change; active Rust consumers are migrated while terminal generated consumers remain governed by the later RCLD checkpoint.

Diffstat:
Mcontracts/api_baselines/radroots_sdk.txt | 191++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mcontracts/api_baselines/radroots_transport_nostr.txt | 7+++----
Mcrates/mobile_core/src/runtime/builder.rs | 12++++++++++--
Mcrates/mobile_core/src/runtime/product_surface/outbox.rs | 30++++++++++++++++++++----------
Mcrates/mobile_core/src/runtime/product_surface/settings.rs | 26++++++++++++++++++--------
Mcrates/mobile_core/src/runtime/sdk.rs | 44+++++++++++++++++++++++++++++++++++---------
Mcrates/sdk/src/client.rs | 20+++++++++++++++-----
Mcrates/sdk/src/transport.rs | 36+++++++++++++++++++++++++++++-------
Mcrates/transport_nostr/README.md | 24+++++++++++++++++-------
Mcrates/transport_nostr/examples/configure_transport.rs | 14+++++++++++---
Mcrates/transport_nostr/src/auth.rs | 11+++++++++--
Mcrates/transport_nostr/src/client.rs | 48++++++++++++++++++++++++++++++++++++++----------
Mcrates/transport_nostr/src/error.rs | 6++++++
Mcrates/transport_nostr/src/lib.rs | 4+---
Mcrates/transport_nostr/src/profile.rs | 217++++++++++++++++++++++++++++++++++++++++---------------------------------------
Mcrates/transport_nostr/src/relay.rs | 7++++++-
Mcrates/transport_nostr/src/sink.rs | 30+++++++++++++++++++++++++-----
Mcrates/transport_nostr/src/source.rs | 26+++++++++++++++++++++-----
Mcrates/transport_nostr/src/status.rs | 28++++++++++++++++++++--------
Mcrates/transport_nostr/tests/local_io.rs | 12++++++++++--
Mcrates/transport_nostr/tests/network_hardening.rs | 17++++++++++++++---
Mcrates/transport_nostr/tests/package_boundary.rs | 33++++++++++++++++++++++++++++++++-
22 files changed, 639 insertions(+), 204 deletions(-)

diff --git a/contracts/api_baselines/radroots_sdk.txt b/contracts/api_baselines/radroots_sdk.txt @@ -47,8 +47,10 @@ pub const fn radroots_sdk::capability::CapabilityStatus::maturity(self) -> radro pub mod radroots_sdk::client pub struct radroots_sdk::client::Client impl radroots_sdk::client::Client +pub fn radroots_sdk::client::Client::blossom(&self) -> radroots_sdk::error::Result<core::option::Option<&radroots_sdk::transport::BlossomSlot>> pub fn radroots_sdk::client::Client::capabilities(&self) -> radroots_sdk::capability::CapabilityReport pub async fn radroots_sdk::client::Client::close(&self) -> radroots_sdk::error::Result<()> +pub fn radroots_sdk::client::Client::configure_blossom(&self, radroots_sdk::transport::BlossomConfig) -> radroots_sdk::error::Result<()> pub fn radroots_sdk::client::Client::configure_nostr(&self, radroots_transport_nostr::profile::RelayProfile) -> radroots_sdk::error::Result<()> pub fn radroots_sdk::client::Client::farm(&self) -> radroots_sdk::error::Result<core::option::Option<radroots_sdk::farm::Operations<'_>>> pub fn radroots_sdk::client::Client::is_closed(&self) -> bool @@ -68,6 +70,7 @@ impl core::fmt::Debug for radroots_sdk::client::Client pub fn radroots_sdk::client::Client::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result pub struct radroots_sdk::client::ClientBuilder impl radroots_sdk::client::ClientBuilder +pub fn radroots_sdk::client::ClientBuilder::blossom(self, radroots_sdk::transport::BlossomSlot) -> Self pub fn radroots_sdk::client::ClientBuilder::build(self) -> radroots_sdk::error::Result<radroots_sdk::client::Client> pub fn radroots_sdk::client::ClientBuilder::capability_availability(self, radroots_sdk::capability::CapabilityId, radroots_sdk::capability::Availability) -> Self pub fn radroots_sdk::client::ClientBuilder::host_sync(self, radroots_sdk::sync::HostPolicy) -> Self @@ -413,7 +416,6 @@ pub const fn radroots_sdk::trade::PrepareRequest::new(radroots_signing::actor::A pub fn radroots_sdk::trade::prepare(radroots_sdk::trade::PrepareRequest) -> core::result::Result<radroots_sdk::trade::Plan, radroots_sdk::trade::PrepareError> pub fn radroots_sdk::trade::project(radroots_trade::trade_contract_v1::RadrootsTradeReductionInputV1) -> radroots_trade::trade_contract_v1::RadrootsTradeProjectionV1 pub mod radroots_sdk::transport -pub use radroots_sdk::transport::DEFAULT_PUBLIC_RELAY pub use radroots_sdk::transport::ReconnectBackoff pub use radroots_sdk::transport::RelayAccess pub use radroots_sdk::transport::RelayAggregateState @@ -427,6 +429,64 @@ pub use radroots_sdk::transport::RelayStatus pub use radroots_sdk::transport::RelayStatusReport pub use radroots_sdk::transport::RelayUrl pub use radroots_sdk::transport::RelayUrlPolicy +#[non_exhaustive] pub enum radroots_sdk::transport::BlossomEndpointAuthority +pub radroots_sdk::transport::BlossomEndpointAuthority::LoopbackDevelopment +pub radroots_sdk::transport::BlossomEndpointAuthority::PrivateNetworkDevelopment +pub radroots_sdk::transport::BlossomEndpointAuthority::PublicWebPki +#[non_exhaustive] pub enum radroots_sdk::transport::BlossomErrorKind +pub radroots_sdk::transport::BlossomErrorKind::Authorization +pub radroots_sdk::transport::BlossomErrorKind::Cancelled +pub radroots_sdk::transport::BlossomErrorKind::ConfigurationChanged +pub radroots_sdk::transport::BlossomErrorKind::ContentEncodingDenied +pub radroots_sdk::transport::BlossomErrorKind::DescriptorMismatch +pub radroots_sdk::transport::BlossomErrorKind::DimensionMismatch +pub radroots_sdk::transport::BlossomErrorKind::DuplicateEndpoint +pub radroots_sdk::transport::BlossomErrorKind::EndpointNotConfigured +pub radroots_sdk::transport::BlossomErrorKind::EndpointSchemeDenied +pub radroots_sdk::transport::BlossomErrorKind::HttpStatus +pub radroots_sdk::transport::BlossomErrorKind::InvalidDescriptor +pub radroots_sdk::transport::BlossomErrorKind::InvalidDimensions +pub radroots_sdk::transport::BlossomErrorKind::InvalidEndpoint +pub radroots_sdk::transport::BlossomErrorKind::InvalidEndpointCount +pub radroots_sdk::transport::BlossomErrorKind::InvalidImageBytes +pub radroots_sdk::transport::BlossomErrorKind::InvalidLimits +pub radroots_sdk::transport::BlossomErrorKind::InvalidRequest +pub radroots_sdk::transport::BlossomErrorKind::MediaTypeMismatch +pub radroots_sdk::transport::BlossomErrorKind::RedirectLimit +pub radroots_sdk::transport::BlossomErrorKind::ResolutionFailed +pub radroots_sdk::transport::BlossomErrorKind::ResolvedAddressDenied +pub radroots_sdk::transport::BlossomErrorKind::ResponseHashMismatch +pub radroots_sdk::transport::BlossomErrorKind::ResponseSizeMismatch +pub radroots_sdk::transport::BlossomErrorKind::ResponseTooLarge +pub radroots_sdk::transport::BlossomErrorKind::RetrievedBytesMismatch +pub radroots_sdk::transport::BlossomErrorKind::Timeout +pub radroots_sdk::transport::BlossomErrorKind::Transport +pub radroots_sdk::transport::BlossomErrorKind::UnsafeRedirect +pub radroots_sdk::transport::BlossomErrorKind::UnsupportedMediaType +#[non_exhaustive] pub enum radroots_sdk::transport::BlossomEvidenceState +pub radroots_sdk::transport::BlossomEvidenceState::ConfiguredUnobserved +pub radroots_sdk::transport::BlossomEvidenceState::DnsPolicyValidated +pub radroots_sdk::transport::BlossomEvidenceState::RetrievalVerified +pub radroots_sdk::transport::BlossomEvidenceState::RetryableFailure +pub radroots_sdk::transport::BlossomEvidenceState::TerminalFailure +pub radroots_sdk::transport::BlossomEvidenceState::TlsHttpObserved +pub radroots_sdk::transport::BlossomEvidenceState::UploadVerified +#[non_exhaustive] pub enum radroots_sdk::transport::BlossomHostKind +pub radroots_sdk::transport::BlossomHostKind::Native +pub radroots_sdk::transport::BlossomHostKind::PhysicalDevice +pub radroots_sdk::transport::BlossomHostKind::Simulator +#[non_exhaustive] pub enum radroots_sdk::transport::BlossomPhase +pub radroots_sdk::transport::BlossomPhase::Authorization +pub radroots_sdk::transport::BlossomPhase::Configuration +pub radroots_sdk::transport::BlossomPhase::Descriptor +pub radroots_sdk::transport::BlossomPhase::Probe +pub radroots_sdk::transport::BlossomPhase::Retrieval +pub radroots_sdk::transport::BlossomPhase::Upload +pub radroots_sdk::transport::BlossomPhase::Verification +#[non_exhaustive] pub enum radroots_sdk::transport::BlossomTransportSecurity +pub radroots_sdk::transport::BlossomTransportSecurity::DevelopmentCleartext +pub radroots_sdk::transport::BlossomTransportSecurity::DevelopmentTls +pub radroots_sdk::transport::BlossomTransportSecurity::PublicWebPki #[non_exhaustive] pub enum radroots_sdk::transport::DaemonAuth pub radroots_sdk::transport::DaemonAuth::BearerToken(alloc::string::String) pub radroots_sdk::transport::DaemonAuth::None @@ -438,6 +498,132 @@ pub radroots_sdk::transport::DaemonErrorKind::InvalidRequest pub radroots_sdk::transport::DaemonErrorKind::InvalidResponse pub radroots_sdk::transport::DaemonErrorKind::Rpc pub radroots_sdk::transport::DaemonErrorKind::Transport +pub struct radroots_sdk::transport::BlossomCancellation +impl radroots_sdk::transport::BlossomCancellation +pub fn radroots_sdk::transport::BlossomCancellation::cancel(&self) +pub fn radroots_sdk::transport::BlossomCancellation::is_cancelled(&self) -> bool +pub struct radroots_sdk::transport::BlossomConfig +impl radroots_sdk::transport::BlossomConfig +pub fn radroots_sdk::transport::BlossomConfig::fingerprint(&self) -> radroots_sdk::transport::BlossomConfigFingerprint +pub fn radroots_sdk::transport::BlossomConfig::from_profile(radroots_sdk::transport::BlossomProfile) -> Self +pub const fn radroots_sdk::transport::BlossomConfig::profile(&self) -> &radroots_sdk::transport::BlossomProfile +pub fn radroots_sdk::transport::BlossomConfig::with_limits(self, u64, usize, u8) -> core::result::Result<Self, radroots_sdk::transport::BlossomError> +pub fn radroots_sdk::transport::BlossomConfig::with_network_policy(self, core::time::Duration, core::time::Duration, u8, core::time::Duration) -> core::result::Result<Self, radroots_sdk::transport::BlossomError> +pub struct radroots_sdk::transport::BlossomConfigFingerprint(_) +impl radroots_sdk::transport::BlossomConfigFingerprint +pub const fn radroots_sdk::transport::BlossomConfigFingerprint::as_bytes(&self) -> &[u8; 32] +pub fn radroots_sdk::transport::BlossomConfigFingerprint::to_hex(self) -> alloc::string::String +impl core::fmt::Display for radroots_sdk::transport::BlossomConfigFingerprint +pub fn radroots_sdk::transport::BlossomConfigFingerprint::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub struct radroots_sdk::transport::BlossomEndpoint +impl radroots_sdk::transport::BlossomEndpoint +pub const fn radroots_sdk::transport::BlossomEndpoint::authority(&self) -> radroots_sdk::transport::BlossomEndpointAuthority +pub fn radroots_sdk::transport::BlossomEndpoint::host(&self) -> &str +pub fn radroots_sdk::transport::BlossomEndpoint::origin(&self) -> &str +pub const fn radroots_sdk::transport::BlossomEndpoint::port(&self) -> u16 +pub struct radroots_sdk::transport::BlossomEndpointEvidence +impl radroots_sdk::transport::BlossomEndpointEvidence +pub const fn radroots_sdk::transport::BlossomEndpointEvidence::attempts(&self) -> u8 +pub const fn radroots_sdk::transport::BlossomEndpointEvidence::config_fingerprint(&self) -> radroots_sdk::transport::BlossomConfigFingerprint +pub const fn radroots_sdk::transport::BlossomEndpointEvidence::error_code(&self) -> core::option::Option<&'static str> +pub const fn radroots_sdk::transport::BlossomEndpointEvidence::error_phase(&self) -> core::option::Option<radroots_sdk::transport::BlossomPhase> +pub const fn radroots_sdk::transport::BlossomEndpointEvidence::http_status(&self) -> core::option::Option<u16> +pub const fn radroots_sdk::transport::BlossomEndpointEvidence::last_successful_state(&self) -> radroots_sdk::transport::BlossomEvidenceState +pub const fn radroots_sdk::transport::BlossomEndpointEvidence::observed_at_unix_ms(&self) -> core::option::Option<u64> +pub fn radroots_sdk::transport::BlossomEndpointEvidence::origin(&self) -> &str +pub const fn radroots_sdk::transport::BlossomEndpointEvidence::possible_orphan(&self) -> bool +pub const fn radroots_sdk::transport::BlossomEndpointEvidence::retryable(&self) -> bool +pub const fn radroots_sdk::transport::BlossomEndpointEvidence::schema_version(&self) -> u16 +pub fn radroots_sdk::transport::BlossomEndpointEvidence::server_error_code(&self) -> core::option::Option<&str> +pub const fn radroots_sdk::transport::BlossomEndpointEvidence::state(&self) -> radroots_sdk::transport::BlossomEvidenceState +pub const fn radroots_sdk::transport::BlossomEndpointEvidence::transport_security(&self) -> radroots_sdk::transport::BlossomTransportSecurity +pub struct radroots_sdk::transport::BlossomError +impl radroots_sdk::transport::BlossomError +pub const fn radroots_sdk::transport::BlossomError::attempts(&self) -> u8 +pub const fn radroots_sdk::transport::BlossomError::code(&self) -> &'static str +pub const fn radroots_sdk::transport::BlossomError::http_status(&self) -> core::option::Option<u16> +pub const fn radroots_sdk::transport::BlossomError::kind(&self) -> radroots_sdk::transport::BlossomErrorKind +pub const fn radroots_sdk::transport::BlossomError::phase(&self) -> radroots_sdk::transport::BlossomPhase +pub const fn radroots_sdk::transport::BlossomError::possible_orphan(&self) -> bool +pub const fn radroots_sdk::transport::BlossomError::retryable(&self) -> bool +pub fn radroots_sdk::transport::BlossomError::server_error_code(&self) -> core::option::Option<&str> +impl core::error::Error for radroots_sdk::transport::BlossomError +impl core::fmt::Debug for radroots_sdk::transport::BlossomError +pub fn radroots_sdk::transport::BlossomError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::fmt::Display for radroots_sdk::transport::BlossomError +pub fn radroots_sdk::transport::BlossomError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub struct radroots_sdk::transport::BlossomImageDimensions +impl radroots_sdk::transport::BlossomImageDimensions +pub const fn radroots_sdk::transport::BlossomImageDimensions::height(self) -> u32 +pub const fn radroots_sdk::transport::BlossomImageDimensions::new(u32, u32) -> core::result::Result<Self, radroots_sdk::transport::BlossomError> +pub const fn radroots_sdk::transport::BlossomImageDimensions::width(self) -> u32 +pub struct radroots_sdk::transport::BlossomInboundReceipt +impl radroots_sdk::transport::BlossomInboundReceipt +pub const fn radroots_sdk::transport::BlossomInboundReceipt::attempts(&self) -> u8 +pub fn radroots_sdk::transport::BlossomInboundReceipt::bytes(&self) -> &[u8] +pub const fn radroots_sdk::transport::BlossomInboundReceipt::commitment(&self) -> &radroots_blossom::descriptor::ByteCommitment +pub const fn radroots_sdk::transport::BlossomInboundReceipt::config_fingerprint(&self) -> radroots_sdk::transport::BlossomConfigFingerprint +pub const fn radroots_sdk::transport::BlossomInboundReceipt::dimensions(&self) -> radroots_sdk::transport::BlossomImageDimensions +pub const fn radroots_sdk::transport::BlossomInboundReceipt::final_url(&self) -> &radroots_blossom::url::BlobUrl +pub const fn radroots_sdk::transport::BlossomInboundReceipt::verified_at_unix_ms(&self) -> u64 +impl core::fmt::Debug for radroots_sdk::transport::BlossomInboundReceipt +pub fn radroots_sdk::transport::BlossomInboundReceipt::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub struct radroots_sdk::transport::BlossomInboundRequest +impl radroots_sdk::transport::BlossomInboundRequest +pub const fn radroots_sdk::transport::BlossomInboundRequest::expected_byte_size(&self) -> core::option::Option<u64> +pub const fn radroots_sdk::transport::BlossomInboundRequest::expected_dimensions(&self) -> core::option::Option<radroots_sdk::transport::BlossomImageDimensions> +pub const fn radroots_sdk::transport::BlossomInboundRequest::expected_media_type(&self) -> core::option::Option<&radroots_blossom::media_type::MediaType> +pub fn radroots_sdk::transport::BlossomInboundRequest::new(radroots_blossom::url::BlobUrl, core::option::Option<radroots_blossom::media_type::MediaType>, core::option::Option<u64>, core::option::Option<radroots_sdk::transport::BlossomImageDimensions>) -> core::result::Result<Self, radroots_sdk::transport::BlossomError> +pub const fn radroots_sdk::transport::BlossomInboundRequest::url(&self) -> &radroots_blossom::url::BlobUrl +pub struct radroots_sdk::transport::BlossomProfile +impl radroots_sdk::transport::BlossomProfile +pub const fn radroots_sdk::transport::BlossomProfile::authority(&self) -> radroots_sdk::transport::BlossomEndpointAuthority +pub fn radroots_sdk::transport::BlossomProfile::fallbacks(&self) -> &[radroots_sdk::transport::BlossomEndpoint] +pub const fn radroots_sdk::transport::BlossomProfile::host_kind(&self) -> radroots_sdk::transport::BlossomHostKind +pub fn radroots_sdk::transport::BlossomProfile::new<I, S>(radroots_sdk::transport::BlossomHostKind, radroots_sdk::transport::BlossomEndpointAuthority, impl core::convert::AsRef<str>, I) -> core::result::Result<Self, radroots_sdk::transport::BlossomError> where I: core::iter::traits::collect::IntoIterator<Item = S>, S: core::convert::AsRef<str> +pub const fn radroots_sdk::transport::BlossomProfile::primary(&self) -> &radroots_sdk::transport::BlossomEndpoint +pub struct radroots_sdk::transport::BlossomSlot +impl radroots_sdk::transport::BlossomSlot +pub fn radroots_sdk::transport::BlossomSlot::authored_upload_claim(&self, &radroots_sdk::transport::BlossomUploadTransaction, radroots_blossom::authorization::AuthorizationContent, u64, u64) -> core::result::Result<radroots_blossom::authorization::AuthoredUploadClaim, radroots_sdk::transport::BlossomError> +pub fn radroots_sdk::transport::BlossomSlot::clear(&self) +pub async fn radroots_sdk::transport::BlossomSlot::complete_native_upload(&self, radroots_sdk::transport::BlossomUploadTransaction, u16, core::option::Option<&str>, core::option::Option<&str>, &[u8], radroots_sdk::transport::BlossomCancellation) -> core::result::Result<radroots_sdk::transport::BlossomUploadReceipt, radroots_sdk::transport::BlossomError> +pub fn radroots_sdk::transport::BlossomSlot::config_fingerprint(&self) -> core::option::Option<radroots_sdk::transport::BlossomConfigFingerprint> +pub fn radroots_sdk::transport::BlossomSlot::configuration(&self) -> core::option::Option<(radroots_sdk::transport::BlossomProfile, radroots_sdk::transport::BlossomConfigFingerprint)> +pub fn radroots_sdk::transport::BlossomSlot::configure(&self, radroots_sdk::transport::BlossomConfig) -> core::result::Result<(), radroots_sdk::transport::BlossomError> +pub fn radroots_sdk::transport::BlossomSlot::endpoint_authority(&self) -> core::option::Option<radroots_sdk::transport::BlossomEndpointAuthority> +pub fn radroots_sdk::transport::BlossomSlot::evidence(&self) -> core::option::Option<radroots_sdk::transport::BlossomEndpointEvidence> +pub fn radroots_sdk::transport::BlossomSlot::host_kind(&self) -> core::option::Option<radroots_sdk::transport::BlossomHostKind> +pub fn radroots_sdk::transport::BlossomSlot::new() -> Self +pub fn radroots_sdk::transport::BlossomSlot::prepare_upload(&self, radroots_sdk::transport::BlossomUploadRequest) -> core::result::Result<radroots_sdk::transport::BlossomUploadTransaction, radroots_sdk::transport::BlossomError> +pub async fn radroots_sdk::transport::BlossomSlot::probe(&self, radroots_sdk::transport::BlossomCancellation) -> core::result::Result<radroots_sdk::transport::BlossomEndpointEvidence, radroots_sdk::transport::BlossomError> +pub fn radroots_sdk::transport::BlossomSlot::profile(&self) -> core::option::Option<radroots_sdk::transport::BlossomProfile> +pub async fn radroots_sdk::transport::BlossomSlot::retrieve(&self, radroots_sdk::transport::BlossomInboundRequest, radroots_sdk::transport::BlossomCancellation) -> core::result::Result<radroots_sdk::transport::BlossomInboundReceipt, radroots_sdk::transport::BlossomError> +pub async fn radroots_sdk::transport::BlossomSlot::upload(&self, radroots_sdk::transport::BlossomUploadTransaction, radroots_nostr::blossom::AuthorizationHeader, radroots_sdk::transport::BlossomCancellation) -> core::result::Result<radroots_sdk::transport::BlossomUploadReceipt, radroots_sdk::transport::BlossomError> +impl core::fmt::Debug for radroots_sdk::transport::BlossomSlot +pub fn radroots_sdk::transport::BlossomSlot::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub struct radroots_sdk::transport::BlossomUploadReceipt +impl radroots_sdk::transport::BlossomUploadReceipt +pub const fn radroots_sdk::transport::BlossomUploadReceipt::attempts(&self) -> u8 +pub const fn radroots_sdk::transport::BlossomUploadReceipt::descriptor(&self) -> &radroots_blossom::descriptor::ByteVerifiedDescriptor +pub const fn radroots_sdk::transport::BlossomUploadReceipt::dimensions(&self) -> radroots_sdk::transport::BlossomImageDimensions +pub fn radroots_sdk::transport::BlossomUploadReceipt::into_descriptor(self) -> radroots_blossom::descriptor::ByteVerifiedDescriptor +pub const fn radroots_sdk::transport::BlossomUploadReceipt::verified_at_unix_ms(&self) -> u64 +pub struct radroots_sdk::transport::BlossomUploadRequest +impl radroots_sdk::transport::BlossomUploadRequest +pub fn radroots_sdk::transport::BlossomUploadRequest::byte_size(&self) -> u64 +pub const fn radroots_sdk::transport::BlossomUploadRequest::dimensions(&self) -> radroots_sdk::transport::BlossomImageDimensions +pub fn radroots_sdk::transport::BlossomUploadRequest::media_type(&self) -> &radroots_blossom::media_type::MediaType +pub fn radroots_sdk::transport::BlossomUploadRequest::new(alloc::sync::Arc<[u8]>, radroots_blossom::media_type::MediaType, radroots_sdk::transport::BlossomImageDimensions, u64) -> core::result::Result<Self, radroots_sdk::transport::BlossomError> +pub const fn radroots_sdk::transport::BlossomUploadRequest::sha256(&self) -> radroots_blossom::hash::Sha256 +impl core::fmt::Debug for radroots_sdk::transport::BlossomUploadRequest +pub fn radroots_sdk::transport::BlossomUploadRequest::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub struct radroots_sdk::transport::BlossomUploadTransaction +impl radroots_sdk::transport::BlossomUploadTransaction +pub const fn radroots_sdk::transport::BlossomUploadTransaction::config_fingerprint(&self) -> radroots_sdk::transport::BlossomConfigFingerprint +pub const fn radroots_sdk::transport::BlossomUploadTransaction::expected_url(&self) -> &radroots_blossom::url::BlobUrl +pub const fn radroots_sdk::transport::BlossomUploadTransaction::request(&self) -> &radroots_sdk::transport::BlossomUploadRequest +impl core::fmt::Debug for radroots_sdk::transport::BlossomUploadTransaction +pub fn radroots_sdk::transport::BlossomUploadTransaction::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result pub struct radroots_sdk::transport::DaemonConfig impl radroots_sdk::transport::DaemonConfig pub fn radroots_sdk::transport::DaemonConfig::new(impl core::convert::Into<alloc::string::String>) -> Self @@ -488,8 +674,10 @@ impl core::default::Default for radroots_sdk::transport::Profile pub fn radroots_sdk::transport::Profile::default() -> Self pub struct radroots_sdk::Client impl radroots_sdk::client::Client +pub fn radroots_sdk::client::Client::blossom(&self) -> radroots_sdk::error::Result<core::option::Option<&radroots_sdk::transport::BlossomSlot>> pub fn radroots_sdk::client::Client::capabilities(&self) -> radroots_sdk::capability::CapabilityReport pub async fn radroots_sdk::client::Client::close(&self) -> radroots_sdk::error::Result<()> +pub fn radroots_sdk::client::Client::configure_blossom(&self, radroots_sdk::transport::BlossomConfig) -> radroots_sdk::error::Result<()> pub fn radroots_sdk::client::Client::configure_nostr(&self, radroots_transport_nostr::profile::RelayProfile) -> radroots_sdk::error::Result<()> pub fn radroots_sdk::client::Client::farm(&self) -> radroots_sdk::error::Result<core::option::Option<radroots_sdk::farm::Operations<'_>>> pub fn radroots_sdk::client::Client::is_closed(&self) -> bool @@ -509,6 +697,7 @@ impl core::fmt::Debug for radroots_sdk::client::Client pub fn radroots_sdk::client::Client::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result pub struct radroots_sdk::ClientBuilder impl radroots_sdk::client::ClientBuilder +pub fn radroots_sdk::client::ClientBuilder::blossom(self, radroots_sdk::transport::BlossomSlot) -> Self pub fn radroots_sdk::client::ClientBuilder::build(self) -> radroots_sdk::error::Result<radroots_sdk::client::Client> pub fn radroots_sdk::client::ClientBuilder::capability_availability(self, radroots_sdk::capability::CapabilityId, radroots_sdk::capability::Availability) -> Self pub fn radroots_sdk::client::ClientBuilder::host_sync(self, radroots_sdk::sync::HostPolicy) -> Self diff --git a/contracts/api_baselines/radroots_transport_nostr.txt b/contracts/api_baselines/radroots_transport_nostr.txt @@ -30,6 +30,7 @@ pub radroots_transport_nostr::Error::InvalidTimeout::value_ms: u64 pub radroots_transport_nostr::Error::RelayDestinationDenied pub radroots_transport_nostr::Error::RelayDestinationDenied::reason: &'static str pub radroots_transport_nostr::Error::RelayDestinationDenied::url: alloc::string::String +pub radroots_transport_nostr::Error::RelayProfilePolicyMismatch pub radroots_transport_nostr::Error::RelaySchemeDenied pub radroots_transport_nostr::Error::RelaySchemeDenied::url: alloc::string::String pub radroots_transport_nostr::Error::ResolvedAddressDenied @@ -129,15 +130,14 @@ pub fn radroots_transport_nostr::RelayCursor::precedes(&self, u64, &str) -> bool pub struct radroots_transport_nostr::RelayEndpoint impl radroots_transport_nostr::RelayEndpoint pub const fn radroots_transport_nostr::RelayEndpoint::access(&self) -> radroots_transport_nostr::RelayAccess +pub fn radroots_transport_nostr::RelayEndpoint::new(impl core::convert::AsRef<str>, radroots_transport_nostr::RelayUrlPolicy, radroots_transport_nostr::RelayAccess) -> core::result::Result<Self, radroots_transport_nostr::Error> pub const fn radroots_transport_nostr::RelayEndpoint::policy(&self) -> radroots_transport_nostr::RelayUrlPolicy pub const fn radroots_transport_nostr::RelayEndpoint::url(&self) -> &radroots_transport_nostr::RelayUrl pub struct radroots_transport_nostr::RelayProfile impl radroots_transport_nostr::RelayProfile -pub fn radroots_transport_nostr::RelayProfile::device<I, S>(I) -> core::result::Result<Self, radroots_transport_nostr::Error> where I: core::iter::traits::collect::IntoIterator<Item = S>, S: core::convert::AsRef<str> pub fn radroots_transport_nostr::RelayProfile::endpoints(&self) -> &[radroots_transport_nostr::RelayEndpoint] +pub fn radroots_transport_nostr::RelayProfile::explicit<I>(radroots_transport_nostr::RelayProfileKind, I) -> core::result::Result<Self, radroots_transport_nostr::Error> where I: core::iter::traits::collect::IntoIterator<Item = radroots_transport_nostr::RelayEndpoint> pub const fn radroots_transport_nostr::RelayProfile::kind(&self) -> radroots_transport_nostr::RelayProfileKind -pub fn radroots_transport_nostr::RelayProfile::public<I, S>(I) -> core::result::Result<Self, radroots_transport_nostr::Error> where I: core::iter::traits::collect::IntoIterator<Item = S>, S: core::convert::AsRef<str> -pub fn radroots_transport_nostr::RelayProfile::simulator<I, S>(I) -> core::result::Result<Self, radroots_transport_nostr::Error> where I: core::iter::traits::collect::IntoIterator<Item = S>, S: core::convert::AsRef<str> pub struct radroots_transport_nostr::RelayStatus impl radroots_transport_nostr::RelayStatus pub const fn radroots_transport_nostr::RelayStatus::endpoint(&self) -> &radroots_transport_nostr::RelayEndpoint @@ -159,4 +159,3 @@ pub fn radroots_transport_nostr::RelayUrl::to_target(&self) -> core::result::Res pub fn radroots_transport_nostr::RelayUrl::validate_resolved_addresses(&self, radroots_transport_nostr::RelayUrlPolicy, impl core::iter::traits::collect::IntoIterator<Item = core::net::ip_addr::IpAddr>) -> core::result::Result<(), radroots_transport_nostr::Error> impl core::fmt::Display for radroots_transport_nostr::RelayUrl pub fn radroots_transport_nostr::RelayUrl::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result -pub const radroots_transport_nostr::DEFAULT_PUBLIC_RELAY: &str diff --git a/crates/mobile_core/src/runtime/builder.rs b/crates/mobile_core/src/runtime/builder.rs @@ -20,8 +20,16 @@ impl RuntimeBuilder { #[cfg(feature = "mobile-social")] signer: None, #[cfg(feature = "mobile-social")] - relay_profile: radroots_sdk::transport::RelayProfile::public(Vec::<String>::new()) - .expect("bundled public relay profile is valid"), + relay_profile: radroots_sdk::transport::RelayProfile::explicit( + radroots_sdk::transport::RelayProfileKind::Public, + [radroots_sdk::transport::RelayEndpoint::new( + "wss://radroots.org", + radroots_sdk::transport::RelayUrlPolicy::Public, + radroots_sdk::transport::RelayAccess::ReadWrite, + ) + .expect("bundled public relay endpoint is valid")], + ) + .expect("bundled public relay profile is valid"), #[cfg(feature = "mobile-social")] blossom_config: None, } diff --git a/crates/mobile_core/src/runtime/product_surface/outbox.rs b/crates/mobile_core/src/runtime/product_surface/outbox.rs @@ -3420,14 +3420,18 @@ mod tests { let profile = radroots_sdk::transport::RelayProfile::explicit( radroots_sdk::transport::RelayProfileKind::Public, [ - ( + radroots_sdk::transport::RelayEndpoint::new( "wss://read.example", + radroots_sdk::transport::RelayUrlPolicy::Public, radroots_sdk::transport::RelayAccess::ReadOnly, - ), - ( + ) + .unwrap(), + radroots_sdk::transport::RelayEndpoint::new( "wss://write.example", + radroots_sdk::transport::RelayUrlPolicy::Public, radroots_sdk::transport::RelayAccess::ReadWrite, - ), + ) + .unwrap(), ], ) .unwrap(); @@ -3475,10 +3479,12 @@ mod tests { async fn profile_metadata_uses_the_durable_outbox_with_stable_operation_identity() { let profile = radroots_sdk::transport::RelayProfile::explicit( radroots_sdk::transport::RelayProfileKind::Public, - [( + [radroots_sdk::transport::RelayEndpoint::new( "wss://write.example", + radroots_sdk::transport::RelayUrlPolicy::Public, radroots_sdk::transport::RelayAccess::ReadWrite, - )], + ) + .unwrap()], ) .unwrap(); let runtime = profiled_runtime(profile); @@ -3531,10 +3537,12 @@ mod tests { async fn add_queue_intent_fails_closed_without_a_writable_relay() { let profile = radroots_sdk::transport::RelayProfile::explicit( radroots_sdk::transport::RelayProfileKind::Public, - [( + [radroots_sdk::transport::RelayEndpoint::new( "wss://read.example", + radroots_sdk::transport::RelayUrlPolicy::Public, radroots_sdk::transport::RelayAccess::ReadOnly, - )], + ) + .unwrap()], ) .unwrap(); let runtime = profiled_runtime(profile); @@ -3733,10 +3741,12 @@ mod tests { async fn kind_one_revision_never_creates_retraction_before_replacement_acceptance() { let profile = radroots_sdk::transport::RelayProfile::explicit( radroots_sdk::transport::RelayProfileKind::Public, - [( + [radroots_sdk::transport::RelayEndpoint::new( "wss://offline.example", + radroots_sdk::transport::RelayUrlPolicy::Public, radroots_sdk::transport::RelayAccess::ReadWrite, - )], + ) + .unwrap()], ) .unwrap(); let signer = radroots_nostr::signing::LocalSigner::new( diff --git a/crates/mobile_core/src/runtime/product_surface/settings.rs b/crates/mobile_core/src/runtime/product_surface/settings.rs @@ -15,7 +15,7 @@ use sha2::{Digest, Sha256}; use super::super::RadrootsRuntime; -pub use radroots_sdk::transport::DEFAULT_PUBLIC_RELAY; +pub const DEFAULT_PUBLIC_RELAY: &str = "wss://radroots.org"; pub const MOBILE_SETTINGS_SCHEMA_VERSION: u16 = 1; pub const DEFAULT_PUBLIC_BLOSSOM_ORIGIN: &str = "https://blossom.radroots.org"; @@ -437,9 +437,17 @@ impl RelayPreferences { radroots_sdk::transport::RelayProfileKind::Device } }; - radroots_sdk::transport::RelayProfile::explicit( - kind, - self.endpoints.iter().map(|endpoint| { + let policy = match self.environment { + MobileNetworkEnvironment::Public => radroots_sdk::transport::RelayUrlPolicy::Public, + MobileNetworkEnvironment::Simulator => radroots_sdk::transport::RelayUrlPolicy::Local, + MobileNetworkEnvironment::PhysicalDevice => { + radroots_sdk::transport::RelayUrlPolicy::PrivateNetwork + } + }; + let endpoints = self + .endpoints + .iter() + .map(|endpoint| { let access = match endpoint.access { RelayAccessPreference::ReadOnly => { radroots_sdk::transport::RelayAccess::ReadOnly @@ -448,10 +456,12 @@ impl RelayPreferences { radroots_sdk::transport::RelayAccess::ReadWrite } }; - (endpoint.url.as_str(), access) - }), - ) - .map_err(|_| SettingsError::InvalidRelayEndpoint) + radroots_sdk::transport::RelayEndpoint::new(endpoint.url.as_str(), policy, access) + }) + .collect::<Result<Vec<_>, _>>() + .map_err(|_| SettingsError::InvalidRelayEndpoint)?; + radroots_sdk::transport::RelayProfile::explicit(kind, endpoints) + .map_err(|_| SettingsError::InvalidRelayEndpoint) } } diff --git a/crates/mobile_core/src/runtime/sdk.rs b/crates/mobile_core/src/runtime/sdk.rs @@ -117,9 +117,10 @@ impl RadrootsRuntime { &self, writable_relays: Vec<String>, ) -> Result<(), RadrootsAppError> { - self.configure_relay_profile( - radroots_sdk::transport::RelayProfile::public(writable_relays) - .map_err(|error| RadrootsAppError::runtime(error.to_string()))?, + self.configure_relay_endpoints( + radroots_sdk::transport::RelayProfileKind::Public, + radroots_sdk::transport::RelayUrlPolicy::Public, + writable_relays, ) } @@ -129,9 +130,10 @@ impl RadrootsRuntime { &self, loopback_relays: Vec<String>, ) -> Result<(), RadrootsAppError> { - self.configure_relay_profile( - radroots_sdk::transport::RelayProfile::simulator(loopback_relays) - .map_err(|error| RadrootsAppError::runtime(error.to_string()))?, + self.configure_relay_endpoints( + radroots_sdk::transport::RelayProfileKind::Simulator, + radroots_sdk::transport::RelayUrlPolicy::Local, + loopback_relays, ) } @@ -141,13 +143,37 @@ impl RadrootsRuntime { &self, writable_relays: Vec<String>, ) -> Result<(), RadrootsAppError> { - self.configure_relay_profile( - radroots_sdk::transport::RelayProfile::device(writable_relays) - .map_err(|error| RadrootsAppError::runtime(error.to_string()))?, + self.configure_relay_endpoints( + radroots_sdk::transport::RelayProfileKind::Device, + radroots_sdk::transport::RelayUrlPolicy::PrivateNetwork, + writable_relays, ) } #[cfg(feature = "mobile-social")] + fn configure_relay_endpoints( + &self, + kind: radroots_sdk::transport::RelayProfileKind, + policy: radroots_sdk::transport::RelayUrlPolicy, + relays: Vec<String>, + ) -> Result<(), RadrootsAppError> { + let endpoints = relays + .into_iter() + .map(|relay| { + radroots_sdk::transport::RelayEndpoint::new( + relay, + policy, + radroots_sdk::transport::RelayAccess::ReadWrite, + ) + }) + .collect::<Result<Vec<_>, _>>() + .map_err(|error| RadrootsAppError::runtime(error.to_string()))?; + let profile = radroots_sdk::transport::RelayProfile::explicit(kind, endpoints) + .map_err(|error| RadrootsAppError::runtime(error.to_string()))?; + self.configure_relay_profile(profile) + } + + #[cfg(feature = "mobile-social")] fn configure_relay_profile( &self, profile: radroots_sdk::transport::RelayProfile, diff --git a/crates/sdk/src/client.rs b/crates/sdk/src/client.rs @@ -695,10 +695,12 @@ mod tests { slot.configure( crate::transport::RelayProfile::explicit( crate::transport::RelayProfileKind::Public, - [( - radroots_transport_nostr::DEFAULT_PUBLIC_RELAY, + [crate::transport::RelayEndpoint::new( + "wss://radroots.org", + crate::transport::RelayUrlPolicy::Public, crate::transport::RelayAccess::ReadOnly, - )], + ) + .expect("read-only endpoint")], ) .expect("read-only public profile"), ) @@ -722,8 +724,16 @@ mod tests { client .configure_nostr( - crate::transport::RelayProfile::public(["wss://write.example"]) - .expect("writable profile"), + crate::transport::RelayProfile::explicit( + crate::transport::RelayProfileKind::Public, + [crate::transport::RelayEndpoint::new( + "wss://write.example", + crate::transport::RelayUrlPolicy::Public, + crate::transport::RelayAccess::ReadWrite, + ) + .expect("writable endpoint")], + ) + .expect("writable profile"), ) .expect("reconfigure"); let capabilities = client.capabilities(); diff --git a/crates/sdk/src/transport.rs b/crates/sdk/src/transport.rs @@ -28,9 +28,9 @@ use radroots_blossom::{ #[cfg(feature = "nostr")] pub use radroots_transport_nostr::{ - DEFAULT_PUBLIC_RELAY, ReconnectBackoff, RelayAccess, RelayAggregateState, - RelayCapabilityEvidence, RelayCursor, RelayEndpoint, RelayEvidenceState, RelayProfile, - RelayProfileKind, RelayStatus, RelayStatusReport, RelayUrl, RelayUrlPolicy, + ReconnectBackoff, RelayAccess, RelayAggregateState, RelayCapabilityEvidence, RelayCursor, + RelayEndpoint, RelayEvidenceState, RelayProfile, RelayProfileKind, RelayStatus, + RelayStatusReport, RelayUrl, RelayUrlPolicy, }; const PREVIEW_UNAVAILABLE_MESSAGE: &str = "preview transport is unavailable in this SDK release"; @@ -3223,8 +3223,19 @@ mod tests { assert!(slot.read_targets().is_none()); assert!(slot.relay_status().is_none()); assert!( - slot.configure(RelayProfile::simulator(["ws://127.0.0.1:7447"]).expect("profile")) - .is_ok() + slot.configure( + RelayProfile::explicit( + RelayProfileKind::Simulator, + [RelayEndpoint::new( + "ws://127.0.0.1:7447", + RelayUrlPolicy::Local, + RelayAccess::ReadWrite, + ) + .expect("endpoint")], + ) + .expect("profile"), + ) + .is_ok() ); let original = slot.read_targets().expect("configured targets"); assert_eq!(slot.write_targets(), Some(original.clone())); @@ -3251,8 +3262,19 @@ mod tests { slot.clear(); assert!(slot.read_targets().is_none()); assert!( - slot.configure(RelayProfile::simulator(["ws://127.0.0.1:7447"]).expect("profile")) - .is_err() + slot.configure( + RelayProfile::explicit( + RelayProfileKind::Simulator, + [RelayEndpoint::new( + "ws://127.0.0.1:7447", + RelayUrlPolicy::Local, + RelayAccess::ReadWrite, + ) + .expect("endpoint")], + ) + .expect("profile"), + ) + .is_err() ); } diff --git a/crates/transport_nostr/README.md b/crates/transport_nostr/README.md @@ -24,9 +24,17 @@ Configuration is explicit, validated, and inert. Constructing ```rust use radroots_transport::{EventSink, EventSource}; -use radroots_transport_nostr::{Config, NostrTransport, RelayProfile}; - -let profile = RelayProfile::public(["wss://relay.example.com"])?; +use radroots_transport_nostr::{ + Config, NostrTransport, RelayAccess, RelayEndpoint, RelayProfile, + RelayProfileKind, RelayUrlPolicy, +}; + +let endpoint = RelayEndpoint::new( + "wss://relay.example.com", + RelayUrlPolicy::Public, + RelayAccess::ReadWrite, +)?; +let profile = RelayProfile::explicit(RelayProfileKind::Public, [endpoint])?; let config = Config::from_profile(profile).with_timeouts(5_000, 20_000, 2_000)?; let transport = NostrTransport::new(config); @@ -66,10 +74,12 @@ relay pool, Tokio handle, signer, storage handle, or retry worker. ## Relay and network security -The public profile always includes `wss://radroots.org` as read-only and -requires separately configured public TLS relays for publication. The -simulator profile admits exact loopback WebSockets only. The device profile -requires explicit TLS endpoints and never reuses simulator loopback. +Profiles never inject a relay or infer destination policy. The caller supplies +every endpoint together with its public-Internet, exact-loopback, or trusted +private-network policy and independent read-only/read-write authority. Public +profiles admit only public endpoints, simulator profiles admit only exact +loopback endpoints, and physical-device profiles admit public or explicitly +trusted private-network TLS endpoints. `RelayUrlPolicy::Public` accepts TLS WebSocket URLs with public hostnames or global addresses. `Local` accepts exact loopback destinations and permits diff --git a/crates/transport_nostr/examples/configure_transport.rs b/crates/transport_nostr/examples/configure_transport.rs @@ -1,9 +1,17 @@ use radroots_transport::{EventSink, EventSource}; -use radroots_transport_nostr::{Config, NostrTransport, RelayProfile}; +use radroots_transport_nostr::{ + Config, NostrTransport, RelayAccess, RelayEndpoint, RelayProfile, RelayProfileKind, + RelayUrlPolicy, +}; fn main() -> Result<(), Box<dyn std::error::Error>> { - let config = Config::from_profile(RelayProfile::public(["wss://relay.example.com"])?) - .with_timeouts(5_000, 20_000, 2_000)?; + let endpoint = RelayEndpoint::new( + "wss://relay.example.com", + RelayUrlPolicy::Public, + RelayAccess::ReadWrite, + )?; + let profile = RelayProfile::explicit(RelayProfileKind::Public, [endpoint])?; + let config = Config::from_profile(profile).with_timeouts(5_000, 20_000, 2_000)?; let transport = NostrTransport::new(config); let source: &dyn EventSource = &transport; diff --git a/crates/transport_nostr/src/auth.rs b/crates/transport_nostr/src/auth.rs @@ -265,7 +265,7 @@ fn has_exact_tag(tags: &[Vec<String>], name: &str, value: &str) -> bool { #[cfg(test)] mod tests { use super::*; - use crate::{Config, RelayProfile, RelayUrlPolicy}; + use crate::{Config, RelayUrlPolicy}; use nostr_sdk::prelude::{ EventBuilder, JsonUtil, Keys, RelayUrl as UpstreamRelayUrl, Timestamp, }; @@ -288,7 +288,14 @@ mod tests { fn transport() -> (NostrTransport, Arc<MockAuthClient>, RelayUrl) { let relay = RelayUrl::parse("wss://relay.example.com", RelayUrlPolicy::Public).expect("relay"); - let config = Config::from_profile(RelayProfile::public([relay.as_str()]).expect("profile")); + let config = Config::from_profile( + crate::profile::test_profile( + crate::RelayProfileKind::Public, + RelayUrlPolicy::Public, + [relay.as_str()], + ) + .expect("profile"), + ); let client = Arc::new(MockAuthClient(AtomicUsize::new(0))); let transport = NostrTransport::new(config).with_auth_client(client.clone()); (transport, client, relay) diff --git a/crates/transport_nostr/src/client.rs b/crates/transport_nostr/src/client.rs @@ -295,19 +295,42 @@ mod tests { #[test] fn config_rejects_empty_duplicate_and_excessive_relay_sets() { - assert!(RelayProfile::simulator(Vec::<String>::new()).is_err()); assert!( - RelayProfile::public(["wss://relay.example.com", "wss://RELAY.EXAMPLE.COM:443/",]) - .is_err() + crate::profile::test_profile( + RelayProfileKind::Simulator, + crate::RelayUrlPolicy::Local, + Vec::<String>::new(), + ) + .is_err() + ); + assert!( + crate::profile::test_profile( + RelayProfileKind::Public, + crate::RelayUrlPolicy::Public, + ["wss://relay.example.com", "wss://RELAY.EXAMPLE.COM:443/"], + ) + .is_err() + ); + let relays = (0..=MAX_RELAYS).map(|index| format!("wss://r{index}.example.com")); + assert!( + crate::profile::test_profile( + RelayProfileKind::Public, + crate::RelayUrlPolicy::Public, + relays, + ) + .is_err() ); - let relays = (0..MAX_RELAYS).map(|index| format!("wss://r{index}.example.com")); - assert!(RelayProfile::public(relays).is_err()); } #[test] fn config_rejects_unbounded_limits() { let config = Config::from_profile( - RelayProfile::public(["wss://relay.example.com"]).expect("profile"), + crate::profile::test_profile( + RelayProfileKind::Public, + crate::RelayUrlPolicy::Public, + ["wss://relay.example.com"], + ) + .expect("profile"), ); assert!(config.clone().with_timeouts(0, 1, 1).is_err()); assert!(config.clone().with_timeouts(1, 120_001, 1).is_err()); @@ -317,15 +340,20 @@ mod tests { #[test] fn valid_configuration_accessors_and_transport_debug_are_complete() { let config = Config::from_profile( - RelayProfile::public(["wss://one.example", "wss://two.example"]).expect("profile"), + crate::profile::test_profile( + RelayProfileKind::Public, + crate::RelayUrlPolicy::Public, + ["wss://one.example", "wss://two.example"], + ) + .expect("profile"), ) .with_timeouts(1, 2, 3) .expect("timeouts") .with_max_connections(2) .expect("connections"); - assert_eq!(config.relays().len(), 3); - assert_eq!(config.read_relays().count(), 3); - assert_eq!(config.write_relays().count(), 3); + assert_eq!(config.relays().len(), 2); + assert_eq!(config.read_relays().count(), 2); + assert_eq!(config.write_relays().count(), 2); assert_eq!(config.profile_kind(), RelayProfileKind::Public); assert_eq!(config.connect_timeout_ms(), 1); assert_eq!(config.request_timeout_ms(), 2); diff --git a/crates/transport_nostr/src/error.rs b/crates/transport_nostr/src/error.rs @@ -22,6 +22,8 @@ pub enum Error { EmptyResolution { url: String }, /// A resolved address violates the selected policy. ResolvedAddressDenied { url: String, address: String }, + /// An endpoint policy is incompatible with its host profile kind. + RelayProfilePolicyMismatch, /// A connection or request timeout is outside its governed bounds. InvalidTimeout { field: &'static str, value_ms: u64 }, /// The per-operation connection limit is outside its governed bounds. @@ -86,6 +88,9 @@ impl fmt::Display for Error { formatter, "relay URL `{url}` resolved to denied address `{address}`" ), + Self::RelayProfilePolicyMismatch => { + formatter.write_str("relay endpoint policy does not match its profile kind") + } Self::InvalidTimeout { field, value_ms } => { write!(formatter, "invalid {field} timeout: {value_ms}ms") } @@ -163,6 +168,7 @@ mod tests { url: "wss://relay.example".into(), address: "127.0.0.1".into(), }, + Error::RelayProfilePolicyMismatch, Error::InvalidTimeout { field: "request", value_ms: 0, diff --git a/crates/transport_nostr/src/lib.rs b/crates/transport_nostr/src/lib.rs @@ -15,9 +15,7 @@ mod status; pub use client::{Config, NostrTransport, ReconnectBackoff}; pub use cursor::RelayCursor; pub use error::Error; -pub use profile::{ - DEFAULT_PUBLIC_RELAY, RelayAccess, RelayEndpoint, RelayProfile, RelayProfileKind, -}; +pub use profile::{RelayAccess, RelayEndpoint, RelayProfile, RelayProfileKind}; pub use relay::{RelayUrl, RelayUrlPolicy}; pub use status::{ RelayAggregateState, RelayCapabilityEvidence, RelayEvidenceState, RelayStatus, diff --git a/crates/transport_nostr/src/profile.rs b/crates/transport_nostr/src/profile.rs @@ -3,9 +3,6 @@ use crate::{Error, RelayUrl, RelayUrlPolicy}; use std::collections::BTreeSet; -/// Bundled canonical Radroots relay used for reads and publication. -pub const DEFAULT_PUBLIC_RELAY: &str = "wss://radroots.org"; - /// Directional access authorized for one configured relay. #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] #[non_exhaustive] @@ -34,7 +31,7 @@ impl RelayAccess { #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] #[non_exhaustive] pub enum RelayProfileKind { - /// Public-Internet profile with the bundled canonical relay. + /// Public-Internet profile. Public, /// Development-only profile restricted to exact loopback destinations. Simulator, @@ -51,7 +48,8 @@ pub struct RelayEndpoint { } impl RelayEndpoint { - fn new( + /// Parses one endpoint with explicit destination and access policy. + pub fn new( value: impl AsRef<str>, policy: RelayUrlPolicy, access: RelayAccess, @@ -94,86 +92,17 @@ impl RelayProfile { /// /// This constructor does not inject a bundled endpoint. Callers provide the /// complete validated set they intend to use. - pub fn explicit<I, S>(kind: RelayProfileKind, endpoints: I) -> Result<Self, Error> + pub fn explicit<I>(kind: RelayProfileKind, endpoints: I) -> Result<Self, Error> where - I: IntoIterator<Item = (S, RelayAccess)>, - S: AsRef<str>, + I: IntoIterator<Item = RelayEndpoint>, { - let policy = match kind { - RelayProfileKind::Public => RelayUrlPolicy::Public, - RelayProfileKind::Simulator => RelayUrlPolicy::Local, - RelayProfileKind::Device => RelayUrlPolicy::PrivateNetwork, - }; let endpoints = endpoints .into_iter() - .map(|(url, access)| RelayEndpoint::new(url, policy, access)) - .collect::<Result<Vec<_>, _>>()?; + .take(crate::client::MAX_RELAYS + 1) + .collect::<Vec<_>>(); Self::validated(kind, endpoints) } - /// Builds the ordinary public profile. - /// - /// `wss://radroots.org/` is always present as read-write. Every additional - /// writable relay must be a TLS public-Internet destination. Supplying the - /// bundled relay again is rejected as a duplicate. - pub fn public<I, S>(writable_relays: I) -> Result<Self, Error> - where - I: IntoIterator<Item = S>, - S: AsRef<str>, - { - let mut endpoints = vec![RelayEndpoint::new( - DEFAULT_PUBLIC_RELAY, - RelayUrlPolicy::Public, - RelayAccess::ReadWrite, - )?]; - endpoints.extend(parse_endpoints( - writable_relays, - RelayUrlPolicy::Public, - RelayAccess::ReadWrite, - )?); - Self::validated(RelayProfileKind::Public, endpoints) - } - - /// Builds a development-only profile from exact loopback relays. - /// - /// Plaintext `ws://` is accepted only by this profile. At least one relay - /// is required and every endpoint is writable. - pub fn simulator<I, S>(loopback_relays: I) -> Result<Self, Error> - where - I: IntoIterator<Item = S>, - S: AsRef<str>, - { - let endpoints = parse_endpoints( - loopback_relays, - RelayUrlPolicy::Local, - RelayAccess::ReadWrite, - )?; - Self::validated(RelayProfileKind::Simulator, endpoints) - } - - /// Builds a physical-device profile from explicit writable TLS endpoints. - /// - /// The bundled canonical relay remains read-write. Additional endpoints may - /// resolve to public or private addresses, but loopback, unspecified, and - /// multicast destinations remain forbidden before and after resolution. - pub fn device<I, S>(writable_relays: I) -> Result<Self, Error> - where - I: IntoIterator<Item = S>, - S: AsRef<str>, - { - let mut endpoints = vec![RelayEndpoint::new( - DEFAULT_PUBLIC_RELAY, - RelayUrlPolicy::Public, - RelayAccess::ReadWrite, - )?]; - endpoints.extend(parse_endpoints( - writable_relays, - RelayUrlPolicy::PrivateNetwork, - RelayAccess::ReadWrite, - )?); - Self::validated(RelayProfileKind::Device, endpoints) - } - fn validated(kind: RelayProfileKind, endpoints: Vec<RelayEndpoint>) -> Result<Self, Error> { if endpoints.is_empty() { return Err(Error::EmptyRelaySet); @@ -186,6 +115,17 @@ impl RelayProfile { } let mut seen = BTreeSet::new(); for endpoint in &endpoints { + let policy_allowed = match kind { + RelayProfileKind::Public => endpoint.policy == RelayUrlPolicy::Public, + RelayProfileKind::Simulator => endpoint.policy == RelayUrlPolicy::Local, + RelayProfileKind::Device => matches!( + endpoint.policy, + RelayUrlPolicy::Public | RelayUrlPolicy::PrivateNetwork + ), + }; + if !policy_allowed { + return Err(Error::RelayProfilePolicyMismatch); + } if !seen.insert(endpoint.url.clone()) { return Err(Error::DuplicateRelayUrl { url: endpoint.url.to_string(), @@ -208,19 +148,21 @@ impl RelayProfile { } } -fn parse_endpoints<I, S>( - values: I, +#[cfg(test)] +pub(crate) fn test_profile<I, S>( + kind: RelayProfileKind, policy: RelayUrlPolicy, - access: RelayAccess, -) -> Result<Vec<RelayEndpoint>, Error> + values: I, +) -> Result<RelayProfile, Error> where I: IntoIterator<Item = S>, S: AsRef<str>, { - values + let endpoints = values .into_iter() - .map(|value| RelayEndpoint::new(value, policy, access)) - .collect() + .map(|value| RelayEndpoint::new(value, policy, RelayAccess::ReadWrite)) + .collect::<Result<Vec<_>, _>>()?; + RelayProfile::explicit(kind, endpoints) } #[cfg(test)] @@ -228,15 +170,21 @@ mod tests { use super::*; #[test] - fn public_profile_authorizes_the_canonical_relay_for_publication() { - let profile = RelayProfile::public(["wss://write.example"]).expect("public profile"); + fn explicit_profile_requires_every_public_destination_and_authority() { + let profile = RelayProfile::explicit( + RelayProfileKind::Public, + [RelayEndpoint::new( + "wss://write.example", + RelayUrlPolicy::Public, + RelayAccess::ReadWrite, + ) + .expect("endpoint")], + ) + .expect("public profile"); assert_eq!(profile.kind(), RelayProfileKind::Public); - assert_eq!(profile.endpoints().len(), 2); - assert_eq!(profile.endpoints()[0].url().as_str(), DEFAULT_PUBLIC_RELAY); + assert_eq!(profile.endpoints().len(), 1); + assert_eq!(profile.endpoints()[0].url().as_str(), "wss://write.example"); assert_eq!(profile.endpoints()[0].access(), RelayAccess::ReadWrite); - assert_eq!(profile.endpoints()[1].access(), RelayAccess::ReadWrite); - assert!(RelayProfile::public([DEFAULT_PUBLIC_RELAY]).is_err()); - assert!(RelayProfile::public(["ws://public.example"]).is_err()); } #[test] @@ -244,8 +192,18 @@ mod tests { let profile = RelayProfile::explicit( RelayProfileKind::Public, [ - ("wss://read.example", RelayAccess::ReadOnly), - ("wss://write.example", RelayAccess::ReadWrite), + RelayEndpoint::new( + "wss://read.example", + RelayUrlPolicy::Public, + RelayAccess::ReadOnly, + ) + .expect("read endpoint"), + RelayEndpoint::new( + "wss://write.example", + RelayUrlPolicy::Public, + RelayAccess::ReadWrite, + ) + .expect("write endpoint"), ], ) .expect("explicit profile"); @@ -258,24 +216,67 @@ mod tests { } #[test] - fn loopback_is_confined_to_the_simulator_profile() { - assert!(RelayProfile::simulator(["ws://127.0.0.1:7447"]).is_ok()); - assert!(RelayProfile::simulator(["wss://localhost:7447"]).is_ok()); - assert!(RelayProfile::simulator(Vec::<String>::new()).is_err()); - assert!(RelayProfile::public(["ws://127.0.0.1:7447"]).is_err()); - assert!(RelayProfile::device(["wss://127.0.0.1:7447"]).is_err()); + fn profile_kind_rejects_mismatched_explicit_destination_policy() { + let public = RelayEndpoint::new( + "wss://relay.example", + RelayUrlPolicy::Public, + RelayAccess::ReadOnly, + ) + .expect("public endpoint"); + let local = RelayEndpoint::new( + "ws://127.0.0.1:7447", + RelayUrlPolicy::Local, + RelayAccess::ReadWrite, + ) + .expect("local endpoint"); + let private = RelayEndpoint::new( + "wss://10.0.0.5:7447", + RelayUrlPolicy::PrivateNetwork, + RelayAccess::ReadWrite, + ) + .expect("private endpoint"); + + assert!(RelayProfile::explicit(RelayProfileKind::Public, [public.clone()]).is_ok()); + assert!(RelayProfile::explicit(RelayProfileKind::Simulator, [local.clone()]).is_ok()); + assert!(RelayProfile::explicit(RelayProfileKind::Device, [public]).is_ok()); + assert!(RelayProfile::explicit(RelayProfileKind::Device, [private]).is_ok()); + assert_eq!( + RelayProfile::explicit(RelayProfileKind::Public, [local]).unwrap_err(), + Error::RelayProfilePolicyMismatch + ); + } + + #[test] + fn empty_and_duplicate_profiles_remain_rejected() { + assert!( + RelayProfile::explicit(RelayProfileKind::Public, Vec::<RelayEndpoint>::new()).is_err() + ); + let endpoint = RelayEndpoint::new( + "wss://relay.example", + RelayUrlPolicy::Public, + RelayAccess::ReadOnly, + ) + .expect("endpoint"); + assert!( + RelayProfile::explicit(RelayProfileKind::Public, [endpoint.clone(), endpoint]).is_err() + ); } #[test] - fn device_profile_requires_tls_and_rejects_duplicate_authority() { - let profile = RelayProfile::device(["wss://10.0.0.5:7447"]).expect("device profile"); - assert_eq!(profile.kind(), RelayProfileKind::Device); - assert_eq!(profile.endpoints()[0].access(), RelayAccess::ReadWrite); + fn excessive_and_infinite_endpoint_iterators_terminate_at_the_public_bound() { + let endpoint = RelayEndpoint::new( + "wss://relay.example", + RelayUrlPolicy::Public, + RelayAccess::ReadOnly, + ) + .expect("endpoint"); assert_eq!( - profile.endpoints()[1].policy(), - RelayUrlPolicy::PrivateNetwork + RelayProfile::explicit(RelayProfileKind::Public, std::iter::repeat(endpoint),) + .unwrap_err(), + Error::TooManyRelays { + max: crate::client::MAX_RELAYS, + actual: crate::client::MAX_RELAYS + 1, + } ); - assert!(RelayProfile::device(["ws://10.0.0.5:7447"]).is_err()); - assert!(RelayProfile::device([DEFAULT_PUBLIC_RELAY]).is_err()); } } diff --git a/crates/transport_nostr/src/relay.rs b/crates/transport_nostr/src/relay.rs @@ -408,7 +408,12 @@ mod tests { RelayUrl::from_target(&local, RelayUrlPolicy::Public), Err(Error::UnexpectedTransport { .. }) )); - let profile = crate::RelayProfile::public(["wss://relay.example.com"]).expect("profile"); + let profile = crate::profile::test_profile( + crate::RelayProfileKind::Public, + RelayUrlPolicy::Public, + ["wss://relay.example.com"], + ) + .expect("profile"); assert!(HardenedWebsocketTransport::new(profile.endpoints()).support_ping()); assert!(!policy_error("denied").to_string().is_empty()); } diff --git a/crates/transport_nostr/src/sink.rs b/crates/transport_nostr/src/sink.rs @@ -222,7 +222,7 @@ fn unix_time_ms() -> u64 { #[cfg(test)] mod tests { use super::*; - use crate::{Config, RelayProfile, RelayUrlPolicy}; + use crate::{Config, RelayUrlPolicy}; use radroots_transport::{ Target, TargetSet, outcome::DeliveryOutcomeKind, @@ -291,7 +291,12 @@ mod tests { #[test] fn sink_returns_normalized_per_relay_partial_success() { let config = Config::from_profile( - RelayProfile::public(["wss://one.example", "wss://two.example"]).expect("profile"), + crate::profile::test_profile( + crate::RelayProfileKind::Public, + RelayUrlPolicy::Public, + ["wss://one.example", "wss://two.example"], + ) + .expect("profile"), ); let two = RelayUrl::parse("wss://two.example", RelayUrlPolicy::Public).expect("two"); let client = MockRelayClient { @@ -350,7 +355,12 @@ mod tests { let calls = Arc::new(AtomicUsize::new(0)); let config = Config::from_profile( - RelayProfile::public(["wss://one.example", "wss://two.example"]).expect("profile"), + crate::profile::test_profile( + crate::RelayProfileKind::Public, + RelayUrlPolicy::Public, + ["wss://one.example", "wss://two.example"], + ) + .expect("profile"), ); let transport = NostrTransport::with_client(config, Arc::new(CountingRelayClient(Arc::clone(&calls)))); @@ -380,7 +390,12 @@ mod tests { let calls = Arc::new(AtomicUsize::new(0)); let config = Config::from_profile( - RelayProfile::public(["wss://one.example", "wss://two.example"]).expect("profile"), + crate::profile::test_profile( + crate::RelayProfileKind::Public, + RelayUrlPolicy::Public, + ["wss://one.example", "wss://two.example"], + ) + .expect("profile"), ); let transport = NostrTransport::with_client(config, Arc::new(CountingRelayClient(Arc::clone(&calls)))); @@ -413,7 +428,12 @@ mod tests { fn scripted(results: Vec<RelayPublishResult>) -> NostrTransport { let config = Config::from_profile( - RelayProfile::public(["wss://one.example", "wss://two.example"]).expect("profile"), + crate::profile::test_profile( + crate::RelayProfileKind::Public, + RelayUrlPolicy::Public, + ["wss://one.example", "wss://two.example"], + ) + .expect("profile"), ); NostrTransport::with_client(config, Arc::new(ScriptedRelayClient(results))) } diff --git a/crates/transport_nostr/src/source.rs b/crates/transport_nostr/src/source.rs @@ -423,7 +423,7 @@ fn unix_time_ms() -> u64 { #[cfg(test)] mod tests { use super::*; - use crate::{Config, RelayProfile, RelayUrlPolicy}; + use crate::{Config, RelayUrlPolicy}; use radroots_transport::{ FetchRequest, Target, TargetSet, source::{FetchBounds, FetchSelector, NextPage}, @@ -456,7 +456,12 @@ mod tests { fn transport() -> NostrTransport { let config = Config::from_profile( - RelayProfile::public(["wss://one.example", "wss://two.example"]).expect("profile"), + crate::profile::test_profile( + crate::RelayProfileKind::Public, + RelayUrlPolicy::Public, + ["wss://one.example", "wss://two.example"], + ) + .expect("profile"), ); NostrTransport::with_source_client(config, Arc::new(MockSourceClient)) } @@ -547,8 +552,14 @@ mod tests { } let calls = Arc::new(AtomicUsize::new(0)); - let config = - Config::from_profile(RelayProfile::public(["wss://one.example"]).expect("profile")); + let config = Config::from_profile( + crate::profile::test_profile( + crate::RelayProfileKind::Public, + RelayUrlPolicy::Public, + ["wss://one.example"], + ) + .expect("profile"), + ); let transport = NostrTransport::with_source_client( config, Arc::new(CountingSourceClient(Arc::clone(&calls))), @@ -590,7 +601,12 @@ mod tests { fn scripted(batches: Vec<RelayFetchBatch>) -> NostrTransport { let config = Config::from_profile( - RelayProfile::public(["wss://one.example", "wss://two.example"]).expect("profile"), + crate::profile::test_profile( + crate::RelayProfileKind::Public, + RelayUrlPolicy::Public, + ["wss://one.example", "wss://two.example"], + ) + .expect("profile"), ); NostrTransport::with_source_client(config, Arc::new(ScriptedSourceClient(batches))) } diff --git a/crates/transport_nostr/src/status.rs b/crates/transport_nostr/src/status.rs @@ -638,15 +638,21 @@ pub(crate) fn delivery_succeeded(outcome: &DeliveryOutcome) -> bool { #[cfg(test)] mod tests { use super::*; - use crate::{ReconnectBackoff, RelayProfile}; + use crate::ReconnectBackoff; fn tracker() -> (StatusTracker, RelayUrl, RelayUrl) { - let config = - Config::from_profile(RelayProfile::public(["wss://write.example"]).expect("profile")) - .with_reconnect_backoff(ReconnectBackoff::new(10, 40).expect("backoff")); - let canonical = config.relays()[0].clone(); + let config = Config::from_profile( + crate::profile::test_profile( + crate::RelayProfileKind::Public, + crate::RelayUrlPolicy::Public, + ["wss://read.example", "wss://write.example"], + ) + .expect("profile"), + ) + .with_reconnect_backoff(ReconnectBackoff::new(10, 40).expect("backoff")); + let readable = config.relays()[0].clone(); let writable = config.relays()[1].clone(); - (StatusTracker::new(&config), canonical, writable) + (StatusTracker::new(&config), readable, writable) } #[test] @@ -780,8 +786,14 @@ mod tests { #[test] fn canonical_relay_read_and_write_success_is_available() { - let config = - Config::from_profile(RelayProfile::public(Vec::<String>::new()).expect("profile")); + let config = Config::from_profile( + crate::profile::test_profile( + crate::RelayProfileKind::Public, + crate::RelayUrlPolicy::Public, + ["wss://relay.example"], + ) + .expect("profile"), + ); let tracker = StatusTracker::new(&config); let relay = config.relays()[0].clone(); tracker.record_read(&relay, true, false, 1); diff --git a/crates/transport_nostr/tests/local_io.rs b/crates/transport_nostr/tests/local_io.rs @@ -4,7 +4,8 @@ use radroots_transport::{ source::FetchBounds, }; use radroots_transport_nostr::{ - Config, NostrTransport, RelayAggregateState, RelayEvidenceState, RelayProfile, + Config, NostrTransport, RelayAccess, RelayAggregateState, RelayEndpoint, RelayEvidenceState, + RelayProfile, RelayProfileKind, RelayUrlPolicy, }; use serde_json::Value; use std::time::{Duration, SystemTime, UNIX_EPOCH}; @@ -44,7 +45,14 @@ async fn simulator_profile_proves_read_capability_against_a_real_loopback_socket } }); - let profile = RelayProfile::simulator([relay_url.as_str()]).expect("simulator profile"); + let endpoint = RelayEndpoint::new( + relay_url.as_str(), + RelayUrlPolicy::Local, + RelayAccess::ReadWrite, + ) + .expect("loopback endpoint"); + let profile = + RelayProfile::explicit(RelayProfileKind::Simulator, [endpoint]).expect("simulator profile"); let config = Config::from_profile(profile) .with_timeouts(1_000, 2_000, 500) .expect("timeouts"); diff --git a/crates/transport_nostr/tests/network_hardening.rs b/crates/transport_nostr/tests/network_hardening.rs @@ -1,5 +1,7 @@ use radroots_transport::{Target, TargetSet, source::FetchBounds}; -use radroots_transport_nostr::{RelayProfile, RelayUrl, RelayUrlPolicy}; +use radroots_transport_nostr::{ + RelayAccess, RelayEndpoint, RelayProfile, RelayProfileKind, RelayUrl, RelayUrlPolicy, +}; const WORKSPACE_MANIFEST: &str = include_str!("../../../Cargo.toml"); const RELAY_SOURCE: &str = include_str!("../src/relay.rs"); @@ -42,8 +44,17 @@ fn page_and_target_limits_reject_oversized_requests() { .collect::<Vec<_>>(); assert!(TargetSet::new(targets).is_err()); - let relays = (0..=64).map(|index| format!("wss://r{index}.example.com")); - assert!(RelayProfile::public(relays).is_err()); + let relays = (0..=64) + .map(|index| { + RelayEndpoint::new( + format!("wss://r{index}.example.com"), + RelayUrlPolicy::Public, + RelayAccess::ReadWrite, + ) + .expect("relay endpoint") + }) + .collect::<Vec<_>>(); + assert!(RelayProfile::explicit(RelayProfileKind::Public, relays).is_err()); } #[test] diff --git a/crates/transport_nostr/tests/package_boundary.rs b/crates/transport_nostr/tests/package_boundary.rs @@ -8,6 +8,7 @@ const EXAMPLE: &str = include_str!("../examples/configure_transport.rs"); const PUBLIC_API: &str = include_str!("../../../contracts/api_baselines/radroots_transport_nostr.txt"); const ROOT: &str = include_str!("../src/lib.rs"); +const PROFILE: &str = include_str!("../src/profile.rs"); #[test] fn manifest_and_root_match_the_governed_transport_boundary() { @@ -68,7 +69,8 @@ fn documentation_example_and_reviewed_api_baseline_are_complete() { } for required in [ "Config::from_profile(", - "RelayProfile::public(", + "RelayEndpoint::new(", + "RelayProfile::explicit(", "NostrTransport::new(config)", "let source: &dyn EventSource", "let sink: &dyn EventSink", @@ -85,6 +87,8 @@ fn documentation_example_and_reviewed_api_baseline_are_complete() { "pub struct radroots_transport_nostr::NostrTransport", "pub enum radroots_transport_nostr::RelayAggregateState", "pub struct radroots_transport_nostr::RelayProfile", + "pub fn radroots_transport_nostr::RelayEndpoint::new(", + "pub fn radroots_transport_nostr::RelayProfile::explicit<I>(", "pub struct radroots_transport_nostr::RelayStatusReport", "pub struct radroots_transport_nostr::RelayUrl(_)", "pub enum radroots_transport_nostr::RelayUrlPolicy", @@ -115,6 +119,33 @@ fn documentation_example_and_reviewed_api_baseline_are_complete() { } } +#[test] +fn relay_profiles_have_no_implicit_destination_or_policy_constructor() { + for forbidden in [ + "DEFAULT_PUBLIC_RELAY", + "pub fn public", + "pub fn simulator", + "pub fn device", + ] { + assert!(!ROOT.contains(forbidden)); + assert!(!PROFILE.contains(forbidden)); + assert!(!PUBLIC_API.contains(forbidden)); + } + for required in [ + "pub fn new(", + "policy: RelayUrlPolicy", + "access: RelayAccess", + "IntoIterator<Item = RelayEndpoint>", + ".take(crate::client::MAX_RELAYS + 1)", + "Error::RelayProfilePolicyMismatch", + ] { + assert!( + PROFILE.contains(required), + "profile is missing `{required}`" + ); + } +} + fn radroots_dependency_keys(manifest: &str) -> BTreeSet<&str> { dependency_keys(manifest) .into_iter()