sdk

Radroots SDK and bindings
git clone https://radroots.dev/git/sdk.git
Log | Files | Refs | README

commit 9b45e557252f9db93d454a9f39bf3dd01b4f9a93
parent 93027d0e0885a2234fbc244559f82b70b5f93010
Author: triesap <tyson@radroots.org>
Date:   Tue,  7 Jul 2026 20:57:17 +0000

transport: clear proxy claims on local validation errors

- validate proxy target policy and delivery policy before local event ingest
- terminally clear claimed events for local proxy validation failures
- cover Reticulum proxy target rejection without stuck claims
- extend radrootsd adapter tests for protocol-field rejection

Diffstat:
Mcrates/sdk/src/sync_runtime.rs | 35+++++++++++++++++++++++++++++++++--
Mcrates/sdk/tests/unit/adapters_radrootsd_tests.rs | 8++++++++
Mcrates/sdk/tests/unit/sync_runtime_tests.rs | 47++++++++++++++++++++++++++++++++++++++++++++++-
3 files changed, 87 insertions(+), 3 deletions(-)

diff --git a/crates/sdk/src/sync_runtime.rs b/crates/sdk/src/sync_runtime.rs @@ -753,6 +753,18 @@ async fn push_proxy_claimed_outbox_event( claimed.outbox_event_id, ), )?; + let target_policy = match proxy_transport_publish_target_policy(claimed) { + Ok(target_policy) => target_policy, + Err(error) => { + return fail_proxy_local_validation(sync, claimed, error, now_ms).await; + } + }; + let delivery_policy = match proxy_delivery_policy(sync, claimed, &target_policy).await { + Ok(delivery_policy) => delivery_policy, + Err(error) => { + return fail_proxy_local_validation(sync, claimed, error, now_ms).await; + } + }; sync.sdk ._outbox .ingest_signed_event_local( @@ -762,10 +774,9 @@ async fn push_proxy_claimed_outbox_event( now_ms, ) .await?; - let target_policy = proxy_transport_publish_target_policy(claimed)?; let request = RadrootsdProxyPublishRequest { signed_event: signed_event.clone(), - delivery_policy: proxy_delivery_policy(sync, claimed, &target_policy).await?, + delivery_policy, target_policy, idempotency_key: Some(proxy_outbox_idempotency_key( claimed.outbox_event_id, @@ -796,6 +807,26 @@ async fn push_proxy_claimed_outbox_event( } #[cfg(all(feature = "runtime", feature = "radrootsd-proxy"))] +async fn fail_proxy_local_validation( + sync: &SyncClient<'_>, + claimed: &RadrootsOutboxClaimedEvent, + error: RadrootsSdkError, + now_ms: i64, +) -> Result<TransportPublishJobView, RadrootsSdkError> { + let message = error.to_string(); + sync.sdk + ._outbox + .mark_publish_failed_terminal( + claimed.outbox_event_id, + claimed.claim_token.as_str(), + message.as_str(), + now_ms, + ) + .await?; + Err(error) +} + +#[cfg(all(feature = "runtime", feature = "radrootsd-proxy"))] async fn proxy_delivery_policy( sync: &SyncClient<'_>, claimed: &RadrootsOutboxClaimedEvent, diff --git a/crates/sdk/tests/unit/adapters_radrootsd_tests.rs b/crates/sdk/tests/unit/adapters_radrootsd_tests.rs @@ -692,6 +692,13 @@ async fn publish_signed_event_rejects_invalid_protocol_requests_before_http() { NostrPublishTargetSourcePolicy::RequestThenAuthorWriteThenDaemonDefault, vec![" ".to_owned()], ); + let mut nostr_preview_behavior = base.clone(); + nostr_preview_behavior.target_policy = + TransportPublishTargetPolicy::explicit_targets(vec![TransportPublishTarget { + transport_kind: "nostr".to_owned(), + endpoint_uri: "wss://relay.example.com".to_owned(), + preview_behavior: Some(TransportPublishPreviewBehavior::RejectDeliveryAttempts), + }]); let mut empty_idempotency = base; empty_idempotency.idempotency_key = Some(" ".to_owned()); @@ -701,6 +708,7 @@ async fn publish_signed_event_rejects_invalid_protocol_requests_before_http() { invalid_quorum, too_many_targets, empty_endpoint_uri, + nostr_preview_behavior, empty_idempotency, ] { assert!(matches!( diff --git a/crates/sdk/tests/unit/sync_runtime_tests.rs b/crates/sdk/tests/unit/sync_runtime_tests.rs @@ -44,7 +44,8 @@ use radroots_outbox::{ use radroots_outbox::{RadrootsOutboxEventState, RadrootsOutboxStatusSummary}; #[cfg(feature = "radrootsd-proxy")] use radroots_transport::{ - RadrootsTransportKind, RadrootsTransportSatisfactionPolicy, RadrootsTransportTarget, + RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI, RadrootsTransportKind, + RadrootsTransportSatisfactionPolicy, RadrootsTransportTarget, }; use radroots_transport_nostr::{ RadrootsRelayOutcomeKind, RadrootsRelayPublishAdapter, RadrootsRelayPublishReceipt, @@ -749,6 +750,50 @@ async fn proxy_claim_publish_marks_retryable_transport_errors() { #[cfg(feature = "radrootsd-proxy")] #[tokio::test] +async fn proxy_local_validation_errors_release_claim_before_daemon_publish() { + let (sdk, mut claimed) = claimed_proxy_event("proxy-local-validation-error").await; + let reticulum_target = RadrootsTransportTarget::new( + RadrootsTransportKind::Reticulum, + RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI, + ) + .expect("Reticulum target"); + claimed.delivery_targets[0].transport_kind = reticulum_target.kind; + claimed.delivery_targets[0].endpoint_uri = reticulum_target.uri; + claimed.delivery_targets[0].endpoint_fingerprint = reticulum_target.fingerprint; + let sync = sdk.sync(); + let adapter = + RadrootsdProxyPublishAdapter::new(RadrootsdProxyConfig::new("http://127.0.0.1:9/rpc")); + let error = + push_proxy_claimed_outbox_event(&sync, &adapter, &claimed, 60_000, 1_700_000_000_000) + .await + .expect_err("local proxy validation error"); + + assert!(matches!( + error, + RadrootsSdkError::InvalidRequest { message } + if message.contains("radrootsd proxy outbox publish") + && message.contains("Reticulum target") + )); + let stored = sdk + ._outbox + .get_event(claimed.outbox_event_id) + .await + .expect("stored") + .expect("stored"); + assert_eq!(stored.state, RadrootsOutboxEventState::FailedTerminal); + assert!(stored.claim_token.is_none()); + assert!(stored.event_store_ingested); + assert!( + stored + .last_error + .as_deref() + .expect("last error") + .contains("Reticulum target") + ); +} + +#[cfg(feature = "radrootsd-proxy")] +#[tokio::test] async fn proxy_completion_updates_outbox_for_success_retryable_and_terminal_receipts() { let cases = [ (