commit 627b6823c9f3f44f0e1bd256ba019c92a046e22d
parent 544d5c40006e8e3b27e1ca1e0a13543237abf4b3
Author: triesap <tyson@radroots.org>
Date: Fri, 10 Jul 2026 01:43:34 +0000
sdk: harden proxy satisfaction metadata
- reject delivered-class radrootsd proxy satisfaction before local ingest or daemon publish
- preserve scoped target metadata through explicit proxy requests and receipts
- correlate proxy completion outcomes by target scope for duplicate relay endpoints
- cover scoped metadata and warning hygiene with SDK tests and source guards
Diffstat:
4 files changed, 375 insertions(+), 18 deletions(-)
diff --git a/crates/sdk/src/sync_runtime.rs b/crates/sdk/src/sync_runtime.rs
@@ -31,14 +31,14 @@ use radroots_trade::projection::{
RadrootsProjectionRefreshReceipt, RadrootsProjectionRefreshRequest,
refresh_product_projections,
};
-#[cfg(all(feature = "runtime", feature = "radrootsd-proxy"))]
-use radroots_transport::RadrootsTransportSatisfactionPolicy;
#[cfg(feature = "runtime")]
use radroots_transport::{
RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE, RadrootsTransportImplementationState,
RadrootsTransportKind, RadrootsTransportOutcomeKind, RadrootsTransportStatus,
RadrootsTransportTarget,
};
+#[cfg(all(feature = "runtime", feature = "radrootsd-proxy"))]
+use radroots_transport::{RadrootsTransportSatisfactionClass, RadrootsTransportSatisfactionPolicy};
#[cfg(all(feature = "runtime", feature = "transport-nostr-runtime"))]
use radroots_transport_nostr::RadrootsNostrClientPublishAdapter;
#[cfg(feature = "runtime")]
@@ -1175,6 +1175,7 @@ fn proxy_delivery_policy_from_remaining(
required_remaining: usize,
satisfaction_policy: &RadrootsTransportSatisfactionPolicy,
) -> Result<TransportPublishDeliveryPolicy, RadrootsSdkError> {
+ reject_delivered_proxy_satisfaction(satisfaction_policy)?;
if ready_target_count == 0 || required_remaining == 0 {
return Ok(TransportPublishDeliveryPolicy::Any);
}
@@ -1200,6 +1201,21 @@ fn proxy_delivery_policy_from_remaining(
}
#[cfg(all(feature = "runtime", feature = "radrootsd-proxy"))]
+fn reject_delivered_proxy_satisfaction(
+ satisfaction_policy: &RadrootsTransportSatisfactionPolicy,
+) -> Result<(), RadrootsSdkError> {
+ if satisfaction_policy.target_satisfaction_class()
+ == Some(RadrootsTransportSatisfactionClass::Delivered)
+ {
+ return Err(RadrootsSdkError::InvalidRequest {
+ message: "radrootsd proxy publish only supports accepted-class satisfaction policies"
+ .to_owned(),
+ });
+ }
+ Ok(())
+}
+
+#[cfg(all(feature = "runtime", feature = "radrootsd-proxy"))]
fn proxy_outbox_idempotency_key(
outbox_event_id: i64,
attempt_count: i64,
@@ -1344,6 +1360,14 @@ fn transport_publish_target_from_outbox_target(
Ok(TransportPublishTarget {
transport_kind: target.transport_kind.canonical_label(),
endpoint_uri: target.endpoint_uri.as_str().to_owned(),
+ target_scope: target
+ .target_scope
+ .as_ref()
+ .map(|scope| scope.as_str().to_owned()),
+ target_label: target
+ .target_label
+ .as_ref()
+ .map(|label| label.as_str().to_owned()),
preview_behavior: None,
})
}
@@ -1355,6 +1379,8 @@ fn proxy_target_matches_outcome(
) -> bool {
target.transport_kind.canonical_label() == outcome.transport_kind
&& target.endpoint_uri.as_str() == outcome.endpoint_uri
+ && target.target_scope.as_ref().map(|scope| scope.as_str())
+ == outcome.target_scope.as_deref()
}
#[cfg(all(feature = "runtime", feature = "radrootsd-proxy"))]
@@ -1614,8 +1640,8 @@ fn push_proxy_target_receipt(outcome: TransportPublishTargetOutcome) -> PushOutb
PushOutboxTargetReceipt {
transport_kind: outcome.transport_kind,
endpoint_uri: outcome.endpoint_uri,
- target_scope: None,
- target_label: None,
+ target_scope: outcome.target_scope,
+ target_label: outcome.target_label,
outcome_kind: push_proxy_target_outcome_kind(outcome.outcome_kind),
transport_outcome_kind: Some(push_proxy_transport_outcome_kind(outcome.outcome_kind)),
attempted: outcome.attempted,
diff --git a/crates/sdk/tests/source_boundary.rs b/crates/sdk/tests/source_boundary.rs
@@ -1477,6 +1477,10 @@ fn sdk_proxy_surfaces_reject_removed_daemon_publish_proxy_identifiers() {
"assert!(!stored_before.event_store_ingested)",
"assert!(!stored.event_store_ingested)",
"with_timeout(Duration::from_millis(50))",
+ "proxy_delivery_policy_rejects_delivered_satisfaction_before_daemon_publish",
+ "proxy_outbox_target_conversion_preserves_nostr_scope_and_label",
+ "proxy_completion_matches_duplicate_endpoint_targets_by_scope",
+ "push_proxy_event_receipt_preserves_daemon_target_metadata",
"proxy_completion_rejects_duplicate_daemon_outcome_before_local_mutation",
] {
assert!(
@@ -1509,6 +1513,12 @@ fn sdk_proxy_surfaces_reject_removed_daemon_publish_proxy_identifiers() {
"PushOutboxEventState::PreviewUnavailable",
"PushOutboxTargetOutcomeKind::DeferredUntilImplemented",
"PushOutboxTargetOutcomeKind::PreviewUnavailable",
+ "reject_delivered_proxy_satisfaction",
+ "RadrootsTransportSatisfactionClass::Delivered",
+ "target.target_scope.as_ref()",
+ "outcome.target_scope.as_deref()",
+ "target_scope: outcome.target_scope",
+ "target_label: outcome.target_label",
] {
assert!(
sync_runtime_source.contains(required),
@@ -1531,6 +1541,18 @@ fn sdk_proxy_surfaces_reject_removed_daemon_publish_proxy_identifiers() {
"push_proxy_event_receipt must not use production panic path `{forbidden}`"
);
}
+
+ let proxy_target_receipt_source = source_between(
+ sync_runtime_source.as_str(),
+ "fn push_proxy_target_receipt",
+ "fn push_proxy_target_outcome_kind",
+ );
+ for forbidden in ["target_scope: None", "target_label: None"] {
+ assert!(
+ !proxy_target_receipt_source.contains(forbidden),
+ "push_proxy_target_receipt must not hard-code daemon metadata field `{forbidden}`"
+ );
+ }
}
#[test]
diff --git a/crates/sdk/tests/unit/adapters_radrootsd_tests.rs b/crates/sdk/tests/unit/adapters_radrootsd_tests.rs
@@ -168,6 +168,8 @@ fn job(outcome_kind: TransportPublishOutcomeKind) -> TransportPublishJobView {
targets: vec![TransportPublishTargetOutcome {
transport_kind: "nostr".to_owned(),
endpoint_uri: "wss://relay.example.com".to_owned(),
+ target_scope: None,
+ target_label: None,
source: TransportPublishTargetSource::Request,
attempted: true,
outcome_kind,
@@ -186,6 +188,8 @@ fn explicit_nostr_job(
.map(|endpoint| TransportPublishTargetOutcome {
transport_kind: "nostr".to_owned(),
endpoint_uri: endpoint.clone(),
+ target_scope: None,
+ target_label: None,
source: TransportPublishTargetSource::Request,
attempted: true,
outcome_kind: TransportPublishOutcomeKind::Accepted,
@@ -244,6 +248,8 @@ fn reticulum_deferred_job() -> TransportPublishJobView {
targets: vec![TransportPublishTargetOutcome {
transport_kind: "reticulum".to_owned(),
endpoint_uri: RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI.to_owned(),
+ target_scope: None,
+ target_label: None,
source: TransportPublishTargetSource::ReticulumPreview,
attempted: false,
outcome_kind: TransportPublishOutcomeKind::DeferredUntilImplemented,
@@ -253,13 +259,6 @@ fn reticulum_deferred_job() -> TransportPublishJobView {
}
}
-fn explicit_nostr_response_json(
- endpoints: Vec<String>,
- delivery_policy: TransportPublishDeliveryPolicy,
-) -> String {
- publish_response_json_for_job(explicit_nostr_job(endpoints, delivery_policy))
-}
-
fn publish_response_json_for_job(job: TransportPublishJobView) -> String {
serde_json::json!({
"jsonrpc": "2.0",
@@ -473,10 +472,18 @@ async fn publish_event_posts_transport_publish_jsonrpc() {
#[tokio::test]
async fn publish_signed_event_posts_typed_proxy_request() {
- let response_json = explicit_nostr_response_json(
+ let mut response_job = explicit_nostr_job(
vec!["wss://relay.example.com".to_owned()],
TransportPublishDeliveryPolicy::All,
);
+ response_job.target_policy = TransportPublishTargetPolicy::explicit_targets(vec![
+ TransportPublishTarget::nostr("wss://relay.example.com")
+ .with_scope("farm.local")
+ .with_label("Farm relay"),
+ ]);
+ response_job.targets[0].target_scope = Some("farm.local".to_owned());
+ response_job.targets[0].target_label = Some("Farm relay".to_owned());
+ let response_json = publish_response_json_for_job(response_job);
let (endpoint, handle) = spawn_http_server("200 OK", response_json.as_str());
let adapter = RadrootsdProxyPublishAdapter::new(
RadrootsdProxyConfig::new(endpoint)
@@ -488,7 +495,9 @@ async fn publish_signed_event_posts_typed_proxy_request() {
.publish_signed_event(RadrootsdProxyPublishRequest {
signed_event: signed_event(),
target_policy: TransportPublishTargetPolicy::explicit_targets(vec![
- TransportPublishTarget::nostr("wss://relay.example.com"),
+ TransportPublishTarget::nostr("wss://relay.example.com")
+ .with_scope("farm.local")
+ .with_label("Farm relay"),
]),
delivery_policy: TransportPublishDeliveryPolicy::All,
idempotency_key: Some("idem-typed".to_owned()),
@@ -498,6 +507,14 @@ async fn publish_signed_event_posts_typed_proxy_request() {
.expect("typed publish");
assert!(receipt.job.delivery_satisfied);
+ assert_eq!(
+ receipt.job.targets[0].target_scope.as_deref(),
+ Some("farm.local")
+ );
+ assert_eq!(
+ receipt.job.targets[0].target_label.as_deref(),
+ Some("Farm relay")
+ );
let recorded = handle.join().expect("server thread");
assert!(
recorded
@@ -512,6 +529,14 @@ async fn publish_signed_event_posts_typed_proxy_request() {
body["params"]["target_policy"]["targets"][0]["endpoint_uri"],
"wss://relay.example.com"
);
+ assert_eq!(
+ body["params"]["target_policy"]["targets"][0]["target_scope"],
+ "farm.local"
+ );
+ assert_eq!(
+ body["params"]["target_policy"]["targets"][0]["target_label"],
+ "Farm relay"
+ );
assert_eq!(body["params"]["idempotency_key"], "idem-typed");
assert_eq!(body["params"]["timeout_ms"], 7_000);
}
@@ -740,6 +765,37 @@ async fn publish_signed_event_rejects_mismatched_explicit_target_outcomes() {
}
#[tokio::test]
+async fn publish_signed_event_rejects_mismatched_scoped_explicit_target_outcomes() {
+ let mut response_job = explicit_nostr_job(
+ vec!["wss://relay.example.com".to_owned()],
+ TransportPublishDeliveryPolicy::Any,
+ );
+ response_job.target_policy = TransportPublishTargetPolicy::explicit_targets(vec![
+ TransportPublishTarget::nostr("wss://relay.example.com").with_scope("farm.local"),
+ ]);
+ response_job.targets[0].target_scope = Some("farm.remote".to_owned());
+ let response_json = publish_response_json_for_job(response_job);
+ let (endpoint, _handle) = spawn_http_server("200 OK", response_json.as_str());
+ let adapter = RadrootsdProxyPublishAdapter::new(RadrootsdProxyConfig::new(endpoint));
+
+ let error = adapter
+ .publish_signed_event(RadrootsdProxyPublishRequest {
+ signed_event: signed_event(),
+ target_policy: TransportPublishTargetPolicy::explicit_targets(vec![
+ TransportPublishTarget::nostr("wss://relay.example.com").with_scope("farm.local"),
+ ]),
+ delivery_policy: TransportPublishDeliveryPolicy::Any,
+ idempotency_key: Some("idem-scoped-explicit-outcome-mismatch".to_owned()),
+ timeout_ms: None,
+ })
+ .await
+ .expect_err("scoped explicit target outcome mismatch");
+
+ assert!(matches!(error, RadrootsdError::MalformedResponse(_)));
+ assert_message(error, "explicit target policy");
+}
+
+#[tokio::test]
async fn publish_event_http_errors_omit_body_and_token_material() {
let body = "{\"error\":\"token-secret content carrots\"}";
let (endpoint, _handle) = spawn_http_server("503 Service Unavailable", body);
@@ -814,6 +870,8 @@ async fn publish_signed_event_rejects_invalid_protocol_requests_before_http() {
TransportPublishTargetPolicy::explicit_targets(vec![TransportPublishTarget {
transport_kind: "nostr".to_owned(),
endpoint_uri: "wss://relay.example.com".to_owned(),
+ target_scope: None,
+ target_label: None,
preview_behavior: Some(TransportPublishPreviewBehavior::RejectDeliveryAttempts),
}]);
let mut explicit_proxy_target = base.clone();
@@ -821,6 +879,8 @@ async fn publish_signed_event_rejects_invalid_protocol_requests_before_http() {
TransportPublishTargetPolicy::explicit_targets(vec![TransportPublishTarget {
transport_kind: "proxy".to_owned(),
endpoint_uri: "radrootsd-proxy:publish".to_owned(),
+ target_scope: None,
+ target_label: None,
preview_behavior: None,
}]);
let proxy_error = adapter
diff --git a/crates/sdk/tests/unit/sync_runtime_tests.rs b/crates/sdk/tests/unit/sync_runtime_tests.rs
@@ -45,8 +45,8 @@ use radroots_outbox::{
use radroots_outbox::{RadrootsOutboxEventState, RadrootsOutboxStatusSummary};
#[cfg(feature = "radrootsd-proxy")]
use radroots_transport::{
- RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI, RadrootsTransportKind,
- RadrootsTransportSatisfactionPolicy, RadrootsTransportTarget,
+ RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI, RadrootsTransportKind, RadrootsTransportMeshScopeId,
+ RadrootsTransportSatisfactionPolicy, RadrootsTransportTarget, RadrootsTransportTargetLabel,
};
use radroots_transport_nostr::{
RadrootsRelayOutcomeKind, RadrootsRelayPublishAdapter, RadrootsRelayPublishReceipt,
@@ -55,8 +55,8 @@ use radroots_transport_nostr::{
#[cfg(feature = "radrootsd-proxy")]
use radroots_transport_publish_protocol::{
NostrPublishTargetSourcePolicy, TransportPublishDeliveryPolicy, TransportPublishJobStatus,
- TransportPublishJobView, TransportPublishOutcomeKind, TransportPublishTargetOutcome,
- TransportPublishTargetPolicy, TransportPublishTargetSource,
+ TransportPublishJobView, TransportPublishOutcomeKind, TransportPublishTarget,
+ TransportPublishTargetOutcome, TransportPublishTargetPolicy, TransportPublishTargetSource,
};
use std::collections::BTreeSet;
#[cfg(feature = "radrootsd-proxy")]
@@ -191,6 +191,20 @@ async fn claimed_uningested_proxy_event(
d_tag: &str,
proxy_endpoint: &str,
) -> (crate::RadrootsClient, RadrootsOutboxClaimedEvent) {
+ claimed_uningested_proxy_event_with_satisfaction(
+ d_tag,
+ proxy_endpoint,
+ RadrootsTransportSatisfactionPolicy::all_accepted(),
+ )
+ .await
+}
+
+#[cfg(feature = "radrootsd-proxy")]
+async fn claimed_uningested_proxy_event_with_satisfaction(
+ d_tag: &str,
+ proxy_endpoint: &str,
+ satisfaction_policy: RadrootsTransportSatisfactionPolicy,
+) -> (crate::RadrootsClient, RadrootsOutboxClaimedEvent) {
let sdk = crate::RadrootsClient::builder()
.fixed_clock(crate::RadrootsSdkTimestamp::from_unix_seconds(
1_700_000_000,
@@ -210,7 +224,7 @@ async fn claimed_uningested_proxy_event(
RadrootsOutboxDeliveryPlanInput::new(
"proxy",
1,
- RadrootsTransportSatisfactionPolicy::all_accepted(),
+ satisfaction_policy,
vec![proxy_target],
),
1_700_000_000_000,
@@ -277,6 +291,29 @@ fn assert_no_transport_publish_request(listener: &TcpListener) {
}
#[cfg(feature = "radrootsd-proxy")]
+fn delivery_target_record(
+ delivery_target_id: i64,
+ delivery_plan_id: i64,
+ target: &RadrootsTransportTarget,
+) -> RadrootsOutboxDeliveryTargetRecord {
+ RadrootsOutboxDeliveryTargetRecord {
+ delivery_target_id,
+ delivery_plan_id,
+ transport_kind: target.kind.clone(),
+ endpoint_uri: target.uri.clone(),
+ target_scope: target.scope.clone(),
+ target_label: target.label.clone(),
+ endpoint_fingerprint: target.fingerprint.clone(),
+ status: RadrootsOutboxDeliveryTargetStatus::Pending,
+ last_outcome_kind: None,
+ attempt_count: 0,
+ last_attempt_at_ms: None,
+ completed_at_ms: None,
+ last_error: None,
+ }
+}
+
+#[cfg(feature = "radrootsd-proxy")]
fn proxy_job(event_id: &str, outcome_kind: TransportPublishOutcomeKind) -> TransportPublishJobView {
let delivery_satisfied = outcome_kind.counts_toward_satisfaction();
let retryable = outcome_kind.is_retryable();
@@ -315,6 +352,8 @@ fn proxy_job(event_id: &str, outcome_kind: TransportPublishOutcomeKind) -> Trans
targets: vec![TransportPublishTargetOutcome {
transport_kind: "nostr".to_owned(),
endpoint_uri: "wss://relay.example.com".to_owned(),
+ target_scope: None,
+ target_label: None,
source: TransportPublishTargetSource::Request,
attempted: true,
outcome_kind,
@@ -735,6 +774,53 @@ async fn proxy_push_empty_queue_and_private_helpers_are_deterministic() {
}
#[cfg(feature = "radrootsd-proxy")]
+#[tokio::test]
+async fn proxy_delivery_policy_rejects_delivered_satisfaction_before_daemon_publish() {
+ let listener = TcpListener::bind("127.0.0.1:0").expect("bind proxy listener");
+ let endpoint = format!("http://{}/rpc", listener.local_addr().expect("addr"));
+ let (sdk, claimed) = claimed_uningested_proxy_event_with_satisfaction(
+ "proxy-delivered-rejected",
+ endpoint.as_str(),
+ RadrootsTransportSatisfactionPolicy::all_delivered(),
+ )
+ .await;
+ let sync = sdk.sync();
+ let adapter = RadrootsdProxyPublishAdapter::new(
+ RadrootsdProxyConfig::new(endpoint).with_timeout(Duration::from_millis(50)),
+ );
+
+ let error =
+ push_proxy_claimed_outbox_event(&sync, &adapter, &claimed, 60_000, 1_700_000_000_000)
+ .await
+ .expect_err("delivered-class proxy satisfaction rejected");
+ assert_no_transport_publish_request(&listener);
+
+ assert!(matches!(
+ error,
+ RadrootsSdkError::InvalidRequest { message }
+ if message.contains("radrootsd proxy publish")
+ && message.contains("accepted-class satisfaction")
+ ));
+ 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_eq!(stored.event_store_ingested_at_ms, None);
+ assert!(
+ stored
+ .last_error
+ .as_deref()
+ .expect("last error")
+ .contains("accepted-class satisfaction")
+ );
+}
+
+#[cfg(feature = "radrootsd-proxy")]
#[test]
fn proxy_outbox_target_conversion_rejects_reticulum_targets_before_behavior_loss() {
let target = RadrootsTransportTarget::new(
@@ -772,6 +858,27 @@ fn proxy_outbox_target_conversion_rejects_reticulum_targets_before_behavior_loss
#[cfg(feature = "radrootsd-proxy")]
#[test]
+fn proxy_outbox_target_conversion_preserves_nostr_scope_and_label() {
+ let target = RadrootsTransportTarget::new_with_metadata(
+ RadrootsTransportKind::Nostr,
+ "wss://relay.example.com",
+ Some(RadrootsTransportMeshScopeId::parse("farm.local").expect("scope")),
+ Some(RadrootsTransportTargetLabel::parse("Farm relay").expect("label")),
+ )
+ .expect("scoped Nostr target");
+ let record = delivery_target_record(1, 1, &target);
+
+ let converted = transport_publish_target_from_outbox_target(&record).expect("converted target");
+
+ assert_eq!(converted.transport_kind, "nostr");
+ assert_eq!(converted.endpoint_uri, "wss://relay.example.com");
+ assert_eq!(converted.target_scope.as_deref(), Some("farm.local"));
+ assert_eq!(converted.target_label.as_deref(), Some("Farm relay"));
+ assert_eq!(converted.preview_behavior, None);
+}
+
+#[cfg(feature = "radrootsd-proxy")]
+#[test]
fn proxy_outbox_target_conversion_rejects_proxy_targets_before_daemon_explicit_target() {
let target =
RadrootsTransportTarget::new(RadrootsTransportKind::Proxy, "http://127.0.0.1:8080/rpc")
@@ -1262,6 +1369,125 @@ async fn proxy_completion_updates_outbox_for_success_retryable_and_terminal_rece
#[cfg(feature = "radrootsd-proxy")]
#[tokio::test]
+async fn proxy_completion_matches_duplicate_endpoint_targets_by_scope() {
+ let sdk = crate::RadrootsClient::builder()
+ .fixed_clock(crate::RadrootsSdkTimestamp::from_unix_seconds(
+ 1_700_000_000,
+ ))
+ .build()
+ .await
+ .expect("sdk");
+ let draft = proxy_frozen_draft("proxy-complete-scoped-targets");
+ let signed_event = ProxyFixtureSigner::new()
+ .sign_frozen_draft(&draft)
+ .expect("signed event");
+ let farm_a = RadrootsTransportTarget::new_with_metadata(
+ RadrootsTransportKind::Nostr,
+ "wss://relay.example.com",
+ Some(RadrootsTransportMeshScopeId::parse("farm.a").expect("farm a scope")),
+ Some(RadrootsTransportTargetLabel::parse("Farm A").expect("farm a label")),
+ )
+ .expect("farm a target");
+ let farm_b = RadrootsTransportTarget::new_with_metadata(
+ RadrootsTransportKind::Nostr,
+ "wss://relay.example.com",
+ Some(RadrootsTransportMeshScopeId::parse("farm.b").expect("farm b scope")),
+ Some(RadrootsTransportTargetLabel::parse("Farm B").expect("farm b label")),
+ )
+ .expect("farm b target");
+ let enqueue = sdk
+ ._outbox
+ .enqueue_signed_operation(
+ RadrootsOutboxSignedOperationInput::new(
+ "sync.proxy.unit.v1",
+ draft,
+ signed_event.clone(),
+ RadrootsOutboxDeliveryPlanInput::new(
+ "proxy.scoped",
+ 2,
+ RadrootsTransportSatisfactionPolicy::all_accepted(),
+ vec![farm_a, farm_b],
+ ),
+ true,
+ 1_700_000_000_000,
+ 1_700_000_000_000,
+ )
+ .with_idempotency_key("proxy-complete-scoped-targets"),
+ )
+ .await
+ .expect("scoped proxy event");
+ let claimed = sdk
+ ._outbox
+ .claim_next_ready_signed_event(
+ CLAIM_OWNER,
+ "proxy-scoped-target-claim",
+ 1_700_000_060_000,
+ 1_700_000_000_000,
+ )
+ .await
+ .expect("claim")
+ .expect("claim");
+ assert_eq!(claimed.outbox_event_id, enqueue.outbox_event_id);
+ let mut publish = proxy_job(
+ signed_event.id.as_str(),
+ TransportPublishOutcomeKind::Accepted,
+ );
+ publish.target_policy = TransportPublishTargetPolicy::explicit_targets(vec![
+ TransportPublishTarget::nostr("wss://relay.example.com")
+ .with_scope("farm.a")
+ .with_label("Farm A"),
+ TransportPublishTarget::nostr("wss://relay.example.com")
+ .with_scope("farm.b")
+ .with_label("Farm B"),
+ ]);
+ publish.delivery_policy = TransportPublishDeliveryPolicy::All;
+ publish.delivery_satisfied = false;
+ publish.status = TransportPublishJobStatus::DeliveryUnsatisfiedRetryable;
+ publish.terminal = false;
+ publish.target_count = 2;
+ publish.acknowledged_count = 1;
+ publish.retryable_count = 1;
+ publish.terminal_count = 0;
+ publish.completed_at_ms = None;
+ publish.targets[0].target_scope = Some("farm.a".to_owned());
+ publish.targets[0].target_label = Some("Farm A".to_owned());
+ let mut farm_b_outcome = publish.targets[0].clone();
+ farm_b_outcome.target_scope = Some("farm.b".to_owned());
+ farm_b_outcome.target_label = Some("Farm B".to_owned());
+ farm_b_outcome.outcome_kind = TransportPublishOutcomeKind::Timeout;
+ farm_b_outcome.message = Some("daemon timeout".to_owned());
+ publish.targets.push(farm_b_outcome);
+
+ let sync = sdk.sync();
+ complete_proxy_publish_attempt(&sync, &claimed, &publish, 60_000, 1_700_000_000_000)
+ .await
+ .expect("complete scoped proxy attempt");
+ let targets = sdk
+ ._outbox
+ .delivery_targets(claimed.outbox_event_id)
+ .await
+ .expect("targets");
+ let farm_a_target = targets
+ .iter()
+ .find(|target| target.target_scope.as_ref().map(|scope| scope.as_str()) == Some("farm.a"))
+ .expect("farm a target");
+ let farm_b_target = targets
+ .iter()
+ .find(|target| target.target_scope.as_ref().map(|scope| scope.as_str()) == Some("farm.b"))
+ .expect("farm b target");
+
+ assert_eq!(
+ farm_a_target.status,
+ RadrootsOutboxDeliveryTargetStatus::Accepted
+ );
+ assert_eq!(
+ farm_b_target.status,
+ RadrootsOutboxDeliveryTargetStatus::FailedRetryable
+ );
+}
+
+#[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(
@@ -1300,6 +1526,29 @@ async fn proxy_completion_rejects_duplicate_daemon_outcome_before_local_mutation
#[cfg(feature = "radrootsd-proxy")]
#[test]
+fn push_proxy_event_receipt_preserves_daemon_target_metadata() {
+ let mut publish = proxy_job(
+ "a".repeat(64).as_str(),
+ TransportPublishOutcomeKind::Accepted,
+ );
+ publish.targets[0].target_scope = Some("farm.local".to_owned());
+ publish.targets[0].target_label = Some("Farm relay".to_owned());
+
+ let receipt = push_proxy_event_receipt(1, publish).expect("receipt");
+
+ assert_eq!(receipt.targets.len(), 1);
+ assert_eq!(
+ receipt.targets[0].target_scope.as_deref(),
+ Some("farm.local")
+ );
+ assert_eq!(
+ receipt.targets[0].target_label.as_deref(),
+ Some("Farm relay")
+ );
+}
+
+#[cfg(feature = "radrootsd-proxy")]
+#[test]
fn push_proxy_event_receipt_returns_typed_error_for_invalid_daemon_event_id() {
let error = push_proxy_event_receipt(
1,