@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.
- package/bin/adcp-storyboard-sandbox.js +16 -0
- package/bin/adcp.js +28 -30
- package/dist/lib/index.d.mts +3 -3
- package/dist/lib/index.d.ts +3 -3
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js +2 -0
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/index.mjs +3 -1
- package/dist/lib/index.mjs.map +1 -1
- package/dist/lib/schemas/index.d.mts +385 -1
- package/dist/lib/schemas/index.d.ts +385 -1
- package/dist/lib/schemas/index.d.ts.map +1 -1
- package/dist/lib/schemas/index.js +13 -0
- package/dist/lib/schemas/index.js.map +1 -1
- package/dist/lib/schemas/index.mjs +11 -0
- package/dist/lib/schemas/index.mjs.map +1 -1
- package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
- package/dist/lib/server/create-adcp-server.d.mts +2 -2
- package/dist/lib/server/create-adcp-server.d.ts +2 -2
- package/dist/lib/server/create-adcp-server.d.ts.map +1 -1
- package/dist/lib/server/create-adcp-server.js +6 -1
- package/dist/lib/server/create-adcp-server.js.map +1 -1
- package/dist/lib/server/create-adcp-server.mjs +6 -1
- package/dist/lib/server/create-adcp-server.mjs.map +1 -1
- package/dist/lib/server/decisioning/index.d.mts +3 -3
- package/dist/lib/server/decisioning/index.d.ts +3 -3
- package/dist/lib/server/decisioning/index.d.ts.map +1 -1
- package/dist/lib/server/decisioning/index.js.map +1 -1
- package/dist/lib/server/decisioning/index.mjs.map +1 -1
- package/dist/lib/server/decisioning/runtime/from-platform.d.mts +24 -17
- package/dist/lib/server/decisioning/runtime/from-platform.d.ts +24 -17
- package/dist/lib/server/decisioning/runtime/from-platform.d.ts.map +1 -1
- package/dist/lib/server/decisioning/runtime/from-platform.js +71 -42
- package/dist/lib/server/decisioning/runtime/from-platform.js.map +1 -1
- package/dist/lib/server/decisioning/runtime/from-platform.mjs +75 -43
- package/dist/lib/server/decisioning/runtime/from-platform.mjs.map +1 -1
- package/dist/lib/server/decisioning/runtime/postgres-task-registry.d.mts +15 -21
- package/dist/lib/server/decisioning/runtime/postgres-task-registry.d.ts +15 -21
- package/dist/lib/server/decisioning/runtime/postgres-task-registry.d.ts.map +1 -1
- package/dist/lib/server/decisioning/runtime/postgres-task-registry.js +100 -30
- package/dist/lib/server/decisioning/runtime/postgres-task-registry.js.map +1 -1
- package/dist/lib/server/decisioning/runtime/postgres-task-registry.mjs +100 -30
- package/dist/lib/server/decisioning/runtime/postgres-task-registry.mjs.map +1 -1
- package/dist/lib/server/decisioning/runtime/task-registry.d.mts +30 -16
- package/dist/lib/server/decisioning/runtime/task-registry.d.ts +30 -16
- package/dist/lib/server/decisioning/runtime/task-registry.d.ts.map +1 -1
- package/dist/lib/server/decisioning/runtime/task-registry.js +40 -20
- package/dist/lib/server/decisioning/runtime/task-registry.js.map +1 -1
- package/dist/lib/server/decisioning/runtime/task-registry.mjs +38 -19
- package/dist/lib/server/decisioning/runtime/task-registry.mjs.map +1 -1
- package/dist/lib/server/decisioning/runtime/to-context.d.mts +2 -2
- package/dist/lib/server/decisioning/runtime/to-context.d.ts +2 -2
- package/dist/lib/server/decisioning/runtime/to-context.d.ts.map +1 -1
- package/dist/lib/server/decisioning/runtime/to-context.js +2 -2
- package/dist/lib/server/decisioning/runtime/to-context.js.map +1 -1
- package/dist/lib/server/decisioning/runtime/to-context.mjs +2 -2
- package/dist/lib/server/decisioning/runtime/to-context.mjs.map +1 -1
- package/dist/lib/server/decisioning/specialisms/creative-ad-server.d.mts +3 -2
- package/dist/lib/server/decisioning/specialisms/creative-ad-server.d.ts +3 -2
- package/dist/lib/server/decisioning/specialisms/creative-ad-server.d.ts.map +1 -1
- package/dist/lib/server/decisioning/specialisms/creative-ad-server.js.map +1 -1
- package/dist/lib/server/decisioning/specialisms/creative.d.mts +10 -10
- package/dist/lib/server/decisioning/specialisms/creative.d.ts +10 -10
- package/dist/lib/server/decisioning/specialisms/creative.d.ts.map +1 -1
- package/dist/lib/server/decisioning/specialisms/creative.js.map +1 -1
- package/dist/lib/server/index.d.mts +1 -1
- package/dist/lib/server/index.d.ts +1 -1
- package/dist/lib/server/index.d.ts.map +1 -1
- package/dist/lib/server/index.js.map +1 -1
- package/dist/lib/server/index.mjs.map +1 -1
- package/dist/lib/signing/agent-context.d.mts +1 -0
- package/dist/lib/signing/agent-context.d.ts +1 -0
- package/dist/lib/signing/agent-context.d.ts.map +1 -1
- package/dist/lib/signing/agent-context.js +37 -2
- package/dist/lib/signing/agent-context.js.map +1 -1
- package/dist/lib/signing/agent-context.mjs +38 -3
- package/dist/lib/signing/agent-context.mjs.map +1 -1
- package/dist/lib/signing/agent-fetch.d.mts +1 -2
- package/dist/lib/signing/agent-fetch.d.ts +1 -2
- package/dist/lib/signing/agent-fetch.d.ts.map +1 -1
- package/dist/lib/signing/agent-fetch.js +2 -0
- package/dist/lib/signing/agent-fetch.js.map +1 -1
- package/dist/lib/signing/agent-fetch.mjs +2 -0
- package/dist/lib/signing/agent-fetch.mjs.map +1 -1
- package/dist/lib/signing/agent-resolver/etld.js.map +1 -1
- package/dist/lib/signing/agent-resolver/etld.mjs.map +1 -1
- package/dist/lib/testing/client.d.mts +6 -0
- package/dist/lib/testing/client.d.ts +6 -0
- package/dist/lib/testing/client.d.ts.map +1 -1
- package/dist/lib/testing/client.js +76 -4
- package/dist/lib/testing/client.js.map +1 -1
- package/dist/lib/testing/client.mjs +75 -4
- package/dist/lib/testing/client.mjs.map +1 -1
- package/dist/lib/testing/compliance/comply.d.ts.map +1 -1
- package/dist/lib/testing/compliance/comply.js +6 -0
- package/dist/lib/testing/compliance/comply.js.map +1 -1
- package/dist/lib/testing/compliance/comply.mjs +7 -1
- package/dist/lib/testing/compliance/comply.mjs.map +1 -1
- package/dist/lib/testing/storyboard/default-invariants.d.ts.map +1 -1
- package/dist/lib/testing/storyboard/default-invariants.js +11 -2
- package/dist/lib/testing/storyboard/default-invariants.js.map +1 -1
- package/dist/lib/testing/storyboard/default-invariants.mjs +11 -2
- package/dist/lib/testing/storyboard/default-invariants.mjs.map +1 -1
- package/dist/lib/testing/storyboard/index.d.mts +1 -1
- package/dist/lib/testing/storyboard/index.d.ts +1 -1
- package/dist/lib/testing/storyboard/index.d.ts.map +1 -1
- package/dist/lib/testing/storyboard/index.js.map +1 -1
- package/dist/lib/testing/storyboard/index.mjs.map +1 -1
- package/dist/lib/testing/storyboard/loader.d.ts.map +1 -1
- package/dist/lib/testing/storyboard/loader.js +14 -0
- package/dist/lib/testing/storyboard/loader.js.map +1 -1
- package/dist/lib/testing/storyboard/loader.mjs +14 -0
- package/dist/lib/testing/storyboard/loader.mjs.map +1 -1
- package/dist/lib/testing/storyboard/phase-condition.d.mts +41 -0
- package/dist/lib/testing/storyboard/phase-condition.d.ts +42 -0
- package/dist/lib/testing/storyboard/phase-condition.d.ts.map +1 -0
- package/dist/lib/testing/storyboard/phase-condition.js +215 -0
- package/dist/lib/testing/storyboard/phase-condition.js.map +1 -0
- package/dist/lib/testing/storyboard/phase-condition.mjs +189 -0
- package/dist/lib/testing/storyboard/phase-condition.mjs.map +1 -0
- package/dist/lib/testing/storyboard/request-builder.d.ts.map +1 -1
- package/dist/lib/testing/storyboard/request-builder.js +15 -2
- package/dist/lib/testing/storyboard/request-builder.js.map +1 -1
- package/dist/lib/testing/storyboard/request-builder.mjs +15 -2
- package/dist/lib/testing/storyboard/request-builder.mjs.map +1 -1
- package/dist/lib/testing/storyboard/request-signing/functional-dispatch.d.mts +15 -0
- package/dist/lib/testing/storyboard/request-signing/functional-dispatch.d.ts +16 -0
- package/dist/lib/testing/storyboard/request-signing/functional-dispatch.d.ts.map +1 -0
- package/dist/lib/testing/storyboard/request-signing/functional-dispatch.js +106 -0
- package/dist/lib/testing/storyboard/request-signing/functional-dispatch.js.map +1 -0
- package/dist/lib/testing/storyboard/request-signing/functional-dispatch.mjs +82 -0
- package/dist/lib/testing/storyboard/request-signing/functional-dispatch.mjs.map +1 -0
- package/dist/lib/testing/storyboard/request-signing/test-kit.d.mts +15 -0
- package/dist/lib/testing/storyboard/request-signing/test-kit.d.ts +15 -0
- package/dist/lib/testing/storyboard/request-signing/test-kit.d.ts.map +1 -1
- package/dist/lib/testing/storyboard/request-signing/test-kit.js +58 -0
- package/dist/lib/testing/storyboard/request-signing/test-kit.js.map +1 -1
- package/dist/lib/testing/storyboard/request-signing/test-kit.mjs +58 -0
- package/dist/lib/testing/storyboard/request-signing/test-kit.mjs.map +1 -1
- package/dist/lib/testing/storyboard/request-signing/vector-loader.d.mts +2 -0
- package/dist/lib/testing/storyboard/request-signing/vector-loader.d.ts +2 -0
- package/dist/lib/testing/storyboard/request-signing/vector-loader.d.ts.map +1 -1
- package/dist/lib/testing/storyboard/request-signing/vector-loader.js +7 -1
- package/dist/lib/testing/storyboard/request-signing/vector-loader.js.map +1 -1
- package/dist/lib/testing/storyboard/request-signing/vector-loader.mjs +6 -1
- package/dist/lib/testing/storyboard/request-signing/vector-loader.mjs.map +1 -1
- package/dist/lib/testing/storyboard/runner.d.ts.map +1 -1
- package/dist/lib/testing/storyboard/runner.js +67 -24
- package/dist/lib/testing/storyboard/runner.js.map +1 -1
- package/dist/lib/testing/storyboard/runner.mjs +67 -24
- package/dist/lib/testing/storyboard/runner.mjs.map +1 -1
- package/dist/lib/testing/storyboard/task-map.d.ts.map +1 -1
- package/dist/lib/testing/storyboard/task-map.js +6 -2
- package/dist/lib/testing/storyboard/task-map.js.map +1 -1
- package/dist/lib/testing/storyboard/task-map.mjs +6 -2
- package/dist/lib/testing/storyboard/task-map.mjs.map +1 -1
- package/dist/lib/testing/storyboard/types.d.mts +5 -4
- package/dist/lib/testing/storyboard/types.d.ts +5 -4
- package/dist/lib/testing/storyboard/types.d.ts.map +1 -1
- package/dist/lib/testing/storyboard/types.js.map +1 -1
- package/dist/lib/testing/storyboard/types.mjs.map +1 -1
- package/dist/lib/testing/storyboard/validations.d.mts +1 -1
- package/dist/lib/testing/storyboard/validations.d.ts +1 -1
- package/dist/lib/testing/storyboard/webhook-assertions.d.ts.map +1 -1
- package/dist/lib/testing/storyboard/webhook-assertions.js +60 -4
- package/dist/lib/testing/storyboard/webhook-assertions.js.map +1 -1
- package/dist/lib/testing/storyboard/webhook-assertions.mjs +60 -4
- package/dist/lib/testing/storyboard/webhook-assertions.mjs.map +1 -1
- package/dist/lib/testing/storyboard/webhook-receiver.d.mts +15 -0
- package/dist/lib/testing/storyboard/webhook-receiver.d.ts +15 -0
- package/dist/lib/testing/storyboard/webhook-receiver.d.ts.map +1 -1
- package/dist/lib/testing/storyboard/webhook-receiver.js +57 -6
- package/dist/lib/testing/storyboard/webhook-receiver.js.map +1 -1
- package/dist/lib/testing/storyboard/webhook-receiver.mjs +57 -6
- package/dist/lib/testing/storyboard/webhook-receiver.mjs.map +1 -1
- package/dist/lib/testing/types.d.mts +12 -0
- package/dist/lib/testing/types.d.ts +12 -0
- package/dist/lib/testing/types.d.ts.map +1 -1
- package/dist/lib/testing/types.js.map +1 -1
- package/dist/lib/types/comply-test-controller.d.ts +1 -14
- package/dist/lib/types/core.generated.d.mts +6 -3
- package/dist/lib/types/core.generated.d.ts +6 -3
- package/dist/lib/types/core.generated.d.ts.map +1 -1
- package/dist/lib/types/core.generated.js.map +1 -1
- package/dist/lib/types/get-products.d.ts +1 -14
- package/dist/lib/types/index.d.mts +1 -1
- package/dist/lib/types/index.d.ts +1 -1
- package/dist/lib/types/index.d.ts.map +1 -1
- package/dist/lib/types/index.js.map +1 -1
- package/dist/lib/types/index.mjs.map +1 -1
- package/dist/lib/types/list-transformers.d.ts +9 -5
- package/dist/lib/types/schemas.generated.d.ts +23 -352
- package/dist/lib/types/schemas.generated.d.ts.map +1 -1
- package/dist/lib/types/schemas.generated.js +7 -4
- package/dist/lib/types/schemas.generated.js.map +1 -1
- package/dist/lib/types/schemas.generated.mjs +6 -4
- package/dist/lib/types/schemas.generated.mjs.map +1 -1
- package/dist/lib/types/server-payload-aliases.d.mts +2 -1
- package/dist/lib/types/server-payload-aliases.d.ts +2 -1
- package/dist/lib/types/server-payload-aliases.d.ts.map +1 -1
- package/dist/lib/types/server-payload-aliases.js.map +1 -1
- package/dist/lib/types/tools.generated.d.mts +6 -3
- package/dist/lib/types/tools.generated.d.ts +6 -3
- package/dist/lib/types/tools.generated.d.ts.map +1 -1
- package/dist/lib/types/tools.generated.js.map +1 -1
- package/dist/lib/utils/build-creative-return-builders.d.mts +5 -1
- package/dist/lib/utils/build-creative-return-builders.d.ts +5 -1
- package/dist/lib/utils/build-creative-return-builders.d.ts.map +1 -1
- package/dist/lib/utils/build-creative-return-builders.js +9 -3
- package/dist/lib/utils/build-creative-return-builders.js.map +1 -1
- package/dist/lib/utils/build-creative-return-builders.mjs +7 -2
- package/dist/lib/utils/build-creative-return-builders.mjs.map +1 -1
- package/dist/lib/utils/response-unwrapper.js +1 -1
- package/dist/lib/utils/response-unwrapper.js.map +1 -1
- package/dist/lib/utils/response-unwrapper.mjs +1 -1
- package/dist/lib/utils/response-unwrapper.mjs.map +1 -1
- package/dist/lib/validation/schema-loader.d.ts.map +1 -1
- package/dist/lib/validation/schema-loader.js +16 -7
- package/dist/lib/validation/schema-loader.js.map +1 -1
- package/dist/lib/validation/schema-loader.mjs +16 -7
- package/dist/lib/validation/schema-loader.mjs.map +1 -1
- package/dist/lib/version.d.mts +3 -3
- package/dist/lib/version.d.ts +3 -3
- package/dist/lib/version.js +3 -3
- package/dist/lib/version.js.map +1 -1
- package/dist/lib/version.mjs +3 -3
- package/dist/lib/version.mjs.map +1 -1
- package/docs/llms.txt +2 -2
- package/docs/migration-5.x-to-6.x.md +3 -2
- package/docs/migration-task-registry-scoping.md +61 -0
- package/examples/hello_seller_adapter_guaranteed.ts +1 -1
- package/package.json +5 -5
- package/skills/build-decisioning-creative-template/SKILL.md +2 -2
- package/skills/build-decisioning-platform/SKILL.md +4 -2
- package/skills/build-decisioning-platform/advanced/REFERENCE.md +7 -6
|
@@ -197,7 +197,7 @@ export interface UpstreamTrafficQueryResult {
|
|
|
197
197
|
*/
|
|
198
198
|
export declare function runValidations(validations: StoryboardValidation[], context: ValidationContext): ValidationResult[];
|
|
199
199
|
/** Capability semver emitted in run summaries and used for advisory expiry. */
|
|
200
|
-
export declare const RUNNER_CAPABILITY_VERSION = "14.0.0-beta.
|
|
200
|
+
export declare const RUNNER_CAPABILITY_VERSION = "14.0.0-beta.13";
|
|
201
201
|
/** True when a failed validation contributes to the owning step's grade. */
|
|
202
202
|
export declare function validationFailsStep(result: ValidationResult): boolean;
|
|
203
203
|
/**
|
|
@@ -197,7 +197,7 @@ export interface UpstreamTrafficQueryResult {
|
|
|
197
197
|
*/
|
|
198
198
|
export declare function runValidations(validations: StoryboardValidation[], context: ValidationContext): ValidationResult[];
|
|
199
199
|
/** Capability semver emitted in run summaries and used for advisory expiry. */
|
|
200
|
-
export declare const RUNNER_CAPABILITY_VERSION = "14.0.0-beta.
|
|
200
|
+
export declare const RUNNER_CAPABILITY_VERSION = "14.0.0-beta.13";
|
|
201
201
|
/** True when a failed validation contributes to the owning step's grade. */
|
|
202
202
|
export declare function validationFailsStep(result: ValidationResult): boolean;
|
|
203
203
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"webhook-assertions.d.ts","sourceRoot":"","sources":["../../../../src/lib/testing/storyboard/webhook-assertions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAGH,OAAO,EAAiB,KAAK,eAAe,EAAE,MAAM,WAAW,CAAC;AAChE,OAAO,KAAK,EACV,eAAe,EAIf,UAAU,EACV,iBAAiB,EACjB,oBAAoB,EACpB,cAAc,EACd,qBAAqB,EACrB,oBAAoB,EAGrB,MAAM,SAAS,CAAC;AAEjB,OAAO,KAAK,EAAqD,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAuC7G,eAAO,MAAM,uBAAuB,EAAE,GAAG,CAAC,MAAM,CAK9C,CAAC;
|
|
1
|
+
{"version":3,"file":"webhook-assertions.d.ts","sourceRoot":"","sources":["../../../../src/lib/testing/storyboard/webhook-assertions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAGH,OAAO,EAAiB,KAAK,eAAe,EAAE,MAAM,WAAW,CAAC;AAChE,OAAO,KAAK,EACV,eAAe,EAIf,UAAU,EACV,iBAAiB,EACjB,oBAAoB,EACpB,cAAc,EACd,qBAAqB,EACrB,oBAAoB,EAGrB,MAAM,SAAS,CAAC;AAEjB,OAAO,KAAK,EAAqD,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAuC7G,eAAO,MAAM,uBAAuB,EAAE,GAAG,CAAC,MAAM,CAK9C,CAAC;AAgDH,UAAU,wBAAwB;IAChC,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC3B,gBAAgB,EAAE,GAAG,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAC;IACpD,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IAC1C,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC,UAAU,CAAC,EAAE,eAAe,CAAC;CAC9B;AAED,UAAU,QAAQ;IAChB,IAAI,EAAE,cAAc,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;CACrB;AAkED,KAAK,cAAc,GAAG,CAAC,aAAa,EAAE,MAAM,KAAK,qBAAqB,GAAG,SAAS,CAAC;AAEnF;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,UAAU,EAAE,UAAU,EACtB,UAAU,EAAE,eAAe,EAC3B,QAAQ,EAAE,eAAe,GACxB,IAAI,CAiBN;AAED;;;;;;GAMG;AACH,wBAAsB,2BAA2B,CAC/C,IAAI,EAAE,cAAc,EACpB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,iBAAiB,EAC1B,QAAQ,EAAE,QAAQ,EAAE,EACpB,OAAO,EAAE,oBAAoB,EAC7B,QAAQ,EAAE,wBAAwB,EAClC,cAAc,CAAC,EAAE,cAAc,GAC9B,OAAO,CAAC,oBAAoB,CAAC,CAmI/B"}
|
|
@@ -59,10 +59,13 @@ const WEBHOOK_ASSERTION_TASKS = /* @__PURE__ */ new Set([
|
|
|
59
59
|
]);
|
|
60
60
|
const DEFAULT_TIMEOUT_SECONDS = 30;
|
|
61
61
|
const DEFAULT_NO_WEBHOOK_TIMEOUT_SECONDS = 5;
|
|
62
|
-
const DEFAULT_RETRY_REPLAY_TIMEOUT_SECONDS =
|
|
62
|
+
const DEFAULT_RETRY_REPLAY_TIMEOUT_SECONDS = 60;
|
|
63
63
|
const DEFAULT_RETRY_COUNT = 3;
|
|
64
64
|
const DEFAULT_RETRY_HTTP_STATUS = 503;
|
|
65
65
|
const DEFAULT_MIN_DELIVERIES = 2;
|
|
66
|
+
function webhookTimeoutDetail(receiver, base) {
|
|
67
|
+
return receiver.mode === "loopback_mock" && receiver.all().length === 0 && receiver.challenges().length === 0 ? `${base} No request reached the loopback receiver; an out-of-process agent must use webhook_receiver.mode="proxy_url".` : base;
|
|
68
|
+
}
|
|
66
69
|
const DEFAULT_WEBHOOK_SIGNING_TAG = "adcp/webhook-signing/v1";
|
|
67
70
|
const MAX_RETRY_REPLAY_COUNT = 10;
|
|
68
71
|
const MAX_TIMEOUT_SECONDS = 300;
|
|
@@ -78,6 +81,46 @@ function clampRetryPolicy(spec) {
|
|
|
78
81
|
const http_status = ALLOWED_RETRY_HTTP_STATUSES.has(requestedStatus) ? requestedStatus : DEFAULT_RETRY_HTTP_STATUS;
|
|
79
82
|
return { count, http_status };
|
|
80
83
|
}
|
|
84
|
+
function collectNotificationConfigs(value, configs = []) {
|
|
85
|
+
if (!value || typeof value !== "object") return configs;
|
|
86
|
+
if (Array.isArray(value)) {
|
|
87
|
+
for (const item of value) collectNotificationConfigs(item, configs);
|
|
88
|
+
return configs;
|
|
89
|
+
}
|
|
90
|
+
const record = value;
|
|
91
|
+
if (Array.isArray(record.notification_configs)) {
|
|
92
|
+
for (const config of record.notification_configs) {
|
|
93
|
+
if (config && typeof config === "object" && !Array.isArray(config)) {
|
|
94
|
+
configs.push(config);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
for (const nested of Object.values(record)) collectNotificationConfigs(nested, configs);
|
|
99
|
+
return configs;
|
|
100
|
+
}
|
|
101
|
+
function notificationSetupMatchesAssertion(setup, assertion) {
|
|
102
|
+
const configs = collectNotificationConfigs(setup.sample_request);
|
|
103
|
+
if (configs.length === 0) return false;
|
|
104
|
+
const filter = assertion.filter && typeof assertion.filter === "object" ? assertion.filter : void 0;
|
|
105
|
+
const body = filter?.body && typeof filter.body === "object" ? filter.body : void 0;
|
|
106
|
+
const subscriberId = filter?.subscriber_id ?? body?.subscriber_id;
|
|
107
|
+
const eventType = filter?.notification_type ?? body?.notification_type ?? filter?.event ?? body?.event;
|
|
108
|
+
if (typeof subscriberId !== "string" && typeof eventType !== "string") return false;
|
|
109
|
+
return configs.some((config) => {
|
|
110
|
+
const subscriberMatches = typeof subscriberId !== "string" || config.subscriber_id === subscriberId;
|
|
111
|
+
const eventMatches = typeof eventType !== "string" || Array.isArray(config.event_types) && config.event_types.includes(eventType);
|
|
112
|
+
return subscriberMatches && eventMatches;
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
function failedNotificationSetup(step, allSteps, priorStepResults) {
|
|
116
|
+
const currentIndex = allSteps.find((entry) => entry.step.id === step.id)?.globalIndex ?? Number.POSITIVE_INFINITY;
|
|
117
|
+
const setup = [...allSteps].reverse().find(
|
|
118
|
+
(entry) => entry.globalIndex < currentIndex && (entry.step.task === "sync_accounts" || entry.step.task === "sync_agent_notification_configs") && notificationSetupMatchesAssertion(entry.step, step)
|
|
119
|
+
);
|
|
120
|
+
if (!setup) return void 0;
|
|
121
|
+
const result = priorStepResults.get(setup.step.id);
|
|
122
|
+
return result && (result.skipped || !result.passed) ? result : void 0;
|
|
123
|
+
}
|
|
81
124
|
function armWebhookAssertions(storyboard, runnerVars, receiver) {
|
|
82
125
|
for (const phase of storyboard.phases) {
|
|
83
126
|
for (const step of phase.steps) {
|
|
@@ -123,6 +166,16 @@ async function executeWebhookAssertionStep(step, phaseId, context, allSteps, opt
|
|
|
123
166
|
next
|
|
124
167
|
});
|
|
125
168
|
}
|
|
169
|
+
const failedSetup = failedNotificationSetup(step, allSteps, runState.priorStepResults);
|
|
170
|
+
if (failedSetup) {
|
|
171
|
+
return skippedResult(step, phaseId, context, start, {
|
|
172
|
+
skip_reason: "prerequisite_failed",
|
|
173
|
+
detail: `Notification setup step "${failedSetup.step_id}" did not complete successfully.`,
|
|
174
|
+
extraction,
|
|
175
|
+
request: requestRecord,
|
|
176
|
+
next
|
|
177
|
+
});
|
|
178
|
+
}
|
|
126
179
|
if (step.triggered_by) {
|
|
127
180
|
const prior = runState.priorStepResults.get(step.triggered_by);
|
|
128
181
|
if (!prior || prior.skipped || !prior.passed) {
|
|
@@ -265,7 +318,7 @@ async function runExpectWebhook(step, filter, receiver) {
|
|
|
265
318
|
return singleFailure(
|
|
266
319
|
step,
|
|
267
320
|
"no_webhook_received",
|
|
268
|
-
`No webhook matching filter arrived within ${timeoutMs}ms
|
|
321
|
+
webhookTimeoutDetail(receiver, `No webhook matching filter arrived within ${timeoutMs}ms.`),
|
|
269
322
|
"webhook delivery matching filter",
|
|
270
323
|
null
|
|
271
324
|
);
|
|
@@ -319,7 +372,10 @@ async function runExpectRetryKeysStable(step, filter, receiver) {
|
|
|
319
372
|
return singleFailure(
|
|
320
373
|
step,
|
|
321
374
|
"insufficient_retries",
|
|
322
|
-
|
|
375
|
+
webhookTimeoutDetail(
|
|
376
|
+
receiver,
|
|
377
|
+
`Observed ${matches.length} deliveries; expected at least ${minDeliveries}. The sender may not be retrying on 5xx responses.`
|
|
378
|
+
),
|
|
323
379
|
`\u2265 ${minDeliveries} deliveries`,
|
|
324
380
|
matches.length
|
|
325
381
|
);
|
|
@@ -385,7 +441,7 @@ async function runExpectSignatureValid(step, filter, receiver, options, runnerVa
|
|
|
385
441
|
return singleFailure(
|
|
386
442
|
step,
|
|
387
443
|
"no_webhook_received",
|
|
388
|
-
`No webhook matching filter arrived within ${timeoutMs}ms
|
|
444
|
+
webhookTimeoutDetail(receiver, `No webhook matching filter arrived within ${timeoutMs}ms.`),
|
|
389
445
|
"signed webhook delivery matching filter",
|
|
390
446
|
null
|
|
391
447
|
);
|
|
@@ -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"]}
|
|
@@ -34,10 +34,13 @@ const WEBHOOK_ASSERTION_TASKS = /* @__PURE__ */ new Set([
|
|
|
34
34
|
]);
|
|
35
35
|
const DEFAULT_TIMEOUT_SECONDS = 30;
|
|
36
36
|
const DEFAULT_NO_WEBHOOK_TIMEOUT_SECONDS = 5;
|
|
37
|
-
const DEFAULT_RETRY_REPLAY_TIMEOUT_SECONDS =
|
|
37
|
+
const DEFAULT_RETRY_REPLAY_TIMEOUT_SECONDS = 60;
|
|
38
38
|
const DEFAULT_RETRY_COUNT = 3;
|
|
39
39
|
const DEFAULT_RETRY_HTTP_STATUS = 503;
|
|
40
40
|
const DEFAULT_MIN_DELIVERIES = 2;
|
|
41
|
+
function webhookTimeoutDetail(receiver, base) {
|
|
42
|
+
return receiver.mode === "loopback_mock" && receiver.all().length === 0 && receiver.challenges().length === 0 ? `${base} No request reached the loopback receiver; an out-of-process agent must use webhook_receiver.mode="proxy_url".` : base;
|
|
43
|
+
}
|
|
41
44
|
const DEFAULT_WEBHOOK_SIGNING_TAG = "adcp/webhook-signing/v1";
|
|
42
45
|
const MAX_RETRY_REPLAY_COUNT = 10;
|
|
43
46
|
const MAX_TIMEOUT_SECONDS = 300;
|
|
@@ -53,6 +56,46 @@ function clampRetryPolicy(spec) {
|
|
|
53
56
|
const http_status = ALLOWED_RETRY_HTTP_STATUSES.has(requestedStatus) ? requestedStatus : DEFAULT_RETRY_HTTP_STATUS;
|
|
54
57
|
return { count, http_status };
|
|
55
58
|
}
|
|
59
|
+
function collectNotificationConfigs(value, configs = []) {
|
|
60
|
+
if (!value || typeof value !== "object") return configs;
|
|
61
|
+
if (Array.isArray(value)) {
|
|
62
|
+
for (const item of value) collectNotificationConfigs(item, configs);
|
|
63
|
+
return configs;
|
|
64
|
+
}
|
|
65
|
+
const record = value;
|
|
66
|
+
if (Array.isArray(record.notification_configs)) {
|
|
67
|
+
for (const config of record.notification_configs) {
|
|
68
|
+
if (config && typeof config === "object" && !Array.isArray(config)) {
|
|
69
|
+
configs.push(config);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
for (const nested of Object.values(record)) collectNotificationConfigs(nested, configs);
|
|
74
|
+
return configs;
|
|
75
|
+
}
|
|
76
|
+
function notificationSetupMatchesAssertion(setup, assertion) {
|
|
77
|
+
const configs = collectNotificationConfigs(setup.sample_request);
|
|
78
|
+
if (configs.length === 0) return false;
|
|
79
|
+
const filter = assertion.filter && typeof assertion.filter === "object" ? assertion.filter : void 0;
|
|
80
|
+
const body = filter?.body && typeof filter.body === "object" ? filter.body : void 0;
|
|
81
|
+
const subscriberId = filter?.subscriber_id ?? body?.subscriber_id;
|
|
82
|
+
const eventType = filter?.notification_type ?? body?.notification_type ?? filter?.event ?? body?.event;
|
|
83
|
+
if (typeof subscriberId !== "string" && typeof eventType !== "string") return false;
|
|
84
|
+
return configs.some((config) => {
|
|
85
|
+
const subscriberMatches = typeof subscriberId !== "string" || config.subscriber_id === subscriberId;
|
|
86
|
+
const eventMatches = typeof eventType !== "string" || Array.isArray(config.event_types) && config.event_types.includes(eventType);
|
|
87
|
+
return subscriberMatches && eventMatches;
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
function failedNotificationSetup(step, allSteps, priorStepResults) {
|
|
91
|
+
const currentIndex = allSteps.find((entry) => entry.step.id === step.id)?.globalIndex ?? Number.POSITIVE_INFINITY;
|
|
92
|
+
const setup = [...allSteps].reverse().find(
|
|
93
|
+
(entry) => entry.globalIndex < currentIndex && (entry.step.task === "sync_accounts" || entry.step.task === "sync_agent_notification_configs") && notificationSetupMatchesAssertion(entry.step, step)
|
|
94
|
+
);
|
|
95
|
+
if (!setup) return void 0;
|
|
96
|
+
const result = priorStepResults.get(setup.step.id);
|
|
97
|
+
return result && (result.skipped || !result.passed) ? result : void 0;
|
|
98
|
+
}
|
|
56
99
|
function armWebhookAssertions(storyboard, runnerVars, receiver) {
|
|
57
100
|
for (const phase of storyboard.phases) {
|
|
58
101
|
for (const step of phase.steps) {
|
|
@@ -98,6 +141,16 @@ async function executeWebhookAssertionStep(step, phaseId, context, allSteps, opt
|
|
|
98
141
|
next
|
|
99
142
|
});
|
|
100
143
|
}
|
|
144
|
+
const failedSetup = failedNotificationSetup(step, allSteps, runState.priorStepResults);
|
|
145
|
+
if (failedSetup) {
|
|
146
|
+
return skippedResult(step, phaseId, context, start, {
|
|
147
|
+
skip_reason: "prerequisite_failed",
|
|
148
|
+
detail: `Notification setup step "${failedSetup.step_id}" did not complete successfully.`,
|
|
149
|
+
extraction,
|
|
150
|
+
request: requestRecord,
|
|
151
|
+
next
|
|
152
|
+
});
|
|
153
|
+
}
|
|
101
154
|
if (step.triggered_by) {
|
|
102
155
|
const prior = runState.priorStepResults.get(step.triggered_by);
|
|
103
156
|
if (!prior || prior.skipped || !prior.passed) {
|
|
@@ -240,7 +293,7 @@ async function runExpectWebhook(step, filter, receiver) {
|
|
|
240
293
|
return singleFailure(
|
|
241
294
|
step,
|
|
242
295
|
"no_webhook_received",
|
|
243
|
-
`No webhook matching filter arrived within ${timeoutMs}ms
|
|
296
|
+
webhookTimeoutDetail(receiver, `No webhook matching filter arrived within ${timeoutMs}ms.`),
|
|
244
297
|
"webhook delivery matching filter",
|
|
245
298
|
null
|
|
246
299
|
);
|
|
@@ -294,7 +347,10 @@ async function runExpectRetryKeysStable(step, filter, receiver) {
|
|
|
294
347
|
return singleFailure(
|
|
295
348
|
step,
|
|
296
349
|
"insufficient_retries",
|
|
297
|
-
|
|
350
|
+
webhookTimeoutDetail(
|
|
351
|
+
receiver,
|
|
352
|
+
`Observed ${matches.length} deliveries; expected at least ${minDeliveries}. The sender may not be retrying on 5xx responses.`
|
|
353
|
+
),
|
|
298
354
|
`\u2265 ${minDeliveries} deliveries`,
|
|
299
355
|
matches.length
|
|
300
356
|
);
|
|
@@ -360,7 +416,7 @@ async function runExpectSignatureValid(step, filter, receiver, options, runnerVa
|
|
|
360
416
|
return singleFailure(
|
|
361
417
|
step,
|
|
362
418
|
"no_webhook_received",
|
|
363
|
-
`No webhook matching filter arrived within ${timeoutMs}ms
|
|
419
|
+
webhookTimeoutDetail(receiver, `No webhook matching filter arrived within ${timeoutMs}ms.`),
|
|
364
420
|
"signed webhook delivery matching filter",
|
|
365
421
|
null
|
|
366
422
|
);
|