lib

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

commit 75f07b7bc0ca2afc276ab279651ce1cf2c46a75a
parent 937d8ba5200dc7c3fe6757ba5ae5651d7eccc1f4
Author: triesap <tyson@radroots.org>
Date:   Tue,  7 Jul 2026 08:33:50 +0000

transport: enforce canonical Reticulum preview endpoint

Diffstat:
Mcrates/transport_publish_protocol/src/lib.rs | 39+++++++++++++++++++++++++++++++--------
Mcrates/transport_reticulum/src/lib.rs | 32+++++++++++++++-----------------
Mcrates/transport_reticulum/tests/reticulum_preview.rs | 45+++++++++++++++++++++++++++++++++------------
3 files changed, 79 insertions(+), 37 deletions(-)

diff --git a/crates/transport_publish_protocol/src/lib.rs b/crates/transport_publish_protocol/src/lib.rs @@ -17,6 +17,7 @@ pub const METHOD_CAPABILITIES: &str = "transport.publish.capabilities"; pub const METHOD_EVENT: &str = "transport.publish.event"; pub const METHOD_JOB_GET: &str = "transport.publish.job.get"; pub const METHOD_JOB_LIST: &str = "transport.publish.job.list"; +pub const RETICULUM_PREVIEW_ENDPOINT_URI: &str = "reticulum:preview-unavailable"; #[derive(Clone, Debug, PartialEq, Eq)] pub enum TransportPublishProtocolError { @@ -35,6 +36,9 @@ pub enum TransportPublishProtocolError { EmptyEndpointUri { index: usize, }, + InvalidReticulumPreviewEndpoint { + index: usize, + }, TargetLimitExceeded { max: usize, actual: usize, @@ -63,6 +67,10 @@ impl fmt::Display for TransportPublishProtocolError { Self::EmptyEndpointUri { index } => { write!(f, "transport target {index} endpoint_uri must not be empty") } + Self::InvalidReticulumPreviewEndpoint { index } => write!( + f, + "transport target {index} Reticulum preview endpoint must be {RETICULUM_PREVIEW_ENDPOINT_URI}" + ), Self::TargetLimitExceeded { max, actual } => { write!(f, "transport target count {actual} exceeds limit {max}") } @@ -143,13 +151,10 @@ impl TransportPublishTarget { } } - pub fn reticulum_preview( - endpoint_uri: impl Into<String>, - behavior: TransportPublishPreviewBehavior, - ) -> Self { + pub fn reticulum_preview(behavior: TransportPublishPreviewBehavior) -> Self { Self { transport_kind: "reticulum".to_owned(), - endpoint_uri: endpoint_uri.into(), + endpoint_uri: RETICULUM_PREVIEW_ENDPOINT_URI.to_owned(), preview_behavior: Some(behavior), } } @@ -161,6 +166,11 @@ impl TransportPublishTarget { if self.endpoint_uri.trim().is_empty() { return Err(TransportPublishProtocolError::EmptyEndpointUri { index }); } + if self.transport_kind.trim() == "reticulum" + && self.endpoint_uri.trim() != RETICULUM_PREVIEW_ENDPOINT_URI + { + return Err(TransportPublishProtocolError::InvalidReticulumPreviewEndpoint { index }); + } Ok(()) } } @@ -707,7 +717,6 @@ mod tests { too_many.target_policy = TransportPublishTargetPolicy::explicit_targets(vec![ TransportPublishTarget::nostr("wss://relay.example.com"), TransportPublishTarget::reticulum_preview( - "reticulum:preview-unavailable", TransportPublishPreviewBehavior::RejectDeliveryAttempts, ), ]); @@ -726,6 +735,18 @@ mod tests { Err(TransportPublishProtocolError::EmptyEndpointUri { index: 0 }) )); + let mut invalid_reticulum_endpoint = request.clone(); + invalid_reticulum_endpoint.target_policy = + TransportPublishTargetPolicy::explicit_targets(vec![TransportPublishTarget { + transport_kind: "reticulum".to_owned(), + endpoint_uri: "reticulum:preview-unavailable-alt".to_owned(), + preview_behavior: Some(TransportPublishPreviewBehavior::RejectDeliveryAttempts), + }]); + assert_eq!( + invalid_reticulum_endpoint.validate(1), + Err(TransportPublishProtocolError::InvalidReticulumPreviewEndpoint { index: 0 }) + ); + let mut empty_key = request.clone(); empty_key.idempotency_key = Some(" ".to_owned()); assert_eq!( @@ -787,7 +808,6 @@ mod tests { event: event(), target_policy: TransportPublishTargetPolicy::explicit_targets(vec![ TransportPublishTarget::reticulum_preview( - "reticulum:preview-unavailable", TransportPublishPreviewBehavior::DeferDeliveryPlans, ), ]), @@ -834,6 +854,10 @@ mod tests { "transport target 3 endpoint_uri must not be empty", ), ( + TransportPublishProtocolError::InvalidReticulumPreviewEndpoint { index: 4 }, + "transport target 4 Reticulum preview endpoint must be reticulum:preview-unavailable", + ), + ( TransportPublishProtocolError::TargetLimitExceeded { max: 1, actual: 2 }, "transport target count 2 exceeds limit 1", ), @@ -912,7 +936,6 @@ mod tests { let explicit = TransportPublishTargetPolicy::explicit_targets(vec![ TransportPublishTarget::nostr("wss://relay.example"), TransportPublishTarget::reticulum_preview( - "reticulum:preview-unavailable", TransportPublishPreviewBehavior::DeferDeliveryPlans, ), ]); diff --git a/crates/transport_reticulum/src/lib.rs b/crates/transport_reticulum/src/lib.rs @@ -15,7 +15,7 @@ use radroots_transport::{ }; const DEFAULT_PROFILE_ID: &str = "transport.reticulum.preview"; -const DEFAULT_ENDPOINT_URI: &str = "reticulum:preview-unavailable"; +pub const RETICULUM_PREVIEW_ENDPOINT_URI: &str = "reticulum:preview-unavailable"; const UNAVAILABLE_CODE: &str = "transport_unavailable"; const DEFERRED_CODE: &str = "deferred_until_implemented"; const PREVIEW_UNAVAILABLE_MESSAGE: &str = @@ -49,17 +49,13 @@ pub struct RadrootsReticulumPreviewEndpoint { impl RadrootsReticulumPreviewEndpoint { pub fn parse(raw: impl AsRef<str>) -> Result<Self, RadrootsReticulumPreviewError> { - let uri = raw.as_ref().trim().to_ascii_lowercase(); - if uri.is_empty() || !uri.starts_with("reticulum:") || uri == "reticulum:" { + let uri = raw.as_ref().trim(); + if uri != RETICULUM_PREVIEW_ENDPOINT_URI { return Err(RadrootsReticulumPreviewError::InvalidEndpoint); } - if uri - .chars() - .any(|ch| ch.is_ascii_control() || ch.is_ascii_whitespace()) - { - return Err(RadrootsReticulumPreviewError::InvalidEndpoint); - } - Ok(Self { uri }) + Ok(Self { + uri: RETICULUM_PREVIEW_ENDPOINT_URI.to_owned(), + }) } pub fn as_str(&self) -> &str { @@ -73,7 +69,7 @@ impl RadrootsReticulumPreviewEndpoint { impl Default for RadrootsReticulumPreviewEndpoint { fn default() -> Self { - Self::parse(DEFAULT_ENDPOINT_URI).expect("default Reticulum preview endpoint") + Self::parse(RETICULUM_PREVIEW_ENDPOINT_URI).expect("default Reticulum preview endpoint") } } @@ -276,13 +272,15 @@ impl fmt::Display for RadrootsReticulumPreviewError { fn ensure_reticulum_targets( targets: &[RadrootsTransportTarget], ) -> Result<(), RadrootsReticulumPreviewError> { - if targets - .iter() - .all(|target| target.kind == RadrootsTransportKind::Reticulum) - { - return Ok(()); + for target in targets { + if target.kind != RadrootsTransportKind::Reticulum { + return Err(RadrootsReticulumPreviewError::NonReticulumTarget); + } + if target.uri.as_str() != RETICULUM_PREVIEW_ENDPOINT_URI { + return Err(RadrootsReticulumPreviewError::InvalidEndpoint); + } } - Err(RadrootsReticulumPreviewError::NonReticulumTarget) + Ok(()) } fn preview_outcome(behavior: RadrootsReticulumPreviewBehavior) -> RadrootsTransportOutcome { diff --git a/crates/transport_reticulum/tests/reticulum_preview.rs b/crates/transport_reticulum/tests/reticulum_preview.rs @@ -53,10 +53,13 @@ fn default_profile_is_configured_preview_unavailable_and_rejecting() { #[test] fn endpoint_and_profile_validation_are_strict_and_canonical() { let endpoint = - RadrootsReticulumPreviewEndpoint::parse(" RETICULUM:Preview-Target ").expect("endpoint"); - assert_eq!(endpoint.as_str(), "reticulum:preview-target"); - assert_eq!(endpoint.to_string(), "reticulum:preview-target"); - assert_eq!(endpoint.clone().into_string(), "reticulum:preview-target"); + RadrootsReticulumPreviewEndpoint::parse("reticulum:preview-unavailable").expect("endpoint"); + assert_eq!(endpoint.as_str(), "reticulum:preview-unavailable"); + assert_eq!(endpoint.to_string(), "reticulum:preview-unavailable"); + assert_eq!( + endpoint.clone().into_string(), + "reticulum:preview-unavailable" + ); assert_eq!( RadrootsReticulumPreviewEndpoint::default().as_str(), "reticulum:preview-unavailable" @@ -75,6 +78,15 @@ fn endpoint_and_profile_validation_are_strict_and_canonical() { RadrootsReticulumPreviewError::InvalidEndpoint ); assert_eq!( + RadrootsReticulumPreviewEndpoint::parse("RETICULUM:Preview-Unavailable") + .expect_err("case drift endpoint"), + RadrootsReticulumPreviewError::InvalidEndpoint + ); + assert_eq!( + RadrootsReticulumPreviewEndpoint::parse("reticulum:custom").expect_err("custom endpoint"), + RadrootsReticulumPreviewError::InvalidEndpoint + ); + assert_eq!( RadrootsReticulumPreviewEndpoint::parse("reticulum:bad target") .expect_err("whitespace endpoint"), RadrootsReticulumPreviewError::InvalidEndpoint @@ -104,12 +116,12 @@ fn endpoint_and_profile_validation_are_strict_and_canonical() { ); let profile = RadrootsReticulumPreviewProfile::new( "transport.reticulum.custom", - RadrootsReticulumPreviewEndpoint::parse("reticulum:custom").expect("custom endpoint"), + RadrootsReticulumPreviewEndpoint::default(), RadrootsReticulumPreviewBehavior::DeferDeliveryPlans, ) - .expect("custom profile"); + .expect("custom behavior profile"); assert_eq!(profile.profile_id(), "transport.reticulum.custom"); - assert_eq!(profile.endpoint().as_str(), "reticulum:custom"); + assert_eq!(profile.endpoint().as_str(), "reticulum:preview-unavailable"); assert_eq!( profile.behavior(), RadrootsReticulumPreviewBehavior::DeferDeliveryPlans @@ -119,13 +131,10 @@ fn endpoint_and_profile_validation_are_strict_and_canonical() { #[test] fn reject_delivery_attempts_returns_unavailable_without_success_or_nostr_routing() { let transport = RadrootsReticulumPreviewTransport::default(); - let request = delivery_request(vec![ - reticulum_target("reticulum:preview-unavailable"), - reticulum_target("reticulum:preview-unavailable-alt"), - ]); + let request = delivery_request(vec![reticulum_target("reticulum:preview-unavailable")]); let receipt = transport.deliver(request).expect("delivery receipt"); - assert_eq!(receipt.target_receipts.len(), 2); + assert_eq!(receipt.target_receipts.len(), 1); assert_eq!( receipt.satisfied_target_count(RadrootsTransportSatisfactionClass::Accepted), 0 @@ -144,6 +153,18 @@ fn reject_delivery_attempts_returns_unavailable_without_success_or_nostr_routing } #[test] +fn noncanonical_reticulum_preview_targets_are_rejected() { + let transport = RadrootsReticulumPreviewTransport::default(); + let err = transport + .deliver(delivery_request(vec![reticulum_target( + "reticulum:preview-unavailable-alt", + )])) + .expect_err("noncanonical Reticulum endpoint"); + + assert_eq!(err, RadrootsReticulumPreviewError::InvalidEndpoint); +} + +#[test] fn deferred_delivery_plan_mode_never_counts_as_satisfied() { let transport = RadrootsReticulumPreviewTransport::new( RadrootsReticulumPreviewProfile::default()