radrootsd

JSON-RPC bridge for Radroots event publishing
git clone https://radroots.dev/git/radrootsd.git
Log | Files | Refs | README | LICENSE

commit d246fa5b4d3de7647bd4f64ecc561f215106f593
parent 519c9f2e74760a2bac06bb28d57ba43dcff78f8f
Author: triesap <tyson@radroots.org>
Date:   Tue,  7 Jul 2026 19:04:15 +0000

transport: reject noncanonical publish target kinds

- classify explicit publish targets through the shared canonical kind parser
- reject mixed-case Reticulum targets before job creation
- reject removed proxy kind requests before dispatch
- cover invalid target-kind requests in daemon publish tests

Diffstat:
MCargo.lock | 1+
Msrc/core/transport_publish.rs | 86++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
2 files changed, 82 insertions(+), 5 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -1918,6 +1918,7 @@ dependencies = [ name = "radroots_transport_publish_protocol" version = "0.1.0-alpha.2" dependencies = [ + "radroots_transport", "serde", ] diff --git a/src/core/transport_publish.rs b/src/core/transport_publish.rs @@ -21,7 +21,7 @@ use radroots_relay_transport::{ RadrootsRelayPublishAdapter, RadrootsRelayPublishRelayReceipt, RadrootsRelayPublishRequest, RadrootsRelayTargetSet, RadrootsRelayTransportError, RadrootsRelayUrl, RadrootsRelayUrlPolicy, }; -use radroots_transport::RadrootsTransportSatisfactionPolicy; +use radroots_transport::{RadrootsTransportKind, RadrootsTransportSatisfactionPolicy}; use radroots_transport_publish_protocol::{ NostrPublishTargetSourcePolicy, SignedNostrEventWire, TransportPublishDeliveryPolicy, TransportPublishEventRequest, TransportPublishEventResponse, TransportPublishJobStatus, @@ -229,13 +229,19 @@ impl TransportPublish { ) -> Result<PublishRelayResolution, TransportPublishError> { let mut resolved = Vec::new(); let mut outcomes = Vec::new(); - for target in targets { - match target.transport_kind.as_str() { - TRANSPORT_KIND_NOSTR => { + for (index, target) in targets.iter().enumerate() { + match RadrootsTransportKind::parse_canonical(target.transport_kind.as_str()).map_err( + |error| { + TransportPublishError::InvalidSignedEvent(format!( + "transport target {index} kind is invalid: {error}" + )) + }, + )? { + RadrootsTransportKind::Nostr => { self.resolve_request_target(&mut resolved, &mut outcomes, target) .await; } - TRANSPORT_KIND_RETICULUM => { + RadrootsTransportKind::Reticulum => { outcomes.push(reticulum_preview_outcome(target)); } _ => outcomes.push(unsupported_transport_outcome(target)), @@ -2954,6 +2960,76 @@ mod tests { } #[tokio::test] + async fn publish_event_rejects_noncanonical_reticulum_kind_before_recording_job() { + let identity = RadrootsIdentity::generate(); + let (proxy, adapter) = transport_publish(TransportPublishConfig::default()); + let principal = + explicit_target_principal(&proxy, identity.public_key_hex(), PublishJobVisibility::Own); + let mut request = reticulum_publish_request( + signed_event(&identity, "{}"), + TransportPublishPreviewBehavior::RejectDeliveryAttempts, + ); + request.target_policy = + TransportPublishTargetPolicy::explicit_targets(vec![TransportPublishTarget { + transport_kind: "Reticulum".to_owned(), + endpoint_uri: "reticulum:preview-unavailable".to_owned(), + preview_behavior: Some(TransportPublishPreviewBehavior::RejectDeliveryAttempts), + }]); + + let err = proxy + .publish_event(&principal, request) + .await + .expect_err("noncanonical Reticulum kind"); + + assert!(matches!(err, TransportPublishError::InvalidSignedEvent(_))); + assert!(adapter.captured_raw_events().is_empty()); + assert!( + proxy + .store + .list_jobs_for_principal(&principal, 10) + .expect("jobs") + .is_empty() + ); + } + + #[tokio::test] + async fn publish_event_rejects_removed_proxy_kind_before_recording_job() { + let identity = RadrootsIdentity::generate(); + let (proxy, adapter) = transport_publish(TransportPublishConfig::default()); + let principal = + explicit_target_principal(&proxy, identity.public_key_hex(), PublishJobVisibility::Own); + let mut request = reticulum_publish_request( + signed_event(&identity, "{}"), + TransportPublishPreviewBehavior::RejectDeliveryAttempts, + ); + request.target_policy = + TransportPublishTargetPolicy::explicit_targets(vec![TransportPublishTarget { + transport_kind: removed_proxy_kind_string(), + endpoint_uri: "radrootsd-proxy:publish".to_owned(), + preview_behavior: None, + }]); + + let err = proxy + .publish_event(&principal, request) + .await + .expect_err("removed proxy kind"); + + assert!(matches!(err, TransportPublishError::InvalidSignedEvent(_))); + assert!(adapter.captured_raw_events().is_empty()); + assert!( + proxy + .store + .list_jobs_for_principal(&principal, 10) + .expect("jobs") + .is_empty() + ); + } + + fn removed_proxy_kind_string() -> String { + ["radrootsd", "_proxy"].concat() + } + + #[tokio::test] async fn publish_event_records_unsafe_request_relay_rejection() { let identity = RadrootsIdentity::generate(); let (proxy, adapter) = transport_publish(TransportPublishConfig::default());