@adcp/sdk 7.11.0 → 7.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (282) hide show
  1. package/compliance/cache/3.1.0-rc.2/domains/brand/index.yaml +160 -0
  2. package/compliance/cache/3.1.0-rc.2/domains/brand/scenarios/distributed_brand_resolution.yaml +415 -0
  3. package/compliance/cache/3.1.0-rc.2/domains/brand/scenarios/single_side_trust_extension.yaml +454 -0
  4. package/compliance/cache/3.1.0-rc.2/domains/creative/index.yaml +339 -0
  5. package/compliance/cache/3.1.0-rc.2/domains/creative/scenarios/billing_out_of_band.yaml +153 -0
  6. package/compliance/cache/3.1.0-rc.2/domains/creative/scenarios/canonical_supported_formats.yaml +212 -0
  7. package/compliance/cache/3.1.0-rc.2/domains/creative/scenarios/creative_lifecycle_webhooks.yaml +389 -0
  8. package/compliance/cache/3.1.0-rc.2/domains/creative/scenarios/native_in_feed.yaml +543 -0
  9. package/compliance/cache/3.1.0-rc.2/domains/governance/index.yaml +682 -0
  10. package/compliance/cache/3.1.0-rc.2/domains/media-buy/index.yaml +789 -0
  11. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/audience_buy_flow.yaml +380 -0
  12. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/available_actions.yaml +565 -0
  13. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/billing_finality_delivery.yaml +354 -0
  14. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/canonical_formats.yaml +861 -0
  15. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/clicks_buy_flow.yaml +264 -0
  16. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/completed_views_buy_flow.yaml +344 -0
  17. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/create_media_buy_async.yaml +234 -0
  18. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/creative_fate_after_cancellation.yaml +419 -0
  19. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/creative_reception.yaml +247 -0
  20. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/delivery_reporting.yaml +357 -0
  21. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/dependency_impairment.yaml +633 -0
  22. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/dependency_impairment_cardinality.yaml +800 -0
  23. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/event_dedup_flow.yaml +399 -0
  24. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/frequency_cap_enforcement.yaml +309 -0
  25. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/governance_approved.yaml +214 -0
  26. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/governance_conditions.yaml +199 -0
  27. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/governance_denied.yaml +204 -0
  28. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/governance_denied_recovery.yaml +252 -0
  29. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/invalid_transitions.yaml +289 -0
  30. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/inventory_list_no_match.yaml +148 -0
  31. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/inventory_list_targeting.yaml +276 -0
  32. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/measurement_accountability.yaml +244 -0
  33. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/measurement_terms_rejected.yaml +203 -0
  34. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/package_correlation_legacy_fallback.yaml +113 -0
  35. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/pending_creatives_to_start.yaml +292 -0
  36. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/per_creative_conversion_attribution.yaml +500 -0
  37. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/performance_buy_flow.yaml +428 -0
  38. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/performance_buy_flow_roas.yaml +470 -0
  39. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/product_signal_targeting.yaml +373 -0
  40. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/proposal_finalize.yaml +399 -0
  41. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/proposal_finalize_asap_timing.yaml +264 -0
  42. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/proposal_not_found_errors.yaml +257 -0
  43. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/provenance_audit_observation.yaml +333 -0
  44. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/provenance_enforcement.yaml +517 -0
  45. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/provenance_truth_of_claim.yaml +294 -0
  46. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/reach_buy_flow.yaml +823 -0
  47. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/refine_finalize_exclusivity.yaml +360 -0
  48. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/refine_products.yaml +148 -0
  49. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/vendor_metric_accountability.yaml +293 -0
  50. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/vendor_metric_catalog_precondition.yaml +307 -0
  51. package/compliance/cache/3.1.0-rc.2/domains/media-buy/scenarios/vendor_metric_optimization_flow.yaml +576 -0
  52. package/compliance/cache/3.1.0-rc.2/domains/media-buy/state-machine.yaml +442 -0
  53. package/compliance/cache/3.1.0-rc.2/domains/signals/index.yaml +266 -0
  54. package/compliance/cache/3.1.0-rc.2/domains/sponsored-intelligence/index.yaml +256 -0
  55. package/compliance/cache/3.1.0-rc.2/index.json +356 -0
  56. package/compliance/cache/3.1.0-rc.2/protocols/brand/index.yaml +160 -0
  57. package/compliance/cache/3.1.0-rc.2/protocols/brand/scenarios/distributed_brand_resolution.yaml +415 -0
  58. package/compliance/cache/3.1.0-rc.2/protocols/brand/scenarios/single_side_trust_extension.yaml +454 -0
  59. package/compliance/cache/3.1.0-rc.2/protocols/creative/index.yaml +339 -0
  60. package/compliance/cache/3.1.0-rc.2/protocols/creative/scenarios/billing_out_of_band.yaml +153 -0
  61. package/compliance/cache/3.1.0-rc.2/protocols/creative/scenarios/canonical_supported_formats.yaml +212 -0
  62. package/compliance/cache/3.1.0-rc.2/protocols/creative/scenarios/creative_lifecycle_webhooks.yaml +389 -0
  63. package/compliance/cache/3.1.0-rc.2/protocols/creative/scenarios/native_in_feed.yaml +543 -0
  64. package/compliance/cache/3.1.0-rc.2/protocols/governance/index.yaml +682 -0
  65. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/index.yaml +789 -0
  66. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/audience_buy_flow.yaml +380 -0
  67. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/available_actions.yaml +565 -0
  68. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/billing_finality_delivery.yaml +354 -0
  69. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/canonical_formats.yaml +861 -0
  70. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/clicks_buy_flow.yaml +264 -0
  71. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/completed_views_buy_flow.yaml +344 -0
  72. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/create_media_buy_async.yaml +234 -0
  73. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/creative_fate_after_cancellation.yaml +419 -0
  74. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/creative_reception.yaml +247 -0
  75. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/delivery_reporting.yaml +357 -0
  76. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/dependency_impairment.yaml +633 -0
  77. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/dependency_impairment_cardinality.yaml +800 -0
  78. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/event_dedup_flow.yaml +399 -0
  79. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/frequency_cap_enforcement.yaml +309 -0
  80. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/governance_approved.yaml +214 -0
  81. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/governance_conditions.yaml +199 -0
  82. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/governance_denied.yaml +204 -0
  83. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/governance_denied_recovery.yaml +252 -0
  84. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/invalid_transitions.yaml +289 -0
  85. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/inventory_list_no_match.yaml +148 -0
  86. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/inventory_list_targeting.yaml +276 -0
  87. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/measurement_accountability.yaml +244 -0
  88. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/measurement_terms_rejected.yaml +203 -0
  89. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/package_correlation_legacy_fallback.yaml +113 -0
  90. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/pending_creatives_to_start.yaml +292 -0
  91. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/per_creative_conversion_attribution.yaml +500 -0
  92. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/performance_buy_flow.yaml +428 -0
  93. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/performance_buy_flow_roas.yaml +470 -0
  94. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/product_signal_targeting.yaml +373 -0
  95. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/proposal_finalize.yaml +399 -0
  96. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/proposal_finalize_asap_timing.yaml +264 -0
  97. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/proposal_not_found_errors.yaml +257 -0
  98. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/provenance_audit_observation.yaml +333 -0
  99. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/provenance_enforcement.yaml +517 -0
  100. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/provenance_truth_of_claim.yaml +294 -0
  101. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/reach_buy_flow.yaml +823 -0
  102. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/refine_finalize_exclusivity.yaml +360 -0
  103. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/refine_products.yaml +148 -0
  104. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/vendor_metric_accountability.yaml +293 -0
  105. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/vendor_metric_catalog_precondition.yaml +307 -0
  106. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/scenarios/vendor_metric_optimization_flow.yaml +576 -0
  107. package/compliance/cache/3.1.0-rc.2/protocols/media-buy/state-machine.yaml +442 -0
  108. package/compliance/cache/3.1.0-rc.2/protocols/signals/index.yaml +266 -0
  109. package/compliance/cache/3.1.0-rc.2/protocols/sponsored-intelligence/index.yaml +256 -0
  110. package/compliance/cache/3.1.0-rc.2/specialisms/audience-sync/index.yaml +313 -0
  111. package/compliance/cache/3.1.0-rc.2/specialisms/brand-rights/index.yaml +350 -0
  112. package/compliance/cache/3.1.0-rc.2/specialisms/brand-rights/scenarios/governance_denied.yaml +226 -0
  113. package/compliance/cache/3.1.0-rc.2/specialisms/collection-lists/index.yaml +359 -0
  114. package/compliance/cache/3.1.0-rc.2/specialisms/content-standards/index.yaml +572 -0
  115. package/compliance/cache/3.1.0-rc.2/specialisms/creative-ad-server/index.yaml +409 -0
  116. package/compliance/cache/3.1.0-rc.2/specialisms/creative-generative/generative-seller.yaml +807 -0
  117. package/compliance/cache/3.1.0-rc.2/specialisms/creative-generative/index.yaml +758 -0
  118. package/compliance/cache/3.1.0-rc.2/specialisms/creative-template/index.yaml +510 -0
  119. package/compliance/cache/3.1.0-rc.2/specialisms/governance-aware-seller/index.yaml +143 -0
  120. package/compliance/cache/3.1.0-rc.2/specialisms/governance-aware-seller/scenarios/governance_multi_agent_rejected.yaml +117 -0
  121. package/compliance/cache/3.1.0-rc.2/specialisms/governance-delivery-monitor/index.yaml +441 -0
  122. package/compliance/cache/3.1.0-rc.2/specialisms/governance-spend-authority/denied.yaml +221 -0
  123. package/compliance/cache/3.1.0-rc.2/specialisms/governance-spend-authority/index.yaml +330 -0
  124. package/compliance/cache/3.1.0-rc.2/specialisms/property-lists/index.yaml +482 -0
  125. package/compliance/cache/3.1.0-rc.2/specialisms/sales-broadcast-tv/index.yaml +738 -0
  126. package/compliance/cache/3.1.0-rc.2/specialisms/sales-catalog-driven/index.yaml +840 -0
  127. package/compliance/cache/3.1.0-rc.2/specialisms/sales-guaranteed/index.yaml +601 -0
  128. package/compliance/cache/3.1.0-rc.2/specialisms/sales-non-guaranteed/index.yaml +546 -0
  129. package/compliance/cache/3.1.0-rc.2/specialisms/sales-proposal-mode/index.yaml +586 -0
  130. package/compliance/cache/3.1.0-rc.2/specialisms/sales-social/index.yaml +919 -0
  131. package/compliance/cache/3.1.0-rc.2/specialisms/signal-marketplace/index.yaml +424 -0
  132. package/compliance/cache/3.1.0-rc.2/specialisms/signal-marketplace/scenarios/governance_denied.yaml +210 -0
  133. package/compliance/cache/3.1.0-rc.2/specialisms/signal-owned/index.yaml +317 -0
  134. package/compliance/cache/3.1.0-rc.2/specialisms/sponsored-intelligence/index.yaml +59 -0
  135. package/compliance/cache/3.1.0-rc.2/test-kits/acme-outdoor-live.yaml +78 -0
  136. package/compliance/cache/3.1.0-rc.2/test-kits/acme-outdoor.yaml +223 -0
  137. package/compliance/cache/3.1.0-rc.2/test-kits/billing-gate-runner.yaml +115 -0
  138. package/compliance/cache/3.1.0-rc.2/test-kits/bistro-oranje.yaml +126 -0
  139. package/compliance/cache/3.1.0-rc.2/test-kits/distributed-brand-runner.yaml +281 -0
  140. package/compliance/cache/3.1.0-rc.2/test-kits/nova-motors.yaml +262 -0
  141. package/compliance/cache/3.1.0-rc.2/test-kits/osei-natural.yaml +126 -0
  142. package/compliance/cache/3.1.0-rc.2/test-kits/parallel-dispatch-runner.yaml +196 -0
  143. package/compliance/cache/3.1.0-rc.2/test-kits/rate-limit-trip-runner.yaml +172 -0
  144. package/compliance/cache/3.1.0-rc.2/test-kits/signed-requests-runner.yaml +155 -0
  145. package/compliance/cache/3.1.0-rc.2/test-kits/single-side-trust-runner.yaml +294 -0
  146. package/compliance/cache/3.1.0-rc.2/test-kits/substitution-observer-runner.yaml +688 -0
  147. package/compliance/cache/3.1.0-rc.2/test-kits/summit-foods.yaml +125 -0
  148. package/compliance/cache/3.1.0-rc.2/test-kits/webhook-receiver-runner.yaml +265 -0
  149. package/compliance/cache/3.1.0-rc.2/test-vectors/plan-hash/001-minimal-plan.json +43 -0
  150. package/compliance/cache/3.1.0-rc.2/test-vectors/plan-hash/002-full-plan.json +217 -0
  151. package/compliance/cache/3.1.0-rc.2/test-vectors/plan-hash/003-bookkeeping-stripped.json +60 -0
  152. package/compliance/cache/3.1.0-rc.2/test-vectors/plan-hash/004a-human-review-omitted.json +43 -0
  153. package/compliance/cache/3.1.0-rc.2/test-vectors/plan-hash/004b-human-review-explicit-null.json +49 -0
  154. package/compliance/cache/3.1.0-rc.2/test-vectors/plan-hash/005a-policy-categories-order-1.json +53 -0
  155. package/compliance/cache/3.1.0-rc.2/test-vectors/plan-hash/005b-policy-categories-order-2.json +57 -0
  156. package/compliance/cache/3.1.0-rc.2/test-vectors/plan-hash/006a-ext-trace-v1.json +49 -0
  157. package/compliance/cache/3.1.0-rc.2/test-vectors/plan-hash/006b-ext-trace-v2.json +53 -0
  158. package/compliance/cache/3.1.0-rc.2/test-vectors/plan-hash/007-unicode-objectives.json +43 -0
  159. package/compliance/cache/3.1.0-rc.2/test-vectors/plan-hash/008-numeric-canonicalization.json +65 -0
  160. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/README.md +220 -0
  161. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/canonicalization.json +241 -0
  162. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/keys.json +60 -0
  163. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/negative/001-no-signature-header.json +24 -0
  164. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/negative/002-wrong-tag.json +26 -0
  165. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/negative/003-expired-signature.json +26 -0
  166. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/negative/004-window-too-long.json +26 -0
  167. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/negative/005-alg-not-allowed.json +26 -0
  168. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/negative/006-missing-covered-component.json +26 -0
  169. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/negative/007-missing-content-digest.json +26 -0
  170. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/negative/008-unknown-keyid.json +26 -0
  171. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/negative/009-key-ops-missing-verify.json +27 -0
  172. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/negative/010-content-digest-mismatch.json +33 -0
  173. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/negative/011-malformed-header.json +27 -0
  174. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/negative/012-missing-expires-param.json +26 -0
  175. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/negative/013-expires-le-created.json +27 -0
  176. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/negative/014-missing-nonce-param.json +27 -0
  177. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/negative/015-signature-invalid.json +28 -0
  178. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/negative/016-replayed-nonce.json +35 -0
  179. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/negative/017-key-revoked.json +38 -0
  180. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/negative/018-digest-covered-when-forbidden.json +28 -0
  181. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/negative/019-signature-without-signature-input.json +26 -0
  182. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/negative/020-rate-abuse.json +34 -0
  183. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/negative/021-duplicate-signature-input-label.json +31 -0
  184. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/negative/022-multi-valued-content-type.json +31 -0
  185. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/negative/023-multi-valued-content-digest.json +32 -0
  186. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/negative/024-unquoted-string-param.json +31 -0
  187. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/negative/025-jwk-alg-crv-mismatch.json +43 -0
  188. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/negative/026-non-ascii-host.json +31 -0
  189. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/negative/027-webhook-registration-authentication-unsigned.json +25 -0
  190. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/negative/028-unsigned-protocol-method-required.json +26 -0
  191. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/positive/001-basic-post.json +30 -0
  192. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/positive/002-post-with-content-digest.json +31 -0
  193. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/positive/003-es256-post.json +30 -0
  194. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/positive/004-multiple-signature-labels.json +26 -0
  195. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/positive/005-default-port-stripped.json +30 -0
  196. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/positive/006-dot-segment-path.json +30 -0
  197. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/positive/007-query-byte-preserved.json +30 -0
  198. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/positive/008-percent-encoded-path.json +30 -0
  199. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/positive/009-percent-encoded-unreserved-decoded.json +30 -0
  200. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/positive/010-percent-encoded-slash-preserved.json +30 -0
  201. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/positive/011-ipv6-authority.json +30 -0
  202. package/compliance/cache/3.1.0-rc.2/test-vectors/request-signing/positive/012-ipv6-authority-default-port-stripped.json +30 -0
  203. package/compliance/cache/3.1.0-rc.2/test-vectors/webhook-signing/README.md +211 -0
  204. package/compliance/cache/3.1.0-rc.2/test-vectors/webhook-signing/keys.json +61 -0
  205. package/compliance/cache/3.1.0-rc.2/test-vectors/webhook-signing/negative/001-wrong-tag.json +26 -0
  206. package/compliance/cache/3.1.0-rc.2/test-vectors/webhook-signing/negative/002-expired-signature.json +26 -0
  207. package/compliance/cache/3.1.0-rc.2/test-vectors/webhook-signing/negative/003-window-too-long.json +26 -0
  208. package/compliance/cache/3.1.0-rc.2/test-vectors/webhook-signing/negative/004-alg-not-allowed.json +26 -0
  209. package/compliance/cache/3.1.0-rc.2/test-vectors/webhook-signing/negative/005-missing-authority-component.json +26 -0
  210. package/compliance/cache/3.1.0-rc.2/test-vectors/webhook-signing/negative/006-missing-content-digest.json +25 -0
  211. package/compliance/cache/3.1.0-rc.2/test-vectors/webhook-signing/negative/007-unknown-keyid.json +26 -0
  212. package/compliance/cache/3.1.0-rc.2/test-vectors/webhook-signing/negative/008-wrong-adcp-use.json +26 -0
  213. package/compliance/cache/3.1.0-rc.2/test-vectors/webhook-signing/negative/009-content-digest-mismatch.json +26 -0
  214. package/compliance/cache/3.1.0-rc.2/test-vectors/webhook-signing/negative/010-malformed-signature-input.json +26 -0
  215. package/compliance/cache/3.1.0-rc.2/test-vectors/webhook-signing/negative/011-signature-without-input.json +25 -0
  216. package/compliance/cache/3.1.0-rc.2/test-vectors/webhook-signing/negative/012-missing-expires-param.json +26 -0
  217. package/compliance/cache/3.1.0-rc.2/test-vectors/webhook-signing/negative/013-expires-le-created.json +26 -0
  218. package/compliance/cache/3.1.0-rc.2/test-vectors/webhook-signing/negative/014-missing-nonce-param.json +26 -0
  219. package/compliance/cache/3.1.0-rc.2/test-vectors/webhook-signing/negative/015-signature-invalid.json +26 -0
  220. package/compliance/cache/3.1.0-rc.2/test-vectors/webhook-signing/negative/016-replayed-nonce.json +37 -0
  221. package/compliance/cache/3.1.0-rc.2/test-vectors/webhook-signing/negative/017-key-revoked.json +32 -0
  222. package/compliance/cache/3.1.0-rc.2/test-vectors/webhook-signing/negative/018-rate-abuse.json +33 -0
  223. package/compliance/cache/3.1.0-rc.2/test-vectors/webhook-signing/negative/019-revocation-stale.json +32 -0
  224. package/compliance/cache/3.1.0-rc.2/test-vectors/webhook-signing/negative/020-key-ops-missing-verify.json +41 -0
  225. package/compliance/cache/3.1.0-rc.2/test-vectors/webhook-signing/negative/021-base64-alphabet-mixing.json +26 -0
  226. package/compliance/cache/3.1.0-rc.2/test-vectors/webhook-signing/positive/001-basic-post.json +24 -0
  227. package/compliance/cache/3.1.0-rc.2/test-vectors/webhook-signing/positive/002-es256-post.json +24 -0
  228. package/compliance/cache/3.1.0-rc.2/test-vectors/webhook-signing/positive/003-multiple-signature-labels.json +24 -0
  229. package/compliance/cache/3.1.0-rc.2/test-vectors/webhook-signing/positive/004-default-port-stripped.json +24 -0
  230. package/compliance/cache/3.1.0-rc.2/test-vectors/webhook-signing/positive/005-percent-encoded-path.json +24 -0
  231. package/compliance/cache/3.1.0-rc.2/test-vectors/webhook-signing/positive/006-query-byte-preserved.json +24 -0
  232. package/compliance/cache/3.1.0-rc.2/test-vectors/webhook-signing/positive/007-body-without-idempotency-key.json +25 -0
  233. package/compliance/cache/3.1.0-rc.2/universal/billing-gate-dispatch.yaml +450 -0
  234. package/compliance/cache/3.1.0-rc.2/universal/canonical-format-validate-input.yaml +640 -0
  235. package/compliance/cache/3.1.0-rc.2/universal/capability-discovery.yaml +125 -0
  236. package/compliance/cache/3.1.0-rc.2/universal/collection-lists-pagination-integrity.yaml +306 -0
  237. package/compliance/cache/3.1.0-rc.2/universal/comply-controller-mode-gate.yaml +141 -0
  238. package/compliance/cache/3.1.0-rc.2/universal/content-standards-pagination-integrity.yaml +326 -0
  239. package/compliance/cache/3.1.0-rc.2/universal/deterministic-testing.yaml +1430 -0
  240. package/compliance/cache/3.1.0-rc.2/universal/error-compliance-signals.yaml +377 -0
  241. package/compliance/cache/3.1.0-rc.2/universal/error-compliance.yaml +528 -0
  242. package/compliance/cache/3.1.0-rc.2/universal/fictional-entities.yaml +307 -0
  243. package/compliance/cache/3.1.0-rc.2/universal/get-media-buys-pagination-integrity.yaml +160 -0
  244. package/compliance/cache/3.1.0-rc.2/universal/get-signals-pagination-integrity.yaml +210 -0
  245. package/compliance/cache/3.1.0-rc.2/universal/idempotency.yaml +861 -0
  246. package/compliance/cache/3.1.0-rc.2/universal/notification-config-event-scope.yaml +119 -0
  247. package/compliance/cache/3.1.0-rc.2/universal/notification-config-lifecycle.yaml +337 -0
  248. package/compliance/cache/3.1.0-rc.2/universal/notification-config-rejections.yaml +107 -0
  249. package/compliance/cache/3.1.0-rc.2/universal/pagination-integrity-creative-formats.yaml +265 -0
  250. package/compliance/cache/3.1.0-rc.2/universal/pagination-integrity-list-accounts.yaml +245 -0
  251. package/compliance/cache/3.1.0-rc.2/universal/pagination-integrity.yaml +263 -0
  252. package/compliance/cache/3.1.0-rc.2/universal/property-lists-pagination-integrity.yaml +307 -0
  253. package/compliance/cache/3.1.0-rc.2/universal/read-tool-idempotency.yaml +405 -0
  254. package/compliance/cache/3.1.0-rc.2/universal/runner-output-contract.yaml +1285 -0
  255. package/compliance/cache/3.1.0-rc.2/universal/schema-validation-signals.yaml +181 -0
  256. package/compliance/cache/3.1.0-rc.2/universal/schema-validation.yaml +548 -0
  257. package/compliance/cache/3.1.0-rc.2/universal/security.yaml +539 -0
  258. package/compliance/cache/3.1.0-rc.2/universal/signed-requests.yaml +217 -0
  259. package/compliance/cache/3.1.0-rc.2/universal/stale-response-advisory.yaml +295 -0
  260. package/compliance/cache/3.1.0-rc.2/universal/storyboard-schema.yaml +2194 -0
  261. package/compliance/cache/3.1.0-rc.2/universal/v3-envelope-integrity.yaml +117 -0
  262. package/compliance/cache/3.1.0-rc.2/universal/version-negotiation.yaml +130 -0
  263. package/compliance/cache/3.1.0-rc.2/universal/webhook-emission.yaml +411 -0
  264. package/compliance/cache/3.1.0-rc.2/universal/wholesale-feed-bulk-webhooks.yaml +82 -0
  265. package/compliance/cache/3.1.0-rc.2/universal/wholesale-feed-product-webhooks.yaml +83 -0
  266. package/compliance/cache/3.1.0-rc.2/universal/wholesale-feed-products.yaml +151 -0
  267. package/compliance/cache/3.1.0-rc.2/universal/wholesale-feed-signal-webhooks.yaml +83 -0
  268. package/compliance/cache/3.1.0-rc.2/universal/wholesale-feed-signals.yaml +149 -0
  269. package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
  270. package/dist/lib/testing/storyboard/default-invariants.js +23 -0
  271. package/dist/lib/testing/storyboard/default-invariants.js.map +1 -1
  272. package/dist/lib/testing/storyboard/runner.d.ts.map +1 -1
  273. package/dist/lib/testing/storyboard/runner.js +84 -21
  274. package/dist/lib/testing/storyboard/runner.js.map +1 -1
  275. package/dist/lib/testing/storyboard/types.d.ts +21 -0
  276. package/dist/lib/testing/storyboard/types.d.ts.map +1 -1
  277. package/dist/lib/testing/storyboard/types.js.map +1 -1
  278. package/dist/lib/testing/types.d.ts +9 -0
  279. package/dist/lib/testing/types.d.ts.map +1 -1
  280. package/dist/lib/version.d.ts +3 -3
  281. package/dist/lib/version.js +3 -3
  282. package/package.json +1 -1
@@ -0,0 +1,861 @@
1
+ id: idempotency
2
+ version: "1.0.0"
3
+ title: "Idempotency enforcement"
4
+ category: core
5
+ summary: "Validates that mutating requests enforce idempotency_key — replays return cached responses, key reuse with a different payload returns IDEMPOTENCY_CONFLICT, fresh keys create new resources, and concurrent retries with the same key produce exactly one resource (first-insert-wins under rule 9)."
6
+ track: core
7
+
8
+ # Cross-step assertions (adcontextprotocol/adcp#2639). These convert two
9
+ # reviewer-only checks from `key_reuse_conflict` and `security` into
10
+ # programmatic gates that fail the storyboard at runtime rather than
11
+ # waiting on human review. `idempotency.conflict_no_payload_leak` catches
12
+ # the stolen-key read oracle the key-reuse phase calls out; `context.no_secret_echo`
13
+ # catches credential echo on any step's response. Both ship as default
14
+ # assertions in `@adcp/client` 5.9+ — `import '@adcp/client/testing'`
15
+ # auto-registers them; no additional module loading required. Consumers
16
+ # who need stricter per-repo checks can re-register with their own spec
17
+ # via `registerAssertion(spec, { override: true })` (adcp-client#752).
18
+ invariants:
19
+ - idempotency.conflict_no_payload_leak
20
+ - context.no_secret_echo
21
+
22
+ # Security note for the test harness: sample_request values below use
23
+ # $generate:uuid_v4 and $generate:uuid_v4#alias placeholders. The harness
24
+ # MUST replace these with fresh UUID v4 values PER RUN (not per file, not
25
+ # per deploy). Hardcoding keys across runs creates a cross-tenant replay
26
+ # oracle — a malicious reviewer could probe the cached responses of the
27
+ # compliance principal using the same keys. Multiple references to the
28
+ # same alias (e.g. $generate:uuid_v4#replay_a) in the same run MUST
29
+ # resolve to the same UUID so the replay phase can use it intentionally.
30
+ #
31
+ # The compliance principal SHOULD be a sandbox account whose idempotency
32
+ # cache is isolated per test run (prefix-scoped, or purged on start).
33
+
34
+ narrative: |
35
+ Every mutating request in AdCP carries an idempotency_key so buyers can safely retry
36
+ after network errors without double-booking. This storyboard walks through the
37
+ observable behaviors a seller MUST implement:
38
+
39
+ 1. First call with a fresh key is processed normally and returns the canonical response.
40
+ 2. Replay with the same key and an equivalent payload returns the cached response
41
+ without re-executing side effects (same media_buy_id, no new webhooks fired).
42
+ 3. Replay with the same key but a materially different payload is rejected with
43
+ IDEMPOTENCY_CONFLICT. The buyer has clearly reused a key by mistake.
44
+ 4. A request with a different key is treated as a new request, even if the payload
45
+ is identical — the key is what makes retries safe, not payload equivalence.
46
+ 5. Error responses (returned envelopes, thrown envelopes, and uncaught exceptions)
47
+ do not cache. The next request carrying the same key re-executes the handler.
48
+ This applies to every recovery class, including terminal — terminal states in
49
+ AdCP are mostly state-dependent (e.g., ACCOUNT_SUSPENDED flips after buyer
50
+ remediation) and cached error replay would mask legitimate recovery. Handler
51
+ authors must mutate state last: a handler that writes state then returns or
52
+ throws an error envelope will double-write on retry. See
53
+ security.mdx#idempotency rule 3 ("Only successful responses are cached"),
54
+ which this storyboard validates structurally via the reviewer check on
55
+ key_reuse_conflict. An end-to-end phase that drives a deterministic terminal
56
+ error and replays the key is deferred pending a generic force-error controller
57
+ verb (see adcontextprotocol/adcp#2760).
58
+ 6. Concurrent retries with the same key produce exactly one resource. The seller
59
+ MAY wait-and-replay or return IDEMPOTENCY_IN_FLIGHT — both are conformant under
60
+ security.mdx#idempotency rule 9 (first-insert-wins) as long as the resolved
61
+ response set converges on a single media_buy_id. Graded via the
62
+ parallel_dispatch_runner contract; runners without parallel-dispatch skip
63
+ this phase with a stable not_applicable marker.
64
+
65
+ Missing idempotency_key on any mutating request MUST be rejected with INVALID_REQUEST.
66
+ Sellers MUST declare adcp.idempotency on get_adcp_capabilities — either
67
+ { supported: true, replay_ttl_seconds: N } to opt into replay protection, or
68
+ { supported: false } to declare they do not deduplicate retries. This storyboard
69
+ validates the supported: true behavior; sellers declaring supported: false
70
+ MUST skip this storyboard (they have no replay window to test).
71
+
72
+ **Cache-growth defense (partial runtime grading).** Per
73
+ security.mdx bullet 8 on the idempotency section, sellers MUST apply per-agent
74
+ rate limits on idempotency-cache inserts and MUST return RATE_LIMITED when the
75
+ per-agent insert rate exceeds the configured ceiling. The recommended
76
+ first-deployment ceiling is 60 inserts/sec sustained per agent (3,600/min)
77
+ with burst allowance to 300/sec over rolling 10-second windows. This storyboard
78
+ does NOT attest the threshold itself — burst-volume measurement is
79
+ structurally non-deterministic in CI (runner CPU, network jitter, agent
80
+ cold-start) and better evaluated via operator-side load testing. What it
81
+ DOES attest, via the `rate_limit_replay_invariant` phase below, is the
82
+ invariant that matters most for client safety: a `RATE_LIMITED` response on
83
+ insert MUST NOT be cached as the canonical idempotency replay for that
84
+ key. Otherwise a client retrying past `retry_after` receives the cached
85
+ rate-limit error forever, defeating the point of `retry_after`. The
86
+ threshold (60/300 req/sec) remains seller self-attestation pending
87
+ operator-side tooling.
88
+
89
+ agent:
90
+ interaction_model: media_buy_seller
91
+ capabilities:
92
+ - sells_media
93
+ examples:
94
+ - "Any AdCP seller agent"
95
+
96
+ caller:
97
+ role: buyer_agent
98
+ example: "Compliance test harness"
99
+
100
+ prerequisites:
101
+ description: |
102
+ No special prerequisites. The storyboard uses create_media_buy as the canonical
103
+ mutating request. Sellers that do not support create_media_buy SHOULD still pass
104
+ idempotency compliance on whichever mutating task they do implement.
105
+ test_kit: "test-kits/acme-outdoor.yaml"
106
+
107
+ phases:
108
+ - id: capability_discovery
109
+ title: "Capability discovery"
110
+ narrative: |
111
+ Confirm the agent supports media buying and check whether it declares an
112
+ explicit replay-window TTL.
113
+
114
+ steps:
115
+ - id: get_capabilities
116
+ title: "Check idempotency capability declaration"
117
+ narrative: |
118
+ Call get_adcp_capabilities and verify adcp.idempotency is declared with
119
+ supported: true and a replay_ttl_seconds value. The block is REQUIRED —
120
+ clients MUST NOT fall back to an assumed default, and a seller that omits
121
+ it is non-compliant. Sellers running this storyboard should declare
122
+ supported: true; sellers declaring supported: false pass this step with
123
+ advisory findings and cascade-skip the remaining idempotency phases (they
124
+ have no replay window to test). The full storyboard-level skip gate is
125
+ pending runner support for a precondition mechanism (adcontextprotocol/adcp#3919).
126
+ task: get_adcp_capabilities
127
+ schema_ref: "protocol/get-adcp-capabilities-request.json"
128
+ response_schema_ref: "protocol/get-adcp-capabilities-response.json"
129
+ doc_ref: "/protocol/get_adcp_capabilities"
130
+ comply_scenario: capability_discovery
131
+ stateful: false
132
+ expected: |
133
+ Return capabilities declaring media_buy in supported_protocols AND
134
+ adcp.idempotency with supported: true and replay_ttl_seconds (minimum
135
+ 3600s; recommended 86400s or longer).
136
+ sample_request:
137
+ context:
138
+ correlation_id: "idempotency--get_capabilities"
139
+ validations:
140
+ - check: response_schema
141
+ description: "Response matches get-adcp-capabilities-response.json schema"
142
+ - check: field_present
143
+ path: "supported_protocols"
144
+ description: "Agent declares supported protocols"
145
+ - check: field_value
146
+ path: "adcp.idempotency.supported"
147
+ value: true
148
+ severity: advisory
149
+ permanent_advisory:
150
+ reason: "supported: false is a valid declaration meaning the agent does not deduplicate retries; these agents are not applicable for this storyboard and must not be failed. The check is advisory so capability_discovery passes cleanly for supported: false agents and downstream phases cascade-skip via missing_tool (not_applicable) rather than prerequisite_failed. Full storyboard-level skip (for agents that implement create_media_buy but declare supported: false) requires runner support for a precondition gate (see adcontextprotocol/adcp#3919)."
151
+ description: "Agent declares supported: true for this storyboard (supported: false sellers skip)"
152
+ - check: field_present
153
+ path: "adcp.idempotency.replay_ttl_seconds"
154
+ severity: advisory
155
+ permanent_advisory:
156
+ reason: "replay_ttl_seconds is only required when adcp.idempotency.supported: true; supported: false agents correctly omit this field and must not be penalised for its absence."
157
+ description: "Agent declares replay window TTL (required when supported: true)"
158
+
159
+ - check: field_present
160
+ path: "context"
161
+ description: "Response echoes back the context object"
162
+ - check: field_value
163
+ path: "context.correlation_id"
164
+ value: "idempotency--get_capabilities"
165
+ description: "Context correlation_id returned unchanged"
166
+ context_outputs:
167
+ - name: idempotency_supported
168
+ path: "adcp.idempotency.supported"
169
+
170
+ - id: missing_key
171
+ title: "Reject requests without idempotency_key"
172
+ narrative: |
173
+ A create_media_buy request without idempotency_key MUST be rejected. The schema
174
+ marks the field as required, so this is typically caught at validation time.
175
+
176
+ The reference `@adcp/client` SDK auto-injects `idempotency_key` on mutating
177
+ tasks via `applyIdempotencyInvariant` + client-side shaping. Without an
178
+ explicit opt-out this vector would never reach the agent with a missing key
179
+ — the runner would inject one before dispatch, and the vector would silently
180
+ pass for every SDK-speaking agent regardless of enforcement. The step below
181
+ sets `omit_idempotency_key: true` so the runner skips both its own
182
+ invariant application and the SDK's auto-inject. Do not remove the flag:
183
+ without it, "certified" agents all carry an unverified assertion on this
184
+ vector.
185
+
186
+ steps:
187
+ - id: create_media_buy_missing_key
188
+ title: "Missing idempotency_key returns INVALID_REQUEST"
189
+ narrative: |
190
+ Send a create_media_buy with no idempotency_key. The agent MUST reject this
191
+ with INVALID_REQUEST (or VALIDATION_ERROR as an accepted alternative) and
192
+ MUST NOT create a media buy.
193
+ task: create_media_buy
194
+ schema_ref: "media-buy/create-media-buy-request.json"
195
+ response_schema_ref: "media-buy/create-media-buy-response.json"
196
+ doc_ref: "/media-buy/task-reference/create_media_buy"
197
+ comply_scenario: error_handling
198
+ expect_error: true
199
+ negative_path: schema_invalid
200
+ omit_idempotency_key: true
201
+ stateful: false
202
+ expected: |
203
+ Reject with:
204
+ - code: INVALID_REQUEST or VALIDATION_ERROR
205
+ - recovery: correctable
206
+ - field: idempotency_key (recommended)
207
+
208
+ sample_request:
209
+ account:
210
+ brand:
211
+ domain: "acmeoutdoor.example"
212
+ operator: "pinnacle-agency.example"
213
+ brand:
214
+ domain: "acmeoutdoor.example"
215
+ start_time: "2026-05-01T00:00:00Z"
216
+ end_time: "2026-05-31T23:59:59Z"
217
+ packages:
218
+ - product_id: "test-product"
219
+ budget: 5000
220
+ pricing_option_id: "test-pricing"
221
+
222
+ context:
223
+ correlation_id: "idempotency--create_media_buy_missing_key"
224
+ validations:
225
+ - check: error_code
226
+ allowed_values: ["INVALID_REQUEST", "VALIDATION_ERROR"]
227
+ description: "Missing idempotency_key rejected with INVALID_REQUEST or VALIDATION_ERROR"
228
+
229
+ - check: field_present
230
+ path: "context"
231
+ description: "Response echoes back the context object"
232
+ - check: field_value
233
+ path: "context.correlation_id"
234
+ value: "idempotency--create_media_buy_missing_key"
235
+ description: "Context correlation_id returned unchanged"
236
+
237
+ - id: replay_same_payload
238
+ title: "Replay returns cached response; key reuse with different payload conflicts"
239
+ narrative: |
240
+ Two halves of the same key-reuse contract, exercised against a single cached
241
+ entry on the seller:
242
+
243
+ 1. Same key + same payload (the canonical retry-safety case): a buyer's first
244
+ request times out but actually succeeded on the server. The buyer retries
245
+ with the same idempotency_key and the same payload. The seller MUST return
246
+ the same response — same media_buy_id, no new side effects.
247
+
248
+ 2. Same key + materially different payload (key reuse): the buyer reuses the
249
+ same idempotency_key with a different payload within the replay window. The
250
+ seller MUST reject with IDEMPOTENCY_CONFLICT. Silently applying the new
251
+ payload would break the at-most-once guarantee; silently returning the old
252
+ response would hide a real bug in the buyer's retry logic.
253
+
254
+ Both branches share a single `$generate:uuid_v4#replay_key` placeholder. They
255
+ live in the same phase so the alias resolves to one UUID across all four steps
256
+ — the seller sees one cached idempotency entry that the conflict step probes
257
+ with a different body. Splitting these into separate phases would reset the
258
+ alias cache at the phase boundary (adcp-client#1657) and mint a fresh UUID for
259
+ the conflict step, defeating the test.
260
+
261
+ steps:
262
+ - id: create_media_buy_initial
263
+ title: "Initial create_media_buy with fresh key"
264
+ narrative: |
265
+ Create a media buy with a fresh UUID v4 idempotency_key. Capture the
266
+ returned media_buy_id for comparison against the replay.
267
+ task: create_media_buy
268
+ schema_ref: "media-buy/create-media-buy-request.json"
269
+ response_schema_ref: "media-buy/create-media-buy-response.json"
270
+ doc_ref: "/media-buy/task-reference/create_media_buy"
271
+ comply_scenario: idempotency_replay
272
+ stateful: true
273
+ context_outputs:
274
+ - name: initial_media_buy_id
275
+ path: "media_buy_id"
276
+ expected: |
277
+ Create a new media buy and return a media_buy_id. This establishes the
278
+ canonical response that subsequent replays must match.
279
+
280
+ sample_request:
281
+ idempotency_key: "$generate:uuid_v4#replay_key"
282
+ account:
283
+ brand:
284
+ domain: "acmeoutdoor.example"
285
+ operator: "pinnacle-agency.example"
286
+ brand:
287
+ domain: "acmeoutdoor.example"
288
+ start_time: "2026-06-01T00:00:00Z"
289
+ end_time: "2026-06-30T23:59:59Z"
290
+ packages:
291
+ - product_id: "test-product"
292
+ budget: 5000
293
+ pricing_option_id: "test-pricing"
294
+ # Both the initial call and the replay below use the SAME webhook URL
295
+ # so the canonical payload hash matches across the two calls. A
296
+ # different URL between the two requests would hash differently and
297
+ # trip IDEMPOTENCY_CONFLICT instead of exercising replay. The URL
298
+ # template resolves against the runner's ephemeral webhook receiver
299
+ # (webhook_receiver_runner contract); runners without a receiver
300
+ # grade `no_duplicate_webhooks_on_replay` as not_applicable.
301
+ push_notification_config:
302
+ url: "{{runner.webhook_url:create_media_buy_initial}}"
303
+
304
+ context:
305
+ correlation_id: "idempotency--create_media_buy_initial"
306
+ validations:
307
+ - check: response_schema
308
+ description: "Response matches create-media-buy-response.json schema"
309
+ - check: field_present
310
+ path: "media_buy_id"
311
+ description: "Agent returns a media_buy_id for the initial request"
312
+ # Per `protocol-envelope.json`, fresh execution MAY omit
313
+ # `replayed` entirely; agents that do report it MUST NOT
314
+ # report `true` on a fresh call. `field_value_or_absent`
315
+ # (adcp-client#873, shipped in 5.16) asserts that tolerance:
316
+ # passes when absent OR present-and-matching. The replay
317
+ # step below asserts the positive side (`replayed: true`
318
+ # on replay) with plain `field_value`.
319
+ - check: field_value_or_absent
320
+ path: "replayed"
321
+ allowed_values: [false]
322
+ description: "If reported on fresh execution, replayed must be false"
323
+
324
+ - check: field_present
325
+ path: "context"
326
+ description: "Response echoes back the context object"
327
+ - check: field_value
328
+ path: "context.correlation_id"
329
+ value: "idempotency--create_media_buy_initial"
330
+ description: "Context correlation_id returned unchanged"
331
+
332
+ - id: create_media_buy_replay
333
+ title: "Replay with same key and payload returns cached response"
334
+ narrative: |
335
+ Re-send the exact same create_media_buy request — same idempotency_key,
336
+ same payload, same everything. The seller MUST return the same media_buy_id
337
+ from the prior step. It MUST NOT create a second media buy or fire a
338
+ second set of webhooks. This is the whole point of idempotency_key.
339
+ task: create_media_buy
340
+ schema_ref: "media-buy/create-media-buy-request.json"
341
+ response_schema_ref: "media-buy/create-media-buy-response.json"
342
+ doc_ref: "/media-buy/task-reference/create_media_buy"
343
+ comply_scenario: idempotency_replay
344
+ stateful: true
345
+ expected: |
346
+ Return the cached response from the initial request:
347
+ - media_buy_id: same as $context.initial_media_buy_id
348
+ - No new side effects (no duplicate webhooks, no new audit log entry)
349
+
350
+ Programmatic verification of "no duplicate webhooks" runs in the next step
351
+ (no_duplicate_webhooks_on_replay) when the storyboard is driven by a runner
352
+ that implements the webhook_receiver_runner contract. Runners without a
353
+ webhook receiver grade that step as not_applicable — agents MAY still claim
354
+ idempotency compliance, but the replay-side-effect invariant is only
355
+ programmatically graded when the webhook_callbacks specialism is also in
356
+ scope.
357
+
358
+ sample_request:
359
+ idempotency_key: "$generate:uuid_v4#replay_key"
360
+ account:
361
+ brand:
362
+ domain: "acmeoutdoor.example"
363
+ operator: "pinnacle-agency.example"
364
+ brand:
365
+ domain: "acmeoutdoor.example"
366
+ start_time: "2026-06-01T00:00:00Z"
367
+ end_time: "2026-06-30T23:59:59Z"
368
+ packages:
369
+ - product_id: "test-product"
370
+ budget: 5000
371
+ pricing_option_id: "test-pricing"
372
+ # Byte-identical to the initial call above — same URL, same step id
373
+ # token — so the canonical payload hash matches and the request
374
+ # lands on the replay branch rather than IDEMPOTENCY_CONFLICT.
375
+ push_notification_config:
376
+ url: "{{runner.webhook_url:create_media_buy_initial}}"
377
+
378
+ context:
379
+ correlation_id: "idempotency--create_media_buy_replay"
380
+ validations:
381
+ - check: response_schema
382
+ description: "Response matches create-media-buy-response.json schema"
383
+ - check: field_present
384
+ path: "media_buy_id"
385
+ description: "Replay returns a media_buy_id (same as initial)"
386
+ - check: field_value
387
+ path: "media_buy_id"
388
+ value: "$context.initial_media_buy_id"
389
+ description: "Replay returns the SAME media_buy_id as the initial call"
390
+ - check: field_value
391
+ path: "replayed"
392
+ value: true
393
+ description: "Replay sets replayed: true on the response envelope"
394
+
395
+ - check: field_present
396
+ path: "context"
397
+ description: "Response echoes back the context object"
398
+ - check: field_value
399
+ path: "context.correlation_id"
400
+ value: "idempotency--create_media_buy_replay"
401
+ description: "Context correlation_id returned unchanged"
402
+
403
+ - id: no_duplicate_webhooks_on_replay
404
+ title: "No duplicate webhooks fired across initial + replay"
405
+ narrative: |
406
+ The central invariant of idempotency: replaying with the same key MUST
407
+ NOT produce duplicate side effects. Webhook emission is the observable
408
+ side effect most likely to leak if the seller re-executes on replay
409
+ instead of returning cached state.
410
+
411
+ This step requires the storyboard runner to implement the
412
+ `webhook_receiver_runner` contract at
413
+ `test-kits/webhook-receiver-runner.yaml`. Runners without a webhook
414
+ receiver skip this step with a stable not_applicable marker; the
415
+ replay-side-effect invariant then relies on the next phase's cached-
416
+ response assertions plus manual audit of the seller's webhook
417
+ delivery history.
418
+
419
+ When the receiver IS in scope, the runner counts webhooks arriving at
420
+ {{runner.webhook_url:create_media_buy_initial}} across the initial
421
+ call's window AND the replay call's window. A conformant seller emits
422
+ webhooks only on the first execution; a non-conformant seller that
423
+ re-executes on replay emits a second set, catchable only by live
424
+ observation.
425
+ task: expect_webhook
426
+ # `triggered_by` points at the initial call (not the replay) because the
427
+ # default filter resolves `step_id` + `operation_id` from `stepOperationIds`
428
+ # for that step — which is populated when the initial sample_request's
429
+ # `push_notification_config.url` expanded `{{runner.webhook_url:create_media_buy_initial}}`
430
+ # above. The execution-order wait still spans the replay window: this
431
+ # step runs after `create_media_buy_replay` in YAML order and `wait_all`
432
+ # drains the full `timeout_seconds` so any webhook re-fired by the
433
+ # replay is captured and counted against the cap below. A non-conformant
434
+ # seller that re-executes on replay would deliver a second webhook with
435
+ # a fresh idempotency_key and trip `duplicate_webhook_on_replay`.
436
+ triggered_by: create_media_buy_initial
437
+ timeout_seconds: 30
438
+ expect_max_deliveries_per_logical_event: 1
439
+ requires_contract: webhook_receiver_runner
440
+ stateful: true
441
+ expected: |
442
+ At most one logical webhook event delivered for the create_media_buy
443
+ operation across both the initial and replay windows. If the runner
444
+ observes a second webhook delivery tagged with a distinct
445
+ idempotency_key but the same media_buy_id/operation_id, the seller is
446
+ re-executing on replay and the step fails with
447
+ duplicate_webhook_on_replay. Not_applicable when the runner does not
448
+ host a webhook receiver.
449
+
450
+ - id: create_media_buy_conflict
451
+ title: "Same key, different payload returns IDEMPOTENCY_CONFLICT"
452
+ narrative: |
453
+ Re-send create_media_buy with the same idempotency_key as the prior
454
+ replay steps but a materially different payload — a different end_time
455
+ and a different budget. The seller MUST reject this with
456
+ IDEMPOTENCY_CONFLICT. CONFLICT is accepted as a fallback for sellers
457
+ that haven't adopted the new error code yet.
458
+
459
+ This step shares the `$generate:uuid_v4#replay_key` alias with the
460
+ initial and replay steps above, so the runner injects the same UUID
461
+ on the wire. Lives in the same phase as those steps because the
462
+ runner's alias cache resets at phase boundaries (adcp-client#1657);
463
+ a separate phase would mint a fresh UUID and the seller would treat
464
+ the call as a new request rather than a conflicting key reuse.
465
+ task: create_media_buy
466
+ schema_ref: "media-buy/create-media-buy-request.json"
467
+ response_schema_ref: "media-buy/create-media-buy-response.json"
468
+ doc_ref: "/media-buy/task-reference/create_media_buy"
469
+ comply_scenario: idempotency_conflict
470
+ expect_error: true
471
+ negative_path: payload_well_formed
472
+ stateful: true
473
+ expected: |
474
+ Reject with:
475
+ - code: IDEMPOTENCY_CONFLICT (preferred) or CONFLICT (fallback)
476
+ - recovery: correctable (buyer should use a fresh UUID v4)
477
+
478
+ sample_request:
479
+ idempotency_key: "$generate:uuid_v4#replay_key"
480
+ account:
481
+ brand:
482
+ domain: "acmeoutdoor.example"
483
+ operator: "pinnacle-agency.example"
484
+ brand:
485
+ domain: "acmeoutdoor.example"
486
+ start_time: "2026-06-01T00:00:00Z"
487
+ end_time: "2026-09-30T23:59:59Z"
488
+ packages:
489
+ - product_id: "test-product"
490
+ budget: 25000
491
+ pricing_option_id: "test-pricing"
492
+
493
+ context:
494
+ correlation_id: "idempotency--create_media_buy_conflict"
495
+ validations:
496
+ - check: error_code
497
+ allowed_values: ["IDEMPOTENCY_CONFLICT", "CONFLICT"]
498
+ description: "Key reuse with different payload returns IDEMPOTENCY_CONFLICT"
499
+
500
+ - check: field_present
501
+ path: "context"
502
+ description: "Response echoes back the context object"
503
+ - check: field_value
504
+ path: "context.correlation_id"
505
+ value: "idempotency--create_media_buy_conflict"
506
+ description: "Context correlation_id returned unchanged"
507
+
508
+ reviewer_checks:
509
+ - "Error body MUST NOT include the cached payload, the original request, or any fingerprint (hash, digest, field diff). Leaking cached state turns key-reuse into a read oracle for attackers who stole a key."
510
+ - "Error body MAY include code + message only. No `field` json-pointer — even a pointer like `/packages/0/budget` reveals schema shape."
511
+ - "Reviewer MUST confirm the seller's documentation states that errored requests release the idempotency claim, or manually probe the behavior (force a deterministic terminal error, then retry with the same key and a valid payload — the seller MUST return a fresh success, not IDEMPOTENCY_CONFLICT and not the cached error). See security.mdx#idempotency rule 3."
512
+
513
+ - id: fresh_key_new_resource
514
+ title: "Different key creates a new resource"
515
+ narrative: |
516
+ A request with a DIFFERENT idempotency_key is treated as a new request, even
517
+ if the payload is byte-for-byte identical to a prior request. The key — not
518
+ payload equivalence — is what provides the dedup boundary. This confirms the
519
+ seller is checking the key, not fingerprinting requests.
520
+
521
+ steps:
522
+ - id: create_media_buy_fresh_key
523
+ title: "Fresh key with identical payload creates a new media buy"
524
+ narrative: |
525
+ Send the same payload as the initial create_media_buy but with a different
526
+ idempotency_key. The seller MUST treat this as a new request and return a
527
+ NEW media_buy_id distinct from $context.initial_media_buy_id.
528
+ task: create_media_buy
529
+ schema_ref: "media-buy/create-media-buy-request.json"
530
+ response_schema_ref: "media-buy/create-media-buy-response.json"
531
+ doc_ref: "/media-buy/task-reference/create_media_buy"
532
+ comply_scenario: idempotency_fresh_key
533
+ stateful: true
534
+ context_outputs:
535
+ - name: fresh_key_media_buy_id
536
+ path: "media_buy_id"
537
+ expected: |
538
+ Return a NEW media_buy_id, distinct from $context.initial_media_buy_id.
539
+ This confirms the seller treats the fresh key as a new request.
540
+
541
+ sample_request:
542
+ idempotency_key: "$generate:uuid_v4#fresh_key"
543
+ account:
544
+ brand:
545
+ domain: "acmeoutdoor.example"
546
+ operator: "pinnacle-agency.example"
547
+ brand:
548
+ domain: "acmeoutdoor.example"
549
+ start_time: "2026-06-01T00:00:00Z"
550
+ end_time: "2026-06-30T23:59:59Z"
551
+ packages:
552
+ - product_id: "test-product"
553
+ budget: 5000
554
+ pricing_option_id: "test-pricing"
555
+
556
+ context:
557
+ correlation_id: "idempotency--create_media_buy_fresh_key"
558
+ validations:
559
+ - check: response_schema
560
+ description: "Response matches create-media-buy-response.json schema"
561
+ - check: field_present
562
+ path: "media_buy_id"
563
+ description: "Fresh key returns a media_buy_id"
564
+ # Fresh-key execution is a non-replay call — same tolerance as
565
+ # create_media_buy_initial applies: `replayed` MAY be omitted,
566
+ # but if present MUST NOT be `true`.
567
+ - check: field_value_or_absent
568
+ path: "replayed"
569
+ allowed_values: [false]
570
+ description: "If reported on fresh-key execution, replayed must be false"
571
+
572
+ - check: field_present
573
+ path: "context"
574
+ description: "Response echoes back the context object"
575
+ - check: field_value
576
+ path: "context.correlation_id"
577
+ value: "idempotency--create_media_buy_fresh_key"
578
+ description: "Context correlation_id returned unchanged"
579
+
580
+ - id: concurrent_retry
581
+ title: "Concurrent retries with the same key produce exactly one resource"
582
+ narrative: |
583
+ A buyer's transport timeout fires before the seller's downstream call
584
+ returns; the buyer retries with the same idempotency_key while the
585
+ first request is still executing. Per L1/security.mdx#idempotency
586
+ rule 9 (first-insert-wins), the seller MUST resolve the race
587
+ deterministically — exactly one resource is created, and both buyer
588
+ requests converge on the same media_buy_id.
589
+
590
+ The seller picks one of two policies and MUST behave consistently:
591
+
592
+ - **Wait-and-replay**: the second request blocks until the first
593
+ completes, then returns the cached response with `replayed: true`.
594
+ - **Reject-and-redirect**: the second request returns
595
+ `IDEMPOTENCY_IN_FLIGHT` immediately with
596
+ `error.details.retry_after`. The buyer retries with the same key
597
+ after the hint elapses and receives the cached response.
598
+
599
+ This phase exercises both policies via the parallel_dispatch_runner
600
+ contract: it fires two requests with the same fresh idempotency_key
601
+ and the same canonical payload, and asserts that the resolved
602
+ response set contains exactly one distinct media_buy_id. The runner
603
+ transparently resolves IDEMPOTENCY_IN_FLIGHT responses by retrying
604
+ with the same key after `retry_after` elapses, so the cross-response
605
+ check operates on the resolved set regardless of which policy the
606
+ seller adopted.
607
+
608
+ Runners without parallel-dispatch support skip this phase with a
609
+ stable not_applicable marker. The phase does not regress the
610
+ sequential idempotency contract — sellers that fail this phase due
611
+ to runner absence are still gradeable on the rest of the storyboard.
612
+
613
+ steps:
614
+ - id: create_media_buy_concurrent
615
+ title: "Two parallel create_media_buy calls with same key converge on one resource"
616
+ narrative: |
617
+ Fire two create_media_buy requests with the same fresh UUID v4
618
+ idempotency_key and byte-identical canonical payloads. The
619
+ runner dispatches them via its parallel-dispatch primitive (SDK
620
+ batch-call or distributed barrier), observes both responses,
621
+ resolves any IDEMPOTENCY_IN_FLIGHT to its eventual cached
622
+ response, and applies the cross-response checks below.
623
+
624
+ Programmatic verification of "exactly one resource created" runs
625
+ via `cross_response_count_distinct` on `media_buy_id`. A seller
626
+ that ignored rule 9 (re-executed both, two media buys created)
627
+ would produce cardinality 2 here and fail; a seller that
628
+ correctly serializes via first-insert-wins produces cardinality
629
+ 1 regardless of which policy (wait-and-replay or
630
+ reject-and-redirect) it adopted.
631
+
632
+ The accompanying `cross_response_field_equal` check is redundant
633
+ when cardinality is 1, but documents the equivalence intent
634
+ directly: every dispatch's resolved response carries the same
635
+ media_buy_id.
636
+ task: create_media_buy
637
+ schema_ref: "media-buy/create-media-buy-request.json"
638
+ response_schema_ref: "media-buy/create-media-buy-response.json"
639
+ doc_ref: "/media-buy/task-reference/create_media_buy"
640
+ comply_scenario: idempotency_concurrent
641
+ stateful: true
642
+ requires_contract: parallel_dispatch_runner
643
+ parallel_dispatch:
644
+ count: 2
645
+ same_idempotency_key: true
646
+ barrier_timeout_ms: 5000
647
+ expected: |
648
+ After resolving any IDEMPOTENCY_IN_FLIGHT responses to their
649
+ eventual cached response, both dispatches' responses carry the
650
+ same media_buy_id. Exactly one media buy was created. The
651
+ resolved-response set has cardinality 1 on media_buy_id.
652
+
653
+ A seller adopting wait-and-replay returns the same response on
654
+ both dispatches directly. A seller adopting reject-and-redirect
655
+ returns the cached response on one dispatch and
656
+ IDEMPOTENCY_IN_FLIGHT on the other; the runner resolves the
657
+ latter by retrying with the same key after `retry_after` and
658
+ observes the cached response. Either path is conformant.
659
+
660
+ sample_request:
661
+ idempotency_key: "$generate:uuid_v4#concurrent_key"
662
+ account:
663
+ brand:
664
+ domain: "acmeoutdoor.example"
665
+ operator: "pinnacle-agency.example"
666
+ brand:
667
+ domain: "acmeoutdoor.example"
668
+ start_time: "2026-07-01T00:00:00Z"
669
+ end_time: "2026-07-31T23:59:59Z"
670
+ packages:
671
+ - product_id: "test-product"
672
+ budget: 5000
673
+ pricing_option_id: "test-pricing"
674
+
675
+ context:
676
+ correlation_id: "idempotency--create_media_buy_concurrent"
677
+ validations:
678
+ - check: cross_response_count_distinct
679
+ path: "media_buy_id"
680
+ allowed_values: [1]
681
+ description: "Concurrent retries produced exactly one distinct media_buy_id across both dispatches (rule 9: first-insert-wins)"
682
+ - check: cross_response_field_equal
683
+ path: "media_buy_id"
684
+ description: "Both dispatches' resolved responses carry the same media_buy_id"
685
+
686
+ reviewer_checks:
687
+ - "Confirm the seller's claim-row / unique-constraint implementation backs the observed first-insert-wins behavior, rather than a happy-path race the test happened to avoid (review code or runbook describing the INSERT … ON CONFLICT pattern on the scope tuple)."
688
+ - "If the seller returned IDEMPOTENCY_IN_FLIGHT on the second dispatch, confirm `error.details.retry_after` was populated with a plausible value (seconds, integer, > 0) and that the cached response was available within the hint window. A seller that consistently overshoots retry_after produces brittle retry timing for buyers."
689
+ - "Confirm the seller's in-flight detection does NOT leak across (authenticated_agent, account_id) scope boundaries — probe with a stolen key from a different scope and verify the response shape and timing match the never-seen-key path."
690
+
691
+ - id: verify_media_buy_count
692
+ title: "Verify dedup actually happened"
693
+ narrative: |
694
+ Call get_media_buys to confirm the two captured media_buy_ids both exist
695
+ and are distinct. `initial_media_buy_id` was created on the first
696
+ `create_media_buy` call and reused on the replay (same key, deduplicated);
697
+ `fresh_key_media_buy_id` was created on the fresh-key request. The
698
+ `concurrent_retry` phase (when graded) also exercises first-insert-wins
699
+ but its resource is not queried here — that phase has its own
700
+ cross-response cardinality check. If the seller ignored the idempotency_key
701
+ on the replay branch, `initial_media_buy_id` would not match across the
702
+ replay step's response.
703
+
704
+ steps:
705
+ - id: get_media_buys_dedup_check
706
+ title: "Confirm captured media_buy_ids exist and are distinct"
707
+ narrative: |
708
+ Query the two captured media_buy_ids from prior steps. Both should
709
+ exist (proving fresh-key and initial calls each created a real
710
+ resource) and be distinct (proving the fresh-key call was treated as
711
+ a new request, not deduplicated against the initial). This is an
712
+ end-to-end verification that the replay did not create a duplicate row.
713
+ task: get_media_buys
714
+ schema_ref: "media-buy/get-media-buys-request.json"
715
+ response_schema_ref: "media-buy/get-media-buys-response.json"
716
+ doc_ref: "/media-buy/task-reference/get_media_buys"
717
+ comply_scenario: idempotency_verify
718
+ stateful: true
719
+ expected: |
720
+ Return both captured media buys with the two captured IDs, distinct.
721
+
722
+ sample_request:
723
+ account:
724
+ brand:
725
+ domain: "acmeoutdoor.example"
726
+ operator: "pinnacle-agency.example"
727
+ media_buy_ids:
728
+ - "$context.initial_media_buy_id"
729
+ - "$context.fresh_key_media_buy_id"
730
+
731
+ context:
732
+ correlation_id: "idempotency--get_media_buys_dedup_check"
733
+ validations:
734
+ - check: response_schema
735
+ description: "Response matches get-media-buys-response.json schema"
736
+ - check: field_present
737
+ path: "media_buys"
738
+ description: "Response contains the queried media buys"
739
+
740
+ - check: field_present
741
+ path: "context"
742
+ description: "Response echoes back the context object"
743
+ - check: field_value
744
+ path: "context.correlation_id"
745
+ value: "idempotency--get_media_buys_dedup_check"
746
+ description: "Context correlation_id returned unchanged"
747
+
748
+ - id: rate_limit_replay_invariant
749
+ title: "RATE_LIMITED responses are not cached as idempotency replays"
750
+ narrative: |
751
+ Per L1/security.mdx#idempotency rule 3 ("Only successful responses
752
+ are cached") and bullet 8 (insert-rate ceiling): when the seller's
753
+ idempotency-cache insert-rate limiter trips and a request receives
754
+ `RATE_LIMITED`, the response MUST NOT be cached as the canonical
755
+ replay for that idempotency_key. A buyer retrying with the same key
756
+ after `error.details.retry_after` elapses MUST observe the real
757
+ handler result (success or a state-dependent error) — not a cached
758
+ rate-limit response that would persist for the entire replay TTL.
759
+
760
+ Without this invariant, `retry_after` is a meaningless signal: a
761
+ buyer who hits the limiter once would receive the cached
762
+ `RATE_LIMITED` on every subsequent retry until the replay window
763
+ expires, regardless of when the limiter actually cleared. The
764
+ invariant collapses a real client-safety regression into a
765
+ cross-response check.
766
+
767
+ This phase runs the `expect_rate_limit_not_replayed` task via the
768
+ `rate_limit_trip_runner` contract. The runner drives a sequential
769
+ fresh-key burst against `create_media_buy` until one request receives
770
+ `RATE_LIMITED`, captures that request's `idempotency_key`, waits the
771
+ response's `retry_after` seconds, then re-submits the captured key
772
+ with the same payload. The cross-response check
773
+ `replay_not_cached_rate_limit` fails if the replay response carries
774
+ `error.code = RATE_LIMITED` (i.e., the cached rate-limit was served
775
+ as the replay).
776
+
777
+ Runners without `rate_limit_trip_runner` skip this phase with a
778
+ stable `not_applicable` marker. Runners with the contract that
779
+ exhaust `max_attempts` without observing `RATE_LIMITED` also grade
780
+ `not_applicable` (reason: `rate_limit_not_triggered`) — a sandbox
781
+ limiter sitting above the contract's burst ceiling is not a seller
782
+ conformance fault, only a configuration choice that prevents the
783
+ invariant from being verified end-to-end.
784
+
785
+ The 60/300 req/sec ceiling itself is NOT attested here. Burst-volume
786
+ measurement is structurally non-deterministic in CI environments and
787
+ remains seller self-attestation per the cache-growth-defense note
788
+ above.
789
+
790
+ steps:
791
+ - id: expect_rate_limit_not_replayed
792
+ title: "Trip the limiter, replay the key after retry_after, assert the cached response is not RATE_LIMITED"
793
+ narrative: |
794
+ The runner fires up to `max_attempts` sequential `create_media_buy`
795
+ requests with fresh UUID v4 idempotency_keys and otherwise-identical
796
+ canonical payloads. On the first response with envelope error_code
797
+ = `RATE_LIMITED`, the runner captures the request's
798
+ `idempotency_key` and the response's `error.details.retry_after`,
799
+ waits that long (capped by `replay_max_wait_seconds`), and
800
+ re-submits the captured key with the same payload but a distinct
801
+ `context.correlation_id` for traceability.
802
+
803
+ The cross-response `replay_not_cached_rate_limit` check compares
804
+ `trip_response.error.code` against `replay_response.error.code`
805
+ (when the replay carries an error envelope) or against the absence
806
+ of error (when the replay carries a success envelope). The check
807
+ fails iff both responses carry `error.code = RATE_LIMITED` — that
808
+ is the cached-replay regression. Any other replay response shape
809
+ passes the invariant: either the handler succeeded (the seller
810
+ correctly re-executed against the cleared budget), or the handler
811
+ returned a different error (the seller correctly re-executed and
812
+ a state-dependent error fired).
813
+
814
+ The `trip_target_sample_request` below intentionally uses a
815
+ minimal `create_media_buy` payload — the goal of the burst is to
816
+ generate insert-rate pressure on the idempotency cache, not to
817
+ exercise the create flow itself. The replay payload is byte-
818
+ identical to the trip request to keep the canonical hash stable.
819
+ task: expect_rate_limit_not_replayed
820
+ requires_contract: rate_limit_trip_runner
821
+ rate_limit_trip:
822
+ trip_target_task: create_media_buy
823
+ max_attempts: 200
824
+ replay_max_wait_seconds: 30
825
+ trip_target_sample_request:
826
+ account:
827
+ brand:
828
+ domain: "acmeoutdoor.example"
829
+ operator: "pinnacle-agency.example"
830
+ brand:
831
+ domain: "acmeoutdoor.example"
832
+ start_time: "2026-07-01T00:00:00Z"
833
+ end_time: "2026-07-31T23:59:59Z"
834
+ packages:
835
+ - product_id: "test-product"
836
+ budget: 5000
837
+ pricing_option_id: "test-pricing"
838
+ context:
839
+ correlation_id: "idempotency--rate_limit_trip_request"
840
+ expected: |
841
+ One of two outcomes:
842
+
843
+ - At least one burst request returns `RATE_LIMITED`. The runner
844
+ captures the request's idempotency_key, waits the response's
845
+ retry_after, replays the captured key, and the replay response
846
+ does NOT carry `error.code = RATE_LIMITED`. The
847
+ `replay_not_cached_rate_limit` check passes.
848
+
849
+ - All `max_attempts` requests complete without producing
850
+ `RATE_LIMITED`. The step grades `not_applicable` with reason
851
+ `rate_limit_not_triggered`. The rest of the storyboard is
852
+ unaffected.
853
+
854
+ validations:
855
+ - check: replay_not_cached_rate_limit
856
+ description: "RATE_LIMITED response on rate-limit trip was not cached as the idempotency replay for the same key"
857
+
858
+ reviewer_checks:
859
+ - "Confirm the seller's idempotency-cache implementation explicitly excludes RATE_LIMITED (and other transient-error) responses from the cached-replay path. Review the storage layer or runbook describing which response classes are persisted under idempotency_key."
860
+ - "Confirm the seller's RATE_LIMITED response carries a usable retry_after value (seconds, integer, > 0, ≤ a documented ceiling). A seller that returns retry_after values larger than buyers' realistic retry windows produces unusable rate-limit signaling."
861
+ - "Confirm the seller's rate-limit budget is scoped per (authenticated_agent, account) tuple — not shared across all agents or accounts globally — so one agent's burst cannot exhaust another's budget."