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:
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(