lib

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

commit 5e0dcf40bef08898f267fd6157617e79a02ee6be
parent 4a3cbe30a88fee436f41094acfcd02caaf8eaa1c
Author: triesap <tyson@radroots.org>
Date:   Wed,  8 Jul 2026 05:23:45 +0000

transport: guard proxy completion matches

Reject duplicate or ambiguous radrootsd proxy outcome matches before mutating local outbox delivery target state. Add unit coverage for duplicate daemon outcomes so a claimed target cannot be completed twice from malformed publish state.

Diffstat:
Mcrates/sdk/src/sync_runtime.rs | 35++++++++++++++++++++++++++++++-----
Mcrates/sdk/tests/unit/sync_runtime_tests.rs | 38++++++++++++++++++++++++++++++++++++++
2 files changed, 68 insertions(+), 5 deletions(-)

diff --git a/crates/sdk/src/sync_runtime.rs b/crates/sdk/src/sync_runtime.rs @@ -966,16 +966,41 @@ async fn complete_proxy_publish_attempt( now_ms: i64, ) -> Result<(), RadrootsSdkError> { let mut completed_target_ids = std::collections::BTreeSet::new(); + let mut matched_outcomes = Vec::new(); for outcome in &publish.targets { - if let Some(target) = claimed + let matched_targets = claimed .delivery_targets .iter() .filter(|target| target.status.is_ready_for_attempt()) - .find(|target| proxy_target_matches_outcome(target, outcome)) - { - complete_proxy_delivery_target(sync, claimed, target, outcome, now_ms).await?; - completed_target_ids.insert(target.delivery_target_id); + .filter(|target| proxy_target_matches_outcome(target, outcome)) + .collect::<Vec<_>>(); + if matched_targets.is_empty() { + continue; } + if matched_targets.len() > 1 { + return Err(RadrootsSdkError::InvalidRequest { + message: format!( + "radrootsd proxy publish outcome for {} {} matched multiple ready delivery targets on outbox event {}", + outcome.transport_kind, outcome.endpoint_uri, claimed.outbox_event_id + ), + }); + } + let target = matched_targets[0]; + if !completed_target_ids.insert(target.delivery_target_id) { + return Err(RadrootsSdkError::InvalidRequest { + message: format!( + "radrootsd proxy publish outcome for {} {} matched delivery target {} more than once on outbox event {}", + outcome.transport_kind, + outcome.endpoint_uri, + target.delivery_target_id, + claimed.outbox_event_id + ), + }); + } + matched_outcomes.push((target, outcome)); + } + for (target, outcome) in matched_outcomes { + complete_proxy_delivery_target(sync, claimed, target, outcome, now_ms).await?; } for target in claimed .delivery_targets diff --git a/crates/sdk/tests/unit/sync_runtime_tests.rs b/crates/sdk/tests/unit/sync_runtime_tests.rs @@ -1250,6 +1250,44 @@ async fn proxy_completion_updates_outbox_for_success_retryable_and_terminal_rece } #[cfg(feature = "radrootsd-proxy")] +#[tokio::test] +async fn proxy_completion_rejects_duplicate_daemon_outcome_before_local_mutation() { + let (sdk, claimed) = claimed_proxy_event("proxy-complete-duplicate-outcome").await; + let mut publish = proxy_job( + claimed + .signed_event + .as_ref() + .expect("signed event") + .id + .as_str(), + TransportPublishOutcomeKind::Accepted, + ); + publish.targets.push(publish.targets[0].clone()); + + let sync = sdk.sync(); + let error = + complete_proxy_publish_attempt(&sync, &claimed, &publish, 60_000, 1_700_000_000_000) + .await + .expect_err("duplicate daemon outcome must fail closed"); + + assert!(matches!( + error, + RadrootsSdkError::InvalidRequest { message } + if message.contains("matched delivery target") + && message.contains("more than once") + )); + let targets = sdk + ._outbox + .delivery_targets(claimed.outbox_event_id) + .await + .expect("targets"); + assert_ne!( + targets[0].status, + RadrootsOutboxDeliveryTargetStatus::Accepted + ); +} + +#[cfg(feature = "radrootsd-proxy")] #[test] fn push_proxy_event_receipt_returns_typed_error_for_invalid_daemon_event_id() { let error = push_proxy_event_receipt(