@adcp/sdk 14.0.0-beta.11 → 14.0.0-beta.13

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 (235) hide show
  1. package/bin/adcp-storyboard-sandbox.js +16 -0
  2. package/bin/adcp.js +28 -30
  3. package/dist/lib/index.d.mts +3 -3
  4. package/dist/lib/index.d.ts +3 -3
  5. package/dist/lib/index.d.ts.map +1 -1
  6. package/dist/lib/index.js +2 -0
  7. package/dist/lib/index.js.map +1 -1
  8. package/dist/lib/index.mjs +3 -1
  9. package/dist/lib/index.mjs.map +1 -1
  10. package/dist/lib/schemas/index.d.mts +385 -1
  11. package/dist/lib/schemas/index.d.ts +385 -1
  12. package/dist/lib/schemas/index.d.ts.map +1 -1
  13. package/dist/lib/schemas/index.js +13 -0
  14. package/dist/lib/schemas/index.js.map +1 -1
  15. package/dist/lib/schemas/index.mjs +11 -0
  16. package/dist/lib/schemas/index.mjs.map +1 -1
  17. package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
  18. package/dist/lib/server/create-adcp-server.d.mts +2 -2
  19. package/dist/lib/server/create-adcp-server.d.ts +2 -2
  20. package/dist/lib/server/create-adcp-server.d.ts.map +1 -1
  21. package/dist/lib/server/create-adcp-server.js +6 -1
  22. package/dist/lib/server/create-adcp-server.js.map +1 -1
  23. package/dist/lib/server/create-adcp-server.mjs +6 -1
  24. package/dist/lib/server/create-adcp-server.mjs.map +1 -1
  25. package/dist/lib/server/decisioning/index.d.mts +3 -3
  26. package/dist/lib/server/decisioning/index.d.ts +3 -3
  27. package/dist/lib/server/decisioning/index.d.ts.map +1 -1
  28. package/dist/lib/server/decisioning/index.js.map +1 -1
  29. package/dist/lib/server/decisioning/index.mjs.map +1 -1
  30. package/dist/lib/server/decisioning/runtime/from-platform.d.mts +24 -17
  31. package/dist/lib/server/decisioning/runtime/from-platform.d.ts +24 -17
  32. package/dist/lib/server/decisioning/runtime/from-platform.d.ts.map +1 -1
  33. package/dist/lib/server/decisioning/runtime/from-platform.js +71 -42
  34. package/dist/lib/server/decisioning/runtime/from-platform.js.map +1 -1
  35. package/dist/lib/server/decisioning/runtime/from-platform.mjs +75 -43
  36. package/dist/lib/server/decisioning/runtime/from-platform.mjs.map +1 -1
  37. package/dist/lib/server/decisioning/runtime/postgres-task-registry.d.mts +15 -21
  38. package/dist/lib/server/decisioning/runtime/postgres-task-registry.d.ts +15 -21
  39. package/dist/lib/server/decisioning/runtime/postgres-task-registry.d.ts.map +1 -1
  40. package/dist/lib/server/decisioning/runtime/postgres-task-registry.js +100 -30
  41. package/dist/lib/server/decisioning/runtime/postgres-task-registry.js.map +1 -1
  42. package/dist/lib/server/decisioning/runtime/postgres-task-registry.mjs +100 -30
  43. package/dist/lib/server/decisioning/runtime/postgres-task-registry.mjs.map +1 -1
  44. package/dist/lib/server/decisioning/runtime/task-registry.d.mts +30 -16
  45. package/dist/lib/server/decisioning/runtime/task-registry.d.ts +30 -16
  46. package/dist/lib/server/decisioning/runtime/task-registry.d.ts.map +1 -1
  47. package/dist/lib/server/decisioning/runtime/task-registry.js +40 -20
  48. package/dist/lib/server/decisioning/runtime/task-registry.js.map +1 -1
  49. package/dist/lib/server/decisioning/runtime/task-registry.mjs +38 -19
  50. package/dist/lib/server/decisioning/runtime/task-registry.mjs.map +1 -1
  51. package/dist/lib/server/decisioning/runtime/to-context.d.mts +2 -2
  52. package/dist/lib/server/decisioning/runtime/to-context.d.ts +2 -2
  53. package/dist/lib/server/decisioning/runtime/to-context.d.ts.map +1 -1
  54. package/dist/lib/server/decisioning/runtime/to-context.js +2 -2
  55. package/dist/lib/server/decisioning/runtime/to-context.js.map +1 -1
  56. package/dist/lib/server/decisioning/runtime/to-context.mjs +2 -2
  57. package/dist/lib/server/decisioning/runtime/to-context.mjs.map +1 -1
  58. package/dist/lib/server/decisioning/specialisms/creative-ad-server.d.mts +3 -2
  59. package/dist/lib/server/decisioning/specialisms/creative-ad-server.d.ts +3 -2
  60. package/dist/lib/server/decisioning/specialisms/creative-ad-server.d.ts.map +1 -1
  61. package/dist/lib/server/decisioning/specialisms/creative-ad-server.js.map +1 -1
  62. package/dist/lib/server/decisioning/specialisms/creative.d.mts +10 -10
  63. package/dist/lib/server/decisioning/specialisms/creative.d.ts +10 -10
  64. package/dist/lib/server/decisioning/specialisms/creative.d.ts.map +1 -1
  65. package/dist/lib/server/decisioning/specialisms/creative.js.map +1 -1
  66. package/dist/lib/server/index.d.mts +1 -1
  67. package/dist/lib/server/index.d.ts +1 -1
  68. package/dist/lib/server/index.d.ts.map +1 -1
  69. package/dist/lib/server/index.js.map +1 -1
  70. package/dist/lib/server/index.mjs.map +1 -1
  71. package/dist/lib/signing/agent-context.d.mts +1 -0
  72. package/dist/lib/signing/agent-context.d.ts +1 -0
  73. package/dist/lib/signing/agent-context.d.ts.map +1 -1
  74. package/dist/lib/signing/agent-context.js +37 -2
  75. package/dist/lib/signing/agent-context.js.map +1 -1
  76. package/dist/lib/signing/agent-context.mjs +38 -3
  77. package/dist/lib/signing/agent-context.mjs.map +1 -1
  78. package/dist/lib/signing/agent-fetch.d.mts +1 -2
  79. package/dist/lib/signing/agent-fetch.d.ts +1 -2
  80. package/dist/lib/signing/agent-fetch.d.ts.map +1 -1
  81. package/dist/lib/signing/agent-fetch.js +2 -0
  82. package/dist/lib/signing/agent-fetch.js.map +1 -1
  83. package/dist/lib/signing/agent-fetch.mjs +2 -0
  84. package/dist/lib/signing/agent-fetch.mjs.map +1 -1
  85. package/dist/lib/signing/agent-resolver/etld.js.map +1 -1
  86. package/dist/lib/signing/agent-resolver/etld.mjs.map +1 -1
  87. package/dist/lib/testing/client.d.mts +6 -0
  88. package/dist/lib/testing/client.d.ts +6 -0
  89. package/dist/lib/testing/client.d.ts.map +1 -1
  90. package/dist/lib/testing/client.js +76 -4
  91. package/dist/lib/testing/client.js.map +1 -1
  92. package/dist/lib/testing/client.mjs +75 -4
  93. package/dist/lib/testing/client.mjs.map +1 -1
  94. package/dist/lib/testing/compliance/comply.d.ts.map +1 -1
  95. package/dist/lib/testing/compliance/comply.js +6 -0
  96. package/dist/lib/testing/compliance/comply.js.map +1 -1
  97. package/dist/lib/testing/compliance/comply.mjs +7 -1
  98. package/dist/lib/testing/compliance/comply.mjs.map +1 -1
  99. package/dist/lib/testing/storyboard/default-invariants.d.ts.map +1 -1
  100. package/dist/lib/testing/storyboard/default-invariants.js +11 -2
  101. package/dist/lib/testing/storyboard/default-invariants.js.map +1 -1
  102. package/dist/lib/testing/storyboard/default-invariants.mjs +11 -2
  103. package/dist/lib/testing/storyboard/default-invariants.mjs.map +1 -1
  104. package/dist/lib/testing/storyboard/index.d.mts +1 -1
  105. package/dist/lib/testing/storyboard/index.d.ts +1 -1
  106. package/dist/lib/testing/storyboard/index.d.ts.map +1 -1
  107. package/dist/lib/testing/storyboard/index.js.map +1 -1
  108. package/dist/lib/testing/storyboard/index.mjs.map +1 -1
  109. package/dist/lib/testing/storyboard/loader.d.ts.map +1 -1
  110. package/dist/lib/testing/storyboard/loader.js +14 -0
  111. package/dist/lib/testing/storyboard/loader.js.map +1 -1
  112. package/dist/lib/testing/storyboard/loader.mjs +14 -0
  113. package/dist/lib/testing/storyboard/loader.mjs.map +1 -1
  114. package/dist/lib/testing/storyboard/phase-condition.d.mts +41 -0
  115. package/dist/lib/testing/storyboard/phase-condition.d.ts +42 -0
  116. package/dist/lib/testing/storyboard/phase-condition.d.ts.map +1 -0
  117. package/dist/lib/testing/storyboard/phase-condition.js +215 -0
  118. package/dist/lib/testing/storyboard/phase-condition.js.map +1 -0
  119. package/dist/lib/testing/storyboard/phase-condition.mjs +189 -0
  120. package/dist/lib/testing/storyboard/phase-condition.mjs.map +1 -0
  121. package/dist/lib/testing/storyboard/request-builder.d.ts.map +1 -1
  122. package/dist/lib/testing/storyboard/request-builder.js +15 -2
  123. package/dist/lib/testing/storyboard/request-builder.js.map +1 -1
  124. package/dist/lib/testing/storyboard/request-builder.mjs +15 -2
  125. package/dist/lib/testing/storyboard/request-builder.mjs.map +1 -1
  126. package/dist/lib/testing/storyboard/request-signing/functional-dispatch.d.mts +15 -0
  127. package/dist/lib/testing/storyboard/request-signing/functional-dispatch.d.ts +16 -0
  128. package/dist/lib/testing/storyboard/request-signing/functional-dispatch.d.ts.map +1 -0
  129. package/dist/lib/testing/storyboard/request-signing/functional-dispatch.js +106 -0
  130. package/dist/lib/testing/storyboard/request-signing/functional-dispatch.js.map +1 -0
  131. package/dist/lib/testing/storyboard/request-signing/functional-dispatch.mjs +82 -0
  132. package/dist/lib/testing/storyboard/request-signing/functional-dispatch.mjs.map +1 -0
  133. package/dist/lib/testing/storyboard/request-signing/test-kit.d.mts +15 -0
  134. package/dist/lib/testing/storyboard/request-signing/test-kit.d.ts +15 -0
  135. package/dist/lib/testing/storyboard/request-signing/test-kit.d.ts.map +1 -1
  136. package/dist/lib/testing/storyboard/request-signing/test-kit.js +58 -0
  137. package/dist/lib/testing/storyboard/request-signing/test-kit.js.map +1 -1
  138. package/dist/lib/testing/storyboard/request-signing/test-kit.mjs +58 -0
  139. package/dist/lib/testing/storyboard/request-signing/test-kit.mjs.map +1 -1
  140. package/dist/lib/testing/storyboard/request-signing/vector-loader.d.mts +2 -0
  141. package/dist/lib/testing/storyboard/request-signing/vector-loader.d.ts +2 -0
  142. package/dist/lib/testing/storyboard/request-signing/vector-loader.d.ts.map +1 -1
  143. package/dist/lib/testing/storyboard/request-signing/vector-loader.js +7 -1
  144. package/dist/lib/testing/storyboard/request-signing/vector-loader.js.map +1 -1
  145. package/dist/lib/testing/storyboard/request-signing/vector-loader.mjs +6 -1
  146. package/dist/lib/testing/storyboard/request-signing/vector-loader.mjs.map +1 -1
  147. package/dist/lib/testing/storyboard/runner.d.ts.map +1 -1
  148. package/dist/lib/testing/storyboard/runner.js +67 -24
  149. package/dist/lib/testing/storyboard/runner.js.map +1 -1
  150. package/dist/lib/testing/storyboard/runner.mjs +67 -24
  151. package/dist/lib/testing/storyboard/runner.mjs.map +1 -1
  152. package/dist/lib/testing/storyboard/task-map.d.ts.map +1 -1
  153. package/dist/lib/testing/storyboard/task-map.js +6 -2
  154. package/dist/lib/testing/storyboard/task-map.js.map +1 -1
  155. package/dist/lib/testing/storyboard/task-map.mjs +6 -2
  156. package/dist/lib/testing/storyboard/task-map.mjs.map +1 -1
  157. package/dist/lib/testing/storyboard/types.d.mts +5 -4
  158. package/dist/lib/testing/storyboard/types.d.ts +5 -4
  159. package/dist/lib/testing/storyboard/types.d.ts.map +1 -1
  160. package/dist/lib/testing/storyboard/types.js.map +1 -1
  161. package/dist/lib/testing/storyboard/types.mjs.map +1 -1
  162. package/dist/lib/testing/storyboard/validations.d.mts +1 -1
  163. package/dist/lib/testing/storyboard/validations.d.ts +1 -1
  164. package/dist/lib/testing/storyboard/webhook-assertions.d.ts.map +1 -1
  165. package/dist/lib/testing/storyboard/webhook-assertions.js +60 -4
  166. package/dist/lib/testing/storyboard/webhook-assertions.js.map +1 -1
  167. package/dist/lib/testing/storyboard/webhook-assertions.mjs +60 -4
  168. package/dist/lib/testing/storyboard/webhook-assertions.mjs.map +1 -1
  169. package/dist/lib/testing/storyboard/webhook-receiver.d.mts +15 -0
  170. package/dist/lib/testing/storyboard/webhook-receiver.d.ts +15 -0
  171. package/dist/lib/testing/storyboard/webhook-receiver.d.ts.map +1 -1
  172. package/dist/lib/testing/storyboard/webhook-receiver.js +57 -6
  173. package/dist/lib/testing/storyboard/webhook-receiver.js.map +1 -1
  174. package/dist/lib/testing/storyboard/webhook-receiver.mjs +57 -6
  175. package/dist/lib/testing/storyboard/webhook-receiver.mjs.map +1 -1
  176. package/dist/lib/testing/types.d.mts +12 -0
  177. package/dist/lib/testing/types.d.ts +12 -0
  178. package/dist/lib/testing/types.d.ts.map +1 -1
  179. package/dist/lib/testing/types.js.map +1 -1
  180. package/dist/lib/types/comply-test-controller.d.ts +1 -14
  181. package/dist/lib/types/core.generated.d.mts +6 -3
  182. package/dist/lib/types/core.generated.d.ts +6 -3
  183. package/dist/lib/types/core.generated.d.ts.map +1 -1
  184. package/dist/lib/types/core.generated.js.map +1 -1
  185. package/dist/lib/types/get-products.d.ts +1 -14
  186. package/dist/lib/types/index.d.mts +1 -1
  187. package/dist/lib/types/index.d.ts +1 -1
  188. package/dist/lib/types/index.d.ts.map +1 -1
  189. package/dist/lib/types/index.js.map +1 -1
  190. package/dist/lib/types/index.mjs.map +1 -1
  191. package/dist/lib/types/list-transformers.d.ts +9 -5
  192. package/dist/lib/types/schemas.generated.d.ts +23 -352
  193. package/dist/lib/types/schemas.generated.d.ts.map +1 -1
  194. package/dist/lib/types/schemas.generated.js +7 -4
  195. package/dist/lib/types/schemas.generated.js.map +1 -1
  196. package/dist/lib/types/schemas.generated.mjs +6 -4
  197. package/dist/lib/types/schemas.generated.mjs.map +1 -1
  198. package/dist/lib/types/server-payload-aliases.d.mts +2 -1
  199. package/dist/lib/types/server-payload-aliases.d.ts +2 -1
  200. package/dist/lib/types/server-payload-aliases.d.ts.map +1 -1
  201. package/dist/lib/types/server-payload-aliases.js.map +1 -1
  202. package/dist/lib/types/tools.generated.d.mts +6 -3
  203. package/dist/lib/types/tools.generated.d.ts +6 -3
  204. package/dist/lib/types/tools.generated.d.ts.map +1 -1
  205. package/dist/lib/types/tools.generated.js.map +1 -1
  206. package/dist/lib/utils/build-creative-return-builders.d.mts +5 -1
  207. package/dist/lib/utils/build-creative-return-builders.d.ts +5 -1
  208. package/dist/lib/utils/build-creative-return-builders.d.ts.map +1 -1
  209. package/dist/lib/utils/build-creative-return-builders.js +9 -3
  210. package/dist/lib/utils/build-creative-return-builders.js.map +1 -1
  211. package/dist/lib/utils/build-creative-return-builders.mjs +7 -2
  212. package/dist/lib/utils/build-creative-return-builders.mjs.map +1 -1
  213. package/dist/lib/utils/response-unwrapper.js +1 -1
  214. package/dist/lib/utils/response-unwrapper.js.map +1 -1
  215. package/dist/lib/utils/response-unwrapper.mjs +1 -1
  216. package/dist/lib/utils/response-unwrapper.mjs.map +1 -1
  217. package/dist/lib/validation/schema-loader.d.ts.map +1 -1
  218. package/dist/lib/validation/schema-loader.js +16 -7
  219. package/dist/lib/validation/schema-loader.js.map +1 -1
  220. package/dist/lib/validation/schema-loader.mjs +16 -7
  221. package/dist/lib/validation/schema-loader.mjs.map +1 -1
  222. package/dist/lib/version.d.mts +3 -3
  223. package/dist/lib/version.d.ts +3 -3
  224. package/dist/lib/version.js +3 -3
  225. package/dist/lib/version.js.map +1 -1
  226. package/dist/lib/version.mjs +3 -3
  227. package/dist/lib/version.mjs.map +1 -1
  228. package/docs/llms.txt +2 -2
  229. package/docs/migration-5.x-to-6.x.md +3 -2
  230. package/docs/migration-task-registry-scoping.md +61 -0
  231. package/examples/hello_seller_adapter_guaranteed.ts +1 -1
  232. package/package.json +5 -5
  233. package/skills/build-decisioning-creative-template/SKILL.md +2 -2
  234. package/skills/build-decisioning-platform/SKILL.md +4 -2
  235. package/skills/build-decisioning-platform/advanced/REFERENCE.md +7 -6
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/lib/testing/storyboard/webhook-assertions.ts"],"sourcesContent":["/**\n * Webhook-assertion pseudo-tasks.\n *\n * Four step `task` values graded by observing the runner's webhook\n * receiver rather than by calling the agent:\n *\n * - `expect_webhook`\n * Assert at least one matching webhook arrived carrying a\n * well-formed `idempotency_key`. Optional: body-schema validation and\n * max-deliveries-per-logical-event cap (catches duplicate-side-effect\n * bugs on replay).\n *\n * - `expect_no_webhook`\n * Assert that no matching webhook arrives during the observation\n * window. Resolves as soon as an unexpected delivery is observed.\n *\n * - `expect_webhook_retry_keys_stable`\n * Configure the receiver to reject the first N deliveries with 5xx so\n * the sender retries, then assert every delivery within the window\n * carries the byte-identical `idempotency_key` (a key rotation across\n * retries breaks at-least-once delivery semantics).\n *\n * - `expect_webhook_signature_valid`\n * Delegate to the @adcp/sdk 9421 webhook verifier (pending; grades\n * `not_applicable` until the client verifier lands). Asserts the\n * arriving webhook verifies under the `adcp/webhook-signing/v1` profile.\n *\n * Spec: adcontextprotocol/adcp#2431 (storyboard-schema.yaml webhook section).\n */\n\nimport { randomUUID } from 'node:crypto';\nimport { injectContext, type RunnerVariables } from './context';\nimport type {\n HttpProbeResult,\n RunnerExtractionRecord,\n RunnerRequestRecord,\n SchemaValidationError,\n Storyboard,\n StoryboardContext,\n StoryboardRunOptions,\n StoryboardStep,\n StoryboardStepPreview,\n StoryboardStepResult,\n ValidationResult,\n WebhookAssertionErrorCode,\n} from './types';\nimport { WEBHOOK_IDEMPOTENCY_KEY_PATTERN } from './types';\nimport type { CapturedWebhook, RetryReplayPolicy, WebhookFilter, WebhookReceiver } from './webhook-receiver';\nimport { verifyWebhookSignature } from '../../signing/webhook-verifier';\nimport { WebhookSignatureError } from '../../signing/errors';\nimport { InMemoryReplayStore, type ReplayStore } from '../../signing/replay';\nimport { InMemoryRevocationStore, type RevocationStore } from '../../signing/revocation';\nimport type { RequestLike } from '../../signing/canonicalize';\nimport { getSchemaValidatorByRef } from '../../validation/schema-loader';\nimport { ADCP_VERSION } from '../../version';\n\nconst RUN_STATE_REPLAY_KEY = '__webhook_signing_replay_store';\nconst RUN_STATE_REVOCATION_KEY = '__webhook_signing_revocation_store';\nconst SCHEMA_URL_BASE = 'https://adcontextprotocol.org';\n\n/**\n * Return a replay store that's shared for the rest of this run, so a\n * replayed `(keyid, nonce)` across two deliveries of the same event is\n * detected. Lazy-constructed on first use so runs without signature\n * assertions pay zero cost. Explicit caller-supplied stores take\n * precedence — operators who want durable cross-run detection wire their\n * own store via `options.webhook_signing.replayStore`.\n */\nfunction getSharedReplayStore(runnerVars: RunnerVariables, override: ReplayStore | undefined): ReplayStore {\n if (override) return override;\n const existing = runnerVars.runState.get(RUN_STATE_REPLAY_KEY);\n if (existing) return existing as ReplayStore;\n const store = new InMemoryReplayStore();\n runnerVars.runState.set(RUN_STATE_REPLAY_KEY, store);\n return store;\n}\n\nfunction getSharedRevocationStore(runnerVars: RunnerVariables, override: RevocationStore | undefined): RevocationStore {\n if (override) return override;\n const existing = runnerVars.runState.get(RUN_STATE_REVOCATION_KEY);\n if (existing) return existing as RevocationStore;\n const store = new InMemoryRevocationStore();\n runnerVars.runState.set(RUN_STATE_REVOCATION_KEY, store);\n return store;\n}\n\nexport const WEBHOOK_ASSERTION_TASKS: Set<string> = new Set([\n 'expect_webhook',\n 'expect_no_webhook',\n 'expect_webhook_retry_keys_stable',\n 'expect_webhook_signature_valid',\n]);\n\nconst DEFAULT_TIMEOUT_SECONDS = 30;\nconst DEFAULT_NO_WEBHOOK_TIMEOUT_SECONDS = 5;\nconst DEFAULT_RETRY_REPLAY_TIMEOUT_SECONDS = 90;\nconst DEFAULT_RETRY_COUNT = 3;\nconst DEFAULT_RETRY_HTTP_STATUS = 503;\nconst DEFAULT_MIN_DELIVERIES = 2;\nconst DEFAULT_WEBHOOK_SIGNING_TAG = 'adcp/webhook-signing/v1';\n\n/**\n * Caps on storyboard-declared knobs. Storyboards are data; treat their\n * fields as untrusted when they drive network behavior against a real\n * seller. A typo'd `retry_trigger.count: 1_000_000` would otherwise turn\n * the runner into a DoS amplifier; a `timeout_seconds: 86400` would wedge\n * CI for a day. Clamps here fail-safe (use the max, don't reject) so a\n * compliance run never dies on a storyboard author's fat-fingered YAML.\n */\nconst MAX_RETRY_REPLAY_COUNT = 10;\nconst MAX_TIMEOUT_SECONDS = 300;\n/** HTTP statuses the retry-replay policy may return. 429 + 5xx are the\n * shapes any conformant at-least-once sender treats as retryable. */\nconst ALLOWED_RETRY_HTTP_STATUSES: ReadonlySet<number> = new Set([429, 500, 502, 503, 504]);\n\nfunction clampTimeoutSeconds(raw: number | undefined, fallback: number): number {\n const resolved = typeof raw === 'number' && raw > 0 ? raw : fallback;\n return Math.min(resolved, MAX_TIMEOUT_SECONDS);\n}\n\nfunction clampRetryPolicy(spec: { count?: number; http_status?: number } | undefined): {\n count: number;\n http_status: number;\n} {\n const requestedCount = spec?.count ?? DEFAULT_RETRY_COUNT;\n const count = Math.max(1, Math.min(requestedCount, MAX_RETRY_REPLAY_COUNT));\n const requestedStatus = spec?.http_status ?? DEFAULT_RETRY_HTTP_STATUS;\n const http_status = ALLOWED_RETRY_HTTP_STATUSES.has(requestedStatus) ? requestedStatus : DEFAULT_RETRY_HTTP_STATUS;\n return { count, http_status };\n}\n\ninterface WebhookAssertionRunState {\n contributions: Set<string>;\n priorStepResults: Map<string, StoryboardStepResult>;\n priorProbes: Map<string, HttpProbeResult>;\n agentUrl: string;\n webhookReceiver?: WebhookReceiver;\n runnerVars?: RunnerVariables;\n}\n\ninterface FlatStep {\n step: StoryboardStep;\n phaseId: string;\n globalIndex: number;\n}\n\ntype GetNextPreview = (currentStepId: string) => StoryboardStepPreview | undefined;\n\n/**\n * Pre-mint operation ids and install retry-replay policies for every\n * `expect_webhook_retry_keys_stable` step declared by the storyboard.\n *\n * Called once at runStoryboard init — ordering matters: the receiver must\n * be armed BEFORE the triggering step runs or the first delivery will be\n * accepted and the sender won't retry.\n */\nexport function armWebhookAssertions(\n storyboard: Storyboard,\n runnerVars: RunnerVariables,\n receiver: WebhookReceiver\n): void {\n for (const phase of storyboard.phases) {\n for (const step of phase.steps) {\n if (step.task !== 'expect_webhook_retry_keys_stable') continue;\n if (!step.triggered_by) continue;\n\n // Pre-mint operation_id so the triggering step's {{runner.webhook_url:<id>}}\n // expansion matches the retry-policy key here.\n let opId = runnerVars.stepOperationIds.get(step.triggered_by);\n if (!opId) {\n opId = randomUUID();\n runnerVars.stepOperationIds.set(step.triggered_by, opId);\n }\n const policy: RetryReplayPolicy = clampRetryPolicy(step.retry_trigger);\n receiver.set_retry_replay({ step_id: step.triggered_by, operation_id: opId }, policy);\n }\n }\n}\n\n/**\n * Dispatch any of the four webhook-assertion pseudo-tasks.\n *\n * Called by runner.executeStep when the step's `task` is in\n * `WEBHOOK_ASSERTION_TASKS`. Every path returns a `StoryboardStepResult`\n * with validations that carry the spec error codes in `actual.code`.\n */\nexport async function executeWebhookAssertionStep(\n step: StoryboardStep,\n phaseId: string,\n context: StoryboardContext,\n allSteps: FlatStep[],\n options: StoryboardRunOptions,\n runState: WebhookAssertionRunState,\n getNextPreview?: GetNextPreview\n): Promise<StoryboardStepResult> {\n const start = Date.now();\n const next = getNextPreview?.(step.id);\n const requestRecord: RunnerRequestRecord = {\n transport: 'http',\n operation: step.task,\n payload: null,\n };\n const extraction: RunnerExtractionRecord = { path: 'none', note: 'webhook-assertion step' };\n\n // `requires_contract` skip — assertion opts out cleanly when the harness\n // contract the author depended on isn't configured on this runner.\n if (step.requires_contract) {\n const contracts = new Set(options.contracts ?? []);\n if (!contracts.has(step.requires_contract)) {\n return skippedResult(step, phaseId, context, start, {\n skip_reason: 'unsatisfied_contract',\n detail: `Test-kit contract \"${step.requires_contract}\" is not configured on this runner.`,\n extraction,\n request: requestRecord,\n next,\n });\n }\n }\n\n if (!runState.webhookReceiver || !runState.runnerVars) {\n // Webhook receiver isn't enabled — equivalent to \"contract not in scope\"\n // for compliance reports. Grade as not_applicable, not fail.\n return skippedResult(step, phaseId, context, start, {\n skip_reason: 'unsatisfied_contract',\n detail:\n `Step \"${step.task}\" requires an ephemeral webhook receiver. Pass ` +\n '`webhook_receiver` on runStoryboard options to enable.',\n extraction,\n request: requestRecord,\n next,\n });\n }\n\n // If the triggering step failed or was skipped, there's no webhook to\n // observe — surface as prerequisite_failed rather than timing out.\n if (step.triggered_by) {\n const prior = runState.priorStepResults.get(step.triggered_by);\n if (!prior || prior.skipped || !prior.passed) {\n return skippedResult(step, phaseId, context, start, {\n skip_reason: 'prerequisite_failed',\n detail: prior\n ? `Triggering step \"${step.triggered_by}\" did not complete successfully.`\n : `Triggering step \"${step.triggered_by}\" has no prior result.`,\n extraction,\n request: requestRecord,\n next,\n });\n }\n }\n\n const receiver = runState.webhookReceiver;\n const runnerVars = runState.runnerVars;\n const filter = buildFilter(step, context, runnerVars);\n\n let validations: ValidationResult[];\n let passed: boolean;\n\n switch (step.task) {\n case 'expect_webhook': {\n const outcome = await runExpectWebhook(step, filter, receiver);\n validations = outcome.validations;\n passed = outcome.passed;\n break;\n }\n case 'expect_no_webhook': {\n const outcome = await runExpectNoWebhook(step, filter, receiver);\n validations = outcome.validations;\n passed = outcome.passed;\n break;\n }\n case 'expect_webhook_retry_keys_stable': {\n const outcome = await runExpectRetryKeysStable(step, filter, receiver);\n validations = outcome.validations;\n passed = outcome.passed;\n break;\n }\n case 'expect_webhook_signature_valid': {\n const outcome = await runExpectSignatureValid(step, filter, receiver, options, runnerVars);\n validations = outcome.validations;\n passed = outcome.passed;\n // When the verifier isn't configured, the spec says not_applicable\n // rather than fail. Return the skipped shape.\n if (outcome.skipped) {\n return skippedResult(step, phaseId, context, start, {\n skip_reason: 'unsatisfied_contract',\n detail: outcome.skipReason ?? 'Signature verifier not configured.',\n extraction,\n request: requestRecord,\n next,\n });\n }\n break;\n }\n default: {\n // Defensive — this function is only called for tasks in WEBHOOK_ASSERTION_TASKS.\n validations = [];\n passed = false;\n }\n }\n\n return {\n step_id: step.id,\n phase_id: phaseId,\n title: step.title,\n task: step.task,\n passed,\n expect_error: step.expect_error,\n duration_ms: Date.now() - start,\n validations,\n context,\n ...(next && { next }),\n request: requestRecord,\n extraction,\n };\n}\n\n// ────────────────────────────────────────────────────────────\n// Filter construction\n// ────────────────────────────────────────────────────────────\n\nfunction buildFilter(step: StoryboardStep, context: StoryboardContext, runnerVars: RunnerVariables): WebhookFilter {\n const filter: WebhookFilter = {};\n\n // Default: scope to the triggering step's URL. Authors can override via\n // an explicit `filter.operation_id` (useful for fan-in tests).\n if (step.triggered_by) {\n filter.step_id = step.triggered_by;\n const priorOpId = runnerVars.stepOperationIds.get(step.triggered_by);\n if (priorOpId) filter.operation_id = priorOpId;\n }\n\n if (step.filter) {\n // Resolve any `{{prior_step.<id>.operation_id}}` / `$context.*` tokens\n // before matching — the storyboard may author operation_id this way.\n const resolved = injectContext({ __f: { ...step.filter } }, context, runnerVars).__f as Record<string, unknown>;\n if (resolved && typeof resolved === 'object') {\n if (typeof resolved.operation_id === 'string') {\n filter.operation_id = resolved.operation_id;\n }\n if (resolved.body && typeof resolved.body === 'object' && !Array.isArray(resolved.body)) {\n filter.body = resolved.body as Record<string, unknown>;\n }\n }\n }\n return filter;\n}\n\n// ────────────────────────────────────────────────────────────\n// expect_no_webhook\n// ────────────────────────────────────────────────────────────\n\nasync function runExpectNoWebhook(\n step: StoryboardStep,\n filter: WebhookFilter,\n receiver: WebhookReceiver\n): Promise<{ validations: ValidationResult[]; passed: boolean }> {\n const timeoutMs = clampTimeoutSeconds(step.timeout_seconds, DEFAULT_NO_WEBHOOK_TIMEOUT_SECONDS) * 1000;\n const outcome = await receiver.wait(filter, timeoutMs);\n\n if (!outcome.timed_out && outcome.webhook) {\n const matches = receiver.matching(filter);\n const webhook = outcome.webhook;\n return singleFailure(\n step,\n 'unexpected_webhook_received',\n `Observed ${matches.length} webhook ${matches.length === 1 ? 'delivery' : 'deliveries'} matching the filter; expected none.`,\n 'zero webhook deliveries matching filter',\n {\n count: matches.length,\n first_delivery: {\n id: webhook.id,\n step_id: webhook.step_id,\n operation_id: webhook.operation_id,\n delivery_index: webhook.delivery_index,\n received_at: webhook.received_at,\n },\n },\n null\n );\n }\n\n return {\n validations: [\n {\n check: 'expect_no_webhook',\n passed: true,\n description: `No webhook matching the filter arrived within ${timeoutMs}ms.`,\n json_pointer: null,\n },\n ],\n passed: true,\n };\n}\n\n// ────────────────────────────────────────────────────────────\n// expect_webhook\n// ────────────────────────────────────────────────────────────\n\nasync function runExpectWebhook(\n step: StoryboardStep,\n filter: WebhookFilter,\n receiver: WebhookReceiver\n): Promise<{ validations: ValidationResult[]; passed: boolean }> {\n const timeoutMs = clampTimeoutSeconds(step.timeout_seconds, DEFAULT_TIMEOUT_SECONDS) * 1000;\n const checkIdempotency = step.expect_idempotency_key !== false;\n const capCount = step.expect_max_deliveries_per_logical_event;\n\n // When we're asserting a cap (e.g., \"at most 1 logical event on replay\"),\n // wait the full window to let any duplicate delivery arrive before judging.\n // When we just need ≥1 delivery, resolve on first match.\n let matches: CapturedWebhook[];\n if (typeof capCount === 'number') {\n matches = await receiver.wait_all(filter, timeoutMs);\n } else {\n const first = await receiver.wait(filter, timeoutMs);\n matches = first.webhook ? [first.webhook] : [];\n }\n\n if (matches.length === 0) {\n return singleFailure(\n step,\n 'no_webhook_received',\n `No webhook matching filter arrived within ${timeoutMs}ms.`,\n 'webhook delivery matching filter',\n null\n );\n }\n\n const first = matches[0]!;\n if (first.parse_error !== undefined) {\n return singleFailure(\n step,\n 'schema_violation',\n `Webhook body was not valid JSON: ${first.parse_error}`,\n 'JSON object body',\n first.raw_body.slice(0, 256)\n );\n }\n\n const schemaCheck = validateWebhookPayloadSchema(step, first);\n if (schemaCheck && !schemaCheck.passed) return { validations: [schemaCheck], passed: false };\n\n if (checkIdempotency) {\n const idempotencyCheck = assertIdempotencyKey(first);\n if (idempotencyCheck) return { validations: [idempotencyCheck], passed: false };\n }\n\n if (typeof capCount === 'number') {\n const distinctKeys = new Set(matches.map(m => extractIdempotencyKey(m)).filter((k): k is string => !!k));\n if (distinctKeys.size > capCount) {\n return singleFailure(\n step,\n 'duplicate_webhook_on_replay',\n `Observed ${distinctKeys.size} distinct logical events in the window (idempotency_keys); expected ≤ ${capCount}.`,\n `≤ ${capCount} distinct idempotency_keys`,\n Array.from(distinctKeys)\n );\n }\n }\n\n return {\n validations: [\n ...(schemaCheck ? [schemaCheck] : []),\n {\n check: 'expect_webhook',\n passed: true,\n description: `Webhook arrived at ${filter.step_id ?? 'receiver'} with valid idempotency_key.`,\n json_pointer: '/idempotency_key',\n },\n ],\n passed: true,\n };\n}\n\n// ────────────────────────────────────────────────────────────\n// expect_webhook_retry_keys_stable\n// ────────────────────────────────────────────────────────────\n\nasync function runExpectRetryKeysStable(\n step: StoryboardStep,\n filter: WebhookFilter,\n receiver: WebhookReceiver\n): Promise<{ validations: ValidationResult[]; passed: boolean }> {\n const timeoutMs = clampTimeoutSeconds(step.timeout_seconds, DEFAULT_RETRY_REPLAY_TIMEOUT_SECONDS) * 1000;\n const minDeliveries = step.expect_min_deliveries ?? DEFAULT_MIN_DELIVERIES;\n\n const matches = await receiver.wait_all(filter, timeoutMs);\n\n if (matches.length < minDeliveries) {\n return singleFailure(\n step,\n 'insufficient_retries',\n `Observed ${matches.length} deliveries; expected at least ${minDeliveries}. ` +\n 'The sender may not be retrying on 5xx responses.',\n `≥ ${minDeliveries} deliveries`,\n matches.length\n );\n }\n\n const keys = matches.map(m => extractIdempotencyKey(m));\n const missingKey = matches.find((_, i) => !keys[i]);\n if (missingKey) {\n return singleFailure(\n step,\n 'missing_idempotency_key',\n `Delivery ${missingKey.delivery_index} carried no idempotency_key.`,\n 'every delivery carries idempotency_key',\n null\n );\n }\n\n const firstKey = keys[0]!;\n const formatValid = keys.every(k => k && WEBHOOK_IDEMPOTENCY_KEY_PATTERN.test(k));\n if (!formatValid) {\n const bad = keys.find(k => !k || !WEBHOOK_IDEMPOTENCY_KEY_PATTERN.test(k));\n return singleFailure(\n step,\n 'idempotency_key_format_changed',\n `One or more deliveries carried a malformed idempotency_key. Required pattern: ${WEBHOOK_IDEMPOTENCY_KEY_PATTERN.source}`,\n WEBHOOK_IDEMPOTENCY_KEY_PATTERN.source,\n bad ?? null\n );\n }\n\n const rotated = keys.some(k => k !== firstKey);\n if (rotated) {\n return singleFailure(\n step,\n 'idempotency_key_rotated',\n \"idempotency_key rotated across retries. Senders MUST reuse the first delivery's key for every retry of the same logical event.\",\n 'byte-identical idempotency_key on every delivery',\n keys\n );\n }\n\n return {\n validations: [\n {\n check: 'expect_webhook_retry_keys_stable',\n passed: true,\n description: `${matches.length} deliveries observed; all carried byte-identical idempotency_key.`,\n json_pointer: '/idempotency_key',\n },\n ],\n passed: true,\n };\n}\n\n// ────────────────────────────────────────────────────────────\n// expect_webhook_signature_valid\n// ────────────────────────────────────────────────────────────\n\nasync function runExpectSignatureValid(\n step: StoryboardStep,\n filter: WebhookFilter,\n receiver: WebhookReceiver,\n options: StoryboardRunOptions,\n runnerVars: RunnerVariables\n): Promise<{ validations: ValidationResult[]; passed: boolean; skipped?: boolean; skipReason?: string }> {\n const config = options.webhook_signing;\n if (!config) {\n return {\n validations: [],\n passed: true,\n skipped: true,\n skipReason:\n '`expect_webhook_signature_valid` requires `webhook_signing` on runStoryboard options ' +\n '(pass a JwksResolver; replay + revocation stores default to in-memory).',\n };\n }\n\n const timeoutMs = clampTimeoutSeconds(step.timeout_seconds, DEFAULT_TIMEOUT_SECONDS) * 1000;\n const wait = await receiver.wait(filter, timeoutMs);\n if (wait.timed_out || !wait.webhook) {\n return singleFailure(\n step,\n 'no_webhook_received',\n `No webhook matching filter arrived within ${timeoutMs}ms.`,\n 'signed webhook delivery matching filter',\n null\n );\n }\n\n const webhook = wait.webhook;\n void runnerVars;\n const requestLike = toRequestLike(webhook, receiver);\n const requiredTag = step.require_tag ?? config.required_tag ?? DEFAULT_WEBHOOK_SIGNING_TAG;\n\n try {\n await verifyWebhookSignature(requestLike, {\n jwks: config.jwks,\n replayStore: getSharedReplayStore(runnerVars, config.replayStore),\n revocationStore: getSharedRevocationStore(runnerVars, config.revocationStore),\n requiredTag,\n });\n } catch (err) {\n if (err instanceof WebhookSignatureError) {\n return singleFailure(\n step,\n mapSignatureErrorCode(err.code),\n err.message,\n `checklist step ${err.failedStep} passes`,\n { code: err.code, step: err.failedStep }\n );\n }\n return singleFailure(\n step,\n 'signature_invalid',\n err instanceof Error ? err.message : String(err),\n '9421 webhook signature verification',\n null\n );\n }\n\n return {\n validations: [\n {\n check: 'expect_webhook_signature_valid',\n passed: true,\n description: `9421 webhook signature verified under ${requiredTag}.`,\n },\n ],\n passed: true,\n };\n}\n\n/**\n * Translate a captured webhook into the `RequestLike` shape the shared\n * canonicalizer expects. The receiver's `base_url` plus `step_id` /\n * `operation_id` reconstruct the `@target-uri` the publisher signed.\n */\nfunction toRequestLike(webhook: CapturedWebhook, receiver: WebhookReceiver): RequestLike {\n const url = `${receiver.base_url}/step/${webhook.step_id}/${webhook.operation_id}`;\n return {\n method: webhook.method,\n url,\n headers: webhook.headers,\n body: webhook.raw_body,\n };\n}\n\n/**\n * Map the verifier's `webhook_signature_*` error codes to the storyboard-side\n * `signature_*` codes defined by adcontextprotocol/adcp#2431. Exhaustive so\n * that adding a new verifier code without a mapping is a TS compile error\n * rather than a silent collapse onto `signature_invalid` — distinct\n * remediation paths deserve distinct codes (a revoked key needs rotation,\n * a malformed window is a signer bug, etc.).\n */\nfunction mapSignatureErrorCode(\n code: import('../../signing/errors').WebhookSignatureErrorCode\n): WebhookAssertionErrorCode {\n switch (code) {\n case 'webhook_signature_header_malformed':\n return 'signature_header_malformed';\n case 'webhook_signature_params_incomplete':\n return 'signature_params_incomplete';\n case 'webhook_signature_tag_invalid':\n return 'signature_tag_invalid';\n case 'webhook_signature_alg_not_allowed':\n return 'signature_alg_not_allowed';\n case 'webhook_signature_window_invalid':\n return 'signature_window_invalid';\n case 'webhook_signature_components_incomplete':\n return 'signature_components_incomplete';\n case 'webhook_signature_key_unknown':\n return 'signature_key_unknown';\n case 'webhook_signature_key_purpose_invalid':\n return 'signature_key_purpose_invalid';\n case 'webhook_mode_mismatch':\n return 'signature_mode_mismatch';\n case 'webhook_target_uri_malformed':\n return 'signature_target_uri_malformed';\n case 'webhook_signature_key_revoked':\n return 'signature_key_revoked';\n case 'webhook_signature_revocation_stale':\n return 'signature_revocation_stale';\n case 'webhook_signature_rate_abuse':\n return 'signature_rate_abuse';\n case 'webhook_signature_digest_mismatch':\n return 'signature_digest_mismatch';\n case 'webhook_signature_replayed':\n return 'signature_replayed';\n case 'webhook_signature_invalid':\n return 'signature_invalid';\n default: {\n // Exhaustiveness: any unhandled code is a compile error.\n const _exhaustive: never = code;\n void _exhaustive;\n return 'signature_invalid';\n }\n }\n}\n\n// ────────────────────────────────────────────────────────────\n// Helpers\n// ────────────────────────────────────────────────────────────\n\nfunction validateWebhookPayloadSchema(step: StoryboardStep, webhook: CapturedWebhook): ValidationResult | undefined {\n const schemaRef = step.webhook_payload_schema_ref ?? step.response_schema_webhook_ref;\n if (!schemaRef) return undefined;\n\n const { schema_id, schema_url } = resolveWebhookSchemaIdentity(schemaRef);\n const validator = getSchemaValidatorByRef(schemaRef);\n if (!validator) {\n return {\n check: 'expect_webhook',\n passed: false,\n description: 'Webhook payload schema validation failed.',\n error: `No webhook payload schema found for ${schemaRef}.`,\n json_pointer: null,\n expected: schema_id,\n actual: { code: 'schema_violation', reason: 'schema_not_found', schema_ref: schemaRef },\n schema_id,\n schema_url,\n };\n }\n\n const body = webhook.body;\n if (body === null || typeof body !== 'object' || Array.isArray(body)) {\n return {\n check: 'expect_webhook',\n passed: false,\n description: 'Webhook payload schema validation failed.',\n error: 'Webhook body was not a JSON object.',\n json_pointer: null,\n expected: schema_id,\n actual: { code: 'schema_violation', reason: 'non_object_body', body },\n schema_id,\n schema_url,\n };\n }\n\n const passed = validator(body);\n if (passed) {\n return {\n check: 'expect_webhook',\n passed: true,\n description: `Webhook payload validates against ${schemaRef}.`,\n json_pointer: null,\n expected: schema_id,\n actual: 'schema_valid',\n schema_id,\n schema_url,\n };\n }\n\n const issues = (validator.errors ?? []).map(ajvErrorToSchemaError);\n const firstIssue = issues[0];\n return {\n check: 'expect_webhook',\n passed: false,\n description: 'Webhook payload schema validation failed.',\n error: firstIssue?.message ?? `Webhook body failed ${schemaRef}.`,\n json_pointer: firstIssue?.instance_path ?? null,\n expected: schema_id,\n actual: { code: 'schema_violation', issues },\n schema_id,\n schema_url,\n };\n}\n\nfunction resolveWebhookSchemaIdentity(schemaRef: string): { schema_id: string; schema_url: string } {\n const trimmed = schemaRef.replace(/^\\/+/, '');\n const schemaId = `/schemas/${ADCP_VERSION}/${trimmed}`;\n return { schema_id: schemaId, schema_url: `${SCHEMA_URL_BASE}${schemaId}` };\n}\n\nfunction ajvErrorToSchemaError(error: {\n instancePath?: string;\n schemaPath?: string;\n keyword?: string;\n message?: string;\n}): SchemaValidationError {\n return {\n instance_path: error.instancePath || '/',\n schema_path: error.schemaPath || '#',\n keyword: error.keyword || 'schema',\n message: error.message || 'Webhook payload failed schema validation.',\n };\n}\n\nfunction extractIdempotencyKey(webhook: CapturedWebhook): string | undefined {\n const body = webhook.body;\n if (body === null || typeof body !== 'object' || Array.isArray(body)) return undefined;\n const key = (body as Record<string, unknown>).idempotency_key;\n return typeof key === 'string' ? key : undefined;\n}\n\nfunction assertIdempotencyKey(webhook: CapturedWebhook): ValidationResult | undefined {\n const body = webhook.body;\n if (body === null || typeof body !== 'object' || Array.isArray(body)) {\n return failure('schema_violation', 'Webhook body was not a JSON object.', 'JSON object body', null);\n }\n const key = (body as Record<string, unknown>).idempotency_key;\n if (typeof key !== 'string' || key.length === 0) {\n return failure(\n 'missing_idempotency_key',\n 'Webhook body is missing a non-empty `idempotency_key` — required by AdCP 3.0.',\n 'non-empty string',\n key ?? null\n );\n }\n if (!WEBHOOK_IDEMPOTENCY_KEY_PATTERN.test(key)) {\n return failure(\n 'invalid_idempotency_key_format',\n `idempotency_key \"${key}\" does not match required pattern ${WEBHOOK_IDEMPOTENCY_KEY_PATTERN.source}.`,\n WEBHOOK_IDEMPOTENCY_KEY_PATTERN.source,\n key\n );\n }\n return undefined;\n}\n\nfunction failure(\n code: WebhookAssertionErrorCode,\n message: string,\n expected: unknown,\n actual: unknown\n): ValidationResult {\n return {\n check: 'expect_webhook',\n passed: false,\n description: 'Webhook conformance assertion failed.',\n error: message,\n json_pointer: '/idempotency_key',\n expected,\n actual: { code, actual },\n };\n}\n\nfunction singleFailure(\n step: StoryboardStep,\n code: WebhookAssertionErrorCode,\n message: string,\n expected: unknown,\n actual: unknown,\n jsonPointer: string | null = '/idempotency_key'\n): { validations: ValidationResult[]; passed: boolean } {\n return {\n validations: [\n {\n check: step.task,\n passed: false,\n description: step.expected ?? step.title,\n error: message,\n json_pointer: jsonPointer,\n expected,\n actual: { code, actual },\n },\n ],\n passed: false,\n };\n}\n\nfunction skippedResult(\n step: StoryboardStep,\n phaseId: string,\n context: StoryboardContext,\n start: number,\n opts: {\n skip_reason: 'unsatisfied_contract' | 'prerequisite_failed';\n detail: string;\n extraction: RunnerExtractionRecord;\n request: RunnerRequestRecord;\n next?: StoryboardStepPreview;\n }\n): StoryboardStepResult {\n return {\n step_id: step.id,\n phase_id: phaseId,\n title: step.title,\n task: step.task,\n passed: true,\n skipped: true,\n skip_reason: opts.skip_reason,\n skip: { reason: opts.skip_reason, detail: opts.detail },\n duration_ms: Date.now() - start,\n validations: [],\n context,\n error: opts.detail,\n ...(opts.next && { next: opts.next }),\n request: opts.request,\n extraction: opts.extraction,\n };\n}\n"],"mappings":"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","names":["first"]}
1
+ {"version":3,"sources":["../../../../src/lib/testing/storyboard/webhook-assertions.ts"],"sourcesContent":["/**\n * Webhook-assertion pseudo-tasks.\n *\n * Four step `task` values graded by observing the runner's webhook\n * receiver rather than by calling the agent:\n *\n * - `expect_webhook`\n * Assert at least one matching webhook arrived carrying a\n * well-formed `idempotency_key`. Optional: body-schema validation and\n * max-deliveries-per-logical-event cap (catches duplicate-side-effect\n * bugs on replay).\n *\n * - `expect_no_webhook`\n * Assert that no matching webhook arrives during the observation\n * window. Resolves as soon as an unexpected delivery is observed.\n *\n * - `expect_webhook_retry_keys_stable`\n * Configure the receiver to reject the first N deliveries with 5xx so\n * the sender retries, then assert every delivery within the window\n * carries the byte-identical `idempotency_key` (a key rotation across\n * retries breaks at-least-once delivery semantics).\n *\n * - `expect_webhook_signature_valid`\n * Delegate to the @adcp/sdk 9421 webhook verifier (pending; grades\n * `not_applicable` until the client verifier lands). Asserts the\n * arriving webhook verifies under the `adcp/webhook-signing/v1` profile.\n *\n * Spec: adcontextprotocol/adcp#2431 (storyboard-schema.yaml webhook section).\n */\n\nimport { randomUUID } from 'node:crypto';\nimport { injectContext, type RunnerVariables } from './context';\nimport type {\n HttpProbeResult,\n RunnerExtractionRecord,\n RunnerRequestRecord,\n SchemaValidationError,\n Storyboard,\n StoryboardContext,\n StoryboardRunOptions,\n StoryboardStep,\n StoryboardStepPreview,\n StoryboardStepResult,\n ValidationResult,\n WebhookAssertionErrorCode,\n} from './types';\nimport { WEBHOOK_IDEMPOTENCY_KEY_PATTERN } from './types';\nimport type { CapturedWebhook, RetryReplayPolicy, WebhookFilter, WebhookReceiver } from './webhook-receiver';\nimport { verifyWebhookSignature } from '../../signing/webhook-verifier';\nimport { WebhookSignatureError } from '../../signing/errors';\nimport { InMemoryReplayStore, type ReplayStore } from '../../signing/replay';\nimport { InMemoryRevocationStore, type RevocationStore } from '../../signing/revocation';\nimport type { RequestLike } from '../../signing/canonicalize';\nimport { getSchemaValidatorByRef } from '../../validation/schema-loader';\nimport { ADCP_VERSION } from '../../version';\n\nconst RUN_STATE_REPLAY_KEY = '__webhook_signing_replay_store';\nconst RUN_STATE_REVOCATION_KEY = '__webhook_signing_revocation_store';\nconst SCHEMA_URL_BASE = 'https://adcontextprotocol.org';\n\n/**\n * Return a replay store that's shared for the rest of this run, so a\n * replayed `(keyid, nonce)` across two deliveries of the same event is\n * detected. Lazy-constructed on first use so runs without signature\n * assertions pay zero cost. Explicit caller-supplied stores take\n * precedence — operators who want durable cross-run detection wire their\n * own store via `options.webhook_signing.replayStore`.\n */\nfunction getSharedReplayStore(runnerVars: RunnerVariables, override: ReplayStore | undefined): ReplayStore {\n if (override) return override;\n const existing = runnerVars.runState.get(RUN_STATE_REPLAY_KEY);\n if (existing) return existing as ReplayStore;\n const store = new InMemoryReplayStore();\n runnerVars.runState.set(RUN_STATE_REPLAY_KEY, store);\n return store;\n}\n\nfunction getSharedRevocationStore(runnerVars: RunnerVariables, override: RevocationStore | undefined): RevocationStore {\n if (override) return override;\n const existing = runnerVars.runState.get(RUN_STATE_REVOCATION_KEY);\n if (existing) return existing as RevocationStore;\n const store = new InMemoryRevocationStore();\n runnerVars.runState.set(RUN_STATE_REVOCATION_KEY, store);\n return store;\n}\n\nexport const WEBHOOK_ASSERTION_TASKS: Set<string> = new Set([\n 'expect_webhook',\n 'expect_no_webhook',\n 'expect_webhook_retry_keys_stable',\n 'expect_webhook_signature_valid',\n]);\n\nconst DEFAULT_TIMEOUT_SECONDS = 30;\nconst DEFAULT_NO_WEBHOOK_TIMEOUT_SECONDS = 5;\n// Leave enough headroom for setup/report serialization inside the documented\n// 120-second isolated-run budget, even when a remote receiver is unreachable.\nconst DEFAULT_RETRY_REPLAY_TIMEOUT_SECONDS = 60;\nconst DEFAULT_RETRY_COUNT = 3;\nconst DEFAULT_RETRY_HTTP_STATUS = 503;\nconst DEFAULT_MIN_DELIVERIES = 2;\n\nfunction webhookTimeoutDetail(receiver: WebhookReceiver, base: string): string {\n return receiver.mode === 'loopback_mock' && receiver.all().length === 0 && receiver.challenges().length === 0\n ? `${base} No request reached the loopback receiver; an out-of-process agent must use webhook_receiver.mode=\"proxy_url\".`\n : base;\n}\nconst DEFAULT_WEBHOOK_SIGNING_TAG = 'adcp/webhook-signing/v1';\n\n/**\n * Caps on storyboard-declared knobs. Storyboards are data; treat their\n * fields as untrusted when they drive network behavior against a real\n * seller. A typo'd `retry_trigger.count: 1_000_000` would otherwise turn\n * the runner into a DoS amplifier; a `timeout_seconds: 86400` would wedge\n * CI for a day. Clamps here fail-safe (use the max, don't reject) so a\n * compliance run never dies on a storyboard author's fat-fingered YAML.\n */\nconst MAX_RETRY_REPLAY_COUNT = 10;\nconst MAX_TIMEOUT_SECONDS = 300;\n/** HTTP statuses the retry-replay policy may return. 429 + 5xx are the\n * shapes any conformant at-least-once sender treats as retryable. */\nconst ALLOWED_RETRY_HTTP_STATUSES: ReadonlySet<number> = new Set([429, 500, 502, 503, 504]);\n\nfunction clampTimeoutSeconds(raw: number | undefined, fallback: number): number {\n const resolved = typeof raw === 'number' && raw > 0 ? raw : fallback;\n return Math.min(resolved, MAX_TIMEOUT_SECONDS);\n}\n\nfunction clampRetryPolicy(spec: { count?: number; http_status?: number } | undefined): {\n count: number;\n http_status: number;\n} {\n const requestedCount = spec?.count ?? DEFAULT_RETRY_COUNT;\n const count = Math.max(1, Math.min(requestedCount, MAX_RETRY_REPLAY_COUNT));\n const requestedStatus = spec?.http_status ?? DEFAULT_RETRY_HTTP_STATUS;\n const http_status = ALLOWED_RETRY_HTTP_STATUSES.has(requestedStatus) ? requestedStatus : DEFAULT_RETRY_HTTP_STATUS;\n return { count, http_status };\n}\n\ninterface WebhookAssertionRunState {\n contributions: Set<string>;\n priorStepResults: Map<string, StoryboardStepResult>;\n priorProbes: Map<string, HttpProbeResult>;\n agentUrl: string;\n webhookReceiver?: WebhookReceiver;\n runnerVars?: RunnerVariables;\n}\n\ninterface FlatStep {\n step: StoryboardStep;\n phaseId: string;\n globalIndex: number;\n}\n\nfunction collectNotificationConfigs(\n value: unknown,\n configs: Record<string, unknown>[] = []\n): Record<string, unknown>[] {\n if (!value || typeof value !== 'object') return configs;\n if (Array.isArray(value)) {\n for (const item of value) collectNotificationConfigs(item, configs);\n return configs;\n }\n const record = value as Record<string, unknown>;\n if (Array.isArray(record.notification_configs)) {\n for (const config of record.notification_configs) {\n if (config && typeof config === 'object' && !Array.isArray(config)) {\n configs.push(config as Record<string, unknown>);\n }\n }\n }\n for (const nested of Object.values(record)) collectNotificationConfigs(nested, configs);\n return configs;\n}\n\nfunction notificationSetupMatchesAssertion(setup: StoryboardStep, assertion: StoryboardStep): boolean {\n const configs = collectNotificationConfigs(setup.sample_request);\n if (configs.length === 0) return false;\n const filter =\n assertion.filter && typeof assertion.filter === 'object'\n ? (assertion.filter as Record<string, unknown>)\n : undefined;\n const body = filter?.body && typeof filter.body === 'object' ? (filter.body as Record<string, unknown>) : undefined;\n const subscriberId = filter?.subscriber_id ?? body?.subscriber_id;\n const eventType = filter?.notification_type ?? body?.notification_type ?? filter?.event ?? body?.event;\n\n // A setup failure is only an implicit prerequisite when the assertion names\n // a subscriber or event configured by that setup. Other webhook assertions\n // retain their own trigger dependency and cannot be masked by an unrelated\n // failed registration elsewhere in the storyboard.\n if (typeof subscriberId !== 'string' && typeof eventType !== 'string') return false;\n return configs.some(config => {\n const subscriberMatches = typeof subscriberId !== 'string' || config.subscriber_id === subscriberId;\n const eventMatches =\n typeof eventType !== 'string' || (Array.isArray(config.event_types) && config.event_types.includes(eventType));\n return subscriberMatches && eventMatches;\n });\n}\n\nfunction failedNotificationSetup(\n step: StoryboardStep,\n allSteps: FlatStep[],\n priorStepResults: Map<string, StoryboardStepResult>\n): StoryboardStepResult | undefined {\n const currentIndex = allSteps.find(entry => entry.step.id === step.id)?.globalIndex ?? Number.POSITIVE_INFINITY;\n const setup = [...allSteps]\n .reverse()\n .find(\n entry =>\n entry.globalIndex < currentIndex &&\n (entry.step.task === 'sync_accounts' || entry.step.task === 'sync_agent_notification_configs') &&\n notificationSetupMatchesAssertion(entry.step, step)\n );\n if (!setup) return undefined;\n const result = priorStepResults.get(setup.step.id);\n return result && (result.skipped || !result.passed) ? result : undefined;\n}\n\ntype GetNextPreview = (currentStepId: string) => StoryboardStepPreview | undefined;\n\n/**\n * Pre-mint operation ids and install retry-replay policies for every\n * `expect_webhook_retry_keys_stable` step declared by the storyboard.\n *\n * Called once at runStoryboard init — ordering matters: the receiver must\n * be armed BEFORE the triggering step runs or the first delivery will be\n * accepted and the sender won't retry.\n */\nexport function armWebhookAssertions(\n storyboard: Storyboard,\n runnerVars: RunnerVariables,\n receiver: WebhookReceiver\n): void {\n for (const phase of storyboard.phases) {\n for (const step of phase.steps) {\n if (step.task !== 'expect_webhook_retry_keys_stable') continue;\n if (!step.triggered_by) continue;\n\n // Pre-mint operation_id so the triggering step's {{runner.webhook_url:<id>}}\n // expansion matches the retry-policy key here.\n let opId = runnerVars.stepOperationIds.get(step.triggered_by);\n if (!opId) {\n opId = randomUUID();\n runnerVars.stepOperationIds.set(step.triggered_by, opId);\n }\n const policy: RetryReplayPolicy = clampRetryPolicy(step.retry_trigger);\n receiver.set_retry_replay({ step_id: step.triggered_by, operation_id: opId }, policy);\n }\n }\n}\n\n/**\n * Dispatch any of the four webhook-assertion pseudo-tasks.\n *\n * Called by runner.executeStep when the step's `task` is in\n * `WEBHOOK_ASSERTION_TASKS`. Every path returns a `StoryboardStepResult`\n * with validations that carry the spec error codes in `actual.code`.\n */\nexport async function executeWebhookAssertionStep(\n step: StoryboardStep,\n phaseId: string,\n context: StoryboardContext,\n allSteps: FlatStep[],\n options: StoryboardRunOptions,\n runState: WebhookAssertionRunState,\n getNextPreview?: GetNextPreview\n): Promise<StoryboardStepResult> {\n const start = Date.now();\n const next = getNextPreview?.(step.id);\n const requestRecord: RunnerRequestRecord = {\n transport: 'http',\n operation: step.task,\n payload: null,\n };\n const extraction: RunnerExtractionRecord = { path: 'none', note: 'webhook-assertion step' };\n\n // `requires_contract` skip — assertion opts out cleanly when the harness\n // contract the author depended on isn't configured on this runner.\n if (step.requires_contract) {\n const contracts = new Set(options.contracts ?? []);\n if (!contracts.has(step.requires_contract)) {\n return skippedResult(step, phaseId, context, start, {\n skip_reason: 'unsatisfied_contract',\n detail: `Test-kit contract \"${step.requires_contract}\" is not configured on this runner.`,\n extraction,\n request: requestRecord,\n next,\n });\n }\n }\n\n if (!runState.webhookReceiver || !runState.runnerVars) {\n // Webhook receiver isn't enabled — equivalent to \"contract not in scope\"\n // for compliance reports. Grade as not_applicable, not fail.\n return skippedResult(step, phaseId, context, start, {\n skip_reason: 'unsatisfied_contract',\n detail:\n `Step \"${step.task}\" requires an ephemeral webhook receiver. Pass ` +\n '`webhook_receiver` on runStoryboard options to enable.',\n extraction,\n request: requestRecord,\n next,\n });\n }\n\n const failedSetup = failedNotificationSetup(step, allSteps, runState.priorStepResults);\n if (failedSetup) {\n return skippedResult(step, phaseId, context, start, {\n skip_reason: 'prerequisite_failed',\n detail: `Notification setup step \"${failedSetup.step_id}\" did not complete successfully.`,\n extraction,\n request: requestRecord,\n next,\n });\n }\n\n // If the triggering step failed or was skipped, there's no webhook to\n // observe — surface as prerequisite_failed rather than timing out.\n if (step.triggered_by) {\n const prior = runState.priorStepResults.get(step.triggered_by);\n if (!prior || prior.skipped || !prior.passed) {\n return skippedResult(step, phaseId, context, start, {\n skip_reason: 'prerequisite_failed',\n detail: prior\n ? `Triggering step \"${step.triggered_by}\" did not complete successfully.`\n : `Triggering step \"${step.triggered_by}\" has no prior result.`,\n extraction,\n request: requestRecord,\n next,\n });\n }\n }\n\n const receiver = runState.webhookReceiver;\n const runnerVars = runState.runnerVars;\n const filter = buildFilter(step, context, runnerVars);\n\n let validations: ValidationResult[];\n let passed: boolean;\n\n switch (step.task) {\n case 'expect_webhook': {\n const outcome = await runExpectWebhook(step, filter, receiver);\n validations = outcome.validations;\n passed = outcome.passed;\n break;\n }\n case 'expect_no_webhook': {\n const outcome = await runExpectNoWebhook(step, filter, receiver);\n validations = outcome.validations;\n passed = outcome.passed;\n break;\n }\n case 'expect_webhook_retry_keys_stable': {\n const outcome = await runExpectRetryKeysStable(step, filter, receiver);\n validations = outcome.validations;\n passed = outcome.passed;\n break;\n }\n case 'expect_webhook_signature_valid': {\n const outcome = await runExpectSignatureValid(step, filter, receiver, options, runnerVars);\n validations = outcome.validations;\n passed = outcome.passed;\n // When the verifier isn't configured, the spec says not_applicable\n // rather than fail. Return the skipped shape.\n if (outcome.skipped) {\n return skippedResult(step, phaseId, context, start, {\n skip_reason: 'unsatisfied_contract',\n detail: outcome.skipReason ?? 'Signature verifier not configured.',\n extraction,\n request: requestRecord,\n next,\n });\n }\n break;\n }\n default: {\n // Defensive — this function is only called for tasks in WEBHOOK_ASSERTION_TASKS.\n validations = [];\n passed = false;\n }\n }\n\n return {\n step_id: step.id,\n phase_id: phaseId,\n title: step.title,\n task: step.task,\n passed,\n expect_error: step.expect_error,\n duration_ms: Date.now() - start,\n validations,\n context,\n ...(next && { next }),\n request: requestRecord,\n extraction,\n };\n}\n\n// ────────────────────────────────────────────────────────────\n// Filter construction\n// ────────────────────────────────────────────────────────────\n\nfunction buildFilter(step: StoryboardStep, context: StoryboardContext, runnerVars: RunnerVariables): WebhookFilter {\n const filter: WebhookFilter = {};\n\n // Default: scope to the triggering step's URL. Authors can override via\n // an explicit `filter.operation_id` (useful for fan-in tests).\n if (step.triggered_by) {\n filter.step_id = step.triggered_by;\n const priorOpId = runnerVars.stepOperationIds.get(step.triggered_by);\n if (priorOpId) filter.operation_id = priorOpId;\n }\n\n if (step.filter) {\n // Resolve any `{{prior_step.<id>.operation_id}}` / `$context.*` tokens\n // before matching — the storyboard may author operation_id this way.\n const resolved = injectContext({ __f: { ...step.filter } }, context, runnerVars).__f as Record<string, unknown>;\n if (resolved && typeof resolved === 'object') {\n if (typeof resolved.operation_id === 'string') {\n filter.operation_id = resolved.operation_id;\n }\n if (resolved.body && typeof resolved.body === 'object' && !Array.isArray(resolved.body)) {\n filter.body = resolved.body as Record<string, unknown>;\n }\n }\n }\n return filter;\n}\n\n// ────────────────────────────────────────────────────────────\n// expect_no_webhook\n// ────────────────────────────────────────────────────────────\n\nasync function runExpectNoWebhook(\n step: StoryboardStep,\n filter: WebhookFilter,\n receiver: WebhookReceiver\n): Promise<{ validations: ValidationResult[]; passed: boolean }> {\n const timeoutMs = clampTimeoutSeconds(step.timeout_seconds, DEFAULT_NO_WEBHOOK_TIMEOUT_SECONDS) * 1000;\n const outcome = await receiver.wait(filter, timeoutMs);\n\n if (!outcome.timed_out && outcome.webhook) {\n const matches = receiver.matching(filter);\n const webhook = outcome.webhook;\n return singleFailure(\n step,\n 'unexpected_webhook_received',\n `Observed ${matches.length} webhook ${matches.length === 1 ? 'delivery' : 'deliveries'} matching the filter; expected none.`,\n 'zero webhook deliveries matching filter',\n {\n count: matches.length,\n first_delivery: {\n id: webhook.id,\n step_id: webhook.step_id,\n operation_id: webhook.operation_id,\n delivery_index: webhook.delivery_index,\n received_at: webhook.received_at,\n },\n },\n null\n );\n }\n\n return {\n validations: [\n {\n check: 'expect_no_webhook',\n passed: true,\n description: `No webhook matching the filter arrived within ${timeoutMs}ms.`,\n json_pointer: null,\n },\n ],\n passed: true,\n };\n}\n\n// ────────────────────────────────────────────────────────────\n// expect_webhook\n// ────────────────────────────────────────────────────────────\n\nasync function runExpectWebhook(\n step: StoryboardStep,\n filter: WebhookFilter,\n receiver: WebhookReceiver\n): Promise<{ validations: ValidationResult[]; passed: boolean }> {\n const timeoutMs = clampTimeoutSeconds(step.timeout_seconds, DEFAULT_TIMEOUT_SECONDS) * 1000;\n const checkIdempotency = step.expect_idempotency_key !== false;\n const capCount = step.expect_max_deliveries_per_logical_event;\n\n // When we're asserting a cap (e.g., \"at most 1 logical event on replay\"),\n // wait the full window to let any duplicate delivery arrive before judging.\n // When we just need ≥1 delivery, resolve on first match.\n let matches: CapturedWebhook[];\n if (typeof capCount === 'number') {\n matches = await receiver.wait_all(filter, timeoutMs);\n } else {\n const first = await receiver.wait(filter, timeoutMs);\n matches = first.webhook ? [first.webhook] : [];\n }\n\n if (matches.length === 0) {\n return singleFailure(\n step,\n 'no_webhook_received',\n webhookTimeoutDetail(receiver, `No webhook matching filter arrived within ${timeoutMs}ms.`),\n 'webhook delivery matching filter',\n null\n );\n }\n\n const first = matches[0]!;\n if (first.parse_error !== undefined) {\n return singleFailure(\n step,\n 'schema_violation',\n `Webhook body was not valid JSON: ${first.parse_error}`,\n 'JSON object body',\n first.raw_body.slice(0, 256)\n );\n }\n\n const schemaCheck = validateWebhookPayloadSchema(step, first);\n if (schemaCheck && !schemaCheck.passed) return { validations: [schemaCheck], passed: false };\n\n if (checkIdempotency) {\n const idempotencyCheck = assertIdempotencyKey(first);\n if (idempotencyCheck) return { validations: [idempotencyCheck], passed: false };\n }\n\n if (typeof capCount === 'number') {\n const distinctKeys = new Set(matches.map(m => extractIdempotencyKey(m)).filter((k): k is string => !!k));\n if (distinctKeys.size > capCount) {\n return singleFailure(\n step,\n 'duplicate_webhook_on_replay',\n `Observed ${distinctKeys.size} distinct logical events in the window (idempotency_keys); expected ≤ ${capCount}.`,\n `≤ ${capCount} distinct idempotency_keys`,\n Array.from(distinctKeys)\n );\n }\n }\n\n return {\n validations: [\n ...(schemaCheck ? [schemaCheck] : []),\n {\n check: 'expect_webhook',\n passed: true,\n description: `Webhook arrived at ${filter.step_id ?? 'receiver'} with valid idempotency_key.`,\n json_pointer: '/idempotency_key',\n },\n ],\n passed: true,\n };\n}\n\n// ────────────────────────────────────────────────────────────\n// expect_webhook_retry_keys_stable\n// ────────────────────────────────────────────────────────────\n\nasync function runExpectRetryKeysStable(\n step: StoryboardStep,\n filter: WebhookFilter,\n receiver: WebhookReceiver\n): Promise<{ validations: ValidationResult[]; passed: boolean }> {\n const timeoutMs = clampTimeoutSeconds(step.timeout_seconds, DEFAULT_RETRY_REPLAY_TIMEOUT_SECONDS) * 1000;\n const minDeliveries = step.expect_min_deliveries ?? DEFAULT_MIN_DELIVERIES;\n\n const matches = await receiver.wait_all(filter, timeoutMs);\n\n if (matches.length < minDeliveries) {\n return singleFailure(\n step,\n 'insufficient_retries',\n webhookTimeoutDetail(\n receiver,\n `Observed ${matches.length} deliveries; expected at least ${minDeliveries}. ` +\n 'The sender may not be retrying on 5xx responses.'\n ),\n `≥ ${minDeliveries} deliveries`,\n matches.length\n );\n }\n\n const keys = matches.map(m => extractIdempotencyKey(m));\n const missingKey = matches.find((_, i) => !keys[i]);\n if (missingKey) {\n return singleFailure(\n step,\n 'missing_idempotency_key',\n `Delivery ${missingKey.delivery_index} carried no idempotency_key.`,\n 'every delivery carries idempotency_key',\n null\n );\n }\n\n const firstKey = keys[0]!;\n const formatValid = keys.every(k => k && WEBHOOK_IDEMPOTENCY_KEY_PATTERN.test(k));\n if (!formatValid) {\n const bad = keys.find(k => !k || !WEBHOOK_IDEMPOTENCY_KEY_PATTERN.test(k));\n return singleFailure(\n step,\n 'idempotency_key_format_changed',\n `One or more deliveries carried a malformed idempotency_key. Required pattern: ${WEBHOOK_IDEMPOTENCY_KEY_PATTERN.source}`,\n WEBHOOK_IDEMPOTENCY_KEY_PATTERN.source,\n bad ?? null\n );\n }\n\n const rotated = keys.some(k => k !== firstKey);\n if (rotated) {\n return singleFailure(\n step,\n 'idempotency_key_rotated',\n \"idempotency_key rotated across retries. Senders MUST reuse the first delivery's key for every retry of the same logical event.\",\n 'byte-identical idempotency_key on every delivery',\n keys\n );\n }\n\n return {\n validations: [\n {\n check: 'expect_webhook_retry_keys_stable',\n passed: true,\n description: `${matches.length} deliveries observed; all carried byte-identical idempotency_key.`,\n json_pointer: '/idempotency_key',\n },\n ],\n passed: true,\n };\n}\n\n// ────────────────────────────────────────────────────────────\n// expect_webhook_signature_valid\n// ────────────────────────────────────────────────────────────\n\nasync function runExpectSignatureValid(\n step: StoryboardStep,\n filter: WebhookFilter,\n receiver: WebhookReceiver,\n options: StoryboardRunOptions,\n runnerVars: RunnerVariables\n): Promise<{ validations: ValidationResult[]; passed: boolean; skipped?: boolean; skipReason?: string }> {\n const config = options.webhook_signing;\n if (!config) {\n return {\n validations: [],\n passed: true,\n skipped: true,\n skipReason:\n '`expect_webhook_signature_valid` requires `webhook_signing` on runStoryboard options ' +\n '(pass a JwksResolver; replay + revocation stores default to in-memory).',\n };\n }\n\n const timeoutMs = clampTimeoutSeconds(step.timeout_seconds, DEFAULT_TIMEOUT_SECONDS) * 1000;\n const wait = await receiver.wait(filter, timeoutMs);\n if (wait.timed_out || !wait.webhook) {\n return singleFailure(\n step,\n 'no_webhook_received',\n webhookTimeoutDetail(receiver, `No webhook matching filter arrived within ${timeoutMs}ms.`),\n 'signed webhook delivery matching filter',\n null\n );\n }\n\n const webhook = wait.webhook;\n void runnerVars;\n const requestLike = toRequestLike(webhook, receiver);\n const requiredTag = step.require_tag ?? config.required_tag ?? DEFAULT_WEBHOOK_SIGNING_TAG;\n\n try {\n await verifyWebhookSignature(requestLike, {\n jwks: config.jwks,\n replayStore: getSharedReplayStore(runnerVars, config.replayStore),\n revocationStore: getSharedRevocationStore(runnerVars, config.revocationStore),\n requiredTag,\n });\n } catch (err) {\n if (err instanceof WebhookSignatureError) {\n return singleFailure(\n step,\n mapSignatureErrorCode(err.code),\n err.message,\n `checklist step ${err.failedStep} passes`,\n { code: err.code, step: err.failedStep }\n );\n }\n return singleFailure(\n step,\n 'signature_invalid',\n err instanceof Error ? err.message : String(err),\n '9421 webhook signature verification',\n null\n );\n }\n\n return {\n validations: [\n {\n check: 'expect_webhook_signature_valid',\n passed: true,\n description: `9421 webhook signature verified under ${requiredTag}.`,\n },\n ],\n passed: true,\n };\n}\n\n/**\n * Translate a captured webhook into the `RequestLike` shape the shared\n * canonicalizer expects. The receiver's `base_url` plus `step_id` /\n * `operation_id` reconstruct the `@target-uri` the publisher signed.\n */\nfunction toRequestLike(webhook: CapturedWebhook, receiver: WebhookReceiver): RequestLike {\n const url = `${receiver.base_url}/step/${webhook.step_id}/${webhook.operation_id}`;\n return {\n method: webhook.method,\n url,\n headers: webhook.headers,\n body: webhook.raw_body,\n };\n}\n\n/**\n * Map the verifier's `webhook_signature_*` error codes to the storyboard-side\n * `signature_*` codes defined by adcontextprotocol/adcp#2431. Exhaustive so\n * that adding a new verifier code without a mapping is a TS compile error\n * rather than a silent collapse onto `signature_invalid` — distinct\n * remediation paths deserve distinct codes (a revoked key needs rotation,\n * a malformed window is a signer bug, etc.).\n */\nfunction mapSignatureErrorCode(\n code: import('../../signing/errors').WebhookSignatureErrorCode\n): WebhookAssertionErrorCode {\n switch (code) {\n case 'webhook_signature_header_malformed':\n return 'signature_header_malformed';\n case 'webhook_signature_params_incomplete':\n return 'signature_params_incomplete';\n case 'webhook_signature_tag_invalid':\n return 'signature_tag_invalid';\n case 'webhook_signature_alg_not_allowed':\n return 'signature_alg_not_allowed';\n case 'webhook_signature_window_invalid':\n return 'signature_window_invalid';\n case 'webhook_signature_components_incomplete':\n return 'signature_components_incomplete';\n case 'webhook_signature_key_unknown':\n return 'signature_key_unknown';\n case 'webhook_signature_key_purpose_invalid':\n return 'signature_key_purpose_invalid';\n case 'webhook_mode_mismatch':\n return 'signature_mode_mismatch';\n case 'webhook_target_uri_malformed':\n return 'signature_target_uri_malformed';\n case 'webhook_signature_key_revoked':\n return 'signature_key_revoked';\n case 'webhook_signature_revocation_stale':\n return 'signature_revocation_stale';\n case 'webhook_signature_rate_abuse':\n return 'signature_rate_abuse';\n case 'webhook_signature_digest_mismatch':\n return 'signature_digest_mismatch';\n case 'webhook_signature_replayed':\n return 'signature_replayed';\n case 'webhook_signature_invalid':\n return 'signature_invalid';\n default: {\n // Exhaustiveness: any unhandled code is a compile error.\n const _exhaustive: never = code;\n void _exhaustive;\n return 'signature_invalid';\n }\n }\n}\n\n// ────────────────────────────────────────────────────────────\n// Helpers\n// ────────────────────────────────────────────────────────────\n\nfunction validateWebhookPayloadSchema(step: StoryboardStep, webhook: CapturedWebhook): ValidationResult | undefined {\n const schemaRef = step.webhook_payload_schema_ref ?? step.response_schema_webhook_ref;\n if (!schemaRef) return undefined;\n\n const { schema_id, schema_url } = resolveWebhookSchemaIdentity(schemaRef);\n const validator = getSchemaValidatorByRef(schemaRef);\n if (!validator) {\n return {\n check: 'expect_webhook',\n passed: false,\n description: 'Webhook payload schema validation failed.',\n error: `No webhook payload schema found for ${schemaRef}.`,\n json_pointer: null,\n expected: schema_id,\n actual: { code: 'schema_violation', reason: 'schema_not_found', schema_ref: schemaRef },\n schema_id,\n schema_url,\n };\n }\n\n const body = webhook.body;\n if (body === null || typeof body !== 'object' || Array.isArray(body)) {\n return {\n check: 'expect_webhook',\n passed: false,\n description: 'Webhook payload schema validation failed.',\n error: 'Webhook body was not a JSON object.',\n json_pointer: null,\n expected: schema_id,\n actual: { code: 'schema_violation', reason: 'non_object_body', body },\n schema_id,\n schema_url,\n };\n }\n\n const passed = validator(body);\n if (passed) {\n return {\n check: 'expect_webhook',\n passed: true,\n description: `Webhook payload validates against ${schemaRef}.`,\n json_pointer: null,\n expected: schema_id,\n actual: 'schema_valid',\n schema_id,\n schema_url,\n };\n }\n\n const issues = (validator.errors ?? []).map(ajvErrorToSchemaError);\n const firstIssue = issues[0];\n return {\n check: 'expect_webhook',\n passed: false,\n description: 'Webhook payload schema validation failed.',\n error: firstIssue?.message ?? `Webhook body failed ${schemaRef}.`,\n json_pointer: firstIssue?.instance_path ?? null,\n expected: schema_id,\n actual: { code: 'schema_violation', issues },\n schema_id,\n schema_url,\n };\n}\n\nfunction resolveWebhookSchemaIdentity(schemaRef: string): { schema_id: string; schema_url: string } {\n const trimmed = schemaRef.replace(/^\\/+/, '');\n const schemaId = `/schemas/${ADCP_VERSION}/${trimmed}`;\n return { schema_id: schemaId, schema_url: `${SCHEMA_URL_BASE}${schemaId}` };\n}\n\nfunction ajvErrorToSchemaError(error: {\n instancePath?: string;\n schemaPath?: string;\n keyword?: string;\n message?: string;\n}): SchemaValidationError {\n return {\n instance_path: error.instancePath || '/',\n schema_path: error.schemaPath || '#',\n keyword: error.keyword || 'schema',\n message: error.message || 'Webhook payload failed schema validation.',\n };\n}\n\nfunction extractIdempotencyKey(webhook: CapturedWebhook): string | undefined {\n const body = webhook.body;\n if (body === null || typeof body !== 'object' || Array.isArray(body)) return undefined;\n const key = (body as Record<string, unknown>).idempotency_key;\n return typeof key === 'string' ? key : undefined;\n}\n\nfunction assertIdempotencyKey(webhook: CapturedWebhook): ValidationResult | undefined {\n const body = webhook.body;\n if (body === null || typeof body !== 'object' || Array.isArray(body)) {\n return failure('schema_violation', 'Webhook body was not a JSON object.', 'JSON object body', null);\n }\n const key = (body as Record<string, unknown>).idempotency_key;\n if (typeof key !== 'string' || key.length === 0) {\n return failure(\n 'missing_idempotency_key',\n 'Webhook body is missing a non-empty `idempotency_key` — required by AdCP 3.0.',\n 'non-empty string',\n key ?? null\n );\n }\n if (!WEBHOOK_IDEMPOTENCY_KEY_PATTERN.test(key)) {\n return failure(\n 'invalid_idempotency_key_format',\n `idempotency_key \"${key}\" does not match required pattern ${WEBHOOK_IDEMPOTENCY_KEY_PATTERN.source}.`,\n WEBHOOK_IDEMPOTENCY_KEY_PATTERN.source,\n key\n );\n }\n return undefined;\n}\n\nfunction failure(\n code: WebhookAssertionErrorCode,\n message: string,\n expected: unknown,\n actual: unknown\n): ValidationResult {\n return {\n check: 'expect_webhook',\n passed: false,\n description: 'Webhook conformance assertion failed.',\n error: message,\n json_pointer: '/idempotency_key',\n expected,\n actual: { code, actual },\n };\n}\n\nfunction singleFailure(\n step: StoryboardStep,\n code: WebhookAssertionErrorCode,\n message: string,\n expected: unknown,\n actual: unknown,\n jsonPointer: string | null = '/idempotency_key'\n): { validations: ValidationResult[]; passed: boolean } {\n return {\n validations: [\n {\n check: step.task,\n passed: false,\n description: step.expected ?? step.title,\n error: message,\n json_pointer: jsonPointer,\n expected,\n actual: { code, actual },\n },\n ],\n passed: false,\n };\n}\n\nfunction skippedResult(\n step: StoryboardStep,\n phaseId: string,\n context: StoryboardContext,\n start: number,\n opts: {\n skip_reason: 'unsatisfied_contract' | 'prerequisite_failed';\n detail: string;\n extraction: RunnerExtractionRecord;\n request: RunnerRequestRecord;\n next?: StoryboardStepPreview;\n }\n): StoryboardStepResult {\n return {\n step_id: step.id,\n phase_id: phaseId,\n title: step.title,\n task: step.task,\n passed: true,\n skipped: true,\n skip_reason: opts.skip_reason,\n skip: { reason: opts.skip_reason, detail: opts.detail },\n duration_ms: Date.now() - start,\n validations: [],\n context,\n error: opts.detail,\n ...(opts.next && { next: opts.next }),\n request: opts.request,\n extraction: opts.extraction,\n };\n}\n"],"mappings":"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","names":["first"]}
@@ -40,6 +40,19 @@ export interface CapturedWebhook {
40
40
  /** HTTP status the receiver returned (reflects retry-replay policy). */
41
41
  response_status: number;
42
42
  }
43
+ /** Proof-of-control challenge retained separately from event deliveries. */
44
+ export interface CapturedWebhookChallenge {
45
+ id: string;
46
+ step_id: string;
47
+ operation_id: string;
48
+ received_at: number;
49
+ method: string;
50
+ path: string;
51
+ headers: Record<string, string>;
52
+ raw_body: string;
53
+ body: Record<string, unknown>;
54
+ response_status: 200;
55
+ }
43
56
  /** Match predicate applied when waiting for a webhook. */
44
57
  export interface WebhookFilter {
45
58
  /** Restrict matches to this step id's URL. */
@@ -69,6 +82,8 @@ export interface WebhookReceiver {
69
82
  readonly mode: 'loopback_mock' | 'proxy_url';
70
83
  /** All webhooks captured so far, in arrival order. */
71
84
  all(): CapturedWebhook[];
85
+ /** Schema-valid proof-of-control challenges, kept separate from event deliveries. */
86
+ challenges(): CapturedWebhookChallenge[];
72
87
  /** Webhooks matching a filter, in arrival order. */
73
88
  matching(filter: WebhookFilter): CapturedWebhook[];
74
89
  /**
@@ -40,6 +40,19 @@ export interface CapturedWebhook {
40
40
  /** HTTP status the receiver returned (reflects retry-replay policy). */
41
41
  response_status: number;
42
42
  }
43
+ /** Proof-of-control challenge retained separately from event deliveries. */
44
+ export interface CapturedWebhookChallenge {
45
+ id: string;
46
+ step_id: string;
47
+ operation_id: string;
48
+ received_at: number;
49
+ method: string;
50
+ path: string;
51
+ headers: Record<string, string>;
52
+ raw_body: string;
53
+ body: Record<string, unknown>;
54
+ response_status: 200;
55
+ }
43
56
  /** Match predicate applied when waiting for a webhook. */
44
57
  export interface WebhookFilter {
45
58
  /** Restrict matches to this step id's URL. */
@@ -69,6 +82,8 @@ export interface WebhookReceiver {
69
82
  readonly mode: 'loopback_mock' | 'proxy_url';
70
83
  /** All webhooks captured so far, in arrival order. */
71
84
  all(): CapturedWebhook[];
85
+ /** Schema-valid proof-of-control challenges, kept separate from event deliveries. */
86
+ challenges(): CapturedWebhookChallenge[];
72
87
  /** Webhooks matching a filter, in arrival order. */
73
88
  matching(filter: WebhookFilter): CapturedWebhook[];
74
89
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"webhook-receiver.d.ts","sourceRoot":"","sources":["../../../../src/lib/testing/storyboard/webhook-receiver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAuEH,MAAM,WAAW,eAAe;IAC9B,wDAAwD;IACxD,EAAE,EAAE,MAAM,CAAC;IACX,2CAA2C;IAC3C,OAAO,EAAE,MAAM,CAAC;IAChB,gDAAgD;IAChD,YAAY,EAAE,MAAM,CAAC;IACrB,2EAA2E;IAC3E,cAAc,EAAE,MAAM,CAAC;IACvB,oCAAoC;IACpC,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,oBAAoB;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,kEAAkE;IAClE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,uEAAuE;IACvE,QAAQ,EAAE,MAAM,CAAC;IACjB,oDAAoD;IACpD,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,0EAA0E;IAC1E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,wEAAwE;IACxE,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,0DAA0D;AAC1D,MAAM,WAAW,aAAa;IAC5B,8CAA8C;IAC9C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,6CAA6C;IAC7C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,2EAA2E;IAC3E,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC;AAED,MAAM,MAAM,iBAAiB,GACzB;IAAE,OAAO,EAAE,eAAe,CAAC;IAAC,SAAS,CAAC,EAAE,KAAK,CAAA;CAAE,GAC/C;IAAE,SAAS,EAAE,IAAI,CAAC;IAAC,OAAO,CAAC,EAAE,SAAS,CAAA;CAAE,CAAC;AAE7C,MAAM,WAAW,iBAAiB;IAChC,sDAAsD;IACtD,KAAK,EAAE,MAAM,CAAC;IACd,0CAA0C;IAC1C,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,eAAe;IAC9B,iFAAiF;IACjF,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,kEAAkE;IAClE,QAAQ,CAAC,IAAI,EAAE,eAAe,GAAG,WAAW,CAAC;IAC7C,sDAAsD;IACtD,GAAG,IAAI,eAAe,EAAE,CAAC;IACzB,oDAAoD;IACpD,QAAQ,CAAC,MAAM,EAAE,aAAa,GAAG,eAAe,EAAE,CAAC;IACnD;;;;OAIG;IACH,gBAAgB,CAAC,GAAG,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAA;KAAE,EAAE,MAAM,EAAE,iBAAiB,GAAG,IAAI,CAAC;IAClG,8EAA8E;IAC9E,IAAI,CAAC,MAAM,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;IAC5E;;;;OAIG;IACH,QAAQ,CAAC,MAAM,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC,CAAC;IAChF,qCAAqC;IACrC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,MAAM,WAAW,4BAA4B;IAC3C,IAAI,CAAC,EAAE,eAAe,GAAG,WAAW,CAAC;IACrC,iEAAiE;IACjE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,2DAA2D;IAC3D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,wDAAwD;IACxD,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAWD,wBAAsB,qBAAqB,CAAC,OAAO,GAAE,4BAAiC,GAAG,OAAO,CAAC,eAAe,CAAC,CA+EhH"}
1
+ {"version":3,"file":"webhook-receiver.d.ts","sourceRoot":"","sources":["../../../../src/lib/testing/storyboard/webhook-receiver.ts"],"names":[],"mappings":"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"}
@@ -23,7 +23,9 @@ __export(webhook_receiver_exports, {
23
23
  module.exports = __toCommonJS(webhook_receiver_exports);
24
24
  var import_node_http = require("node:http");
25
25
  var import_node_crypto = require("node:crypto");
26
+ var import_schema_loader = require('../../validation/schema-loader.js');
26
27
  const MAX_BODY_BYTES = 1048576;
28
+ const MAX_CHALLENGE_BODY_BYTES = 16384;
27
29
  const MAX_CAPTURED_TOTAL = 1e3;
28
30
  const MAX_CAPTURED_PER_KEY = 100;
29
31
  const HEADERS_TIMEOUT_MS = 1e4;
@@ -68,13 +70,15 @@ async function createWebhookReceiver(options = {}) {
68
70
  const port = options.port ?? 0;
69
71
  const proxyBase = mode === "proxy_url" ? validateProxyUrl(options.public_url) : void 0;
70
72
  const captured = [];
73
+ const challenges = [];
71
74
  const waiters = [];
72
75
  const waitAllTimers = [];
73
76
  const retryPolicies = /* @__PURE__ */ new Map();
74
77
  const deliveryCounts = /* @__PURE__ */ new Map();
78
+ const challengeCounts = /* @__PURE__ */ new Map();
75
79
  let closed = false;
76
80
  const server = (0, import_node_http.createServer)(
77
- (req, res) => handleRequest(req, res, { captured, waiters, retryPolicies, deliveryCounts })
81
+ (req, res) => handleRequest(req, res, { captured, challenges, waiters, retryPolicies, deliveryCounts, challengeCounts })
78
82
  );
79
83
  server.headersTimeout = HEADERS_TIMEOUT_MS;
80
84
  server.requestTimeout = REQUEST_TIMEOUT_MS;
@@ -95,6 +99,7 @@ async function createWebhookReceiver(options = {}) {
95
99
  base_url,
96
100
  mode,
97
101
  all: () => captured.slice(),
102
+ challenges: () => challenges.slice(),
98
103
  matching: (filter) => captured.filter((w) => matchesFilter(w, filter)),
99
104
  set_retry_replay: (key, policy) => {
100
105
  retryPolicies.set(retryKeyString(key), { policy, delivered: 0 });
@@ -146,11 +151,52 @@ function handleRequest(req, res, state) {
146
151
  }
147
152
  const raw = Buffer.concat(chunks).toString("utf8");
148
153
  const headers = normalizeHeaders(req.headers);
154
+ const parsedBody = parseBody(raw, headers["content-type"]);
155
+ if (isWebhookChallenge(parsedBody.body)) {
156
+ if (Buffer.byteLength(raw, "utf8") > MAX_CHALLENGE_BODY_BYTES) {
157
+ res.statusCode = 413;
158
+ res.end();
159
+ return;
160
+ }
161
+ const challenge = parsedBody.body.challenge;
162
+ const schemaRef = parsedBody.body.scope === "agent" ? "core/agent-webhook-challenge.json" : "core/webhook-challenge.json";
163
+ const validate = (0, import_schema_loader.getSchemaValidatorByRef)(schemaRef);
164
+ if (!validate || !validate(parsedBody.body) || typeof challenge !== "string") {
165
+ res.statusCode = 400;
166
+ res.end();
167
+ return;
168
+ }
169
+ const key2 = retryKeyString({ step_id, operation_id });
170
+ const perKey2 = state.challengeCounts.get(key2) ?? 0;
171
+ if (state.captured.length + state.challenges.length >= MAX_CAPTURED_TOTAL || perKey2 >= MAX_CAPTURED_PER_KEY) {
172
+ res.statusCode = 429;
173
+ res.setHeader("retry-after", "1");
174
+ res.end();
175
+ return;
176
+ }
177
+ state.challengeCounts.set(key2, perKey2 + 1);
178
+ state.challenges.push({
179
+ id: (0, import_node_crypto.randomUUID)(),
180
+ step_id,
181
+ operation_id,
182
+ received_at: Date.now(),
183
+ method: req.method ?? "POST",
184
+ path: req.url ?? "/",
185
+ headers: redactHeaders(headers),
186
+ raw_body: raw,
187
+ body: parsedBody.body,
188
+ response_status: 200
189
+ });
190
+ res.statusCode = 200;
191
+ res.setHeader("content-type", "application/json");
192
+ res.end(JSON.stringify({ challenge }));
193
+ return;
194
+ }
149
195
  const key = retryKeyString({ step_id, operation_id });
150
196
  const deliveryIndex = (state.deliveryCounts.get(key) ?? 0) + 1;
151
197
  const status = nextResponseStatus(state.retryPolicies, key);
152
198
  const perKey = state.deliveryCounts.get(key) ?? 0;
153
- if (state.captured.length >= MAX_CAPTURED_TOTAL || perKey >= MAX_CAPTURED_PER_KEY) {
199
+ if (state.captured.length + state.challenges.length >= MAX_CAPTURED_TOTAL || perKey >= MAX_CAPTURED_PER_KEY) {
154
200
  res.statusCode = status;
155
201
  if (status === 429 || status >= 500) res.setHeader("retry-after", "1");
156
202
  res.end();
@@ -167,7 +213,7 @@ function handleRequest(req, res, state) {
167
213
  path: req.url ?? "/",
168
214
  headers: redactHeaders(headers),
169
215
  raw_body: raw,
170
- ...parseBody(raw, headers["content-type"]),
216
+ ...parsedBody,
171
217
  response_status: status
172
218
  };
173
219
  state.captured.push(webhook);
@@ -189,10 +235,15 @@ function handleRequest(req, res, state) {
189
235
  }
190
236
  });
191
237
  }
192
- function parseStepPath(reqUrl) {
238
+ function isWebhookChallenge(body) {
239
+ return body !== null && typeof body === "object" && !Array.isArray(body) && body.type === "webhook.challenge";
240
+ }
241
+ function requestPathname(reqUrl) {
193
242
  const q = reqUrl.indexOf("?");
194
- const pathname = q === -1 ? reqUrl : reqUrl.slice(0, q);
195
- const match = STEP_PATH_RE.exec(pathname);
243
+ return q === -1 ? reqUrl : reqUrl.slice(0, q);
244
+ }
245
+ function parseStepPath(reqUrl) {
246
+ const match = STEP_PATH_RE.exec(requestPathname(reqUrl));
196
247
  if (!match) return void 0;
197
248
  return { step_id: match[1], operation_id: match[2] };
198
249
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/lib/testing/storyboard/webhook-receiver.ts"],"sourcesContent":["/**\n * Ephemeral webhook receiver for storyboard runs.\n *\n * The runner binds an HTTP listener, mints per-step URLs under\n * `/step/<step_id>/<operation_id>`, and captures inbound POSTs so\n * `expect_webhook*` pseudo-steps can observe outbound webhook behavior\n * (idempotency_key presence, retry-key stability, 9421 signature validity).\n *\n * Matches the `webhook_receiver_runner` test-kit contract in\n * adcontextprotocol/adcp#2431:\n * - Per-step URL template `{{runner.webhook_base}}/step/<step_id>/<operation_id>`.\n * - Retry-replay: the receiver returns a configurable 5xx for the first N\n * deliveries on a given `(step_id, operation_id)`, then 2xx, so senders\n * are forced to retry and retry-key stability can be graded.\n * - Loopback-mock mode (HTTP on 127.0.0.1, zero external deps) by default;\n * proxy-URL mode swaps in an operator-supplied public base.\n */\n\nimport { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http';\nimport { randomUUID } from 'node:crypto';\nimport type { AddressInfo } from 'node:net';\n\n/**\n * Size cap on any single webhook body. A non-conformant sender retrying\n * with oversize payloads shouldn't be able to exhaust the runner's memory.\n */\nconst MAX_BODY_BYTES = 1_048_576; // 1 MiB\n\n/**\n * Caps on cumulative capture state. `MAX_CAPTURED_TOTAL` bounds the total\n * number of webhooks retained for the run; `MAX_CAPTURED_PER_KEY` bounds\n * per-(step_id, operation_id) deliveries — a publisher stuck in a retry\n * loop can't exhaust memory. Once a cap is hit the receiver stops retaining\n * bodies but continues the configured retry-status sequence (or returns 204\n * when no rejection remains), so the cap cannot trap a sender in a retry loop.\n */\nconst MAX_CAPTURED_TOTAL = 1000;\nconst MAX_CAPTURED_PER_KEY = 100;\n\n/**\n * HTTP transport hardening. Defaults in Node 20 are generous (headers\n * 60s, no body timeout). A hostile / slow publisher shouldn't be able to\n * hold the runner's event loop with trickled requests or exhaust the\n * socket table.\n */\nconst HEADERS_TIMEOUT_MS = 10_000;\nconst REQUEST_TIMEOUT_MS = 30_000;\nconst KEEP_ALIVE_TIMEOUT_MS = 5_000;\nconst MAX_CONNECTIONS = 64;\nconst MAX_HEADERS_COUNT = 64;\n\n/** Default path shape under the receiver base. */\nconst STEP_PATH_RE = /^\\/step\\/([A-Za-z0-9_]+)\\/([A-Za-z0-9_-]+)\\/?$/;\n\n/**\n * Header names whose values are redacted before a captured webhook lands\n * in a compliance report. Mirrors the `SECRET_KEY_PATTERN` in runner.ts\n * so both paths honor the contract's \"Secrets SHOULD be redacted\" rule.\n */\nconst SECRET_HEADER_PATTERN =\n /^(authorization|credentials?|token|api[_-]?key|x-api[_-]?key|x-auth[_-]?token|password|secret|client[_-]secret|refresh[_-]token|access[_-]token|bearer|session[_-]token|cookie|set[_-]cookie)$/i;\n\n/**\n * Strip user-info, scheme-policed, and CRLF/NUL-rejected proxy URLs so a\n * misconfigured operator (or a compromised storyboard loader) can't\n * advertise an `http://169.254.169.254`, `file://…`, or header-splitting\n * URL that would then be embedded in outbound `push_notification_config.url`.\n */\nfunction validateProxyUrl(raw: string): string {\n if (/[\\r\\n\\x00]/.test(raw)) {\n throw new Error('webhook_receiver.public_url must not contain CR/LF/NUL');\n }\n let parsed: URL;\n try {\n parsed = new URL(raw);\n } catch {\n throw new Error(`webhook_receiver.public_url is not a valid URL: ${raw}`);\n }\n if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {\n throw new Error(`webhook_receiver.public_url must be http(s); got ${parsed.protocol}`);\n }\n if (parsed.username || parsed.password) {\n throw new Error('webhook_receiver.public_url must not include userinfo');\n }\n return raw.replace(/\\/$/, '');\n}\n\nexport interface CapturedWebhook {\n /** Runner-assigned id; stable across filter matches. */\n id: string;\n /** Step id extracted from the URL path. */\n step_id: string;\n /** Operation id extracted from the URL path. */\n operation_id: string;\n /** Delivery sequence number for this (step_id, operation_id) — 1-based. */\n delivery_index: number;\n /** Wall-clock millis at receipt. */\n received_at: number;\n method: string;\n /** Path + query. */\n path: string;\n /** Lowercased header names; duplicate values joined with \", \". */\n headers: Record<string, string>;\n /** Raw request body — needed for RFC 9421 content-digest recompute. */\n raw_body: string;\n /** Parsed JSON body; undefined on parse failure. */\n body?: unknown;\n /** Parse error message when raw_body was non-empty but not valid JSON. */\n parse_error?: string;\n /** HTTP status the receiver returned (reflects retry-replay policy). */\n response_status: number;\n}\n\n/** Match predicate applied when waiting for a webhook. */\nexport interface WebhookFilter {\n /** Restrict matches to this step id's URL. */\n step_id?: string;\n /** Restrict matches to this operation id. */\n operation_id?: string;\n /** Dotted-path → expected-value pairs compared against the parsed body. */\n body?: Record<string, unknown>;\n}\n\nexport type WebhookWaitResult =\n | { webhook: CapturedWebhook; timed_out?: false }\n | { timed_out: true; webhook?: undefined };\n\nexport interface RetryReplayPolicy {\n /** How many deliveries to reject before accepting. */\n count: number;\n /** HTTP status to return on rejection. */\n http_status: number;\n}\n\nexport interface WebhookReceiver {\n /** Public base URL (`http://host:port` for loopback, operator URL for proxy). */\n readonly base_url: string;\n /** Whether the receiver is in loopback-mock or proxy-url mode. */\n readonly mode: 'loopback_mock' | 'proxy_url';\n /** All webhooks captured so far, in arrival order. */\n all(): CapturedWebhook[];\n /** Webhooks matching a filter, in arrival order. */\n matching(filter: WebhookFilter): CapturedWebhook[];\n /**\n * Configure the receiver to return `http_status` for the first `count`\n * deliveries at `(step_id, operation_id)` and 2xx afterwards. Idempotent:\n * setting a new policy replaces any existing one for the same key.\n */\n set_retry_replay(key: { step_id: string; operation_id: string }, policy: RetryReplayPolicy): void;\n /** Resolve with the first match within `timeout_ms`, or `timed_out: true`. */\n wait(filter: WebhookFilter, timeout_ms: number): Promise<WebhookWaitResult>;\n /**\n * Resolve with every match observed between call time and deadline. Used\n * by `expect_webhook_retry_keys_stable` which needs the full sequence of\n * deliveries for the retry-replay assertion.\n */\n wait_all(filter: WebhookFilter, timeout_ms: number): Promise<CapturedWebhook[]>;\n /** Stop the listener. Idempotent. */\n close(): Promise<void>;\n}\n\nexport interface CreateWebhookReceiverOptions {\n mode?: 'loopback_mock' | 'proxy_url';\n /** Bind host for the local listener. Defaults to `127.0.0.1`. */\n host?: string;\n /** Bind port. `0` (default) lets the kernel assign one. */\n port?: number;\n /** Public URL to advertise when `mode: 'proxy_url'`. */\n public_url?: string;\n}\n\ninterface RetryKey {\n step_id: string;\n operation_id: string;\n}\n\nfunction retryKeyString(k: RetryKey): string {\n return `${k.step_id}::${k.operation_id}`;\n}\n\nexport async function createWebhookReceiver(options: CreateWebhookReceiverOptions = {}): Promise<WebhookReceiver> {\n const mode = options.mode ?? 'loopback_mock';\n if (mode === 'proxy_url' && !options.public_url) {\n throw new Error('webhook_receiver.mode=proxy_url requires `public_url`');\n }\n\n const host = options.host ?? '127.0.0.1';\n // A loopback_mock receiver bound on 0.0.0.0 is publicly reachable from any\n // interface — which would turn a CI runner into an open POST endpoint.\n // Operators who genuinely want public exposure use mode=proxy_url.\n if (mode === 'loopback_mock' && (host === '0.0.0.0' || host === '::')) {\n throw new Error(\n `webhook_receiver host ${host} is not permitted in loopback_mock mode. ` +\n 'Use mode=proxy_url with an explicit public_url for publicly-reachable runs.'\n );\n }\n const port = options.port ?? 0;\n const proxyBase = mode === 'proxy_url' ? validateProxyUrl(options.public_url!) : undefined;\n\n const captured: CapturedWebhook[] = [];\n const waiters: Array<{\n filter: WebhookFilter;\n resolve: (result: WebhookWaitResult) => void;\n timer: NodeJS.Timeout;\n }> = [];\n const waitAllTimers: Array<{\n filter: WebhookFilter;\n resolve: (result: CapturedWebhook[]) => void;\n timer: NodeJS.Timeout;\n }> = [];\n const retryPolicies = new Map<string, { policy: RetryReplayPolicy; delivered: number }>();\n const deliveryCounts = new Map<string, number>();\n let closed = false;\n\n const server = createServer((req, res) =>\n handleRequest(req, res, { captured, waiters, retryPolicies, deliveryCounts })\n );\n // Transport-level hardening — trim Node's generous defaults so a slow /\n // hostile publisher can't wedge the runner.\n server.headersTimeout = HEADERS_TIMEOUT_MS;\n server.requestTimeout = REQUEST_TIMEOUT_MS;\n server.keepAliveTimeout = KEEP_ALIVE_TIMEOUT_MS;\n server.maxConnections = MAX_CONNECTIONS;\n server.maxHeadersCount = MAX_HEADERS_COUNT;\n\n await new Promise<void>((resolve, reject) => {\n const onError = (err: Error) => reject(err);\n server.once('error', onError);\n server.listen(port, host, () => {\n server.off('error', onError);\n resolve();\n });\n });\n\n const bound = server.address() as AddressInfo;\n const base_url = proxyBase ?? `http://${formatHost(bound.address)}:${bound.port}`;\n\n return {\n base_url,\n mode,\n all: () => captured.slice(),\n matching: filter => captured.filter(w => matchesFilter(w, filter)),\n set_retry_replay: (key, policy) => {\n retryPolicies.set(retryKeyString(key), { policy, delivered: 0 });\n },\n wait: (filter, timeout_ms) => {\n if (!closed) return wait(filter, timeout_ms, captured, waiters);\n const already = captured.find(w => matchesFilter(w, filter));\n return Promise.resolve(already ? { webhook: already } : { timed_out: true });\n },\n wait_all: (filter, timeout_ms) =>\n closed\n ? Promise.resolve(captured.filter(w => matchesFilter(w, filter)))\n : waitAll(filter, timeout_ms, captured, waitAllTimers),\n close: () => {\n closed = true;\n return closeServer(server, captured, waiters, waitAllTimers);\n },\n };\n}\n\n// ────────────────────────────────────────────────────────────\n// Request handling\n// ────────────────────────────────────────────────────────────\n\ninterface HandlerState {\n captured: CapturedWebhook[];\n waiters: Array<{\n filter: WebhookFilter;\n resolve: (r: WebhookWaitResult) => void;\n timer: NodeJS.Timeout;\n }>;\n retryPolicies: Map<string, { policy: RetryReplayPolicy; delivered: number }>;\n deliveryCounts: Map<string, number>;\n}\n\nfunction handleRequest(req: IncomingMessage, res: ServerResponse, state: HandlerState): void {\n if (req.method !== 'POST') {\n res.statusCode = 405;\n res.end();\n return;\n }\n\n const pathParts = parseStepPath(req.url ?? '');\n if (!pathParts) {\n res.statusCode = 404;\n res.end();\n return;\n }\n const { step_id, operation_id } = pathParts;\n\n let size = 0;\n const chunks: Buffer[] = [];\n let tooLarge = false;\n\n req.on('data', (chunk: Buffer) => {\n size += chunk.length;\n if (size > MAX_BODY_BYTES) {\n tooLarge = true;\n req.destroy();\n return;\n }\n chunks.push(chunk);\n });\n\n req.on('end', () => {\n if (tooLarge) {\n if (!res.headersSent) {\n res.statusCode = 413;\n res.end();\n }\n return;\n }\n const raw = Buffer.concat(chunks).toString('utf8');\n const headers = normalizeHeaders(req.headers);\n\n const key = retryKeyString({ step_id, operation_id });\n const deliveryIndex = (state.deliveryCounts.get(key) ?? 0) + 1;\n const status = nextResponseStatus(state.retryPolicies, key);\n\n // Stop retaining bodies once a capture cap is exhausted, but preserve the\n // configured retry-replay status sequence. Unconditionally returning 503\n // here masks the policy's eventual 2xx forever and can amplify a sender's\n // retries after the runner has already reached its memory bound (#2653).\n const perKey = state.deliveryCounts.get(key) ?? 0;\n if (state.captured.length >= MAX_CAPTURED_TOTAL || perKey >= MAX_CAPTURED_PER_KEY) {\n res.statusCode = status;\n if (status === 429 || status >= 500) res.setHeader('retry-after', '1');\n res.end();\n return;\n }\n\n state.deliveryCounts.set(key, deliveryIndex);\n\n const webhook: CapturedWebhook = {\n id: randomUUID(),\n step_id,\n operation_id,\n delivery_index: deliveryIndex,\n received_at: Date.now(),\n method: req.method ?? 'POST',\n path: req.url ?? '/',\n headers: redactHeaders(headers),\n raw_body: raw,\n ...parseBody(raw, headers['content-type']),\n response_status: status,\n };\n state.captured.push(webhook);\n\n // Deliver to any matching waiters. First-match-wins per waiter; later\n // arrivals may still deliver to other waiters in the queue.\n for (let i = state.waiters.length - 1; i >= 0; i--) {\n const waiter = state.waiters[i]!;\n if (matchesFilter(webhook, waiter.filter)) {\n clearTimeout(waiter.timer);\n state.waiters.splice(i, 1);\n waiter.resolve({ webhook });\n }\n }\n\n res.statusCode = status;\n res.end();\n });\n\n req.on('error', () => {\n if (!res.headersSent) {\n res.statusCode = 400;\n res.end();\n }\n });\n}\n\nfunction parseStepPath(reqUrl: string): { step_id: string; operation_id: string } | undefined {\n const q = reqUrl.indexOf('?');\n const pathname = q === -1 ? reqUrl : reqUrl.slice(0, q);\n const match = STEP_PATH_RE.exec(pathname);\n if (!match) return undefined;\n return { step_id: match[1]!, operation_id: match[2]! };\n}\n\nfunction normalizeHeaders(raw: IncomingMessage['headers']): Record<string, string> {\n const out: Record<string, string> = {};\n for (const [key, value] of Object.entries(raw)) {\n if (value === undefined) continue;\n out[key.toLowerCase()] = Array.isArray(value) ? value.join(', ') : value;\n }\n return out;\n}\n\nfunction redactHeaders(headers: Record<string, string>): Record<string, string> {\n const out: Record<string, string> = {};\n for (const [k, v] of Object.entries(headers)) {\n out[k] = SECRET_HEADER_PATTERN.test(k) ? '[redacted]' : v;\n }\n return out;\n}\n\nfunction parseBody(raw: string, contentType: string | undefined): Pick<CapturedWebhook, 'body' | 'parse_error'> {\n if (raw.length === 0) return {};\n const looksJson = !contentType || /json/i.test(contentType);\n if (!looksJson) return {};\n try {\n return { body: JSON.parse(raw) };\n } catch (err) {\n return { parse_error: err instanceof Error ? err.message : String(err) };\n }\n}\n\n// ────────────────────────────────────────────────────────────\n// Filter matching\n// ────────────────────────────────────────────────────────────\n\nfunction matchesFilter(webhook: CapturedWebhook, filter: WebhookFilter): boolean {\n if (filter.step_id && webhook.step_id !== filter.step_id) return false;\n if (filter.operation_id && webhook.operation_id !== filter.operation_id) return false;\n if (filter.body && Object.keys(filter.body).length > 0) {\n if (webhook.body === undefined) return false;\n for (const [path, expected] of Object.entries(filter.body)) {\n const actual = resolveDottedPath(webhook.body, path);\n if (!deepEqual(actual, expected)) return false;\n }\n }\n return true;\n}\n\nfunction resolveDottedPath(obj: unknown, path: string): unknown {\n if (obj === null || obj === undefined) return undefined;\n const segments = path.split('.');\n let cursor: unknown = obj;\n for (const seg of segments) {\n if (cursor === null || cursor === undefined) return undefined;\n if (typeof cursor !== 'object') return undefined;\n cursor = (cursor as Record<string, unknown>)[seg];\n }\n return cursor;\n}\n\n/**\n * Structural deep-equal. Stringify-based comparison is key-order-sensitive\n * (`{a:1,b:2}` !== `{b:2,a:1}`); for filter bodies that's a false negative\n * when a publisher's JSON serializer emits fields in a different order\n * than the storyboard author. Recurse explicitly instead.\n */\nfunction deepEqual(a: unknown, b: unknown): boolean {\n if (a === b) return true;\n if (typeof a !== typeof b) return false;\n if (a === null || b === null) return a === b;\n if (typeof a !== 'object') return false;\n if (Array.isArray(a) || Array.isArray(b)) {\n if (!Array.isArray(a) || !Array.isArray(b)) return false;\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (!deepEqual(a[i], b[i])) return false;\n }\n return true;\n }\n const keysA = Object.keys(a as Record<string, unknown>);\n const keysB = Object.keys(b as Record<string, unknown>);\n if (keysA.length !== keysB.length) return false;\n for (const k of keysA) {\n if (!Object.prototype.hasOwnProperty.call(b, k)) return false;\n if (!deepEqual((a as Record<string, unknown>)[k], (b as Record<string, unknown>)[k])) return false;\n }\n return true;\n}\n\n// ────────────────────────────────────────────────────────────\n// Retry-replay policy\n// ────────────────────────────────────────────────────────────\n\nfunction nextResponseStatus(\n retryPolicies: Map<string, { policy: RetryReplayPolicy; delivered: number }>,\n key: string\n): number {\n const entry = retryPolicies.get(key);\n if (!entry) return 204;\n if (entry.delivered < entry.policy.count) {\n entry.delivered += 1;\n return entry.policy.http_status;\n }\n return 204;\n}\n\n// ────────────────────────────────────────────────────────────\n// wait / wait_all\n// ────────────────────────────────────────────────────────────\n\nfunction wait(\n filter: WebhookFilter,\n timeout_ms: number,\n captured: CapturedWebhook[],\n waiters: Array<{ filter: WebhookFilter; resolve: (r: WebhookWaitResult) => void; timer: NodeJS.Timeout }>\n): Promise<WebhookWaitResult> {\n const already = captured.find(w => matchesFilter(w, filter));\n if (already) return Promise.resolve({ webhook: already });\n return new Promise<WebhookWaitResult>(resolve => {\n const timer = setTimeout(\n () => {\n const idx = waiters.findIndex(w => w.timer === timer);\n if (idx >= 0) waiters.splice(idx, 1);\n resolve({ timed_out: true });\n },\n Math.max(0, timeout_ms)\n );\n timer.unref?.();\n waiters.push({ filter, resolve, timer });\n });\n}\n\n/**\n * Poll until the deadline, then return every capture matching `filter`.\n * Used when the assertion needs the whole delivery sequence (retry-stability).\n * Waits a minimum of `timeout_ms` even if matches arrive earlier — callers\n * that need \"resolve on first match\" should use `wait` instead.\n */\nfunction waitAll(\n filter: WebhookFilter,\n timeout_ms: number,\n captured: CapturedWebhook[],\n waitAllTimers: Array<{\n filter: WebhookFilter;\n resolve: (result: CapturedWebhook[]) => void;\n timer: NodeJS.Timeout;\n }>\n): Promise<CapturedWebhook[]> {\n return new Promise<CapturedWebhook[]>(resolve => {\n const timer = setTimeout(\n () => {\n const idx = waitAllTimers.findIndex(entry => entry.timer === timer);\n if (idx >= 0) waitAllTimers.splice(idx, 1);\n resolve(captured.filter(w => matchesFilter(w, filter)));\n },\n Math.max(0, timeout_ms)\n );\n timer.unref?.();\n waitAllTimers.push({ filter, resolve, timer });\n });\n}\n\n// ────────────────────────────────────────────────────────────\n// close\n// ────────────────────────────────────────────────────────────\n\nfunction closeServer(\n server: Server,\n captured: CapturedWebhook[],\n waiters: Array<{ filter: WebhookFilter; resolve: (r: WebhookWaitResult) => void; timer: NodeJS.Timeout }>,\n waitAllTimers: Array<{\n filter: WebhookFilter;\n resolve: (result: CapturedWebhook[]) => void;\n timer: NodeJS.Timeout;\n }>\n): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n server.close(err => {\n if (err && (err as NodeJS.ErrnoException).code !== 'ERR_SERVER_NOT_RUNNING') reject(err);\n else resolve();\n });\n // Stop accepting new sockets before force-closing existing keep-alive\n // connections; the opposite order leaves a small acceptance race.\n server.closeAllConnections?.();\n while (waiters.length > 0) {\n const w = waiters.pop()!;\n clearTimeout(w.timer);\n w.resolve({ timed_out: true });\n }\n while (waitAllTimers.length > 0) {\n const pending = waitAllTimers.pop()!;\n clearTimeout(pending.timer);\n pending.resolve(captured.filter(w => matchesFilter(w, pending.filter)));\n }\n });\n}\n\nfunction formatHost(address: string): string {\n return address.includes(':') ? `[${address}]` : address;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../../src/lib/testing/storyboard/webhook-receiver.ts"],"sourcesContent":["/**\n * Ephemeral webhook receiver for storyboard runs.\n *\n * The runner binds an HTTP listener, mints per-step URLs under\n * `/step/<step_id>/<operation_id>`, and captures inbound POSTs so\n * `expect_webhook*` pseudo-steps can observe outbound webhook behavior\n * (idempotency_key presence, retry-key stability, 9421 signature validity).\n *\n * Matches the `webhook_receiver_runner` test-kit contract in\n * adcontextprotocol/adcp#2431:\n * - Per-step URL template `{{runner.webhook_base}}/step/<step_id>/<operation_id>`.\n * - Retry-replay: the receiver returns a configurable 5xx for the first N\n * deliveries on a given `(step_id, operation_id)`, then 2xx, so senders\n * are forced to retry and retry-key stability can be graded.\n * - Loopback-mock mode (HTTP on 127.0.0.1, zero external deps) by default;\n * proxy-URL mode swaps in an operator-supplied public base.\n */\n\nimport { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http';\nimport { randomUUID } from 'node:crypto';\nimport type { AddressInfo } from 'node:net';\nimport { getSchemaValidatorByRef } from '../../validation/schema-loader';\n\n/**\n * Size cap on any single webhook body. A non-conformant sender retrying\n * with oversize payloads shouldn't be able to exhaust the runner's memory.\n */\nconst MAX_BODY_BYTES = 1_048_576; // 1 MiB\nconst MAX_CHALLENGE_BODY_BYTES = 16_384; // 16 KiB; challenge envelopes are tiny\n\n/**\n * Caps on cumulative capture state. `MAX_CAPTURED_TOTAL` bounds the total\n * number of webhooks retained for the run; `MAX_CAPTURED_PER_KEY` bounds\n * per-(step_id, operation_id) deliveries — a publisher stuck in a retry\n * loop can't exhaust memory. Once a cap is hit the receiver stops retaining\n * bodies but continues the configured retry-status sequence (or returns 204\n * when no rejection remains), so the cap cannot trap a sender in a retry loop.\n */\nconst MAX_CAPTURED_TOTAL = 1000;\nconst MAX_CAPTURED_PER_KEY = 100;\n\n/**\n * HTTP transport hardening. Defaults in Node 20 are generous (headers\n * 60s, no body timeout). A hostile / slow publisher shouldn't be able to\n * hold the runner's event loop with trickled requests or exhaust the\n * socket table.\n */\nconst HEADERS_TIMEOUT_MS = 10_000;\nconst REQUEST_TIMEOUT_MS = 30_000;\nconst KEEP_ALIVE_TIMEOUT_MS = 5_000;\nconst MAX_CONNECTIONS = 64;\nconst MAX_HEADERS_COUNT = 64;\n\n/** Default path shape under the receiver base. */\nconst STEP_PATH_RE = /^\\/step\\/([A-Za-z0-9_]+)\\/([A-Za-z0-9_-]+)\\/?$/;\n\n/**\n * Header names whose values are redacted before a captured webhook lands\n * in a compliance report. Mirrors the `SECRET_KEY_PATTERN` in runner.ts\n * so both paths honor the contract's \"Secrets SHOULD be redacted\" rule.\n */\nconst SECRET_HEADER_PATTERN =\n /^(authorization|credentials?|token|api[_-]?key|x-api[_-]?key|x-auth[_-]?token|password|secret|client[_-]secret|refresh[_-]token|access[_-]token|bearer|session[_-]token|cookie|set[_-]cookie)$/i;\n\n/**\n * Strip user-info, scheme-policed, and CRLF/NUL-rejected proxy URLs so a\n * misconfigured operator (or a compromised storyboard loader) can't\n * advertise an `http://169.254.169.254`, `file://…`, or header-splitting\n * URL that would then be embedded in outbound `push_notification_config.url`.\n */\nfunction validateProxyUrl(raw: string): string {\n if (/[\\r\\n\\x00]/.test(raw)) {\n throw new Error('webhook_receiver.public_url must not contain CR/LF/NUL');\n }\n let parsed: URL;\n try {\n parsed = new URL(raw);\n } catch {\n throw new Error(`webhook_receiver.public_url is not a valid URL: ${raw}`);\n }\n if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {\n throw new Error(`webhook_receiver.public_url must be http(s); got ${parsed.protocol}`);\n }\n if (parsed.username || parsed.password) {\n throw new Error('webhook_receiver.public_url must not include userinfo');\n }\n return raw.replace(/\\/$/, '');\n}\n\nexport interface CapturedWebhook {\n /** Runner-assigned id; stable across filter matches. */\n id: string;\n /** Step id extracted from the URL path. */\n step_id: string;\n /** Operation id extracted from the URL path. */\n operation_id: string;\n /** Delivery sequence number for this (step_id, operation_id) — 1-based. */\n delivery_index: number;\n /** Wall-clock millis at receipt. */\n received_at: number;\n method: string;\n /** Path + query. */\n path: string;\n /** Lowercased header names; duplicate values joined with \", \". */\n headers: Record<string, string>;\n /** Raw request body — needed for RFC 9421 content-digest recompute. */\n raw_body: string;\n /** Parsed JSON body; undefined on parse failure. */\n body?: unknown;\n /** Parse error message when raw_body was non-empty but not valid JSON. */\n parse_error?: string;\n /** HTTP status the receiver returned (reflects retry-replay policy). */\n response_status: number;\n}\n\n/** Proof-of-control challenge retained separately from event deliveries. */\nexport interface CapturedWebhookChallenge {\n id: string;\n step_id: string;\n operation_id: string;\n received_at: number;\n method: string;\n path: string;\n headers: Record<string, string>;\n raw_body: string;\n body: Record<string, unknown>;\n response_status: 200;\n}\n\n/** Match predicate applied when waiting for a webhook. */\nexport interface WebhookFilter {\n /** Restrict matches to this step id's URL. */\n step_id?: string;\n /** Restrict matches to this operation id. */\n operation_id?: string;\n /** Dotted-path → expected-value pairs compared against the parsed body. */\n body?: Record<string, unknown>;\n}\n\nexport type WebhookWaitResult =\n | { webhook: CapturedWebhook; timed_out?: false }\n | { timed_out: true; webhook?: undefined };\n\nexport interface RetryReplayPolicy {\n /** How many deliveries to reject before accepting. */\n count: number;\n /** HTTP status to return on rejection. */\n http_status: number;\n}\n\nexport interface WebhookReceiver {\n /** Public base URL (`http://host:port` for loopback, operator URL for proxy). */\n readonly base_url: string;\n /** Whether the receiver is in loopback-mock or proxy-url mode. */\n readonly mode: 'loopback_mock' | 'proxy_url';\n /** All webhooks captured so far, in arrival order. */\n all(): CapturedWebhook[];\n /** Schema-valid proof-of-control challenges, kept separate from event deliveries. */\n challenges(): CapturedWebhookChallenge[];\n /** Webhooks matching a filter, in arrival order. */\n matching(filter: WebhookFilter): CapturedWebhook[];\n /**\n * Configure the receiver to return `http_status` for the first `count`\n * deliveries at `(step_id, operation_id)` and 2xx afterwards. Idempotent:\n * setting a new policy replaces any existing one for the same key.\n */\n set_retry_replay(key: { step_id: string; operation_id: string }, policy: RetryReplayPolicy): void;\n /** Resolve with the first match within `timeout_ms`, or `timed_out: true`. */\n wait(filter: WebhookFilter, timeout_ms: number): Promise<WebhookWaitResult>;\n /**\n * Resolve with every match observed between call time and deadline. Used\n * by `expect_webhook_retry_keys_stable` which needs the full sequence of\n * deliveries for the retry-replay assertion.\n */\n wait_all(filter: WebhookFilter, timeout_ms: number): Promise<CapturedWebhook[]>;\n /** Stop the listener. Idempotent. */\n close(): Promise<void>;\n}\n\nexport interface CreateWebhookReceiverOptions {\n mode?: 'loopback_mock' | 'proxy_url';\n /** Bind host for the local listener. Defaults to `127.0.0.1`. */\n host?: string;\n /** Bind port. `0` (default) lets the kernel assign one. */\n port?: number;\n /** Public URL to advertise when `mode: 'proxy_url'`. */\n public_url?: string;\n}\n\ninterface RetryKey {\n step_id: string;\n operation_id: string;\n}\n\nfunction retryKeyString(k: RetryKey): string {\n return `${k.step_id}::${k.operation_id}`;\n}\n\nexport async function createWebhookReceiver(options: CreateWebhookReceiverOptions = {}): Promise<WebhookReceiver> {\n const mode = options.mode ?? 'loopback_mock';\n if (mode === 'proxy_url' && !options.public_url) {\n throw new Error('webhook_receiver.mode=proxy_url requires `public_url`');\n }\n\n const host = options.host ?? '127.0.0.1';\n // A loopback_mock receiver bound on 0.0.0.0 is publicly reachable from any\n // interface — which would turn a CI runner into an open POST endpoint.\n // Operators who genuinely want public exposure use mode=proxy_url.\n if (mode === 'loopback_mock' && (host === '0.0.0.0' || host === '::')) {\n throw new Error(\n `webhook_receiver host ${host} is not permitted in loopback_mock mode. ` +\n 'Use mode=proxy_url with an explicit public_url for publicly-reachable runs.'\n );\n }\n const port = options.port ?? 0;\n const proxyBase = mode === 'proxy_url' ? validateProxyUrl(options.public_url!) : undefined;\n\n const captured: CapturedWebhook[] = [];\n const challenges: CapturedWebhookChallenge[] = [];\n const waiters: Array<{\n filter: WebhookFilter;\n resolve: (result: WebhookWaitResult) => void;\n timer: NodeJS.Timeout;\n }> = [];\n const waitAllTimers: Array<{\n filter: WebhookFilter;\n resolve: (result: CapturedWebhook[]) => void;\n timer: NodeJS.Timeout;\n }> = [];\n const retryPolicies = new Map<string, { policy: RetryReplayPolicy; delivered: number }>();\n const deliveryCounts = new Map<string, number>();\n const challengeCounts = new Map<string, number>();\n let closed = false;\n\n const server = createServer((req, res) =>\n handleRequest(req, res, { captured, challenges, waiters, retryPolicies, deliveryCounts, challengeCounts })\n );\n // Transport-level hardening — trim Node's generous defaults so a slow /\n // hostile publisher can't wedge the runner.\n server.headersTimeout = HEADERS_TIMEOUT_MS;\n server.requestTimeout = REQUEST_TIMEOUT_MS;\n server.keepAliveTimeout = KEEP_ALIVE_TIMEOUT_MS;\n server.maxConnections = MAX_CONNECTIONS;\n server.maxHeadersCount = MAX_HEADERS_COUNT;\n\n await new Promise<void>((resolve, reject) => {\n const onError = (err: Error) => reject(err);\n server.once('error', onError);\n server.listen(port, host, () => {\n server.off('error', onError);\n resolve();\n });\n });\n\n const bound = server.address() as AddressInfo;\n const base_url = proxyBase ?? `http://${formatHost(bound.address)}:${bound.port}`;\n\n return {\n base_url,\n mode,\n all: () => captured.slice(),\n challenges: () => challenges.slice(),\n matching: filter => captured.filter(w => matchesFilter(w, filter)),\n set_retry_replay: (key, policy) => {\n retryPolicies.set(retryKeyString(key), { policy, delivered: 0 });\n },\n wait: (filter, timeout_ms) => {\n if (!closed) return wait(filter, timeout_ms, captured, waiters);\n const already = captured.find(w => matchesFilter(w, filter));\n return Promise.resolve(already ? { webhook: already } : { timed_out: true });\n },\n wait_all: (filter, timeout_ms) =>\n closed\n ? Promise.resolve(captured.filter(w => matchesFilter(w, filter)))\n : waitAll(filter, timeout_ms, captured, waitAllTimers),\n close: () => {\n closed = true;\n return closeServer(server, captured, waiters, waitAllTimers);\n },\n };\n}\n\n// ────────────────────────────────────────────────────────────\n// Request handling\n// ────────────────────────────────────────────────────────────\n\ninterface HandlerState {\n captured: CapturedWebhook[];\n challenges: CapturedWebhookChallenge[];\n waiters: Array<{\n filter: WebhookFilter;\n resolve: (r: WebhookWaitResult) => void;\n timer: NodeJS.Timeout;\n }>;\n retryPolicies: Map<string, { policy: RetryReplayPolicy; delivered: number }>;\n deliveryCounts: Map<string, number>;\n challengeCounts: Map<string, number>;\n}\n\nfunction handleRequest(req: IncomingMessage, res: ServerResponse, state: HandlerState): void {\n if (req.method !== 'POST') {\n res.statusCode = 405;\n res.end();\n return;\n }\n\n const pathParts = parseStepPath(req.url ?? '');\n if (!pathParts) {\n res.statusCode = 404;\n res.end();\n return;\n }\n const { step_id, operation_id } = pathParts;\n\n let size = 0;\n const chunks: Buffer[] = [];\n let tooLarge = false;\n\n req.on('data', (chunk: Buffer) => {\n size += chunk.length;\n if (size > MAX_BODY_BYTES) {\n tooLarge = true;\n req.destroy();\n return;\n }\n chunks.push(chunk);\n });\n\n req.on('end', () => {\n if (tooLarge) {\n if (!res.headersSent) {\n res.statusCode = 413;\n res.end();\n }\n return;\n }\n const raw = Buffer.concat(chunks).toString('utf8');\n const headers = normalizeHeaders(req.headers);\n const parsedBody = parseBody(raw, headers['content-type']);\n\n // Notification-config proof is POSTed to the configured webhook URL\n // itself. Validate the complete protocol envelope, echo only the\n // challenge field, and retain the request separately so conformance\n // checks can inspect its RFC 9421 headers without treating it as an\n // event delivery.\n if (isWebhookChallenge(parsedBody.body)) {\n if (Buffer.byteLength(raw, 'utf8') > MAX_CHALLENGE_BODY_BYTES) {\n res.statusCode = 413;\n res.end();\n return;\n }\n const challenge = parsedBody.body.challenge;\n const schemaRef =\n parsedBody.body.scope === 'agent' ? 'core/agent-webhook-challenge.json' : 'core/webhook-challenge.json';\n const validate = getSchemaValidatorByRef(schemaRef);\n if (!validate || !validate(parsedBody.body) || typeof challenge !== 'string') {\n res.statusCode = 400;\n res.end();\n return;\n }\n const key = retryKeyString({ step_id, operation_id });\n const perKey = state.challengeCounts.get(key) ?? 0;\n if (state.captured.length + state.challenges.length >= MAX_CAPTURED_TOTAL || perKey >= MAX_CAPTURED_PER_KEY) {\n res.statusCode = 429;\n res.setHeader('retry-after', '1');\n res.end();\n return;\n }\n state.challengeCounts.set(key, perKey + 1);\n state.challenges.push({\n id: randomUUID(),\n step_id,\n operation_id,\n received_at: Date.now(),\n method: req.method ?? 'POST',\n path: req.url ?? '/',\n headers: redactHeaders(headers),\n raw_body: raw,\n body: parsedBody.body as Record<string, unknown>,\n response_status: 200,\n });\n res.statusCode = 200;\n res.setHeader('content-type', 'application/json');\n res.end(JSON.stringify({ challenge }));\n return;\n }\n\n const key = retryKeyString({ step_id, operation_id });\n const deliveryIndex = (state.deliveryCounts.get(key) ?? 0) + 1;\n const status = nextResponseStatus(state.retryPolicies, key);\n\n // Stop retaining bodies once a capture cap is exhausted, but preserve the\n // configured retry-replay status sequence. Unconditionally returning 503\n // here masks the policy's eventual 2xx forever and can amplify a sender's\n // retries after the runner has already reached its memory bound (#2653).\n const perKey = state.deliveryCounts.get(key) ?? 0;\n if (state.captured.length + state.challenges.length >= MAX_CAPTURED_TOTAL || perKey >= MAX_CAPTURED_PER_KEY) {\n res.statusCode = status;\n if (status === 429 || status >= 500) res.setHeader('retry-after', '1');\n res.end();\n return;\n }\n\n state.deliveryCounts.set(key, deliveryIndex);\n\n const webhook: CapturedWebhook = {\n id: randomUUID(),\n step_id,\n operation_id,\n delivery_index: deliveryIndex,\n received_at: Date.now(),\n method: req.method ?? 'POST',\n path: req.url ?? '/',\n headers: redactHeaders(headers),\n raw_body: raw,\n ...parsedBody,\n response_status: status,\n };\n state.captured.push(webhook);\n\n // Deliver to any matching waiters. First-match-wins per waiter; later\n // arrivals may still deliver to other waiters in the queue.\n for (let i = state.waiters.length - 1; i >= 0; i--) {\n const waiter = state.waiters[i]!;\n if (matchesFilter(webhook, waiter.filter)) {\n clearTimeout(waiter.timer);\n state.waiters.splice(i, 1);\n waiter.resolve({ webhook });\n }\n }\n\n res.statusCode = status;\n res.end();\n });\n\n req.on('error', () => {\n if (!res.headersSent) {\n res.statusCode = 400;\n res.end();\n }\n });\n}\n\nfunction isWebhookChallenge(\n body: unknown\n): body is { type: 'webhook.challenge'; challenge: unknown } & Record<string, unknown> {\n return (\n body !== null &&\n typeof body === 'object' &&\n !Array.isArray(body) &&\n (body as Record<string, unknown>).type === 'webhook.challenge'\n );\n}\n\nfunction requestPathname(reqUrl: string): string {\n const q = reqUrl.indexOf('?');\n return q === -1 ? reqUrl : reqUrl.slice(0, q);\n}\n\nfunction parseStepPath(reqUrl: string): { step_id: string; operation_id: string } | undefined {\n const match = STEP_PATH_RE.exec(requestPathname(reqUrl));\n if (!match) return undefined;\n return { step_id: match[1]!, operation_id: match[2]! };\n}\n\nfunction normalizeHeaders(raw: IncomingMessage['headers']): Record<string, string> {\n const out: Record<string, string> = {};\n for (const [key, value] of Object.entries(raw)) {\n if (value === undefined) continue;\n out[key.toLowerCase()] = Array.isArray(value) ? value.join(', ') : value;\n }\n return out;\n}\n\nfunction redactHeaders(headers: Record<string, string>): Record<string, string> {\n const out: Record<string, string> = {};\n for (const [k, v] of Object.entries(headers)) {\n out[k] = SECRET_HEADER_PATTERN.test(k) ? '[redacted]' : v;\n }\n return out;\n}\n\nfunction parseBody(raw: string, contentType: string | undefined): Pick<CapturedWebhook, 'body' | 'parse_error'> {\n if (raw.length === 0) return {};\n const looksJson = !contentType || /json/i.test(contentType);\n if (!looksJson) return {};\n try {\n return { body: JSON.parse(raw) };\n } catch (err) {\n return { parse_error: err instanceof Error ? err.message : String(err) };\n }\n}\n\n// ────────────────────────────────────────────────────────────\n// Filter matching\n// ────────────────────────────────────────────────────────────\n\nfunction matchesFilter(webhook: CapturedWebhook, filter: WebhookFilter): boolean {\n if (filter.step_id && webhook.step_id !== filter.step_id) return false;\n if (filter.operation_id && webhook.operation_id !== filter.operation_id) return false;\n if (filter.body && Object.keys(filter.body).length > 0) {\n if (webhook.body === undefined) return false;\n for (const [path, expected] of Object.entries(filter.body)) {\n const actual = resolveDottedPath(webhook.body, path);\n if (!deepEqual(actual, expected)) return false;\n }\n }\n return true;\n}\n\nfunction resolveDottedPath(obj: unknown, path: string): unknown {\n if (obj === null || obj === undefined) return undefined;\n const segments = path.split('.');\n let cursor: unknown = obj;\n for (const seg of segments) {\n if (cursor === null || cursor === undefined) return undefined;\n if (typeof cursor !== 'object') return undefined;\n cursor = (cursor as Record<string, unknown>)[seg];\n }\n return cursor;\n}\n\n/**\n * Structural deep-equal. Stringify-based comparison is key-order-sensitive\n * (`{a:1,b:2}` !== `{b:2,a:1}`); for filter bodies that's a false negative\n * when a publisher's JSON serializer emits fields in a different order\n * than the storyboard author. Recurse explicitly instead.\n */\nfunction deepEqual(a: unknown, b: unknown): boolean {\n if (a === b) return true;\n if (typeof a !== typeof b) return false;\n if (a === null || b === null) return a === b;\n if (typeof a !== 'object') return false;\n if (Array.isArray(a) || Array.isArray(b)) {\n if (!Array.isArray(a) || !Array.isArray(b)) return false;\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (!deepEqual(a[i], b[i])) return false;\n }\n return true;\n }\n const keysA = Object.keys(a as Record<string, unknown>);\n const keysB = Object.keys(b as Record<string, unknown>);\n if (keysA.length !== keysB.length) return false;\n for (const k of keysA) {\n if (!Object.prototype.hasOwnProperty.call(b, k)) return false;\n if (!deepEqual((a as Record<string, unknown>)[k], (b as Record<string, unknown>)[k])) return false;\n }\n return true;\n}\n\n// ────────────────────────────────────────────────────────────\n// Retry-replay policy\n// ────────────────────────────────────────────────────────────\n\nfunction nextResponseStatus(\n retryPolicies: Map<string, { policy: RetryReplayPolicy; delivered: number }>,\n key: string\n): number {\n const entry = retryPolicies.get(key);\n if (!entry) return 204;\n if (entry.delivered < entry.policy.count) {\n entry.delivered += 1;\n return entry.policy.http_status;\n }\n return 204;\n}\n\n// ────────────────────────────────────────────────────────────\n// wait / wait_all\n// ────────────────────────────────────────────────────────────\n\nfunction wait(\n filter: WebhookFilter,\n timeout_ms: number,\n captured: CapturedWebhook[],\n waiters: Array<{ filter: WebhookFilter; resolve: (r: WebhookWaitResult) => void; timer: NodeJS.Timeout }>\n): Promise<WebhookWaitResult> {\n const already = captured.find(w => matchesFilter(w, filter));\n if (already) return Promise.resolve({ webhook: already });\n return new Promise<WebhookWaitResult>(resolve => {\n const timer = setTimeout(\n () => {\n const idx = waiters.findIndex(w => w.timer === timer);\n if (idx >= 0) waiters.splice(idx, 1);\n resolve({ timed_out: true });\n },\n Math.max(0, timeout_ms)\n );\n timer.unref?.();\n waiters.push({ filter, resolve, timer });\n });\n}\n\n/**\n * Poll until the deadline, then return every capture matching `filter`.\n * Used when the assertion needs the whole delivery sequence (retry-stability).\n * Waits a minimum of `timeout_ms` even if matches arrive earlier — callers\n * that need \"resolve on first match\" should use `wait` instead.\n */\nfunction waitAll(\n filter: WebhookFilter,\n timeout_ms: number,\n captured: CapturedWebhook[],\n waitAllTimers: Array<{\n filter: WebhookFilter;\n resolve: (result: CapturedWebhook[]) => void;\n timer: NodeJS.Timeout;\n }>\n): Promise<CapturedWebhook[]> {\n return new Promise<CapturedWebhook[]>(resolve => {\n const timer = setTimeout(\n () => {\n const idx = waitAllTimers.findIndex(entry => entry.timer === timer);\n if (idx >= 0) waitAllTimers.splice(idx, 1);\n resolve(captured.filter(w => matchesFilter(w, filter)));\n },\n Math.max(0, timeout_ms)\n );\n timer.unref?.();\n waitAllTimers.push({ filter, resolve, timer });\n });\n}\n\n// ────────────────────────────────────────────────────────────\n// close\n// ────────────────────────────────────────────────────────────\n\nfunction closeServer(\n server: Server,\n captured: CapturedWebhook[],\n waiters: Array<{ filter: WebhookFilter; resolve: (r: WebhookWaitResult) => void; timer: NodeJS.Timeout }>,\n waitAllTimers: Array<{\n filter: WebhookFilter;\n resolve: (result: CapturedWebhook[]) => void;\n timer: NodeJS.Timeout;\n }>\n): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n server.close(err => {\n if (err && (err as NodeJS.ErrnoException).code !== 'ERR_SERVER_NOT_RUNNING') reject(err);\n else resolve();\n });\n // Stop accepting new sockets before force-closing existing keep-alive\n // connections; the opposite order leaves a small acceptance race.\n server.closeAllConnections?.();\n while (waiters.length > 0) {\n const w = waiters.pop()!;\n clearTimeout(w.timer);\n w.resolve({ timed_out: true });\n }\n while (waitAllTimers.length > 0) {\n const pending = waitAllTimers.pop()!;\n clearTimeout(pending.timer);\n pending.resolve(captured.filter(w => matchesFilter(w, pending.filter)));\n }\n });\n}\n\nfunction formatHost(address: string): string {\n return address.includes(':') ? `[${address}]` : address;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAkBA,uBAAqF;AACrF,yBAA2B;AAE3B,2BAAwC;AAMxC,MAAM,iBAAiB;AACvB,MAAM,2BAA2B;AAUjC,MAAM,qBAAqB;AAC3B,MAAM,uBAAuB;AAQ7B,MAAM,qBAAqB;AAC3B,MAAM,qBAAqB;AAC3B,MAAM,wBAAwB;AAC9B,MAAM,kBAAkB;AACxB,MAAM,oBAAoB;AAG1B,MAAM,eAAe;AAOrB,MAAM,wBACJ;AAQF,SAAS,iBAAiB,KAAqB;AAC7C,MAAI,aAAa,KAAK,GAAG,GAAG;AAC1B,UAAM,IAAI,MAAM,wDAAwD;AAAA,EAC1E;AACA,MAAI;AACJ,MAAI;AACF,aAAS,IAAI,IAAI,GAAG;AAAA,EACtB,QAAQ;AACN,UAAM,IAAI,MAAM,mDAAmD,GAAG,EAAE;AAAA,EAC1E;AACA,MAAI,OAAO,aAAa,WAAW,OAAO,aAAa,UAAU;AAC/D,UAAM,IAAI,MAAM,oDAAoD,OAAO,QAAQ,EAAE;AAAA,EACvF;AACA,MAAI,OAAO,YAAY,OAAO,UAAU;AACtC,UAAM,IAAI,MAAM,uDAAuD;AAAA,EACzE;AACA,SAAO,IAAI,QAAQ,OAAO,EAAE;AAC9B;AA2GA,SAAS,eAAe,GAAqB;AAC3C,SAAO,GAAG,EAAE,OAAO,KAAK,EAAE,YAAY;AACxC;AAEA,eAAsB,sBAAsB,UAAwC,CAAC,GAA6B;AAChH,QAAM,OAAO,QAAQ,QAAQ;AAC7B,MAAI,SAAS,eAAe,CAAC,QAAQ,YAAY;AAC/C,UAAM,IAAI,MAAM,uDAAuD;AAAA,EACzE;AAEA,QAAM,OAAO,QAAQ,QAAQ;AAI7B,MAAI,SAAS,oBAAoB,SAAS,aAAa,SAAS,OAAO;AACrE,UAAM,IAAI;AAAA,MACR,yBAAyB,IAAI;AAAA,IAE/B;AAAA,EACF;AACA,QAAM,OAAO,QAAQ,QAAQ;AAC7B,QAAM,YAAY,SAAS,cAAc,iBAAiB,QAAQ,UAAW,IAAI;AAEjF,QAAM,WAA8B,CAAC;AACrC,QAAM,aAAyC,CAAC;AAChD,QAAM,UAID,CAAC;AACN,QAAM,gBAID,CAAC;AACN,QAAM,gBAAgB,oBAAI,IAA8D;AACxF,QAAM,iBAAiB,oBAAI,IAAoB;AAC/C,QAAM,kBAAkB,oBAAI,IAAoB;AAChD,MAAI,SAAS;AAEb,QAAM,aAAS;AAAA,IAAa,CAAC,KAAK,QAChC,cAAc,KAAK,KAAK,EAAE,UAAU,YAAY,SAAS,eAAe,gBAAgB,gBAAgB,CAAC;AAAA,EAC3G;AAGA,SAAO,iBAAiB;AACxB,SAAO,iBAAiB;AACxB,SAAO,mBAAmB;AAC1B,SAAO,iBAAiB;AACxB,SAAO,kBAAkB;AAEzB,QAAM,IAAI,QAAc,CAAC,SAAS,WAAW;AAC3C,UAAM,UAAU,CAAC,QAAe,OAAO,GAAG;AAC1C,WAAO,KAAK,SAAS,OAAO;AAC5B,WAAO,OAAO,MAAM,MAAM,MAAM;AAC9B,aAAO,IAAI,SAAS,OAAO;AAC3B,cAAQ;AAAA,IACV,CAAC;AAAA,EACH,CAAC;AAED,QAAM,QAAQ,OAAO,QAAQ;AAC7B,QAAM,WAAW,aAAa,UAAU,WAAW,MAAM,OAAO,CAAC,IAAI,MAAM,IAAI;AAE/E,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,KAAK,MAAM,SAAS,MAAM;AAAA,IAC1B,YAAY,MAAM,WAAW,MAAM;AAAA,IACnC,UAAU,YAAU,SAAS,OAAO,OAAK,cAAc,GAAG,MAAM,CAAC;AAAA,IACjE,kBAAkB,CAAC,KAAK,WAAW;AACjC,oBAAc,IAAI,eAAe,GAAG,GAAG,EAAE,QAAQ,WAAW,EAAE,CAAC;AAAA,IACjE;AAAA,IACA,MAAM,CAAC,QAAQ,eAAe;AAC5B,UAAI,CAAC,OAAQ,QAAO,KAAK,QAAQ,YAAY,UAAU,OAAO;AAC9D,YAAM,UAAU,SAAS,KAAK,OAAK,cAAc,GAAG,MAAM,CAAC;AAC3D,aAAO,QAAQ,QAAQ,UAAU,EAAE,SAAS,QAAQ,IAAI,EAAE,WAAW,KAAK,CAAC;AAAA,IAC7E;AAAA,IACA,UAAU,CAAC,QAAQ,eACjB,SACI,QAAQ,QAAQ,SAAS,OAAO,OAAK,cAAc,GAAG,MAAM,CAAC,CAAC,IAC9D,QAAQ,QAAQ,YAAY,UAAU,aAAa;AAAA,IACzD,OAAO,MAAM;AACX,eAAS;AACT,aAAO,YAAY,QAAQ,UAAU,SAAS,aAAa;AAAA,IAC7D;AAAA,EACF;AACF;AAmBA,SAAS,cAAc,KAAsB,KAAqB,OAA2B;AAC3F,MAAI,IAAI,WAAW,QAAQ;AACzB,QAAI,aAAa;AACjB,QAAI,IAAI;AACR;AAAA,EACF;AAEA,QAAM,YAAY,cAAc,IAAI,OAAO,EAAE;AAC7C,MAAI,CAAC,WAAW;AACd,QAAI,aAAa;AACjB,QAAI,IAAI;AACR;AAAA,EACF;AACA,QAAM,EAAE,SAAS,aAAa,IAAI;AAElC,MAAI,OAAO;AACX,QAAM,SAAmB,CAAC;AAC1B,MAAI,WAAW;AAEf,MAAI,GAAG,QAAQ,CAAC,UAAkB;AAChC,YAAQ,MAAM;AACd,QAAI,OAAO,gBAAgB;AACzB,iBAAW;AACX,UAAI,QAAQ;AACZ;AAAA,IACF;AACA,WAAO,KAAK,KAAK;AAAA,EACnB,CAAC;AAED,MAAI,GAAG,OAAO,MAAM;AAClB,QAAI,UAAU;AACZ,UAAI,CAAC,IAAI,aAAa;AACpB,YAAI,aAAa;AACjB,YAAI,IAAI;AAAA,MACV;AACA;AAAA,IACF;AACA,UAAM,MAAM,OAAO,OAAO,MAAM,EAAE,SAAS,MAAM;AACjD,UAAM,UAAU,iBAAiB,IAAI,OAAO;AAC5C,UAAM,aAAa,UAAU,KAAK,QAAQ,cAAc,CAAC;AAOzD,QAAI,mBAAmB,WAAW,IAAI,GAAG;AACvC,UAAI,OAAO,WAAW,KAAK,MAAM,IAAI,0BAA0B;AAC7D,YAAI,aAAa;AACjB,YAAI,IAAI;AACR;AAAA,MACF;AACA,YAAM,YAAY,WAAW,KAAK;AAClC,YAAM,YACJ,WAAW,KAAK,UAAU,UAAU,sCAAsC;AAC5E,YAAM,eAAW,8CAAwB,SAAS;AAClD,UAAI,CAAC,YAAY,CAAC,SAAS,WAAW,IAAI,KAAK,OAAO,cAAc,UAAU;AAC5E,YAAI,aAAa;AACjB,YAAI,IAAI;AACR;AAAA,MACF;AACA,YAAMA,OAAM,eAAe,EAAE,SAAS,aAAa,CAAC;AACpD,YAAMC,UAAS,MAAM,gBAAgB,IAAID,IAAG,KAAK;AACjD,UAAI,MAAM,SAAS,SAAS,MAAM,WAAW,UAAU,sBAAsBC,WAAU,sBAAsB;AAC3G,YAAI,aAAa;AACjB,YAAI,UAAU,eAAe,GAAG;AAChC,YAAI,IAAI;AACR;AAAA,MACF;AACA,YAAM,gBAAgB,IAAID,MAAKC,UAAS,CAAC;AACzC,YAAM,WAAW,KAAK;AAAA,QACpB,QAAI,+BAAW;AAAA,QACf;AAAA,QACA;AAAA,QACA,aAAa,KAAK,IAAI;AAAA,QACtB,QAAQ,IAAI,UAAU;AAAA,QACtB,MAAM,IAAI,OAAO;AAAA,QACjB,SAAS,cAAc,OAAO;AAAA,QAC9B,UAAU;AAAA,QACV,MAAM,WAAW;AAAA,QACjB,iBAAiB;AAAA,MACnB,CAAC;AACD,UAAI,aAAa;AACjB,UAAI,UAAU,gBAAgB,kBAAkB;AAChD,UAAI,IAAI,KAAK,UAAU,EAAE,UAAU,CAAC,CAAC;AACrC;AAAA,IACF;AAEA,UAAM,MAAM,eAAe,EAAE,SAAS,aAAa,CAAC;AACpD,UAAM,iBAAiB,MAAM,eAAe,IAAI,GAAG,KAAK,KAAK;AAC7D,UAAM,SAAS,mBAAmB,MAAM,eAAe,GAAG;AAM1D,UAAM,SAAS,MAAM,eAAe,IAAI,GAAG,KAAK;AAChD,QAAI,MAAM,SAAS,SAAS,MAAM,WAAW,UAAU,sBAAsB,UAAU,sBAAsB;AAC3G,UAAI,aAAa;AACjB,UAAI,WAAW,OAAO,UAAU,IAAK,KAAI,UAAU,eAAe,GAAG;AACrE,UAAI,IAAI;AACR;AAAA,IACF;AAEA,UAAM,eAAe,IAAI,KAAK,aAAa;AAE3C,UAAM,UAA2B;AAAA,MAC/B,QAAI,+BAAW;AAAA,MACf;AAAA,MACA;AAAA,MACA,gBAAgB;AAAA,MAChB,aAAa,KAAK,IAAI;AAAA,MACtB,QAAQ,IAAI,UAAU;AAAA,MACtB,MAAM,IAAI,OAAO;AAAA,MACjB,SAAS,cAAc,OAAO;AAAA,MAC9B,UAAU;AAAA,MACV,GAAG;AAAA,MACH,iBAAiB;AAAA,IACnB;AACA,UAAM,SAAS,KAAK,OAAO;AAI3B,aAAS,IAAI,MAAM,QAAQ,SAAS,GAAG,KAAK,GAAG,KAAK;AAClD,YAAM,SAAS,MAAM,QAAQ,CAAC;AAC9B,UAAI,cAAc,SAAS,OAAO,MAAM,GAAG;AACzC,qBAAa,OAAO,KAAK;AACzB,cAAM,QAAQ,OAAO,GAAG,CAAC;AACzB,eAAO,QAAQ,EAAE,QAAQ,CAAC;AAAA,MAC5B;AAAA,IACF;AAEA,QAAI,aAAa;AACjB,QAAI,IAAI;AAAA,EACV,CAAC;AAED,MAAI,GAAG,SAAS,MAAM;AACpB,QAAI,CAAC,IAAI,aAAa;AACpB,UAAI,aAAa;AACjB,UAAI,IAAI;AAAA,IACV;AAAA,EACF,CAAC;AACH;AAEA,SAAS,mBACP,MACqF;AACrF,SACE,SAAS,QACT,OAAO,SAAS,YAChB,CAAC,MAAM,QAAQ,IAAI,KAClB,KAAiC,SAAS;AAE/C;AAEA,SAAS,gBAAgB,QAAwB;AAC/C,QAAM,IAAI,OAAO,QAAQ,GAAG;AAC5B,SAAO,MAAM,KAAK,SAAS,OAAO,MAAM,GAAG,CAAC;AAC9C;AAEA,SAAS,cAAc,QAAuE;AAC5F,QAAM,QAAQ,aAAa,KAAK,gBAAgB,MAAM,CAAC;AACvD,MAAI,CAAC,MAAO,QAAO;AACnB,SAAO,EAAE,SAAS,MAAM,CAAC,GAAI,cAAc,MAAM,CAAC,EAAG;AACvD;AAEA,SAAS,iBAAiB,KAAyD;AACjF,QAAM,MAA8B,CAAC;AACrC,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,GAAG,GAAG;AAC9C,QAAI,UAAU,OAAW;AACzB,QAAI,IAAI,YAAY,CAAC,IAAI,MAAM,QAAQ,KAAK,IAAI,MAAM,KAAK,IAAI,IAAI;AAAA,EACrE;AACA,SAAO;AACT;AAEA,SAAS,cAAc,SAAyD;AAC9E,QAAM,MAA8B,CAAC;AACrC,aAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,OAAO,GAAG;AAC5C,QAAI,CAAC,IAAI,sBAAsB,KAAK,CAAC,IAAI,eAAe;AAAA,EAC1D;AACA,SAAO;AACT;AAEA,SAAS,UAAU,KAAa,aAAgF;AAC9G,MAAI,IAAI,WAAW,EAAG,QAAO,CAAC;AAC9B,QAAM,YAAY,CAAC,eAAe,QAAQ,KAAK,WAAW;AAC1D,MAAI,CAAC,UAAW,QAAO,CAAC;AACxB,MAAI;AACF,WAAO,EAAE,MAAM,KAAK,MAAM,GAAG,EAAE;AAAA,EACjC,SAAS,KAAK;AACZ,WAAO,EAAE,aAAa,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,EAAE;AAAA,EACzE;AACF;AAMA,SAAS,cAAc,SAA0B,QAAgC;AAC/E,MAAI,OAAO,WAAW,QAAQ,YAAY,OAAO,QAAS,QAAO;AACjE,MAAI,OAAO,gBAAgB,QAAQ,iBAAiB,OAAO,aAAc,QAAO;AAChF,MAAI,OAAO,QAAQ,OAAO,KAAK,OAAO,IAAI,EAAE,SAAS,GAAG;AACtD,QAAI,QAAQ,SAAS,OAAW,QAAO;AACvC,eAAW,CAAC,MAAM,QAAQ,KAAK,OAAO,QAAQ,OAAO,IAAI,GAAG;AAC1D,YAAM,SAAS,kBAAkB,QAAQ,MAAM,IAAI;AACnD,UAAI,CAAC,UAAU,QAAQ,QAAQ,EAAG,QAAO;AAAA,IAC3C;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,kBAAkB,KAAc,MAAuB;AAC9D,MAAI,QAAQ,QAAQ,QAAQ,OAAW,QAAO;AAC9C,QAAM,WAAW,KAAK,MAAM,GAAG;AAC/B,MAAI,SAAkB;AACtB,aAAW,OAAO,UAAU;AAC1B,QAAI,WAAW,QAAQ,WAAW,OAAW,QAAO;AACpD,QAAI,OAAO,WAAW,SAAU,QAAO;AACvC,aAAU,OAAmC,GAAG;AAAA,EAClD;AACA,SAAO;AACT;AAQA,SAAS,UAAU,GAAY,GAAqB;AAClD,MAAI,MAAM,EAAG,QAAO;AACpB,MAAI,OAAO,MAAM,OAAO,EAAG,QAAO;AAClC,MAAI,MAAM,QAAQ,MAAM,KAAM,QAAO,MAAM;AAC3C,MAAI,OAAO,MAAM,SAAU,QAAO;AAClC,MAAI,MAAM,QAAQ,CAAC,KAAK,MAAM,QAAQ,CAAC,GAAG;AACxC,QAAI,CAAC,MAAM,QAAQ,CAAC,KAAK,CAAC,MAAM,QAAQ,CAAC,EAAG,QAAO;AACnD,QAAI,EAAE,WAAW,EAAE,OAAQ,QAAO;AAClC,aAAS,IAAI,GAAG,IAAI,EAAE,QAAQ,KAAK;AACjC,UAAI,CAAC,UAAU,EAAE,CAAC,GAAG,EAAE,CAAC,CAAC,EAAG,QAAO;AAAA,IACrC;AACA,WAAO;AAAA,EACT;AACA,QAAM,QAAQ,OAAO,KAAK,CAA4B;AACtD,QAAM,QAAQ,OAAO,KAAK,CAA4B;AACtD,MAAI,MAAM,WAAW,MAAM,OAAQ,QAAO;AAC1C,aAAW,KAAK,OAAO;AACrB,QAAI,CAAC,OAAO,UAAU,eAAe,KAAK,GAAG,CAAC,EAAG,QAAO;AACxD,QAAI,CAAC,UAAW,EAA8B,CAAC,GAAI,EAA8B,CAAC,CAAC,EAAG,QAAO;AAAA,EAC/F;AACA,SAAO;AACT;AAMA,SAAS,mBACP,eACA,KACQ;AACR,QAAM,QAAQ,cAAc,IAAI,GAAG;AACnC,MAAI,CAAC,MAAO,QAAO;AACnB,MAAI,MAAM,YAAY,MAAM,OAAO,OAAO;AACxC,UAAM,aAAa;AACnB,WAAO,MAAM,OAAO;AAAA,EACtB;AACA,SAAO;AACT;AAMA,SAAS,KACP,QACA,YACA,UACA,SAC4B;AAC5B,QAAM,UAAU,SAAS,KAAK,OAAK,cAAc,GAAG,MAAM,CAAC;AAC3D,MAAI,QAAS,QAAO,QAAQ,QAAQ,EAAE,SAAS,QAAQ,CAAC;AACxD,SAAO,IAAI,QAA2B,aAAW;AAC/C,UAAM,QAAQ;AAAA,MACZ,MAAM;AACJ,cAAM,MAAM,QAAQ,UAAU,OAAK,EAAE,UAAU,KAAK;AACpD,YAAI,OAAO,EAAG,SAAQ,OAAO,KAAK,CAAC;AACnC,gBAAQ,EAAE,WAAW,KAAK,CAAC;AAAA,MAC7B;AAAA,MACA,KAAK,IAAI,GAAG,UAAU;AAAA,IACxB;AACA,UAAM,QAAQ;AACd,YAAQ,KAAK,EAAE,QAAQ,SAAS,MAAM,CAAC;AAAA,EACzC,CAAC;AACH;AAQA,SAAS,QACP,QACA,YACA,UACA,eAK4B;AAC5B,SAAO,IAAI,QAA2B,aAAW;AAC/C,UAAM,QAAQ;AAAA,MACZ,MAAM;AACJ,cAAM,MAAM,cAAc,UAAU,WAAS,MAAM,UAAU,KAAK;AAClE,YAAI,OAAO,EAAG,eAAc,OAAO,KAAK,CAAC;AACzC,gBAAQ,SAAS,OAAO,OAAK,cAAc,GAAG,MAAM,CAAC,CAAC;AAAA,MACxD;AAAA,MACA,KAAK,IAAI,GAAG,UAAU;AAAA,IACxB;AACA,UAAM,QAAQ;AACd,kBAAc,KAAK,EAAE,QAAQ,SAAS,MAAM,CAAC;AAAA,EAC/C,CAAC;AACH;AAMA,SAAS,YACP,QACA,UACA,SACA,eAKe;AACf,SAAO,IAAI,QAAc,CAAC,SAAS,WAAW;AAC5C,WAAO,MAAM,SAAO;AAClB,UAAI,OAAQ,IAA8B,SAAS,yBAA0B,QAAO,GAAG;AAAA,UAClF,SAAQ;AAAA,IACf,CAAC;AAGD,WAAO,sBAAsB;AAC7B,WAAO,QAAQ,SAAS,GAAG;AACzB,YAAM,IAAI,QAAQ,IAAI;AACtB,mBAAa,EAAE,KAAK;AACpB,QAAE,QAAQ,EAAE,WAAW,KAAK,CAAC;AAAA,IAC/B;AACA,WAAO,cAAc,SAAS,GAAG;AAC/B,YAAM,UAAU,cAAc,IAAI;AAClC,mBAAa,QAAQ,KAAK;AAC1B,cAAQ,QAAQ,SAAS,OAAO,OAAK,cAAc,GAAG,QAAQ,MAAM,CAAC,CAAC;AAAA,IACxE;AAAA,EACF,CAAC;AACH;AAEA,SAAS,WAAW,SAAyB;AAC3C,SAAO,QAAQ,SAAS,GAAG,IAAI,IAAI,OAAO,MAAM;AAClD;","names":["key","perKey"]}