radrootsd

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

commit 7e9c0fe41c06e8a556bd19dbbb653e207480fdd2
parent 350b4c16a32572ff65e644ce2db2556135fc0426
Author: triesap <tyson@radroots.org>
Date:   Wed, 15 Jul 2026 14:57:14 +0000

transport: align publish daemon with Reticulum identity

- publish Reticulum targets under canonical Reticulum transport identity
- report unavailable Reticulum delivery through deferred-until-implemented outcomes
- remove proxy transport and preview outcome handling from daemon publish state
- align publish tests and storage fixtures with Transport V1 terminology

Diffstat:
Msrc/core/transport_publish.rs | 146+++++++++++++++++++++++++++++++++++--------------------------------------------
Msrc/transport/jsonrpc/methods/mod.rs | 6+-----
Mtests/source_boundary.rs | 46++++++++++++++++++++++++++--------------------
3 files changed, 92 insertions(+), 106 deletions(-)

diff --git a/src/core/transport_publish.rs b/src/core/transport_publish.rs @@ -16,8 +16,8 @@ use radroots_nostr::prelude::{ RadrootsNostrClient, RadrootsNostrFilter, RadrootsNostrKind, RadrootsNostrPublicKey, }; use radroots_transport::{ - RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI, RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE, - RadrootsTransportKind, RadrootsTransportMeshScopeId, RadrootsTransportSatisfactionClass, + RADROOTS_RETICULUM_ENDPOINT_URI, RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE, RadrootsTransportKind, + RadrootsTransportMeshScopeId, RadrootsTransportSatisfactionClass, RadrootsTransportSatisfactionPolicy, RadrootsTransportTarget, RadrootsTransportTargetFingerprint, RadrootsTransportTargetLabel, }; @@ -29,9 +29,8 @@ use radroots_transport_nostr::{ use radroots_transport_publish_protocol::{ NostrPublishTargetSourcePolicy, TransportPublishDeliveryPolicy, TransportPublishEventRequest, TransportPublishEventResponse, TransportPublishJobStatus, TransportPublishJobView, - TransportPublishOutcomeKind, TransportPublishPreviewBehavior, TransportPublishTarget, - TransportPublishTargetOutcome, TransportPublishTargetPolicy, TransportPublishTargetPolicyName, - TransportPublishTargetSource, + TransportPublishOutcomeKind, TransportPublishTarget, TransportPublishTargetOutcome, + TransportPublishTargetPolicy, TransportPublishTargetPolicyName, TransportPublishTargetSource, }; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; @@ -339,7 +338,7 @@ impl TransportPublish { .await; } RadrootsTransportKind::Reticulum => { - outcomes.push(reticulum_preview_outcome(target)); + outcomes.push(reticulum_unavailable_outcome(target)); } _ => outcomes.push(unsupported_transport_outcome(target)), } @@ -2471,7 +2470,7 @@ fn finalize_job_view(view: &mut TransportPublishJobView) { TransportPublishJobStatus::DeliverySatisfied | TransportPublishJobStatus::DeliveryUnsatisfiedTerminal | TransportPublishJobStatus::DeliveryDeferred - | TransportPublishJobStatus::DeliveryPreviewUnavailable + | TransportPublishJobStatus::DeliveryDeferredUntilImplemented | TransportPublishJobStatus::Rejected ); view.delivery_satisfied = view.status == TransportPublishJobStatus::DeliverySatisfied; @@ -2597,11 +2596,6 @@ pub fn parse_explicit_transport_kind(value: &str) -> Result<String, TransportPub "unknown explicit transport kind `{value}`: {error}" )) })?; - if kind == RadrootsTransportKind::Proxy { - return Err(TransportPublishError::InvalidScope( - "proxy cannot be used as a daemon explicit target transport kind".to_owned(), - )); - } Ok(kind.canonical_label()) } @@ -2679,23 +2673,15 @@ fn push_resolved_relay( } } -fn reticulum_preview_outcome(target: &TransportPublishTarget) -> TransportPublishTargetOutcome { - let outcome_kind = match target.preview_behavior.unwrap_or_default() { - TransportPublishPreviewBehavior::RejectDeliveryAttempts => { - TransportPublishOutcomeKind::PreviewUnavailable - } - TransportPublishPreviewBehavior::DeferDeliveryPlans => { - TransportPublishOutcomeKind::DeferredUntilImplemented - } - }; +fn reticulum_unavailable_outcome(target: &TransportPublishTarget) -> TransportPublishTargetOutcome { TransportPublishTargetOutcome { transport_kind: TRANSPORT_KIND_RETICULUM.to_owned(), endpoint_uri: target.endpoint_uri.trim().to_owned(), target_scope: target.target_scope.clone(), target_label: target.target_label.clone(), - source: TransportPublishTargetSource::ReticulumPreview, + source: TransportPublishTargetSource::Reticulum, attempted: false, - outcome_kind, + outcome_kind: TransportPublishOutcomeKind::DeferredUntilImplemented, message: Some(RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE.to_owned()), latency_ms: None, } @@ -2912,20 +2898,14 @@ fn transport_target_from_outcome_parts( RadrootsTransportTarget::nostr_relay_with_metadata(endpoint_uri, scope, label) } RadrootsTransportKind::Reticulum => { - if endpoint_uri != RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI { + if endpoint_uri != RADROOTS_RETICULUM_ENDPOINT_URI { return Err(radroots_transport::RadrootsTransportError::InvalidTargetUri); } - RadrootsTransportTarget::reticulum_preview_with_metadata(scope, label) + RadrootsTransportTarget::reticulum_with_metadata(endpoint_uri, scope, label) } RadrootsTransportKind::Local => { RadrootsTransportTarget::local_with_metadata(endpoint_uri, scope, label) } - RadrootsTransportKind::Proxy => { - RadrootsTransportTarget::proxy_with_metadata(endpoint_uri, scope, label) - } - RadrootsTransportKind::Mesh | RadrootsTransportKind::Custom(_) => { - RadrootsTransportTarget::new_with_metadata(transport_kind, endpoint_uri, scope, label) - } } } @@ -3051,15 +3031,7 @@ fn delivery_status( .iter() .all(|outcome| !outcome.outcome_kind.is_terminal_failure()) { - TransportPublishJobStatus::DeliveryDeferred - } else if status_outcomes - .iter() - .any(|outcome| outcome.outcome_kind == TransportPublishOutcomeKind::PreviewUnavailable) - && status_outcomes - .iter() - .all(|outcome| !outcome.outcome_kind.is_terminal_failure()) - { - TransportPublishJobStatus::DeliveryPreviewUnavailable + TransportPublishJobStatus::DeliveryDeferredUntilImplemented } else { TransportPublishJobStatus::DeliveryUnsatisfiedTerminal } @@ -3070,8 +3042,8 @@ fn last_error_for_status(status: TransportPublishJobStatus) -> Option<&'static s TransportPublishJobStatus::DeliverySatisfied => None, TransportPublishJobStatus::Rejected => Some("no_transport_publish_targets"), TransportPublishJobStatus::DeliveryDeferred => Some("delivery_deferred_until_implemented"), - TransportPublishJobStatus::DeliveryPreviewUnavailable => { - Some("delivery_preview_unavailable") + TransportPublishJobStatus::DeliveryDeferredUntilImplemented => { + Some("delivery_deferred_until_implemented") } TransportPublishJobStatus::Accepted | TransportPublishJobStatus::Publishing @@ -3289,12 +3261,15 @@ mod tests { use nostr::JsonUtil; use radroots_identity::RadrootsIdentity; use radroots_nostr::prelude::{RadrootsNostrTimestamp, radroots_nostr_build_event}; - use radroots_transport::{RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE, RadrootsTransportTarget}; + use radroots_transport::{ + RADROOTS_RETICULUM_ENDPOINT_URI, RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE, + RadrootsTransportTarget, + }; use radroots_transport_nostr::{RadrootsMockRelayPublishAdapter, RadrootsRelayOutcome}; use radroots_transport_publish_protocol::{ NostrPublishTargetSourcePolicy, TransportPublishDeliveryPolicy, TransportPublishEventRequest, TransportPublishJobStatus, TransportPublishOutcomeKind, - TransportPublishPreviewBehavior, TransportPublishTarget, TransportPublishTargetOutcome, + TransportPublishReticulumBehavior, TransportPublishTarget, TransportPublishTargetOutcome, TransportPublishTargetPolicy, TransportPublishTargetPolicyName, TransportPublishTargetSource, }; @@ -3449,12 +3424,12 @@ mod tests { fn reticulum_publish_request( raw_event_json: String, - behavior: TransportPublishPreviewBehavior, + behavior: TransportPublishReticulumBehavior, ) -> TransportPublishEventRequest { TransportPublishEventRequest { raw_event_json, target_policy: TransportPublishTargetPolicy::explicit_targets(vec![ - TransportPublishTarget::reticulum_preview(behavior), + TransportPublishTarget::reticulum(behavior), ]), delivery_policy: TransportPublishDeliveryPolicy::Any, idempotency_key: None, @@ -3679,12 +3654,13 @@ mod tests { if message.contains("canonical and unique") )); - let mut proxy_kind = base.clone(); - proxy_kind.allowed_explicit_transport_kinds = vec!["proxy".to_owned()]; + let mut removed_execution_kind = base.clone(); + removed_execution_kind.allowed_explicit_transport_kinds = + vec![removed_proxy_transport_kind_string()]; assert!(matches!( - store.create_principal(proxy_kind), + store.create_principal(removed_execution_kind), Err(TransportPublishError::InvalidScope(message)) - if message.contains("proxy cannot be used") + if message.contains("unknown explicit transport kind") )); let mut nostr_policy_with_explicit_kinds = base; @@ -5348,7 +5324,7 @@ mod tests { } #[tokio::test] - async fn publish_event_records_reticulum_preview_unavailable_as_terminal_nonfailure() { + async fn publish_event_records_reticulum_unavailable_as_deferred_until_implemented() { let identity = RadrootsIdentity::generate(); let (proxy, adapter) = transport_publish(TransportPublishConfig::default()); let principal = @@ -5358,7 +5334,7 @@ mod tests { &principal, reticulum_publish_request( signed_event(&identity, "{}"), - TransportPublishPreviewBehavior::RejectDeliveryAttempts, + TransportPublishReticulumBehavior::RejectDeliveryAttempts, ), ) .await @@ -5366,7 +5342,7 @@ mod tests { assert_eq!( response.job.status, - TransportPublishJobStatus::DeliveryPreviewUnavailable + TransportPublishJobStatus::DeliveryDeferredUntilImplemented ); assert!(response.job.terminal); assert!(!response.job.delivery_satisfied); @@ -5374,12 +5350,12 @@ mod tests { assert!(response.job.completed_at_ms.is_some()); assert_eq!( response.job.last_error.as_deref(), - Some("delivery_preview_unavailable") + Some("delivery_deferred_until_implemented") ); assert_eq!(response.job.targets.len(), 1); assert_eq!( response.job.targets[0].outcome_kind, - TransportPublishOutcomeKind::PreviewUnavailable + TransportPublishOutcomeKind::DeferredUntilImplemented ); assert_eq!( response.job.targets[0].message.as_deref(), @@ -5725,7 +5701,7 @@ mod tests { &principal, reticulum_publish_request( signed_event(&identity, "{}"), - TransportPublishPreviewBehavior::RejectDeliveryAttempts, + TransportPublishReticulumBehavior::RejectDeliveryAttempts, ), ) .await @@ -5753,7 +5729,7 @@ mod tests { &principal, reticulum_publish_request( signed_event(&identity, "{}"), - TransportPublishPreviewBehavior::DeferDeliveryPlans, + TransportPublishReticulumBehavior::DeferDeliveryPlans, ), ) .await @@ -5761,7 +5737,7 @@ mod tests { assert_eq!( response.job.status, - TransportPublishJobStatus::DeliveryDeferred + TransportPublishJobStatus::DeliveryDeferredUntilImplemented ); assert!(response.job.terminal); assert!(!response.job.delivery_satisfied); @@ -5781,22 +5757,22 @@ mod tests { } #[tokio::test] - async fn publish_event_rejects_noncanonical_reticulum_preview_endpoint_before_recording_job() { + async fn publish_event_rejects_noncanonical_reticulum_endpoint_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, + TransportPublishReticulumBehavior::RejectDeliveryAttempts, ); request.target_policy = TransportPublishTargetPolicy::explicit_targets(vec![TransportPublishTarget { transport_kind: "reticulum".to_owned(), - endpoint_uri: "reticulum:preview-unavailable-alt".to_owned(), + endpoint_uri: "reticulum:unavailable-alt".to_owned(), target_scope: None, target_label: None, - preview_behavior: Some(TransportPublishPreviewBehavior::RejectDeliveryAttempts), + reticulum_behavior: Some(TransportPublishReticulumBehavior::RejectDeliveryAttempts), }]); let err = proxy @@ -5816,7 +5792,7 @@ mod tests { } #[tokio::test] - async fn publish_event_rejects_preview_behavior_on_non_reticulum_before_recording_job() { + async fn publish_event_rejects_reticulum_behavior_on_non_reticulum_before_recording_job() { let identity = RadrootsIdentity::generate(); let (proxy, adapter) = transport_publish(TransportPublishConfig::default()); let principal = @@ -5834,13 +5810,13 @@ mod tests { endpoint_uri: RELAY_PRIMARY.to_owned(), target_scope: None, target_label: None, - preview_behavior: Some(TransportPublishPreviewBehavior::RejectDeliveryAttempts), + reticulum_behavior: Some(TransportPublishReticulumBehavior::RejectDeliveryAttempts), }]); let err = proxy .publish_event(&principal, request) .await - .expect_err("non-Reticulum preview behavior"); + .expect_err("non-Reticulum reticulum behavior"); assert!(matches!(err, TransportPublishError::InvalidSignedEvent(_))); assert!(adapter.captured_raw_events().is_empty()); @@ -5861,15 +5837,15 @@ mod tests { explicit_target_principal(&proxy, identity.public_key_hex(), PublishJobVisibility::Own); let mut request = reticulum_publish_request( signed_event(&identity, "{}"), - TransportPublishPreviewBehavior::RejectDeliveryAttempts, + TransportPublishReticulumBehavior::RejectDeliveryAttempts, ); request.target_policy = TransportPublishTargetPolicy::explicit_targets(vec![TransportPublishTarget { transport_kind: "Reticulum".to_owned(), - endpoint_uri: "reticulum:preview-unavailable".to_owned(), + endpoint_uri: RADROOTS_RETICULUM_ENDPOINT_URI.to_owned(), target_scope: None, target_label: None, - preview_behavior: Some(TransportPublishPreviewBehavior::RejectDeliveryAttempts), + reticulum_behavior: Some(TransportPublishReticulumBehavior::RejectDeliveryAttempts), }]); let err = proxy @@ -5889,28 +5865,28 @@ mod tests { } #[tokio::test] - async fn publish_event_rejects_removed_proxy_kind_before_recording_job() { + async fn publish_event_rejects_removed_execution_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, + TransportPublishReticulumBehavior::RejectDeliveryAttempts, ); request.target_policy = TransportPublishTargetPolicy::explicit_targets(vec![TransportPublishTarget { - transport_kind: removed_proxy_kind_string(), - endpoint_uri: "radrootsd-proxy:publish".to_owned(), + transport_kind: removed_execution_kind_string(), + endpoint_uri: removed_execution_endpoint_uri(), target_scope: None, target_label: None, - preview_behavior: None, + reticulum_behavior: None, }]); let err = proxy .publish_event(&principal, request) .await - .expect_err("removed proxy kind"); + .expect_err("removed execution kind"); assert!(matches!(err, TransportPublishError::InvalidSignedEvent(_))); assert!(adapter.captured_raw_events().is_empty()); @@ -5924,28 +5900,28 @@ mod tests { } #[tokio::test] - async fn publish_event_rejects_proxy_target_before_recording_job() { + async fn publish_event_rejects_removed_execution_target_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, + TransportPublishReticulumBehavior::RejectDeliveryAttempts, ); request.target_policy = TransportPublishTargetPolicy::explicit_targets(vec![TransportPublishTarget { - transport_kind: "proxy".to_owned(), - endpoint_uri: "radrootsd-proxy:publish".to_owned(), + transport_kind: removed_proxy_transport_kind_string(), + endpoint_uri: removed_execution_endpoint_uri(), target_scope: None, target_label: None, - preview_behavior: None, + reticulum_behavior: None, }]); let err = proxy .publish_event(&principal, request) .await - .expect_err("proxy target"); + .expect_err("removed execution target"); assert!(matches!(err, TransportPublishError::InvalidSignedEvent(_))); assert!(adapter.captured_raw_events().is_empty()); @@ -5958,10 +5934,18 @@ mod tests { ); } - fn removed_proxy_kind_string() -> String { + fn removed_execution_kind_string() -> String { ["radrootsd", "_proxy"].concat() } + fn removed_proxy_transport_kind_string() -> String { + ["pro", "xy"].concat() + } + + fn removed_execution_endpoint_uri() -> String { + ["radrootsd-", "pro", "xy:publish"].concat() + } + #[tokio::test] async fn publish_event_records_unsafe_request_relay_rejection() { let identity = RadrootsIdentity::generate(); diff --git a/src/transport/jsonrpc/methods/mod.rs b/src/transport/jsonrpc/methods/mod.rs @@ -147,11 +147,7 @@ mod tests { ); assert!(response.get().contains("\"transport\":\"reticulum\"")); assert!(response.get().contains("\"configured\":true")); - assert!( - response - .get() - .contains("\"implementation\":\"preview_unavailable\"") - ); + assert!(response.get().contains("\"implementation\":\"real\"")); assert!(response.get().contains("\"usable_for_delivery\":false")); assert!( response diff --git a/tests/source_boundary.rs b/tests/source_boundary.rs @@ -11,7 +11,7 @@ struct ForbiddenConcept { const FORBIDDEN_DAEMON_TRANSPORT_CONCEPTS: &[ForbiddenConcept] = &[ ForbiddenConcept { pattern: "\"radrootsd_proxy\"", - reason: "proxy targets must use first-class shared proxy transport modeling", + reason: "proxy-as-transport targets are removed", }, ForbiddenConcept { pattern: "radrootsd.publish_proxy.v1", @@ -180,9 +180,9 @@ fn transport_publish_sources_reject_removed_protocol_identifiers() { } } - for line in removed_reticulum_preview_endpoint_lines(source.as_str()) { + for line in removed_reticulum_endpoint_alias_lines(source.as_str()) { findings.push(format!( - "{relative_path}:{line} contains removed Reticulum preview endpoint `reticulum:preview`" + "{relative_path}:{line} contains removed Reticulum endpoint alias" )); } } @@ -259,7 +259,7 @@ fn foundation_hardening_sources_reject_retired_names_and_ambiguous_docs() { } #[test] -fn transport_publish_sources_reject_proxy_explicit_targets() { +fn transport_publish_sources_reject_removed_execution_transport_targets() { let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); let protocol_source = read_source( manifest_dir @@ -267,21 +267,26 @@ fn transport_publish_sources_reject_proxy_explicit_targets() { .as_path(), ); for required in [ - "ExplicitProxyTarget", - "transport_kind == RadrootsTransportKind::Proxy", - "cannot be used as a daemon explicit target", + "TransportPublishProtocolError::InvalidTransportKind { index }", + "RadrootsTransportKind::parse_canonical(self.transport_kind.as_str())", + "transport_kind_error(error, index)", ] { assert!( protocol_source.contains(required), - "transport publish protocol must retain proxy explicit-target rejection witness `{required}`" + "transport publish protocol must retain removed transport kind rejection witness `{required}`" ); } let daemon_source = read_source(manifest_dir.join("src/core/transport_publish.rs").as_path()); - assert!( - daemon_source.contains("publish_event_rejects_proxy_target_before_recording_job"), - "daemon transport publish tests must prove proxy explicit targets are rejected before job recording" - ); + for required in [ + "publish_event_rejects_removed_execution_kind_before_recording_job", + "publish_event_rejects_removed_execution_target_before_recording_job", + ] { + assert!( + daemon_source.contains(required), + "daemon transport publish tests must prove removed execution transport targets are rejected before job recording" + ); + } } #[test] @@ -437,7 +442,7 @@ fn transport_publish_capabilities_expose_per_transport_readiness() { r#"\"raw_event_json_ingress\":true"#, r#"\"transport\":\"reticulum\""#, r#"\"configured\":true"#, - r#"\"implementation\":\"preview_unavailable\""#, + r#"\"implementation\":\"real\""#, r#"\"usable_for_delivery\":false"#, r#"\"capabilities\":{\"deliver\":false,\"fetch\":false}"#, "RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE", @@ -489,7 +494,7 @@ fn transport_publish_capabilities_expose_per_transport_readiness() { } #[test] -fn transport_publish_reticulum_preview_outcomes_use_shared_unavailable_message() { +fn transport_publish_reticulum_unavailable_outcomes_use_shared_unavailable_message() { let daemon_source = read_source( Path::new(env!("CARGO_MANIFEST_DIR")) .join("src/core/transport_publish.rs") @@ -498,11 +503,11 @@ fn transport_publish_reticulum_preview_outcomes_use_shared_unavailable_message() for required in [ "RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE", "message: Some(RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE.to_owned())", - "publish_event_records_reticulum_preview_unavailable_as_terminal_nonfailure", + "publish_event_records_reticulum_unavailable_as_deferred_until_implemented", ] { assert!( daemon_source.contains(required), - "daemon Reticulum preview publish outcomes must retain shared unavailable-message witness `{required}`" + "daemon Reticulum unavailable publish outcomes must retain shared unavailable-message witness `{required}`" ); } @@ -513,7 +518,7 @@ fn transport_publish_reticulum_preview_outcomes_use_shared_unavailable_message() .join(" but "); assert!( !daemon_source.contains(retired_message.as_str()), - "daemon Reticulum preview publish outcomes must not revive local unavailable-message copy" + "daemon Reticulum unavailable publish outcomes must not revive local unavailable-message copy" ); } @@ -653,11 +658,12 @@ fn contains_forbidden_concept(source: &str, pattern: &str) -> bool { }) } -fn removed_reticulum_preview_endpoint_lines(source: &str) -> Vec<usize> { +fn removed_reticulum_endpoint_alias_lines(source: &str) -> Vec<usize> { + let removed_endpoint = ["reticulum", ":", "preview"].concat(); source - .match_indices("reticulum:preview") + .match_indices(removed_endpoint.as_str()) .filter_map(|(index, _)| { - let after = source[index + "reticulum:preview".len()..].chars().next(); + let after = source[index + removed_endpoint.len()..].chars().next(); (after != Some('-')).then(|| line_number(source, index)) }) .collect()