@adcp/sdk 14.0.0-beta.10 → 14.0.0-beta.12
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/core/AgentClient.d.ts.map +1 -1
- package/dist/lib/core/SingleAgentClient.d.mts +2 -1
- package/dist/lib/core/SingleAgentClient.d.ts +2 -1
- package/dist/lib/core/SingleAgentClient.d.ts.map +1 -1
- package/dist/lib/core/SingleAgentClient.js +113 -64
- package/dist/lib/core/SingleAgentClient.js.map +1 -1
- package/dist/lib/core/SingleAgentClient.mjs +113 -64
- package/dist/lib/core/SingleAgentClient.mjs.map +1 -1
- package/dist/lib/index.d.mts +1 -1
- package/dist/lib/index.d.ts +1 -1
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/index.mjs.map +1 -1
- package/dist/lib/protocols/a2a.d.mts +10 -3
- package/dist/lib/protocols/a2a.d.ts +10 -3
- package/dist/lib/protocols/a2a.d.ts.map +1 -1
- package/dist/lib/protocols/a2a.js +162 -34
- package/dist/lib/protocols/a2a.js.map +1 -1
- package/dist/lib/protocols/a2a.mjs +166 -34
- package/dist/lib/protocols/a2a.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/a2a-adapter.d.mts +59 -33
- package/dist/lib/server/a2a-adapter.d.ts +59 -33
- package/dist/lib/server/a2a-adapter.d.ts.map +1 -1
- package/dist/lib/server/a2a-adapter.js +278 -67
- package/dist/lib/server/a2a-adapter.js.map +1 -1
- package/dist/lib/server/a2a-adapter.mjs +280 -68
- package/dist/lib/server/a2a-adapter.mjs.map +1 -1
- package/dist/lib/server/create-adcp-server.d.ts.map +1 -1
- package/dist/lib/server/create-adcp-server.js +35 -108
- package/dist/lib/server/create-adcp-server.js.map +1 -1
- package/dist/lib/server/create-adcp-server.mjs +37 -109
- package/dist/lib/server/create-adcp-server.mjs.map +1 -1
- package/dist/lib/server/decisioning/index.d.mts +2 -0
- package/dist/lib/server/decisioning/index.d.ts +2 -0
- package/dist/lib/server/decisioning/index.d.ts.map +1 -1
- package/dist/lib/server/decisioning/index.js +5 -2
- package/dist/lib/server/decisioning/index.js.map +1 -1
- package/dist/lib/server/decisioning/index.mjs +3 -1
- package/dist/lib/server/decisioning/index.mjs.map +1 -1
- package/dist/lib/server/decisioning/response-summary.d.mts +33 -0
- package/dist/lib/server/decisioning/response-summary.d.ts +34 -0
- package/dist/lib/server/decisioning/response-summary.d.ts.map +1 -0
- package/dist/lib/server/decisioning/response-summary.js +62 -0
- package/dist/lib/server/decisioning/response-summary.js.map +1 -0
- package/dist/lib/server/decisioning/response-summary.mjs +37 -0
- package/dist/lib/server/decisioning/response-summary.mjs.map +1 -0
- package/dist/lib/server/decisioning/runtime/from-platform.d.ts.map +1 -1
- package/dist/lib/server/decisioning/runtime/from-platform.js +27 -5
- package/dist/lib/server/decisioning/runtime/from-platform.js.map +1 -1
- package/dist/lib/server/decisioning/runtime/from-platform.mjs +27 -5
- package/dist/lib/server/decisioning/runtime/from-platform.mjs.map +1 -1
- package/dist/lib/server/decisioning/specialisms/sales.d.mts +7 -1
- package/dist/lib/server/decisioning/specialisms/sales.d.ts +7 -1
- package/dist/lib/server/decisioning/specialisms/sales.d.ts.map +1 -1
- package/dist/lib/server/decisioning/specialisms/sales.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/server/responses.d.ts.map +1 -1
- package/dist/lib/server/responses.js +9 -1
- package/dist/lib/server/responses.js.map +1 -1
- package/dist/lib/server/responses.mjs +8 -1
- package/dist/lib/server/responses.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/context.d.ts.map +1 -1
- package/dist/lib/testing/storyboard/context.js +9 -4
- package/dist/lib/testing/storyboard/context.js.map +1 -1
- package/dist/lib/testing/storyboard/context.mjs +9 -4
- package/dist/lib/testing/storyboard/context.mjs.map +1 -1
- package/dist/lib/testing/storyboard/request-builder.d.ts.map +1 -1
- package/dist/lib/testing/storyboard/request-builder.js +28 -4
- package/dist/lib/testing/storyboard/request-builder.js.map +1 -1
- package/dist/lib/testing/storyboard/request-builder.mjs +28 -4
- 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 +31 -7
- package/dist/lib/testing/storyboard/runner.js.map +1 -1
- package/dist/lib/testing/storyboard/runner.mjs +31 -7
- package/dist/lib/testing/storyboard/runner.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/validations.d.ts.map +1 -1
- package/dist/lib/testing/storyboard/validations.js +18 -7
- package/dist/lib/testing/storyboard/validations.js.map +1 -1
- package/dist/lib/testing/storyboard/validations.mjs +18 -7
- package/dist/lib/testing/storyboard/validations.mjs.map +1 -1
- package/dist/lib/testing/storyboard/webhook-receiver.d.ts.map +1 -1
- package/dist/lib/testing/storyboard/webhook-receiver.js +32 -20
- package/dist/lib/testing/storyboard/webhook-receiver.js.map +1 -1
- package/dist/lib/testing/storyboard/webhook-receiver.mjs +32 -20
- package/dist/lib/testing/storyboard/webhook-receiver.mjs.map +1 -1
- package/dist/lib/testing/types.d.mts +10 -0
- package/dist/lib/testing/types.d.ts +10 -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 +1 -1
- package/dist/lib/types/core.generated.d.ts +1 -1
- 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/schemas.generated.d.ts +519 -571
- package/dist/lib/types/schemas.generated.d.ts.map +1 -1
- package/dist/lib/types/schemas.generated.js +2 -2
- package/dist/lib/types/schemas.generated.js.map +1 -1
- package/dist/lib/types/schemas.generated.mjs +2 -2
- package/dist/lib/types/schemas.generated.mjs.map +1 -1
- package/dist/lib/types/tools.generated.d.mts +1 -1
- package/dist/lib/types/tools.generated.d.ts +1 -1
- 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/a2a-artifacts.d.ts.map +1 -1
- package/dist/lib/utils/a2a-artifacts.js +2 -2
- package/dist/lib/utils/a2a-artifacts.js.map +1 -1
- package/dist/lib/utils/a2a-artifacts.mjs +2 -2
- package/dist/lib/utils/a2a-artifacts.mjs.map +1 -1
- package/dist/lib/utils/a2a-discovery.d.mts +3 -3
- package/dist/lib/utils/a2a-discovery.d.ts +3 -3
- package/dist/lib/utils/a2a-discovery.js +1 -1
- package/dist/lib/utils/a2a-discovery.js.map +1 -1
- package/dist/lib/utils/a2a-discovery.mjs +1 -1
- package/dist/lib/utils/a2a-discovery.mjs.map +1 -1
- package/dist/lib/utils/tool-request-schemas.d.mts +31 -31
- package/dist/lib/utils/tool-request-schemas.d.ts +31 -31
- package/dist/lib/v2/projection/index.d.mts +2 -0
- package/dist/lib/v2/projection/index.d.ts +2 -0
- package/dist/lib/v2/projection/index.d.ts.map +1 -1
- package/dist/lib/v2/projection/index.js +3 -0
- package/dist/lib/v2/projection/index.js.map +1 -1
- package/dist/lib/v2/projection/index.mjs +2 -0
- package/dist/lib/v2/projection/index.mjs.map +1 -1
- package/dist/lib/v2/projection/legacy-normalization.d.mts +24 -0
- package/dist/lib/v2/projection/legacy-normalization.d.ts +25 -0
- package/dist/lib/v2/projection/legacy-normalization.d.ts.map +1 -0
- package/dist/lib/v2/projection/legacy-normalization.js +32 -0
- package/dist/lib/v2/projection/legacy-normalization.js.map +1 -0
- package/dist/lib/v2/projection/legacy-normalization.mjs +8 -0
- package/dist/lib/v2/projection/legacy-normalization.mjs.map +1 -0
- 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/guides/BUILD-AN-AGENT.md +42 -6
- package/docs/llms.txt +2 -2
- package/docs/migration-13-to-14.md +17 -0
- package/package.json +44 -41
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"webhook-receiver.d.ts","sourceRoot":"","sources":["../../../../src/lib/testing/storyboard/webhook-receiver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;
|
|
1
|
+
{"version":3,"file":"webhook-receiver.d.ts","sourceRoot":"","sources":["../../../../src/lib/testing/storyboard/webhook-receiver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAuEH,MAAM,WAAW,eAAe;IAC9B,wDAAwD;IACxD,EAAE,EAAE,MAAM,CAAC;IACX,2CAA2C;IAC3C,OAAO,EAAE,MAAM,CAAC;IAChB,gDAAgD;IAChD,YAAY,EAAE,MAAM,CAAC;IACrB,2EAA2E;IAC3E,cAAc,EAAE,MAAM,CAAC;IACvB,oCAAoC;IACpC,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,oBAAoB;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,kEAAkE;IAClE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,uEAAuE;IACvE,QAAQ,EAAE,MAAM,CAAC;IACjB,oDAAoD;IACpD,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,0EAA0E;IAC1E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,wEAAwE;IACxE,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,0DAA0D;AAC1D,MAAM,WAAW,aAAa;IAC5B,8CAA8C;IAC9C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,6CAA6C;IAC7C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,2EAA2E;IAC3E,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC;AAED,MAAM,MAAM,iBAAiB,GACzB;IAAE,OAAO,EAAE,eAAe,CAAC;IAAC,SAAS,CAAC,EAAE,KAAK,CAAA;CAAE,GAC/C;IAAE,SAAS,EAAE,IAAI,CAAC;IAAC,OAAO,CAAC,EAAE,SAAS,CAAA;CAAE,CAAC;AAE7C,MAAM,WAAW,iBAAiB;IAChC,sDAAsD;IACtD,KAAK,EAAE,MAAM,CAAC;IACd,0CAA0C;IAC1C,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,eAAe;IAC9B,iFAAiF;IACjF,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,kEAAkE;IAClE,QAAQ,CAAC,IAAI,EAAE,eAAe,GAAG,WAAW,CAAC;IAC7C,sDAAsD;IACtD,GAAG,IAAI,eAAe,EAAE,CAAC;IACzB,oDAAoD;IACpD,QAAQ,CAAC,MAAM,EAAE,aAAa,GAAG,eAAe,EAAE,CAAC;IACnD;;;;OAIG;IACH,gBAAgB,CAAC,GAAG,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAA;KAAE,EAAE,MAAM,EAAE,iBAAiB,GAAG,IAAI,CAAC;IAClG,8EAA8E;IAC9E,IAAI,CAAC,MAAM,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;IAC5E;;;;OAIG;IACH,QAAQ,CAAC,MAAM,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC,CAAC;IAChF,qCAAqC;IACrC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,MAAM,WAAW,4BAA4B;IAC3C,IAAI,CAAC,EAAE,eAAe,GAAG,WAAW,CAAC;IACrC,iEAAiE;IACjE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,2DAA2D;IAC3D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,wDAAwD;IACxD,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAWD,wBAAsB,qBAAqB,CAAC,OAAO,GAAE,4BAAiC,GAAG,OAAO,CAAC,eAAe,CAAC,CA+EhH"}
|
|
@@ -69,8 +69,10 @@ async function createWebhookReceiver(options = {}) {
|
|
|
69
69
|
const proxyBase = mode === "proxy_url" ? validateProxyUrl(options.public_url) : void 0;
|
|
70
70
|
const captured = [];
|
|
71
71
|
const waiters = [];
|
|
72
|
+
const waitAllTimers = [];
|
|
72
73
|
const retryPolicies = /* @__PURE__ */ new Map();
|
|
73
74
|
const deliveryCounts = /* @__PURE__ */ new Map();
|
|
75
|
+
let closed = false;
|
|
74
76
|
const server = (0, import_node_http.createServer)(
|
|
75
77
|
(req, res) => handleRequest(req, res, { captured, waiters, retryPolicies, deliveryCounts })
|
|
76
78
|
);
|
|
@@ -97,9 +99,16 @@ async function createWebhookReceiver(options = {}) {
|
|
|
97
99
|
set_retry_replay: (key, policy) => {
|
|
98
100
|
retryPolicies.set(retryKeyString(key), { policy, delivered: 0 });
|
|
99
101
|
},
|
|
100
|
-
wait: (filter, timeout_ms) =>
|
|
101
|
-
|
|
102
|
-
|
|
102
|
+
wait: (filter, timeout_ms) => {
|
|
103
|
+
if (!closed) return wait(filter, timeout_ms, captured, waiters);
|
|
104
|
+
const already = captured.find((w) => matchesFilter(w, filter));
|
|
105
|
+
return Promise.resolve(already ? { webhook: already } : { timed_out: true });
|
|
106
|
+
},
|
|
107
|
+
wait_all: (filter, timeout_ms) => closed ? Promise.resolve(captured.filter((w) => matchesFilter(w, filter))) : waitAll(filter, timeout_ms, captured, waitAllTimers),
|
|
108
|
+
close: () => {
|
|
109
|
+
closed = true;
|
|
110
|
+
return closeServer(server, captured, waiters, waitAllTimers);
|
|
111
|
+
}
|
|
103
112
|
};
|
|
104
113
|
}
|
|
105
114
|
function handleRequest(req, res, state) {
|
|
@@ -139,15 +148,15 @@ function handleRequest(req, res, state) {
|
|
|
139
148
|
const headers = normalizeHeaders(req.headers);
|
|
140
149
|
const key = retryKeyString({ step_id, operation_id });
|
|
141
150
|
const deliveryIndex = (state.deliveryCounts.get(key) ?? 0) + 1;
|
|
142
|
-
const
|
|
151
|
+
const status = nextResponseStatus(state.retryPolicies, key);
|
|
152
|
+
const perKey = state.deliveryCounts.get(key) ?? 0;
|
|
143
153
|
if (state.captured.length >= MAX_CAPTURED_TOTAL || perKey >= MAX_CAPTURED_PER_KEY) {
|
|
144
|
-
res.statusCode =
|
|
145
|
-
res.setHeader("retry-after", "1");
|
|
154
|
+
res.statusCode = status;
|
|
155
|
+
if (status === 429 || status >= 500) res.setHeader("retry-after", "1");
|
|
146
156
|
res.end();
|
|
147
157
|
return;
|
|
148
158
|
}
|
|
149
159
|
state.deliveryCounts.set(key, deliveryIndex);
|
|
150
|
-
const status = nextResponseStatus(state.retryPolicies, key);
|
|
151
160
|
const webhook = {
|
|
152
161
|
id: (0, import_node_crypto.randomUUID)(),
|
|
153
162
|
step_id,
|
|
@@ -202,11 +211,6 @@ function redactHeaders(headers) {
|
|
|
202
211
|
}
|
|
203
212
|
return out;
|
|
204
213
|
}
|
|
205
|
-
function countPerKey(captured, step_id, operation_id) {
|
|
206
|
-
let n = 0;
|
|
207
|
-
for (const w of captured) if (w.step_id === step_id && w.operation_id === operation_id) n++;
|
|
208
|
-
return n;
|
|
209
|
-
}
|
|
210
214
|
function parseBody(raw, contentType) {
|
|
211
215
|
if (raw.length === 0) return {};
|
|
212
216
|
const looksJson = !contentType || /json/i.test(contentType);
|
|
@@ -287,29 +291,37 @@ function wait(filter, timeout_ms, captured, waiters) {
|
|
|
287
291
|
waiters.push({ filter, resolve, timer });
|
|
288
292
|
});
|
|
289
293
|
}
|
|
290
|
-
function waitAll(filter, timeout_ms, captured) {
|
|
294
|
+
function waitAll(filter, timeout_ms, captured, waitAllTimers) {
|
|
291
295
|
return new Promise((resolve) => {
|
|
292
296
|
const timer = setTimeout(
|
|
293
297
|
() => {
|
|
298
|
+
const idx = waitAllTimers.findIndex((entry) => entry.timer === timer);
|
|
299
|
+
if (idx >= 0) waitAllTimers.splice(idx, 1);
|
|
294
300
|
resolve(captured.filter((w) => matchesFilter(w, filter)));
|
|
295
301
|
},
|
|
296
302
|
Math.max(0, timeout_ms)
|
|
297
303
|
);
|
|
298
304
|
timer.unref?.();
|
|
305
|
+
waitAllTimers.push({ filter, resolve, timer });
|
|
299
306
|
});
|
|
300
307
|
}
|
|
301
|
-
function closeServer(server, waiters) {
|
|
302
|
-
while (waiters.length > 0) {
|
|
303
|
-
const w = waiters.pop();
|
|
304
|
-
clearTimeout(w.timer);
|
|
305
|
-
w.resolve({ timed_out: true });
|
|
306
|
-
}
|
|
307
|
-
server.closeAllConnections?.();
|
|
308
|
+
function closeServer(server, captured, waiters, waitAllTimers) {
|
|
308
309
|
return new Promise((resolve, reject) => {
|
|
309
310
|
server.close((err) => {
|
|
310
311
|
if (err && err.code !== "ERR_SERVER_NOT_RUNNING") reject(err);
|
|
311
312
|
else resolve();
|
|
312
313
|
});
|
|
314
|
+
server.closeAllConnections?.();
|
|
315
|
+
while (waiters.length > 0) {
|
|
316
|
+
const w = waiters.pop();
|
|
317
|
+
clearTimeout(w.timer);
|
|
318
|
+
w.resolve({ timed_out: true });
|
|
319
|
+
}
|
|
320
|
+
while (waitAllTimers.length > 0) {
|
|
321
|
+
const pending = waitAllTimers.pop();
|
|
322
|
+
clearTimeout(pending.timer);
|
|
323
|
+
pending.resolve(captured.filter((w) => matchesFilter(w, pending.filter)));
|
|
324
|
+
}
|
|
313
325
|
});
|
|
314
326
|
}
|
|
315
327
|
function formatHost(address) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/lib/testing/storyboard/webhook-receiver.ts"],"sourcesContent":["/**\n * Ephemeral webhook receiver for storyboard runs.\n *\n * The runner binds an HTTP listener, mints per-step URLs under\n * `/step/<step_id>/<operation_id>`, and captures inbound POSTs so\n * `expect_webhook*` pseudo-steps can observe outbound webhook behavior\n * (idempotency_key presence, retry-key stability, 9421 signature validity).\n *\n * Matches the `webhook_receiver_runner` test-kit contract in\n * adcontextprotocol/adcp#2431:\n * - Per-step URL template `{{runner.webhook_base}}/step/<step_id>/<operation_id>`.\n * - Retry-replay: the receiver returns a configurable 5xx for the first N\n * deliveries on a given `(step_id, operation_id)`, then 2xx, so senders\n * are forced to retry and retry-key stability can be graded.\n * - Loopback-mock mode (HTTP on 127.0.0.1, zero external deps) by default;\n * proxy-URL mode swaps in an operator-supplied public base.\n */\n\nimport { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http';\nimport { randomUUID } from 'node:crypto';\nimport type { AddressInfo } from 'node:net';\n\n/**\n * Size cap on any single webhook body. A non-conformant sender retrying\n * with oversize payloads shouldn't be able to exhaust the runner's memory.\n */\nconst MAX_BODY_BYTES = 1_048_576; // 1 MiB\n\n/**\n * Caps on cumulative capture state. `MAX_CAPTURED_TOTAL` bounds the total\n * number of webhooks retained for the run; `MAX_CAPTURED_PER_KEY` bounds\n * per-(step_id, operation_id) deliveries — a publisher stuck in a retry\n * loop can't exhaust memory. Once a cap is hit the receiver returns 503\n * (signalling back-pressure to the sender) and drops the capture.\n */\nconst MAX_CAPTURED_TOTAL = 1000;\nconst MAX_CAPTURED_PER_KEY = 100;\n\n/**\n * HTTP transport hardening. Defaults in Node 20 are generous (headers\n * 60s, no body timeout). A hostile / slow publisher shouldn't be able to\n * hold the runner's event loop with trickled requests or exhaust the\n * socket table.\n */\nconst HEADERS_TIMEOUT_MS = 10_000;\nconst REQUEST_TIMEOUT_MS = 30_000;\nconst KEEP_ALIVE_TIMEOUT_MS = 5_000;\nconst MAX_CONNECTIONS = 64;\nconst MAX_HEADERS_COUNT = 64;\n\n/** Default path shape under the receiver base. */\nconst STEP_PATH_RE = /^\\/step\\/([A-Za-z0-9_]+)\\/([A-Za-z0-9_-]+)\\/?$/;\n\n/**\n * Header names whose values are redacted before a captured webhook lands\n * in a compliance report. Mirrors the `SECRET_KEY_PATTERN` in runner.ts\n * so both paths honor the contract's \"Secrets SHOULD be redacted\" rule.\n */\nconst SECRET_HEADER_PATTERN =\n /^(authorization|credentials?|token|api[_-]?key|x-api[_-]?key|x-auth[_-]?token|password|secret|client[_-]secret|refresh[_-]token|access[_-]token|bearer|session[_-]token|cookie|set[_-]cookie)$/i;\n\n/**\n * Strip user-info, scheme-policed, and CRLF/NUL-rejected proxy URLs so a\n * misconfigured operator (or a compromised storyboard loader) can't\n * advertise an `http://169.254.169.254`, `file://…`, or header-splitting\n * URL that would then be embedded in outbound `push_notification_config.url`.\n */\nfunction validateProxyUrl(raw: string): string {\n if (/[\\r\\n\\x00]/.test(raw)) {\n throw new Error('webhook_receiver.public_url must not contain CR/LF/NUL');\n }\n let parsed: URL;\n try {\n parsed = new URL(raw);\n } catch {\n throw new Error(`webhook_receiver.public_url is not a valid URL: ${raw}`);\n }\n if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {\n throw new Error(`webhook_receiver.public_url must be http(s); got ${parsed.protocol}`);\n }\n if (parsed.username || parsed.password) {\n throw new Error('webhook_receiver.public_url must not include userinfo');\n }\n return raw.replace(/\\/$/, '');\n}\n\nexport interface CapturedWebhook {\n /** Runner-assigned id; stable across filter matches. */\n id: string;\n /** Step id extracted from the URL path. */\n step_id: string;\n /** Operation id extracted from the URL path. */\n operation_id: string;\n /** Delivery sequence number for this (step_id, operation_id) — 1-based. */\n delivery_index: number;\n /** Wall-clock millis at receipt. */\n received_at: number;\n method: string;\n /** Path + query. */\n path: string;\n /** Lowercased header names; duplicate values joined with \", \". */\n headers: Record<string, string>;\n /** Raw request body — needed for RFC 9421 content-digest recompute. */\n raw_body: string;\n /** Parsed JSON body; undefined on parse failure. */\n body?: unknown;\n /** Parse error message when raw_body was non-empty but not valid JSON. */\n parse_error?: string;\n /** HTTP status the receiver returned (reflects retry-replay policy). */\n response_status: number;\n}\n\n/** Match predicate applied when waiting for a webhook. */\nexport interface WebhookFilter {\n /** Restrict matches to this step id's URL. */\n step_id?: string;\n /** Restrict matches to this operation id. */\n operation_id?: string;\n /** Dotted-path → expected-value pairs compared against the parsed body. */\n body?: Record<string, unknown>;\n}\n\nexport type WebhookWaitResult =\n | { webhook: CapturedWebhook; timed_out?: false }\n | { timed_out: true; webhook?: undefined };\n\nexport interface RetryReplayPolicy {\n /** How many deliveries to reject before accepting. */\n count: number;\n /** HTTP status to return on rejection. */\n http_status: number;\n}\n\nexport interface WebhookReceiver {\n /** Public base URL (`http://host:port` for loopback, operator URL for proxy). */\n readonly base_url: string;\n /** Whether the receiver is in loopback-mock or proxy-url mode. */\n readonly mode: 'loopback_mock' | 'proxy_url';\n /** All webhooks captured so far, in arrival order. */\n all(): CapturedWebhook[];\n /** Webhooks matching a filter, in arrival order. */\n matching(filter: WebhookFilter): CapturedWebhook[];\n /**\n * Configure the receiver to return `http_status` for the first `count`\n * deliveries at `(step_id, operation_id)` and 2xx afterwards. Idempotent:\n * setting a new policy replaces any existing one for the same key.\n */\n set_retry_replay(key: { step_id: string; operation_id: string }, policy: RetryReplayPolicy): void;\n /** Resolve with the first match within `timeout_ms`, or `timed_out: true`. */\n wait(filter: WebhookFilter, timeout_ms: number): Promise<WebhookWaitResult>;\n /**\n * Resolve with every match observed between call time and deadline. Used\n * by `expect_webhook_retry_keys_stable` which needs the full sequence of\n * deliveries for the retry-replay assertion.\n */\n wait_all(filter: WebhookFilter, timeout_ms: number): Promise<CapturedWebhook[]>;\n /** Stop the listener. Idempotent. */\n close(): Promise<void>;\n}\n\nexport interface CreateWebhookReceiverOptions {\n mode?: 'loopback_mock' | 'proxy_url';\n /** Bind host for the local listener. Defaults to `127.0.0.1`. */\n host?: string;\n /** Bind port. `0` (default) lets the kernel assign one. */\n port?: number;\n /** Public URL to advertise when `mode: 'proxy_url'`. */\n public_url?: string;\n}\n\ninterface RetryKey {\n step_id: string;\n operation_id: string;\n}\n\nfunction retryKeyString(k: RetryKey): string {\n return `${k.step_id}::${k.operation_id}`;\n}\n\nexport async function createWebhookReceiver(options: CreateWebhookReceiverOptions = {}): Promise<WebhookReceiver> {\n const mode = options.mode ?? 'loopback_mock';\n if (mode === 'proxy_url' && !options.public_url) {\n throw new Error('webhook_receiver.mode=proxy_url requires `public_url`');\n }\n\n const host = options.host ?? '127.0.0.1';\n // A loopback_mock receiver bound on 0.0.0.0 is publicly reachable from any\n // interface — which would turn a CI runner into an open POST endpoint.\n // Operators who genuinely want public exposure use mode=proxy_url.\n if (mode === 'loopback_mock' && (host === '0.0.0.0' || host === '::')) {\n throw new Error(\n `webhook_receiver host ${host} is not permitted in loopback_mock mode. ` +\n 'Use mode=proxy_url with an explicit public_url for publicly-reachable runs.'\n );\n }\n const port = options.port ?? 0;\n const proxyBase = mode === 'proxy_url' ? validateProxyUrl(options.public_url!) : undefined;\n\n const captured: CapturedWebhook[] = [];\n const waiters: Array<{\n filter: WebhookFilter;\n resolve: (result: WebhookWaitResult) => void;\n timer: NodeJS.Timeout;\n }> = [];\n const retryPolicies = new Map<string, { policy: RetryReplayPolicy; delivered: number }>();\n const deliveryCounts = new Map<string, number>();\n\n const server = createServer((req, res) =>\n handleRequest(req, res, { captured, waiters, retryPolicies, deliveryCounts })\n );\n // Transport-level hardening — trim Node's generous defaults so a slow /\n // hostile publisher can't wedge the runner.\n server.headersTimeout = HEADERS_TIMEOUT_MS;\n server.requestTimeout = REQUEST_TIMEOUT_MS;\n server.keepAliveTimeout = KEEP_ALIVE_TIMEOUT_MS;\n server.maxConnections = MAX_CONNECTIONS;\n server.maxHeadersCount = MAX_HEADERS_COUNT;\n\n await new Promise<void>((resolve, reject) => {\n const onError = (err: Error) => reject(err);\n server.once('error', onError);\n server.listen(port, host, () => {\n server.off('error', onError);\n resolve();\n });\n });\n\n const bound = server.address() as AddressInfo;\n const base_url = proxyBase ?? `http://${formatHost(bound.address)}:${bound.port}`;\n\n return {\n base_url,\n mode,\n all: () => captured.slice(),\n matching: filter => captured.filter(w => matchesFilter(w, filter)),\n set_retry_replay: (key, policy) => {\n retryPolicies.set(retryKeyString(key), { policy, delivered: 0 });\n },\n wait: (filter, timeout_ms) => wait(filter, timeout_ms, captured, waiters),\n wait_all: (filter, timeout_ms) => waitAll(filter, timeout_ms, captured),\n close: () => closeServer(server, waiters),\n };\n}\n\n// ────────────────────────────────────────────────────────────\n// Request handling\n// ────────────────────────────────────────────────────────────\n\ninterface HandlerState {\n captured: CapturedWebhook[];\n waiters: Array<{\n filter: WebhookFilter;\n resolve: (r: WebhookWaitResult) => void;\n timer: NodeJS.Timeout;\n }>;\n retryPolicies: Map<string, { policy: RetryReplayPolicy; delivered: number }>;\n deliveryCounts: Map<string, number>;\n}\n\nfunction handleRequest(req: IncomingMessage, res: ServerResponse, state: HandlerState): void {\n if (req.method !== 'POST') {\n res.statusCode = 405;\n res.end();\n return;\n }\n\n const pathParts = parseStepPath(req.url ?? '');\n if (!pathParts) {\n res.statusCode = 404;\n res.end();\n return;\n }\n const { step_id, operation_id } = pathParts;\n\n let size = 0;\n const chunks: Buffer[] = [];\n let tooLarge = false;\n\n req.on('data', (chunk: Buffer) => {\n size += chunk.length;\n if (size > MAX_BODY_BYTES) {\n tooLarge = true;\n req.destroy();\n return;\n }\n chunks.push(chunk);\n });\n\n req.on('end', () => {\n if (tooLarge) {\n if (!res.headersSent) {\n res.statusCode = 413;\n res.end();\n }\n return;\n }\n const raw = Buffer.concat(chunks).toString('utf8');\n const headers = normalizeHeaders(req.headers);\n\n const key = retryKeyString({ step_id, operation_id });\n const deliveryIndex = (state.deliveryCounts.get(key) ?? 0) + 1;\n\n // Back-pressure on capture exhaustion — signal 503 so a conformant\n // sender retries later, drop the capture so the runner's memory\n // footprint stays bounded. Drops a delivery quietly is worse than\n // surfacing a clearly observable refusal.\n const perKey = countPerKey(state.captured, step_id, operation_id);\n if (state.captured.length >= MAX_CAPTURED_TOTAL || perKey >= MAX_CAPTURED_PER_KEY) {\n res.statusCode = 503;\n res.setHeader('retry-after', '1');\n res.end();\n return;\n }\n\n state.deliveryCounts.set(key, deliveryIndex);\n const status = nextResponseStatus(state.retryPolicies, key);\n\n const webhook: CapturedWebhook = {\n id: randomUUID(),\n step_id,\n operation_id,\n delivery_index: deliveryIndex,\n received_at: Date.now(),\n method: req.method ?? 'POST',\n path: req.url ?? '/',\n headers: redactHeaders(headers),\n raw_body: raw,\n ...parseBody(raw, headers['content-type']),\n response_status: status,\n };\n state.captured.push(webhook);\n\n // Deliver to any matching waiters. First-match-wins per waiter; later\n // arrivals may still deliver to other waiters in the queue.\n for (let i = state.waiters.length - 1; i >= 0; i--) {\n const waiter = state.waiters[i]!;\n if (matchesFilter(webhook, waiter.filter)) {\n clearTimeout(waiter.timer);\n state.waiters.splice(i, 1);\n waiter.resolve({ webhook });\n }\n }\n\n res.statusCode = status;\n res.end();\n });\n\n req.on('error', () => {\n if (!res.headersSent) {\n res.statusCode = 400;\n res.end();\n }\n });\n}\n\nfunction parseStepPath(reqUrl: string): { step_id: string; operation_id: string } | undefined {\n const q = reqUrl.indexOf('?');\n const pathname = q === -1 ? reqUrl : reqUrl.slice(0, q);\n const match = STEP_PATH_RE.exec(pathname);\n if (!match) return undefined;\n return { step_id: match[1]!, operation_id: match[2]! };\n}\n\nfunction normalizeHeaders(raw: IncomingMessage['headers']): Record<string, string> {\n const out: Record<string, string> = {};\n for (const [key, value] of Object.entries(raw)) {\n if (value === undefined) continue;\n out[key.toLowerCase()] = Array.isArray(value) ? value.join(', ') : value;\n }\n return out;\n}\n\nfunction redactHeaders(headers: Record<string, string>): Record<string, string> {\n const out: Record<string, string> = {};\n for (const [k, v] of Object.entries(headers)) {\n out[k] = SECRET_HEADER_PATTERN.test(k) ? '[redacted]' : v;\n }\n return out;\n}\n\nfunction countPerKey(captured: ReadonlyArray<CapturedWebhook>, step_id: string, operation_id: string): number {\n let n = 0;\n for (const w of captured) if (w.step_id === step_id && w.operation_id === operation_id) n++;\n return n;\n}\n\nfunction parseBody(raw: string, contentType: string | undefined): Pick<CapturedWebhook, 'body' | 'parse_error'> {\n if (raw.length === 0) return {};\n const looksJson = !contentType || /json/i.test(contentType);\n if (!looksJson) return {};\n try {\n return { body: JSON.parse(raw) };\n } catch (err) {\n return { parse_error: err instanceof Error ? err.message : String(err) };\n }\n}\n\n// ────────────────────────────────────────────────────────────\n// Filter matching\n// ────────────────────────────────────────────────────────────\n\nfunction matchesFilter(webhook: CapturedWebhook, filter: WebhookFilter): boolean {\n if (filter.step_id && webhook.step_id !== filter.step_id) return false;\n if (filter.operation_id && webhook.operation_id !== filter.operation_id) return false;\n if (filter.body && Object.keys(filter.body).length > 0) {\n if (webhook.body === undefined) return false;\n for (const [path, expected] of Object.entries(filter.body)) {\n const actual = resolveDottedPath(webhook.body, path);\n if (!deepEqual(actual, expected)) return false;\n }\n }\n return true;\n}\n\nfunction resolveDottedPath(obj: unknown, path: string): unknown {\n if (obj === null || obj === undefined) return undefined;\n const segments = path.split('.');\n let cursor: unknown = obj;\n for (const seg of segments) {\n if (cursor === null || cursor === undefined) return undefined;\n if (typeof cursor !== 'object') return undefined;\n cursor = (cursor as Record<string, unknown>)[seg];\n }\n return cursor;\n}\n\n/**\n * Structural deep-equal. Stringify-based comparison is key-order-sensitive\n * (`{a:1,b:2}` !== `{b:2,a:1}`); for filter bodies that's a false negative\n * when a publisher's JSON serializer emits fields in a different order\n * than the storyboard author. Recurse explicitly instead.\n */\nfunction deepEqual(a: unknown, b: unknown): boolean {\n if (a === b) return true;\n if (typeof a !== typeof b) return false;\n if (a === null || b === null) return a === b;\n if (typeof a !== 'object') return false;\n if (Array.isArray(a) || Array.isArray(b)) {\n if (!Array.isArray(a) || !Array.isArray(b)) return false;\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (!deepEqual(a[i], b[i])) return false;\n }\n return true;\n }\n const keysA = Object.keys(a as Record<string, unknown>);\n const keysB = Object.keys(b as Record<string, unknown>);\n if (keysA.length !== keysB.length) return false;\n for (const k of keysA) {\n if (!Object.prototype.hasOwnProperty.call(b, k)) return false;\n if (!deepEqual((a as Record<string, unknown>)[k], (b as Record<string, unknown>)[k])) return false;\n }\n return true;\n}\n\n// ────────────────────────────────────────────────────────────\n// Retry-replay policy\n// ────────────────────────────────────────────────────────────\n\nfunction nextResponseStatus(\n retryPolicies: Map<string, { policy: RetryReplayPolicy; delivered: number }>,\n key: string\n): number {\n const entry = retryPolicies.get(key);\n if (!entry) return 204;\n if (entry.delivered < entry.policy.count) {\n entry.delivered += 1;\n return entry.policy.http_status;\n }\n return 204;\n}\n\n// ────────────────────────────────────────────────────────────\n// wait / wait_all\n// ────────────────────────────────────────────────────────────\n\nfunction wait(\n filter: WebhookFilter,\n timeout_ms: number,\n captured: CapturedWebhook[],\n waiters: Array<{ filter: WebhookFilter; resolve: (r: WebhookWaitResult) => void; timer: NodeJS.Timeout }>\n): Promise<WebhookWaitResult> {\n const already = captured.find(w => matchesFilter(w, filter));\n if (already) return Promise.resolve({ webhook: already });\n return new Promise<WebhookWaitResult>(resolve => {\n const timer = setTimeout(\n () => {\n const idx = waiters.findIndex(w => w.timer === timer);\n if (idx >= 0) waiters.splice(idx, 1);\n resolve({ timed_out: true });\n },\n Math.max(0, timeout_ms)\n );\n timer.unref?.();\n waiters.push({ filter, resolve, timer });\n });\n}\n\n/**\n * Poll until the deadline, then return every capture matching `filter`.\n * Used when the assertion needs the whole delivery sequence (retry-stability).\n * Waits a minimum of `timeout_ms` even if matches arrive earlier — callers\n * that need \"resolve on first match\" should use `wait` instead.\n */\nfunction waitAll(filter: WebhookFilter, timeout_ms: number, captured: CapturedWebhook[]): Promise<CapturedWebhook[]> {\n return new Promise<CapturedWebhook[]>(resolve => {\n const timer = setTimeout(\n () => {\n resolve(captured.filter(w => matchesFilter(w, filter)));\n },\n Math.max(0, timeout_ms)\n );\n timer.unref?.();\n });\n}\n\n// ────────────────────────────────────────────────────────────\n// close\n// ────────────────────────────────────────────────────────────\n\nfunction closeServer(\n server: Server,\n waiters: Array<{ filter: WebhookFilter; resolve: (r: WebhookWaitResult) => void; timer: NodeJS.Timeout }>\n): Promise<void> {\n while (waiters.length > 0) {\n const w = waiters.pop()!;\n clearTimeout(w.timer);\n w.resolve({ timed_out: true });\n }\n // Force-close keep-alive sockets so shutdown doesn't hang up to\n // keepAliveTimeout waiting for idle connections to drain.\n server.closeAllConnections?.();\n return new Promise<void>((resolve, reject) => {\n server.close(err => {\n if (err && (err as NodeJS.ErrnoException).code !== 'ERR_SERVER_NOT_RUNNING') reject(err);\n else resolve();\n });\n });\n}\n\nfunction formatHost(address: string): string {\n return address.includes(':') ? `[${address}]` : address;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../../src/lib/testing/storyboard/webhook-receiver.ts"],"sourcesContent":["/**\n * Ephemeral webhook receiver for storyboard runs.\n *\n * The runner binds an HTTP listener, mints per-step URLs under\n * `/step/<step_id>/<operation_id>`, and captures inbound POSTs so\n * `expect_webhook*` pseudo-steps can observe outbound webhook behavior\n * (idempotency_key presence, retry-key stability, 9421 signature validity).\n *\n * Matches the `webhook_receiver_runner` test-kit contract in\n * adcontextprotocol/adcp#2431:\n * - Per-step URL template `{{runner.webhook_base}}/step/<step_id>/<operation_id>`.\n * - Retry-replay: the receiver returns a configurable 5xx for the first N\n * deliveries on a given `(step_id, operation_id)`, then 2xx, so senders\n * are forced to retry and retry-key stability can be graded.\n * - Loopback-mock mode (HTTP on 127.0.0.1, zero external deps) by default;\n * proxy-URL mode swaps in an operator-supplied public base.\n */\n\nimport { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http';\nimport { randomUUID } from 'node:crypto';\nimport type { AddressInfo } from 'node:net';\n\n/**\n * Size cap on any single webhook body. A non-conformant sender retrying\n * with oversize payloads shouldn't be able to exhaust the runner's memory.\n */\nconst MAX_BODY_BYTES = 1_048_576; // 1 MiB\n\n/**\n * Caps on cumulative capture state. `MAX_CAPTURED_TOTAL` bounds the total\n * number of webhooks retained for the run; `MAX_CAPTURED_PER_KEY` bounds\n * per-(step_id, operation_id) deliveries — a publisher stuck in a retry\n * loop can't exhaust memory. Once a cap is hit the receiver stops retaining\n * bodies but continues the configured retry-status sequence (or returns 204\n * when no rejection remains), so the cap cannot trap a sender in a retry loop.\n */\nconst MAX_CAPTURED_TOTAL = 1000;\nconst MAX_CAPTURED_PER_KEY = 100;\n\n/**\n * HTTP transport hardening. Defaults in Node 20 are generous (headers\n * 60s, no body timeout). A hostile / slow publisher shouldn't be able to\n * hold the runner's event loop with trickled requests or exhaust the\n * socket table.\n */\nconst HEADERS_TIMEOUT_MS = 10_000;\nconst REQUEST_TIMEOUT_MS = 30_000;\nconst KEEP_ALIVE_TIMEOUT_MS = 5_000;\nconst MAX_CONNECTIONS = 64;\nconst MAX_HEADERS_COUNT = 64;\n\n/** Default path shape under the receiver base. */\nconst STEP_PATH_RE = /^\\/step\\/([A-Za-z0-9_]+)\\/([A-Za-z0-9_-]+)\\/?$/;\n\n/**\n * Header names whose values are redacted before a captured webhook lands\n * in a compliance report. Mirrors the `SECRET_KEY_PATTERN` in runner.ts\n * so both paths honor the contract's \"Secrets SHOULD be redacted\" rule.\n */\nconst SECRET_HEADER_PATTERN =\n /^(authorization|credentials?|token|api[_-]?key|x-api[_-]?key|x-auth[_-]?token|password|secret|client[_-]secret|refresh[_-]token|access[_-]token|bearer|session[_-]token|cookie|set[_-]cookie)$/i;\n\n/**\n * Strip user-info, scheme-policed, and CRLF/NUL-rejected proxy URLs so a\n * misconfigured operator (or a compromised storyboard loader) can't\n * advertise an `http://169.254.169.254`, `file://…`, or header-splitting\n * URL that would then be embedded in outbound `push_notification_config.url`.\n */\nfunction validateProxyUrl(raw: string): string {\n if (/[\\r\\n\\x00]/.test(raw)) {\n throw new Error('webhook_receiver.public_url must not contain CR/LF/NUL');\n }\n let parsed: URL;\n try {\n parsed = new URL(raw);\n } catch {\n throw new Error(`webhook_receiver.public_url is not a valid URL: ${raw}`);\n }\n if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {\n throw new Error(`webhook_receiver.public_url must be http(s); got ${parsed.protocol}`);\n }\n if (parsed.username || parsed.password) {\n throw new Error('webhook_receiver.public_url must not include userinfo');\n }\n return raw.replace(/\\/$/, '');\n}\n\nexport interface CapturedWebhook {\n /** Runner-assigned id; stable across filter matches. */\n id: string;\n /** Step id extracted from the URL path. */\n step_id: string;\n /** Operation id extracted from the URL path. */\n operation_id: string;\n /** Delivery sequence number for this (step_id, operation_id) — 1-based. */\n delivery_index: number;\n /** Wall-clock millis at receipt. */\n received_at: number;\n method: string;\n /** Path + query. */\n path: string;\n /** Lowercased header names; duplicate values joined with \", \". */\n headers: Record<string, string>;\n /** Raw request body — needed for RFC 9421 content-digest recompute. */\n raw_body: string;\n /** Parsed JSON body; undefined on parse failure. */\n body?: unknown;\n /** Parse error message when raw_body was non-empty but not valid JSON. */\n parse_error?: string;\n /** HTTP status the receiver returned (reflects retry-replay policy). */\n response_status: number;\n}\n\n/** Match predicate applied when waiting for a webhook. */\nexport interface WebhookFilter {\n /** Restrict matches to this step id's URL. */\n step_id?: string;\n /** Restrict matches to this operation id. */\n operation_id?: string;\n /** Dotted-path → expected-value pairs compared against the parsed body. */\n body?: Record<string, unknown>;\n}\n\nexport type WebhookWaitResult =\n | { webhook: CapturedWebhook; timed_out?: false }\n | { timed_out: true; webhook?: undefined };\n\nexport interface RetryReplayPolicy {\n /** How many deliveries to reject before accepting. */\n count: number;\n /** HTTP status to return on rejection. */\n http_status: number;\n}\n\nexport interface WebhookReceiver {\n /** Public base URL (`http://host:port` for loopback, operator URL for proxy). */\n readonly base_url: string;\n /** Whether the receiver is in loopback-mock or proxy-url mode. */\n readonly mode: 'loopback_mock' | 'proxy_url';\n /** All webhooks captured so far, in arrival order. */\n all(): CapturedWebhook[];\n /** Webhooks matching a filter, in arrival order. */\n matching(filter: WebhookFilter): CapturedWebhook[];\n /**\n * Configure the receiver to return `http_status` for the first `count`\n * deliveries at `(step_id, operation_id)` and 2xx afterwards. Idempotent:\n * setting a new policy replaces any existing one for the same key.\n */\n set_retry_replay(key: { step_id: string; operation_id: string }, policy: RetryReplayPolicy): void;\n /** Resolve with the first match within `timeout_ms`, or `timed_out: true`. */\n wait(filter: WebhookFilter, timeout_ms: number): Promise<WebhookWaitResult>;\n /**\n * Resolve with every match observed between call time and deadline. Used\n * by `expect_webhook_retry_keys_stable` which needs the full sequence of\n * deliveries for the retry-replay assertion.\n */\n wait_all(filter: WebhookFilter, timeout_ms: number): Promise<CapturedWebhook[]>;\n /** Stop the listener. Idempotent. */\n close(): Promise<void>;\n}\n\nexport interface CreateWebhookReceiverOptions {\n mode?: 'loopback_mock' | 'proxy_url';\n /** Bind host for the local listener. Defaults to `127.0.0.1`. */\n host?: string;\n /** Bind port. `0` (default) lets the kernel assign one. */\n port?: number;\n /** Public URL to advertise when `mode: 'proxy_url'`. */\n public_url?: string;\n}\n\ninterface RetryKey {\n step_id: string;\n operation_id: string;\n}\n\nfunction retryKeyString(k: RetryKey): string {\n return `${k.step_id}::${k.operation_id}`;\n}\n\nexport async function createWebhookReceiver(options: CreateWebhookReceiverOptions = {}): Promise<WebhookReceiver> {\n const mode = options.mode ?? 'loopback_mock';\n if (mode === 'proxy_url' && !options.public_url) {\n throw new Error('webhook_receiver.mode=proxy_url requires `public_url`');\n }\n\n const host = options.host ?? '127.0.0.1';\n // A loopback_mock receiver bound on 0.0.0.0 is publicly reachable from any\n // interface — which would turn a CI runner into an open POST endpoint.\n // Operators who genuinely want public exposure use mode=proxy_url.\n if (mode === 'loopback_mock' && (host === '0.0.0.0' || host === '::')) {\n throw new Error(\n `webhook_receiver host ${host} is not permitted in loopback_mock mode. ` +\n 'Use mode=proxy_url with an explicit public_url for publicly-reachable runs.'\n );\n }\n const port = options.port ?? 0;\n const proxyBase = mode === 'proxy_url' ? validateProxyUrl(options.public_url!) : undefined;\n\n const captured: CapturedWebhook[] = [];\n const waiters: Array<{\n filter: WebhookFilter;\n resolve: (result: WebhookWaitResult) => void;\n timer: NodeJS.Timeout;\n }> = [];\n const waitAllTimers: Array<{\n filter: WebhookFilter;\n resolve: (result: CapturedWebhook[]) => void;\n timer: NodeJS.Timeout;\n }> = [];\n const retryPolicies = new Map<string, { policy: RetryReplayPolicy; delivered: number }>();\n const deliveryCounts = new Map<string, number>();\n let closed = false;\n\n const server = createServer((req, res) =>\n handleRequest(req, res, { captured, waiters, retryPolicies, deliveryCounts })\n );\n // Transport-level hardening — trim Node's generous defaults so a slow /\n // hostile publisher can't wedge the runner.\n server.headersTimeout = HEADERS_TIMEOUT_MS;\n server.requestTimeout = REQUEST_TIMEOUT_MS;\n server.keepAliveTimeout = KEEP_ALIVE_TIMEOUT_MS;\n server.maxConnections = MAX_CONNECTIONS;\n server.maxHeadersCount = MAX_HEADERS_COUNT;\n\n await new Promise<void>((resolve, reject) => {\n const onError = (err: Error) => reject(err);\n server.once('error', onError);\n server.listen(port, host, () => {\n server.off('error', onError);\n resolve();\n });\n });\n\n const bound = server.address() as AddressInfo;\n const base_url = proxyBase ?? `http://${formatHost(bound.address)}:${bound.port}`;\n\n return {\n base_url,\n mode,\n all: () => captured.slice(),\n matching: filter => captured.filter(w => matchesFilter(w, filter)),\n set_retry_replay: (key, policy) => {\n retryPolicies.set(retryKeyString(key), { policy, delivered: 0 });\n },\n wait: (filter, timeout_ms) => {\n if (!closed) return wait(filter, timeout_ms, captured, waiters);\n const already = captured.find(w => matchesFilter(w, filter));\n return Promise.resolve(already ? { webhook: already } : { timed_out: true });\n },\n wait_all: (filter, timeout_ms) =>\n closed\n ? Promise.resolve(captured.filter(w => matchesFilter(w, filter)))\n : waitAll(filter, timeout_ms, captured, waitAllTimers),\n close: () => {\n closed = true;\n return closeServer(server, captured, waiters, waitAllTimers);\n },\n };\n}\n\n// ────────────────────────────────────────────────────────────\n// Request handling\n// ────────────────────────────────────────────────────────────\n\ninterface HandlerState {\n captured: CapturedWebhook[];\n waiters: Array<{\n filter: WebhookFilter;\n resolve: (r: WebhookWaitResult) => void;\n timer: NodeJS.Timeout;\n }>;\n retryPolicies: Map<string, { policy: RetryReplayPolicy; delivered: number }>;\n deliveryCounts: Map<string, number>;\n}\n\nfunction handleRequest(req: IncomingMessage, res: ServerResponse, state: HandlerState): void {\n if (req.method !== 'POST') {\n res.statusCode = 405;\n res.end();\n return;\n }\n\n const pathParts = parseStepPath(req.url ?? '');\n if (!pathParts) {\n res.statusCode = 404;\n res.end();\n return;\n }\n const { step_id, operation_id } = pathParts;\n\n let size = 0;\n const chunks: Buffer[] = [];\n let tooLarge = false;\n\n req.on('data', (chunk: Buffer) => {\n size += chunk.length;\n if (size > MAX_BODY_BYTES) {\n tooLarge = true;\n req.destroy();\n return;\n }\n chunks.push(chunk);\n });\n\n req.on('end', () => {\n if (tooLarge) {\n if (!res.headersSent) {\n res.statusCode = 413;\n res.end();\n }\n return;\n }\n const raw = Buffer.concat(chunks).toString('utf8');\n const headers = normalizeHeaders(req.headers);\n\n const key = retryKeyString({ step_id, operation_id });\n const deliveryIndex = (state.deliveryCounts.get(key) ?? 0) + 1;\n const status = nextResponseStatus(state.retryPolicies, key);\n\n // Stop retaining bodies once a capture cap is exhausted, but preserve the\n // configured retry-replay status sequence. Unconditionally returning 503\n // here masks the policy's eventual 2xx forever and can amplify a sender's\n // retries after the runner has already reached its memory bound (#2653).\n const perKey = state.deliveryCounts.get(key) ?? 0;\n if (state.captured.length >= MAX_CAPTURED_TOTAL || perKey >= MAX_CAPTURED_PER_KEY) {\n res.statusCode = status;\n if (status === 429 || status >= 500) res.setHeader('retry-after', '1');\n res.end();\n return;\n }\n\n state.deliveryCounts.set(key, deliveryIndex);\n\n const webhook: CapturedWebhook = {\n id: randomUUID(),\n step_id,\n operation_id,\n delivery_index: deliveryIndex,\n received_at: Date.now(),\n method: req.method ?? 'POST',\n path: req.url ?? '/',\n headers: redactHeaders(headers),\n raw_body: raw,\n ...parseBody(raw, headers['content-type']),\n response_status: status,\n };\n state.captured.push(webhook);\n\n // Deliver to any matching waiters. First-match-wins per waiter; later\n // arrivals may still deliver to other waiters in the queue.\n for (let i = state.waiters.length - 1; i >= 0; i--) {\n const waiter = state.waiters[i]!;\n if (matchesFilter(webhook, waiter.filter)) {\n clearTimeout(waiter.timer);\n state.waiters.splice(i, 1);\n waiter.resolve({ webhook });\n }\n }\n\n res.statusCode = status;\n res.end();\n });\n\n req.on('error', () => {\n if (!res.headersSent) {\n res.statusCode = 400;\n res.end();\n }\n });\n}\n\nfunction parseStepPath(reqUrl: string): { step_id: string; operation_id: string } | undefined {\n const q = reqUrl.indexOf('?');\n const pathname = q === -1 ? reqUrl : reqUrl.slice(0, q);\n const match = STEP_PATH_RE.exec(pathname);\n if (!match) return undefined;\n return { step_id: match[1]!, operation_id: match[2]! };\n}\n\nfunction normalizeHeaders(raw: IncomingMessage['headers']): Record<string, string> {\n const out: Record<string, string> = {};\n for (const [key, value] of Object.entries(raw)) {\n if (value === undefined) continue;\n out[key.toLowerCase()] = Array.isArray(value) ? value.join(', ') : value;\n }\n return out;\n}\n\nfunction redactHeaders(headers: Record<string, string>): Record<string, string> {\n const out: Record<string, string> = {};\n for (const [k, v] of Object.entries(headers)) {\n out[k] = SECRET_HEADER_PATTERN.test(k) ? '[redacted]' : v;\n }\n return out;\n}\n\nfunction parseBody(raw: string, contentType: string | undefined): Pick<CapturedWebhook, 'body' | 'parse_error'> {\n if (raw.length === 0) return {};\n const looksJson = !contentType || /json/i.test(contentType);\n if (!looksJson) return {};\n try {\n return { body: JSON.parse(raw) };\n } catch (err) {\n return { parse_error: err instanceof Error ? err.message : String(err) };\n }\n}\n\n// ────────────────────────────────────────────────────────────\n// Filter matching\n// ────────────────────────────────────────────────────────────\n\nfunction matchesFilter(webhook: CapturedWebhook, filter: WebhookFilter): boolean {\n if (filter.step_id && webhook.step_id !== filter.step_id) return false;\n if (filter.operation_id && webhook.operation_id !== filter.operation_id) return false;\n if (filter.body && Object.keys(filter.body).length > 0) {\n if (webhook.body === undefined) return false;\n for (const [path, expected] of Object.entries(filter.body)) {\n const actual = resolveDottedPath(webhook.body, path);\n if (!deepEqual(actual, expected)) return false;\n }\n }\n return true;\n}\n\nfunction resolveDottedPath(obj: unknown, path: string): unknown {\n if (obj === null || obj === undefined) return undefined;\n const segments = path.split('.');\n let cursor: unknown = obj;\n for (const seg of segments) {\n if (cursor === null || cursor === undefined) return undefined;\n if (typeof cursor !== 'object') return undefined;\n cursor = (cursor as Record<string, unknown>)[seg];\n }\n return cursor;\n}\n\n/**\n * Structural deep-equal. Stringify-based comparison is key-order-sensitive\n * (`{a:1,b:2}` !== `{b:2,a:1}`); for filter bodies that's a false negative\n * when a publisher's JSON serializer emits fields in a different order\n * than the storyboard author. Recurse explicitly instead.\n */\nfunction deepEqual(a: unknown, b: unknown): boolean {\n if (a === b) return true;\n if (typeof a !== typeof b) return false;\n if (a === null || b === null) return a === b;\n if (typeof a !== 'object') return false;\n if (Array.isArray(a) || Array.isArray(b)) {\n if (!Array.isArray(a) || !Array.isArray(b)) return false;\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (!deepEqual(a[i], b[i])) return false;\n }\n return true;\n }\n const keysA = Object.keys(a as Record<string, unknown>);\n const keysB = Object.keys(b as Record<string, unknown>);\n if (keysA.length !== keysB.length) return false;\n for (const k of keysA) {\n if (!Object.prototype.hasOwnProperty.call(b, k)) return false;\n if (!deepEqual((a as Record<string, unknown>)[k], (b as Record<string, unknown>)[k])) return false;\n }\n return true;\n}\n\n// ────────────────────────────────────────────────────────────\n// Retry-replay policy\n// ────────────────────────────────────────────────────────────\n\nfunction nextResponseStatus(\n retryPolicies: Map<string, { policy: RetryReplayPolicy; delivered: number }>,\n key: string\n): number {\n const entry = retryPolicies.get(key);\n if (!entry) return 204;\n if (entry.delivered < entry.policy.count) {\n entry.delivered += 1;\n return entry.policy.http_status;\n }\n return 204;\n}\n\n// ────────────────────────────────────────────────────────────\n// wait / wait_all\n// ────────────────────────────────────────────────────────────\n\nfunction wait(\n filter: WebhookFilter,\n timeout_ms: number,\n captured: CapturedWebhook[],\n waiters: Array<{ filter: WebhookFilter; resolve: (r: WebhookWaitResult) => void; timer: NodeJS.Timeout }>\n): Promise<WebhookWaitResult> {\n const already = captured.find(w => matchesFilter(w, filter));\n if (already) return Promise.resolve({ webhook: already });\n return new Promise<WebhookWaitResult>(resolve => {\n const timer = setTimeout(\n () => {\n const idx = waiters.findIndex(w => w.timer === timer);\n if (idx >= 0) waiters.splice(idx, 1);\n resolve({ timed_out: true });\n },\n Math.max(0, timeout_ms)\n );\n timer.unref?.();\n waiters.push({ filter, resolve, timer });\n });\n}\n\n/**\n * Poll until the deadline, then return every capture matching `filter`.\n * Used when the assertion needs the whole delivery sequence (retry-stability).\n * Waits a minimum of `timeout_ms` even if matches arrive earlier — callers\n * that need \"resolve on first match\" should use `wait` instead.\n */\nfunction waitAll(\n filter: WebhookFilter,\n timeout_ms: number,\n captured: CapturedWebhook[],\n waitAllTimers: Array<{\n filter: WebhookFilter;\n resolve: (result: CapturedWebhook[]) => void;\n timer: NodeJS.Timeout;\n }>\n): Promise<CapturedWebhook[]> {\n return new Promise<CapturedWebhook[]>(resolve => {\n const timer = setTimeout(\n () => {\n const idx = waitAllTimers.findIndex(entry => entry.timer === timer);\n if (idx >= 0) waitAllTimers.splice(idx, 1);\n resolve(captured.filter(w => matchesFilter(w, filter)));\n },\n Math.max(0, timeout_ms)\n );\n timer.unref?.();\n waitAllTimers.push({ filter, resolve, timer });\n });\n}\n\n// ────────────────────────────────────────────────────────────\n// close\n// ────────────────────────────────────────────────────────────\n\nfunction closeServer(\n server: Server,\n captured: CapturedWebhook[],\n waiters: Array<{ filter: WebhookFilter; resolve: (r: WebhookWaitResult) => void; timer: NodeJS.Timeout }>,\n waitAllTimers: Array<{\n filter: WebhookFilter;\n resolve: (result: CapturedWebhook[]) => void;\n timer: NodeJS.Timeout;\n }>\n): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n server.close(err => {\n if (err && (err as NodeJS.ErrnoException).code !== 'ERR_SERVER_NOT_RUNNING') reject(err);\n else resolve();\n });\n // Stop accepting new sockets before force-closing existing keep-alive\n // connections; the opposite order leaves a small acceptance race.\n server.closeAllConnections?.();\n while (waiters.length > 0) {\n const w = waiters.pop()!;\n clearTimeout(w.timer);\n w.resolve({ timed_out: true });\n }\n while (waitAllTimers.length > 0) {\n const pending = waitAllTimers.pop()!;\n clearTimeout(pending.timer);\n pending.resolve(captured.filter(w => matchesFilter(w, pending.filter)));\n }\n });\n}\n\nfunction formatHost(address: string): string {\n return address.includes(':') ? `[${address}]` : address;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -46,8 +46,10 @@ async function createWebhookReceiver(options = {}) {
|
|
|
46
46
|
const proxyBase = mode === "proxy_url" ? validateProxyUrl(options.public_url) : void 0;
|
|
47
47
|
const captured = [];
|
|
48
48
|
const waiters = [];
|
|
49
|
+
const waitAllTimers = [];
|
|
49
50
|
const retryPolicies = /* @__PURE__ */ new Map();
|
|
50
51
|
const deliveryCounts = /* @__PURE__ */ new Map();
|
|
52
|
+
let closed = false;
|
|
51
53
|
const server = createServer(
|
|
52
54
|
(req, res) => handleRequest(req, res, { captured, waiters, retryPolicies, deliveryCounts })
|
|
53
55
|
);
|
|
@@ -74,9 +76,16 @@ async function createWebhookReceiver(options = {}) {
|
|
|
74
76
|
set_retry_replay: (key, policy) => {
|
|
75
77
|
retryPolicies.set(retryKeyString(key), { policy, delivered: 0 });
|
|
76
78
|
},
|
|
77
|
-
wait: (filter, timeout_ms) =>
|
|
78
|
-
|
|
79
|
-
|
|
79
|
+
wait: (filter, timeout_ms) => {
|
|
80
|
+
if (!closed) return wait(filter, timeout_ms, captured, waiters);
|
|
81
|
+
const already = captured.find((w) => matchesFilter(w, filter));
|
|
82
|
+
return Promise.resolve(already ? { webhook: already } : { timed_out: true });
|
|
83
|
+
},
|
|
84
|
+
wait_all: (filter, timeout_ms) => closed ? Promise.resolve(captured.filter((w) => matchesFilter(w, filter))) : waitAll(filter, timeout_ms, captured, waitAllTimers),
|
|
85
|
+
close: () => {
|
|
86
|
+
closed = true;
|
|
87
|
+
return closeServer(server, captured, waiters, waitAllTimers);
|
|
88
|
+
}
|
|
80
89
|
};
|
|
81
90
|
}
|
|
82
91
|
function handleRequest(req, res, state) {
|
|
@@ -116,15 +125,15 @@ function handleRequest(req, res, state) {
|
|
|
116
125
|
const headers = normalizeHeaders(req.headers);
|
|
117
126
|
const key = retryKeyString({ step_id, operation_id });
|
|
118
127
|
const deliveryIndex = (state.deliveryCounts.get(key) ?? 0) + 1;
|
|
119
|
-
const
|
|
128
|
+
const status = nextResponseStatus(state.retryPolicies, key);
|
|
129
|
+
const perKey = state.deliveryCounts.get(key) ?? 0;
|
|
120
130
|
if (state.captured.length >= MAX_CAPTURED_TOTAL || perKey >= MAX_CAPTURED_PER_KEY) {
|
|
121
|
-
res.statusCode =
|
|
122
|
-
res.setHeader("retry-after", "1");
|
|
131
|
+
res.statusCode = status;
|
|
132
|
+
if (status === 429 || status >= 500) res.setHeader("retry-after", "1");
|
|
123
133
|
res.end();
|
|
124
134
|
return;
|
|
125
135
|
}
|
|
126
136
|
state.deliveryCounts.set(key, deliveryIndex);
|
|
127
|
-
const status = nextResponseStatus(state.retryPolicies, key);
|
|
128
137
|
const webhook = {
|
|
129
138
|
id: randomUUID(),
|
|
130
139
|
step_id,
|
|
@@ -179,11 +188,6 @@ function redactHeaders(headers) {
|
|
|
179
188
|
}
|
|
180
189
|
return out;
|
|
181
190
|
}
|
|
182
|
-
function countPerKey(captured, step_id, operation_id) {
|
|
183
|
-
let n = 0;
|
|
184
|
-
for (const w of captured) if (w.step_id === step_id && w.operation_id === operation_id) n++;
|
|
185
|
-
return n;
|
|
186
|
-
}
|
|
187
191
|
function parseBody(raw, contentType) {
|
|
188
192
|
if (raw.length === 0) return {};
|
|
189
193
|
const looksJson = !contentType || /json/i.test(contentType);
|
|
@@ -264,29 +268,37 @@ function wait(filter, timeout_ms, captured, waiters) {
|
|
|
264
268
|
waiters.push({ filter, resolve, timer });
|
|
265
269
|
});
|
|
266
270
|
}
|
|
267
|
-
function waitAll(filter, timeout_ms, captured) {
|
|
271
|
+
function waitAll(filter, timeout_ms, captured, waitAllTimers) {
|
|
268
272
|
return new Promise((resolve) => {
|
|
269
273
|
const timer = setTimeout(
|
|
270
274
|
() => {
|
|
275
|
+
const idx = waitAllTimers.findIndex((entry) => entry.timer === timer);
|
|
276
|
+
if (idx >= 0) waitAllTimers.splice(idx, 1);
|
|
271
277
|
resolve(captured.filter((w) => matchesFilter(w, filter)));
|
|
272
278
|
},
|
|
273
279
|
Math.max(0, timeout_ms)
|
|
274
280
|
);
|
|
275
281
|
timer.unref?.();
|
|
282
|
+
waitAllTimers.push({ filter, resolve, timer });
|
|
276
283
|
});
|
|
277
284
|
}
|
|
278
|
-
function closeServer(server, waiters) {
|
|
279
|
-
while (waiters.length > 0) {
|
|
280
|
-
const w = waiters.pop();
|
|
281
|
-
clearTimeout(w.timer);
|
|
282
|
-
w.resolve({ timed_out: true });
|
|
283
|
-
}
|
|
284
|
-
server.closeAllConnections?.();
|
|
285
|
+
function closeServer(server, captured, waiters, waitAllTimers) {
|
|
285
286
|
return new Promise((resolve, reject) => {
|
|
286
287
|
server.close((err) => {
|
|
287
288
|
if (err && err.code !== "ERR_SERVER_NOT_RUNNING") reject(err);
|
|
288
289
|
else resolve();
|
|
289
290
|
});
|
|
291
|
+
server.closeAllConnections?.();
|
|
292
|
+
while (waiters.length > 0) {
|
|
293
|
+
const w = waiters.pop();
|
|
294
|
+
clearTimeout(w.timer);
|
|
295
|
+
w.resolve({ timed_out: true });
|
|
296
|
+
}
|
|
297
|
+
while (waitAllTimers.length > 0) {
|
|
298
|
+
const pending = waitAllTimers.pop();
|
|
299
|
+
clearTimeout(pending.timer);
|
|
300
|
+
pending.resolve(captured.filter((w) => matchesFilter(w, pending.filter)));
|
|
301
|
+
}
|
|
290
302
|
});
|
|
291
303
|
}
|
|
292
304
|
function formatHost(address) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/lib/testing/storyboard/webhook-receiver.ts"],"sourcesContent":["/**\n * Ephemeral webhook receiver for storyboard runs.\n *\n * The runner binds an HTTP listener, mints per-step URLs under\n * `/step/<step_id>/<operation_id>`, and captures inbound POSTs so\n * `expect_webhook*` pseudo-steps can observe outbound webhook behavior\n * (idempotency_key presence, retry-key stability, 9421 signature validity).\n *\n * Matches the `webhook_receiver_runner` test-kit contract in\n * adcontextprotocol/adcp#2431:\n * - Per-step URL template `{{runner.webhook_base}}/step/<step_id>/<operation_id>`.\n * - Retry-replay: the receiver returns a configurable 5xx for the first N\n * deliveries on a given `(step_id, operation_id)`, then 2xx, so senders\n * are forced to retry and retry-key stability can be graded.\n * - Loopback-mock mode (HTTP on 127.0.0.1, zero external deps) by default;\n * proxy-URL mode swaps in an operator-supplied public base.\n */\n\nimport { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http';\nimport { randomUUID } from 'node:crypto';\nimport type { AddressInfo } from 'node:net';\n\n/**\n * Size cap on any single webhook body. A non-conformant sender retrying\n * with oversize payloads shouldn't be able to exhaust the runner's memory.\n */\nconst MAX_BODY_BYTES = 1_048_576; // 1 MiB\n\n/**\n * Caps on cumulative capture state. `MAX_CAPTURED_TOTAL` bounds the total\n * number of webhooks retained for the run; `MAX_CAPTURED_PER_KEY` bounds\n * per-(step_id, operation_id) deliveries — a publisher stuck in a retry\n * loop can't exhaust memory. Once a cap is hit the receiver returns 503\n * (signalling back-pressure to the sender) and drops the capture.\n */\nconst MAX_CAPTURED_TOTAL = 1000;\nconst MAX_CAPTURED_PER_KEY = 100;\n\n/**\n * HTTP transport hardening. Defaults in Node 20 are generous (headers\n * 60s, no body timeout). A hostile / slow publisher shouldn't be able to\n * hold the runner's event loop with trickled requests or exhaust the\n * socket table.\n */\nconst HEADERS_TIMEOUT_MS = 10_000;\nconst REQUEST_TIMEOUT_MS = 30_000;\nconst KEEP_ALIVE_TIMEOUT_MS = 5_000;\nconst MAX_CONNECTIONS = 64;\nconst MAX_HEADERS_COUNT = 64;\n\n/** Default path shape under the receiver base. */\nconst STEP_PATH_RE = /^\\/step\\/([A-Za-z0-9_]+)\\/([A-Za-z0-9_-]+)\\/?$/;\n\n/**\n * Header names whose values are redacted before a captured webhook lands\n * in a compliance report. Mirrors the `SECRET_KEY_PATTERN` in runner.ts\n * so both paths honor the contract's \"Secrets SHOULD be redacted\" rule.\n */\nconst SECRET_HEADER_PATTERN =\n /^(authorization|credentials?|token|api[_-]?key|x-api[_-]?key|x-auth[_-]?token|password|secret|client[_-]secret|refresh[_-]token|access[_-]token|bearer|session[_-]token|cookie|set[_-]cookie)$/i;\n\n/**\n * Strip user-info, scheme-policed, and CRLF/NUL-rejected proxy URLs so a\n * misconfigured operator (or a compromised storyboard loader) can't\n * advertise an `http://169.254.169.254`, `file://…`, or header-splitting\n * URL that would then be embedded in outbound `push_notification_config.url`.\n */\nfunction validateProxyUrl(raw: string): string {\n if (/[\\r\\n\\x00]/.test(raw)) {\n throw new Error('webhook_receiver.public_url must not contain CR/LF/NUL');\n }\n let parsed: URL;\n try {\n parsed = new URL(raw);\n } catch {\n throw new Error(`webhook_receiver.public_url is not a valid URL: ${raw}`);\n }\n if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {\n throw new Error(`webhook_receiver.public_url must be http(s); got ${parsed.protocol}`);\n }\n if (parsed.username || parsed.password) {\n throw new Error('webhook_receiver.public_url must not include userinfo');\n }\n return raw.replace(/\\/$/, '');\n}\n\nexport interface CapturedWebhook {\n /** Runner-assigned id; stable across filter matches. */\n id: string;\n /** Step id extracted from the URL path. */\n step_id: string;\n /** Operation id extracted from the URL path. */\n operation_id: string;\n /** Delivery sequence number for this (step_id, operation_id) — 1-based. */\n delivery_index: number;\n /** Wall-clock millis at receipt. */\n received_at: number;\n method: string;\n /** Path + query. */\n path: string;\n /** Lowercased header names; duplicate values joined with \", \". */\n headers: Record<string, string>;\n /** Raw request body — needed for RFC 9421 content-digest recompute. */\n raw_body: string;\n /** Parsed JSON body; undefined on parse failure. */\n body?: unknown;\n /** Parse error message when raw_body was non-empty but not valid JSON. */\n parse_error?: string;\n /** HTTP status the receiver returned (reflects retry-replay policy). */\n response_status: number;\n}\n\n/** Match predicate applied when waiting for a webhook. */\nexport interface WebhookFilter {\n /** Restrict matches to this step id's URL. */\n step_id?: string;\n /** Restrict matches to this operation id. */\n operation_id?: string;\n /** Dotted-path → expected-value pairs compared against the parsed body. */\n body?: Record<string, unknown>;\n}\n\nexport type WebhookWaitResult =\n | { webhook: CapturedWebhook; timed_out?: false }\n | { timed_out: true; webhook?: undefined };\n\nexport interface RetryReplayPolicy {\n /** How many deliveries to reject before accepting. */\n count: number;\n /** HTTP status to return on rejection. */\n http_status: number;\n}\n\nexport interface WebhookReceiver {\n /** Public base URL (`http://host:port` for loopback, operator URL for proxy). */\n readonly base_url: string;\n /** Whether the receiver is in loopback-mock or proxy-url mode. */\n readonly mode: 'loopback_mock' | 'proxy_url';\n /** All webhooks captured so far, in arrival order. */\n all(): CapturedWebhook[];\n /** Webhooks matching a filter, in arrival order. */\n matching(filter: WebhookFilter): CapturedWebhook[];\n /**\n * Configure the receiver to return `http_status` for the first `count`\n * deliveries at `(step_id, operation_id)` and 2xx afterwards. Idempotent:\n * setting a new policy replaces any existing one for the same key.\n */\n set_retry_replay(key: { step_id: string; operation_id: string }, policy: RetryReplayPolicy): void;\n /** Resolve with the first match within `timeout_ms`, or `timed_out: true`. */\n wait(filter: WebhookFilter, timeout_ms: number): Promise<WebhookWaitResult>;\n /**\n * Resolve with every match observed between call time and deadline. Used\n * by `expect_webhook_retry_keys_stable` which needs the full sequence of\n * deliveries for the retry-replay assertion.\n */\n wait_all(filter: WebhookFilter, timeout_ms: number): Promise<CapturedWebhook[]>;\n /** Stop the listener. Idempotent. */\n close(): Promise<void>;\n}\n\nexport interface CreateWebhookReceiverOptions {\n mode?: 'loopback_mock' | 'proxy_url';\n /** Bind host for the local listener. Defaults to `127.0.0.1`. */\n host?: string;\n /** Bind port. `0` (default) lets the kernel assign one. */\n port?: number;\n /** Public URL to advertise when `mode: 'proxy_url'`. */\n public_url?: string;\n}\n\ninterface RetryKey {\n step_id: string;\n operation_id: string;\n}\n\nfunction retryKeyString(k: RetryKey): string {\n return `${k.step_id}::${k.operation_id}`;\n}\n\nexport async function createWebhookReceiver(options: CreateWebhookReceiverOptions = {}): Promise<WebhookReceiver> {\n const mode = options.mode ?? 'loopback_mock';\n if (mode === 'proxy_url' && !options.public_url) {\n throw new Error('webhook_receiver.mode=proxy_url requires `public_url`');\n }\n\n const host = options.host ?? '127.0.0.1';\n // A loopback_mock receiver bound on 0.0.0.0 is publicly reachable from any\n // interface — which would turn a CI runner into an open POST endpoint.\n // Operators who genuinely want public exposure use mode=proxy_url.\n if (mode === 'loopback_mock' && (host === '0.0.0.0' || host === '::')) {\n throw new Error(\n `webhook_receiver host ${host} is not permitted in loopback_mock mode. ` +\n 'Use mode=proxy_url with an explicit public_url for publicly-reachable runs.'\n );\n }\n const port = options.port ?? 0;\n const proxyBase = mode === 'proxy_url' ? validateProxyUrl(options.public_url!) : undefined;\n\n const captured: CapturedWebhook[] = [];\n const waiters: Array<{\n filter: WebhookFilter;\n resolve: (result: WebhookWaitResult) => void;\n timer: NodeJS.Timeout;\n }> = [];\n const retryPolicies = new Map<string, { policy: RetryReplayPolicy; delivered: number }>();\n const deliveryCounts = new Map<string, number>();\n\n const server = createServer((req, res) =>\n handleRequest(req, res, { captured, waiters, retryPolicies, deliveryCounts })\n );\n // Transport-level hardening — trim Node's generous defaults so a slow /\n // hostile publisher can't wedge the runner.\n server.headersTimeout = HEADERS_TIMEOUT_MS;\n server.requestTimeout = REQUEST_TIMEOUT_MS;\n server.keepAliveTimeout = KEEP_ALIVE_TIMEOUT_MS;\n server.maxConnections = MAX_CONNECTIONS;\n server.maxHeadersCount = MAX_HEADERS_COUNT;\n\n await new Promise<void>((resolve, reject) => {\n const onError = (err: Error) => reject(err);\n server.once('error', onError);\n server.listen(port, host, () => {\n server.off('error', onError);\n resolve();\n });\n });\n\n const bound = server.address() as AddressInfo;\n const base_url = proxyBase ?? `http://${formatHost(bound.address)}:${bound.port}`;\n\n return {\n base_url,\n mode,\n all: () => captured.slice(),\n matching: filter => captured.filter(w => matchesFilter(w, filter)),\n set_retry_replay: (key, policy) => {\n retryPolicies.set(retryKeyString(key), { policy, delivered: 0 });\n },\n wait: (filter, timeout_ms) => wait(filter, timeout_ms, captured, waiters),\n wait_all: (filter, timeout_ms) => waitAll(filter, timeout_ms, captured),\n close: () => closeServer(server, waiters),\n };\n}\n\n// ────────────────────────────────────────────────────────────\n// Request handling\n// ────────────────────────────────────────────────────────────\n\ninterface HandlerState {\n captured: CapturedWebhook[];\n waiters: Array<{\n filter: WebhookFilter;\n resolve: (r: WebhookWaitResult) => void;\n timer: NodeJS.Timeout;\n }>;\n retryPolicies: Map<string, { policy: RetryReplayPolicy; delivered: number }>;\n deliveryCounts: Map<string, number>;\n}\n\nfunction handleRequest(req: IncomingMessage, res: ServerResponse, state: HandlerState): void {\n if (req.method !== 'POST') {\n res.statusCode = 405;\n res.end();\n return;\n }\n\n const pathParts = parseStepPath(req.url ?? '');\n if (!pathParts) {\n res.statusCode = 404;\n res.end();\n return;\n }\n const { step_id, operation_id } = pathParts;\n\n let size = 0;\n const chunks: Buffer[] = [];\n let tooLarge = false;\n\n req.on('data', (chunk: Buffer) => {\n size += chunk.length;\n if (size > MAX_BODY_BYTES) {\n tooLarge = true;\n req.destroy();\n return;\n }\n chunks.push(chunk);\n });\n\n req.on('end', () => {\n if (tooLarge) {\n if (!res.headersSent) {\n res.statusCode = 413;\n res.end();\n }\n return;\n }\n const raw = Buffer.concat(chunks).toString('utf8');\n const headers = normalizeHeaders(req.headers);\n\n const key = retryKeyString({ step_id, operation_id });\n const deliveryIndex = (state.deliveryCounts.get(key) ?? 0) + 1;\n\n // Back-pressure on capture exhaustion — signal 503 so a conformant\n // sender retries later, drop the capture so the runner's memory\n // footprint stays bounded. Drops a delivery quietly is worse than\n // surfacing a clearly observable refusal.\n const perKey = countPerKey(state.captured, step_id, operation_id);\n if (state.captured.length >= MAX_CAPTURED_TOTAL || perKey >= MAX_CAPTURED_PER_KEY) {\n res.statusCode = 503;\n res.setHeader('retry-after', '1');\n res.end();\n return;\n }\n\n state.deliveryCounts.set(key, deliveryIndex);\n const status = nextResponseStatus(state.retryPolicies, key);\n\n const webhook: CapturedWebhook = {\n id: randomUUID(),\n step_id,\n operation_id,\n delivery_index: deliveryIndex,\n received_at: Date.now(),\n method: req.method ?? 'POST',\n path: req.url ?? '/',\n headers: redactHeaders(headers),\n raw_body: raw,\n ...parseBody(raw, headers['content-type']),\n response_status: status,\n };\n state.captured.push(webhook);\n\n // Deliver to any matching waiters. First-match-wins per waiter; later\n // arrivals may still deliver to other waiters in the queue.\n for (let i = state.waiters.length - 1; i >= 0; i--) {\n const waiter = state.waiters[i]!;\n if (matchesFilter(webhook, waiter.filter)) {\n clearTimeout(waiter.timer);\n state.waiters.splice(i, 1);\n waiter.resolve({ webhook });\n }\n }\n\n res.statusCode = status;\n res.end();\n });\n\n req.on('error', () => {\n if (!res.headersSent) {\n res.statusCode = 400;\n res.end();\n }\n });\n}\n\nfunction parseStepPath(reqUrl: string): { step_id: string; operation_id: string } | undefined {\n const q = reqUrl.indexOf('?');\n const pathname = q === -1 ? reqUrl : reqUrl.slice(0, q);\n const match = STEP_PATH_RE.exec(pathname);\n if (!match) return undefined;\n return { step_id: match[1]!, operation_id: match[2]! };\n}\n\nfunction normalizeHeaders(raw: IncomingMessage['headers']): Record<string, string> {\n const out: Record<string, string> = {};\n for (const [key, value] of Object.entries(raw)) {\n if (value === undefined) continue;\n out[key.toLowerCase()] = Array.isArray(value) ? value.join(', ') : value;\n }\n return out;\n}\n\nfunction redactHeaders(headers: Record<string, string>): Record<string, string> {\n const out: Record<string, string> = {};\n for (const [k, v] of Object.entries(headers)) {\n out[k] = SECRET_HEADER_PATTERN.test(k) ? '[redacted]' : v;\n }\n return out;\n}\n\nfunction countPerKey(captured: ReadonlyArray<CapturedWebhook>, step_id: string, operation_id: string): number {\n let n = 0;\n for (const w of captured) if (w.step_id === step_id && w.operation_id === operation_id) n++;\n return n;\n}\n\nfunction parseBody(raw: string, contentType: string | undefined): Pick<CapturedWebhook, 'body' | 'parse_error'> {\n if (raw.length === 0) return {};\n const looksJson = !contentType || /json/i.test(contentType);\n if (!looksJson) return {};\n try {\n return { body: JSON.parse(raw) };\n } catch (err) {\n return { parse_error: err instanceof Error ? err.message : String(err) };\n }\n}\n\n// ────────────────────────────────────────────────────────────\n// Filter matching\n// ────────────────────────────────────────────────────────────\n\nfunction matchesFilter(webhook: CapturedWebhook, filter: WebhookFilter): boolean {\n if (filter.step_id && webhook.step_id !== filter.step_id) return false;\n if (filter.operation_id && webhook.operation_id !== filter.operation_id) return false;\n if (filter.body && Object.keys(filter.body).length > 0) {\n if (webhook.body === undefined) return false;\n for (const [path, expected] of Object.entries(filter.body)) {\n const actual = resolveDottedPath(webhook.body, path);\n if (!deepEqual(actual, expected)) return false;\n }\n }\n return true;\n}\n\nfunction resolveDottedPath(obj: unknown, path: string): unknown {\n if (obj === null || obj === undefined) return undefined;\n const segments = path.split('.');\n let cursor: unknown = obj;\n for (const seg of segments) {\n if (cursor === null || cursor === undefined) return undefined;\n if (typeof cursor !== 'object') return undefined;\n cursor = (cursor as Record<string, unknown>)[seg];\n }\n return cursor;\n}\n\n/**\n * Structural deep-equal. Stringify-based comparison is key-order-sensitive\n * (`{a:1,b:2}` !== `{b:2,a:1}`); for filter bodies that's a false negative\n * when a publisher's JSON serializer emits fields in a different order\n * than the storyboard author. Recurse explicitly instead.\n */\nfunction deepEqual(a: unknown, b: unknown): boolean {\n if (a === b) return true;\n if (typeof a !== typeof b) return false;\n if (a === null || b === null) return a === b;\n if (typeof a !== 'object') return false;\n if (Array.isArray(a) || Array.isArray(b)) {\n if (!Array.isArray(a) || !Array.isArray(b)) return false;\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (!deepEqual(a[i], b[i])) return false;\n }\n return true;\n }\n const keysA = Object.keys(a as Record<string, unknown>);\n const keysB = Object.keys(b as Record<string, unknown>);\n if (keysA.length !== keysB.length) return false;\n for (const k of keysA) {\n if (!Object.prototype.hasOwnProperty.call(b, k)) return false;\n if (!deepEqual((a as Record<string, unknown>)[k], (b as Record<string, unknown>)[k])) return false;\n }\n return true;\n}\n\n// ────────────────────────────────────────────────────────────\n// Retry-replay policy\n// ────────────────────────────────────────────────────────────\n\nfunction nextResponseStatus(\n retryPolicies: Map<string, { policy: RetryReplayPolicy; delivered: number }>,\n key: string\n): number {\n const entry = retryPolicies.get(key);\n if (!entry) return 204;\n if (entry.delivered < entry.policy.count) {\n entry.delivered += 1;\n return entry.policy.http_status;\n }\n return 204;\n}\n\n// ────────────────────────────────────────────────────────────\n// wait / wait_all\n// ────────────────────────────────────────────────────────────\n\nfunction wait(\n filter: WebhookFilter,\n timeout_ms: number,\n captured: CapturedWebhook[],\n waiters: Array<{ filter: WebhookFilter; resolve: (r: WebhookWaitResult) => void; timer: NodeJS.Timeout }>\n): Promise<WebhookWaitResult> {\n const already = captured.find(w => matchesFilter(w, filter));\n if (already) return Promise.resolve({ webhook: already });\n return new Promise<WebhookWaitResult>(resolve => {\n const timer = setTimeout(\n () => {\n const idx = waiters.findIndex(w => w.timer === timer);\n if (idx >= 0) waiters.splice(idx, 1);\n resolve({ timed_out: true });\n },\n Math.max(0, timeout_ms)\n );\n timer.unref?.();\n waiters.push({ filter, resolve, timer });\n });\n}\n\n/**\n * Poll until the deadline, then return every capture matching `filter`.\n * Used when the assertion needs the whole delivery sequence (retry-stability).\n * Waits a minimum of `timeout_ms` even if matches arrive earlier — callers\n * that need \"resolve on first match\" should use `wait` instead.\n */\nfunction waitAll(filter: WebhookFilter, timeout_ms: number, captured: CapturedWebhook[]): Promise<CapturedWebhook[]> {\n return new Promise<CapturedWebhook[]>(resolve => {\n const timer = setTimeout(\n () => {\n resolve(captured.filter(w => matchesFilter(w, filter)));\n },\n Math.max(0, timeout_ms)\n );\n timer.unref?.();\n });\n}\n\n// ────────────────────────────────────────────────────────────\n// close\n// ────────────────────────────────────────────────────────────\n\nfunction closeServer(\n server: Server,\n waiters: Array<{ filter: WebhookFilter; resolve: (r: WebhookWaitResult) => void; timer: NodeJS.Timeout }>\n): Promise<void> {\n while (waiters.length > 0) {\n const w = waiters.pop()!;\n clearTimeout(w.timer);\n w.resolve({ timed_out: true });\n }\n // Force-close keep-alive sockets so shutdown doesn't hang up to\n // keepAliveTimeout waiting for idle connections to drain.\n server.closeAllConnections?.();\n return new Promise<void>((resolve, reject) => {\n server.close(err => {\n if (err && (err as NodeJS.ErrnoException).code !== 'ERR_SERVER_NOT_RUNNING') reject(err);\n else resolve();\n });\n });\n}\n\nfunction formatHost(address: string): string {\n return address.includes(':') ? `[${address}]` : address;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../../src/lib/testing/storyboard/webhook-receiver.ts"],"sourcesContent":["/**\n * Ephemeral webhook receiver for storyboard runs.\n *\n * The runner binds an HTTP listener, mints per-step URLs under\n * `/step/<step_id>/<operation_id>`, and captures inbound POSTs so\n * `expect_webhook*` pseudo-steps can observe outbound webhook behavior\n * (idempotency_key presence, retry-key stability, 9421 signature validity).\n *\n * Matches the `webhook_receiver_runner` test-kit contract in\n * adcontextprotocol/adcp#2431:\n * - Per-step URL template `{{runner.webhook_base}}/step/<step_id>/<operation_id>`.\n * - Retry-replay: the receiver returns a configurable 5xx for the first N\n * deliveries on a given `(step_id, operation_id)`, then 2xx, so senders\n * are forced to retry and retry-key stability can be graded.\n * - Loopback-mock mode (HTTP on 127.0.0.1, zero external deps) by default;\n * proxy-URL mode swaps in an operator-supplied public base.\n */\n\nimport { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http';\nimport { randomUUID } from 'node:crypto';\nimport type { AddressInfo } from 'node:net';\n\n/**\n * Size cap on any single webhook body. A non-conformant sender retrying\n * with oversize payloads shouldn't be able to exhaust the runner's memory.\n */\nconst MAX_BODY_BYTES = 1_048_576; // 1 MiB\n\n/**\n * Caps on cumulative capture state. `MAX_CAPTURED_TOTAL` bounds the total\n * number of webhooks retained for the run; `MAX_CAPTURED_PER_KEY` bounds\n * per-(step_id, operation_id) deliveries — a publisher stuck in a retry\n * loop can't exhaust memory. Once a cap is hit the receiver stops retaining\n * bodies but continues the configured retry-status sequence (or returns 204\n * when no rejection remains), so the cap cannot trap a sender in a retry loop.\n */\nconst MAX_CAPTURED_TOTAL = 1000;\nconst MAX_CAPTURED_PER_KEY = 100;\n\n/**\n * HTTP transport hardening. Defaults in Node 20 are generous (headers\n * 60s, no body timeout). A hostile / slow publisher shouldn't be able to\n * hold the runner's event loop with trickled requests or exhaust the\n * socket table.\n */\nconst HEADERS_TIMEOUT_MS = 10_000;\nconst REQUEST_TIMEOUT_MS = 30_000;\nconst KEEP_ALIVE_TIMEOUT_MS = 5_000;\nconst MAX_CONNECTIONS = 64;\nconst MAX_HEADERS_COUNT = 64;\n\n/** Default path shape under the receiver base. */\nconst STEP_PATH_RE = /^\\/step\\/([A-Za-z0-9_]+)\\/([A-Za-z0-9_-]+)\\/?$/;\n\n/**\n * Header names whose values are redacted before a captured webhook lands\n * in a compliance report. Mirrors the `SECRET_KEY_PATTERN` in runner.ts\n * so both paths honor the contract's \"Secrets SHOULD be redacted\" rule.\n */\nconst SECRET_HEADER_PATTERN =\n /^(authorization|credentials?|token|api[_-]?key|x-api[_-]?key|x-auth[_-]?token|password|secret|client[_-]secret|refresh[_-]token|access[_-]token|bearer|session[_-]token|cookie|set[_-]cookie)$/i;\n\n/**\n * Strip user-info, scheme-policed, and CRLF/NUL-rejected proxy URLs so a\n * misconfigured operator (or a compromised storyboard loader) can't\n * advertise an `http://169.254.169.254`, `file://…`, or header-splitting\n * URL that would then be embedded in outbound `push_notification_config.url`.\n */\nfunction validateProxyUrl(raw: string): string {\n if (/[\\r\\n\\x00]/.test(raw)) {\n throw new Error('webhook_receiver.public_url must not contain CR/LF/NUL');\n }\n let parsed: URL;\n try {\n parsed = new URL(raw);\n } catch {\n throw new Error(`webhook_receiver.public_url is not a valid URL: ${raw}`);\n }\n if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {\n throw new Error(`webhook_receiver.public_url must be http(s); got ${parsed.protocol}`);\n }\n if (parsed.username || parsed.password) {\n throw new Error('webhook_receiver.public_url must not include userinfo');\n }\n return raw.replace(/\\/$/, '');\n}\n\nexport interface CapturedWebhook {\n /** Runner-assigned id; stable across filter matches. */\n id: string;\n /** Step id extracted from the URL path. */\n step_id: string;\n /** Operation id extracted from the URL path. */\n operation_id: string;\n /** Delivery sequence number for this (step_id, operation_id) — 1-based. */\n delivery_index: number;\n /** Wall-clock millis at receipt. */\n received_at: number;\n method: string;\n /** Path + query. */\n path: string;\n /** Lowercased header names; duplicate values joined with \", \". */\n headers: Record<string, string>;\n /** Raw request body — needed for RFC 9421 content-digest recompute. */\n raw_body: string;\n /** Parsed JSON body; undefined on parse failure. */\n body?: unknown;\n /** Parse error message when raw_body was non-empty but not valid JSON. */\n parse_error?: string;\n /** HTTP status the receiver returned (reflects retry-replay policy). */\n response_status: number;\n}\n\n/** Match predicate applied when waiting for a webhook. */\nexport interface WebhookFilter {\n /** Restrict matches to this step id's URL. */\n step_id?: string;\n /** Restrict matches to this operation id. */\n operation_id?: string;\n /** Dotted-path → expected-value pairs compared against the parsed body. */\n body?: Record<string, unknown>;\n}\n\nexport type WebhookWaitResult =\n | { webhook: CapturedWebhook; timed_out?: false }\n | { timed_out: true; webhook?: undefined };\n\nexport interface RetryReplayPolicy {\n /** How many deliveries to reject before accepting. */\n count: number;\n /** HTTP status to return on rejection. */\n http_status: number;\n}\n\nexport interface WebhookReceiver {\n /** Public base URL (`http://host:port` for loopback, operator URL for proxy). */\n readonly base_url: string;\n /** Whether the receiver is in loopback-mock or proxy-url mode. */\n readonly mode: 'loopback_mock' | 'proxy_url';\n /** All webhooks captured so far, in arrival order. */\n all(): CapturedWebhook[];\n /** Webhooks matching a filter, in arrival order. */\n matching(filter: WebhookFilter): CapturedWebhook[];\n /**\n * Configure the receiver to return `http_status` for the first `count`\n * deliveries at `(step_id, operation_id)` and 2xx afterwards. Idempotent:\n * setting a new policy replaces any existing one for the same key.\n */\n set_retry_replay(key: { step_id: string; operation_id: string }, policy: RetryReplayPolicy): void;\n /** Resolve with the first match within `timeout_ms`, or `timed_out: true`. */\n wait(filter: WebhookFilter, timeout_ms: number): Promise<WebhookWaitResult>;\n /**\n * Resolve with every match observed between call time and deadline. Used\n * by `expect_webhook_retry_keys_stable` which needs the full sequence of\n * deliveries for the retry-replay assertion.\n */\n wait_all(filter: WebhookFilter, timeout_ms: number): Promise<CapturedWebhook[]>;\n /** Stop the listener. Idempotent. */\n close(): Promise<void>;\n}\n\nexport interface CreateWebhookReceiverOptions {\n mode?: 'loopback_mock' | 'proxy_url';\n /** Bind host for the local listener. Defaults to `127.0.0.1`. */\n host?: string;\n /** Bind port. `0` (default) lets the kernel assign one. */\n port?: number;\n /** Public URL to advertise when `mode: 'proxy_url'`. */\n public_url?: string;\n}\n\ninterface RetryKey {\n step_id: string;\n operation_id: string;\n}\n\nfunction retryKeyString(k: RetryKey): string {\n return `${k.step_id}::${k.operation_id}`;\n}\n\nexport async function createWebhookReceiver(options: CreateWebhookReceiverOptions = {}): Promise<WebhookReceiver> {\n const mode = options.mode ?? 'loopback_mock';\n if (mode === 'proxy_url' && !options.public_url) {\n throw new Error('webhook_receiver.mode=proxy_url requires `public_url`');\n }\n\n const host = options.host ?? '127.0.0.1';\n // A loopback_mock receiver bound on 0.0.0.0 is publicly reachable from any\n // interface — which would turn a CI runner into an open POST endpoint.\n // Operators who genuinely want public exposure use mode=proxy_url.\n if (mode === 'loopback_mock' && (host === '0.0.0.0' || host === '::')) {\n throw new Error(\n `webhook_receiver host ${host} is not permitted in loopback_mock mode. ` +\n 'Use mode=proxy_url with an explicit public_url for publicly-reachable runs.'\n );\n }\n const port = options.port ?? 0;\n const proxyBase = mode === 'proxy_url' ? validateProxyUrl(options.public_url!) : undefined;\n\n const captured: CapturedWebhook[] = [];\n const waiters: Array<{\n filter: WebhookFilter;\n resolve: (result: WebhookWaitResult) => void;\n timer: NodeJS.Timeout;\n }> = [];\n const waitAllTimers: Array<{\n filter: WebhookFilter;\n resolve: (result: CapturedWebhook[]) => void;\n timer: NodeJS.Timeout;\n }> = [];\n const retryPolicies = new Map<string, { policy: RetryReplayPolicy; delivered: number }>();\n const deliveryCounts = new Map<string, number>();\n let closed = false;\n\n const server = createServer((req, res) =>\n handleRequest(req, res, { captured, waiters, retryPolicies, deliveryCounts })\n );\n // Transport-level hardening — trim Node's generous defaults so a slow /\n // hostile publisher can't wedge the runner.\n server.headersTimeout = HEADERS_TIMEOUT_MS;\n server.requestTimeout = REQUEST_TIMEOUT_MS;\n server.keepAliveTimeout = KEEP_ALIVE_TIMEOUT_MS;\n server.maxConnections = MAX_CONNECTIONS;\n server.maxHeadersCount = MAX_HEADERS_COUNT;\n\n await new Promise<void>((resolve, reject) => {\n const onError = (err: Error) => reject(err);\n server.once('error', onError);\n server.listen(port, host, () => {\n server.off('error', onError);\n resolve();\n });\n });\n\n const bound = server.address() as AddressInfo;\n const base_url = proxyBase ?? `http://${formatHost(bound.address)}:${bound.port}`;\n\n return {\n base_url,\n mode,\n all: () => captured.slice(),\n matching: filter => captured.filter(w => matchesFilter(w, filter)),\n set_retry_replay: (key, policy) => {\n retryPolicies.set(retryKeyString(key), { policy, delivered: 0 });\n },\n wait: (filter, timeout_ms) => {\n if (!closed) return wait(filter, timeout_ms, captured, waiters);\n const already = captured.find(w => matchesFilter(w, filter));\n return Promise.resolve(already ? { webhook: already } : { timed_out: true });\n },\n wait_all: (filter, timeout_ms) =>\n closed\n ? Promise.resolve(captured.filter(w => matchesFilter(w, filter)))\n : waitAll(filter, timeout_ms, captured, waitAllTimers),\n close: () => {\n closed = true;\n return closeServer(server, captured, waiters, waitAllTimers);\n },\n };\n}\n\n// ────────────────────────────────────────────────────────────\n// Request handling\n// ────────────────────────────────────────────────────────────\n\ninterface HandlerState {\n captured: CapturedWebhook[];\n waiters: Array<{\n filter: WebhookFilter;\n resolve: (r: WebhookWaitResult) => void;\n timer: NodeJS.Timeout;\n }>;\n retryPolicies: Map<string, { policy: RetryReplayPolicy; delivered: number }>;\n deliveryCounts: Map<string, number>;\n}\n\nfunction handleRequest(req: IncomingMessage, res: ServerResponse, state: HandlerState): void {\n if (req.method !== 'POST') {\n res.statusCode = 405;\n res.end();\n return;\n }\n\n const pathParts = parseStepPath(req.url ?? '');\n if (!pathParts) {\n res.statusCode = 404;\n res.end();\n return;\n }\n const { step_id, operation_id } = pathParts;\n\n let size = 0;\n const chunks: Buffer[] = [];\n let tooLarge = false;\n\n req.on('data', (chunk: Buffer) => {\n size += chunk.length;\n if (size > MAX_BODY_BYTES) {\n tooLarge = true;\n req.destroy();\n return;\n }\n chunks.push(chunk);\n });\n\n req.on('end', () => {\n if (tooLarge) {\n if (!res.headersSent) {\n res.statusCode = 413;\n res.end();\n }\n return;\n }\n const raw = Buffer.concat(chunks).toString('utf8');\n const headers = normalizeHeaders(req.headers);\n\n const key = retryKeyString({ step_id, operation_id });\n const deliveryIndex = (state.deliveryCounts.get(key) ?? 0) + 1;\n const status = nextResponseStatus(state.retryPolicies, key);\n\n // Stop retaining bodies once a capture cap is exhausted, but preserve the\n // configured retry-replay status sequence. Unconditionally returning 503\n // here masks the policy's eventual 2xx forever and can amplify a sender's\n // retries after the runner has already reached its memory bound (#2653).\n const perKey = state.deliveryCounts.get(key) ?? 0;\n if (state.captured.length >= MAX_CAPTURED_TOTAL || perKey >= MAX_CAPTURED_PER_KEY) {\n res.statusCode = status;\n if (status === 429 || status >= 500) res.setHeader('retry-after', '1');\n res.end();\n return;\n }\n\n state.deliveryCounts.set(key, deliveryIndex);\n\n const webhook: CapturedWebhook = {\n id: randomUUID(),\n step_id,\n operation_id,\n delivery_index: deliveryIndex,\n received_at: Date.now(),\n method: req.method ?? 'POST',\n path: req.url ?? '/',\n headers: redactHeaders(headers),\n raw_body: raw,\n ...parseBody(raw, headers['content-type']),\n response_status: status,\n };\n state.captured.push(webhook);\n\n // Deliver to any matching waiters. First-match-wins per waiter; later\n // arrivals may still deliver to other waiters in the queue.\n for (let i = state.waiters.length - 1; i >= 0; i--) {\n const waiter = state.waiters[i]!;\n if (matchesFilter(webhook, waiter.filter)) {\n clearTimeout(waiter.timer);\n state.waiters.splice(i, 1);\n waiter.resolve({ webhook });\n }\n }\n\n res.statusCode = status;\n res.end();\n });\n\n req.on('error', () => {\n if (!res.headersSent) {\n res.statusCode = 400;\n res.end();\n }\n });\n}\n\nfunction parseStepPath(reqUrl: string): { step_id: string; operation_id: string } | undefined {\n const q = reqUrl.indexOf('?');\n const pathname = q === -1 ? reqUrl : reqUrl.slice(0, q);\n const match = STEP_PATH_RE.exec(pathname);\n if (!match) return undefined;\n return { step_id: match[1]!, operation_id: match[2]! };\n}\n\nfunction normalizeHeaders(raw: IncomingMessage['headers']): Record<string, string> {\n const out: Record<string, string> = {};\n for (const [key, value] of Object.entries(raw)) {\n if (value === undefined) continue;\n out[key.toLowerCase()] = Array.isArray(value) ? value.join(', ') : value;\n }\n return out;\n}\n\nfunction redactHeaders(headers: Record<string, string>): Record<string, string> {\n const out: Record<string, string> = {};\n for (const [k, v] of Object.entries(headers)) {\n out[k] = SECRET_HEADER_PATTERN.test(k) ? '[redacted]' : v;\n }\n return out;\n}\n\nfunction parseBody(raw: string, contentType: string | undefined): Pick<CapturedWebhook, 'body' | 'parse_error'> {\n if (raw.length === 0) return {};\n const looksJson = !contentType || /json/i.test(contentType);\n if (!looksJson) return {};\n try {\n return { body: JSON.parse(raw) };\n } catch (err) {\n return { parse_error: err instanceof Error ? err.message : String(err) };\n }\n}\n\n// ────────────────────────────────────────────────────────────\n// Filter matching\n// ────────────────────────────────────────────────────────────\n\nfunction matchesFilter(webhook: CapturedWebhook, filter: WebhookFilter): boolean {\n if (filter.step_id && webhook.step_id !== filter.step_id) return false;\n if (filter.operation_id && webhook.operation_id !== filter.operation_id) return false;\n if (filter.body && Object.keys(filter.body).length > 0) {\n if (webhook.body === undefined) return false;\n for (const [path, expected] of Object.entries(filter.body)) {\n const actual = resolveDottedPath(webhook.body, path);\n if (!deepEqual(actual, expected)) return false;\n }\n }\n return true;\n}\n\nfunction resolveDottedPath(obj: unknown, path: string): unknown {\n if (obj === null || obj === undefined) return undefined;\n const segments = path.split('.');\n let cursor: unknown = obj;\n for (const seg of segments) {\n if (cursor === null || cursor === undefined) return undefined;\n if (typeof cursor !== 'object') return undefined;\n cursor = (cursor as Record<string, unknown>)[seg];\n }\n return cursor;\n}\n\n/**\n * Structural deep-equal. Stringify-based comparison is key-order-sensitive\n * (`{a:1,b:2}` !== `{b:2,a:1}`); for filter bodies that's a false negative\n * when a publisher's JSON serializer emits fields in a different order\n * than the storyboard author. Recurse explicitly instead.\n */\nfunction deepEqual(a: unknown, b: unknown): boolean {\n if (a === b) return true;\n if (typeof a !== typeof b) return false;\n if (a === null || b === null) return a === b;\n if (typeof a !== 'object') return false;\n if (Array.isArray(a) || Array.isArray(b)) {\n if (!Array.isArray(a) || !Array.isArray(b)) return false;\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (!deepEqual(a[i], b[i])) return false;\n }\n return true;\n }\n const keysA = Object.keys(a as Record<string, unknown>);\n const keysB = Object.keys(b as Record<string, unknown>);\n if (keysA.length !== keysB.length) return false;\n for (const k of keysA) {\n if (!Object.prototype.hasOwnProperty.call(b, k)) return false;\n if (!deepEqual((a as Record<string, unknown>)[k], (b as Record<string, unknown>)[k])) return false;\n }\n return true;\n}\n\n// ────────────────────────────────────────────────────────────\n// Retry-replay policy\n// ────────────────────────────────────────────────────────────\n\nfunction nextResponseStatus(\n retryPolicies: Map<string, { policy: RetryReplayPolicy; delivered: number }>,\n key: string\n): number {\n const entry = retryPolicies.get(key);\n if (!entry) return 204;\n if (entry.delivered < entry.policy.count) {\n entry.delivered += 1;\n return entry.policy.http_status;\n }\n return 204;\n}\n\n// ────────────────────────────────────────────────────────────\n// wait / wait_all\n// ────────────────────────────────────────────────────────────\n\nfunction wait(\n filter: WebhookFilter,\n timeout_ms: number,\n captured: CapturedWebhook[],\n waiters: Array<{ filter: WebhookFilter; resolve: (r: WebhookWaitResult) => void; timer: NodeJS.Timeout }>\n): Promise<WebhookWaitResult> {\n const already = captured.find(w => matchesFilter(w, filter));\n if (already) return Promise.resolve({ webhook: already });\n return new Promise<WebhookWaitResult>(resolve => {\n const timer = setTimeout(\n () => {\n const idx = waiters.findIndex(w => w.timer === timer);\n if (idx >= 0) waiters.splice(idx, 1);\n resolve({ timed_out: true });\n },\n Math.max(0, timeout_ms)\n );\n timer.unref?.();\n waiters.push({ filter, resolve, timer });\n });\n}\n\n/**\n * Poll until the deadline, then return every capture matching `filter`.\n * Used when the assertion needs the whole delivery sequence (retry-stability).\n * Waits a minimum of `timeout_ms` even if matches arrive earlier — callers\n * that need \"resolve on first match\" should use `wait` instead.\n */\nfunction waitAll(\n filter: WebhookFilter,\n timeout_ms: number,\n captured: CapturedWebhook[],\n waitAllTimers: Array<{\n filter: WebhookFilter;\n resolve: (result: CapturedWebhook[]) => void;\n timer: NodeJS.Timeout;\n }>\n): Promise<CapturedWebhook[]> {\n return new Promise<CapturedWebhook[]>(resolve => {\n const timer = setTimeout(\n () => {\n const idx = waitAllTimers.findIndex(entry => entry.timer === timer);\n if (idx >= 0) waitAllTimers.splice(idx, 1);\n resolve(captured.filter(w => matchesFilter(w, filter)));\n },\n Math.max(0, timeout_ms)\n );\n timer.unref?.();\n waitAllTimers.push({ filter, resolve, timer });\n });\n}\n\n// ────────────────────────────────────────────────────────────\n// close\n// ────────────────────────────────────────────────────────────\n\nfunction closeServer(\n server: Server,\n captured: CapturedWebhook[],\n waiters: Array<{ filter: WebhookFilter; resolve: (r: WebhookWaitResult) => void; timer: NodeJS.Timeout }>,\n waitAllTimers: Array<{\n filter: WebhookFilter;\n resolve: (result: CapturedWebhook[]) => void;\n timer: NodeJS.Timeout;\n }>\n): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n server.close(err => {\n if (err && (err as NodeJS.ErrnoException).code !== 'ERR_SERVER_NOT_RUNNING') reject(err);\n else resolve();\n });\n // Stop accepting new sockets before force-closing existing keep-alive\n // connections; the opposite order leaves a small acceptance race.\n server.closeAllConnections?.();\n while (waiters.length > 0) {\n const w = waiters.pop()!;\n clearTimeout(w.timer);\n w.resolve({ timed_out: true });\n }\n while (waitAllTimers.length > 0) {\n const pending = waitAllTimers.pop()!;\n clearTimeout(pending.timer);\n pending.resolve(captured.filter(w => matchesFilter(w, pending.filter)));\n }\n });\n}\n\nfunction formatHost(address: string): string {\n return address.includes(':') ? `[${address}]` : address;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import type { FormatReferenceStructuredObject as FormatID } from '../types/core.generated.mjs';
|
|
5
5
|
import type { AdcpVersion } from '../version.mjs';
|
|
6
6
|
import type { TransportOptions, VersionEnvelopeMode } from '../protocols/index.mjs';
|
|
7
|
+
import type { AgentRequestSigningConfig } from '../types/adcp.mjs';
|
|
7
8
|
export type TestScenario = 'health_check' | 'discovery' | 'create_media_buy' | 'full_sales_flow' | 'reporting_flow' | 'creative_sync' | 'creative_inline' | 'creative_reference' | 'pricing_models' | 'creative_flow' | 'creative_lifecycle' | 'signals_flow' | 'error_handling' | 'validation' | 'pricing_edge_cases' | 'temporal_validation' | 'behavior_analysis' | 'response_consistency' | 'governance_property_lists' | 'governance_content_standards' | 'property_list_filters' | 'campaign_governance' | 'campaign_governance_denied' | 'campaign_governance_conditions' | 'campaign_governance_delivery' | 'seller_governance_context' | 'si_session_lifecycle' | 'si_availability' | 'si_handoff' | 'capability_discovery' | 'schema_compliance' | 'sync_audiences' | 'media_buy_lifecycle' | 'terminal_state_enforcement' | 'package_lifecycle' | 'error_codes' | 'error_structure' | 'error_transport' | 'deterministic_creative' | 'deterministic_media_buy' | 'deterministic_account' | 'deterministic_session' | 'deterministic_delivery' | 'deterministic_budget' | 'controller_validation' | 'brand_identity' | 'brand_rights_flow' | 'creative_approval';
|
|
8
9
|
export interface TestOptions {
|
|
9
10
|
protocol?: 'mcp' | 'a2a';
|
|
@@ -15,6 +16,15 @@ export interface TestOptions {
|
|
|
15
16
|
* enforce a request-scoped network policy without replacing global fetch.
|
|
16
17
|
*/
|
|
17
18
|
transport?: TransportOptions;
|
|
19
|
+
/**
|
|
20
|
+
* RFC 9421 identity used for ordinary functional test/storyboard calls.
|
|
21
|
+
* Distinct from `StoryboardRunOptions.request_signing`, which configures
|
|
22
|
+
* the dedicated verifier-vector grader. Compliance runners populate this
|
|
23
|
+
* automatically from `signed-requests-runner.yaml > functional_dispatch`
|
|
24
|
+
* only for sandbox runs; callers may provide their own key/provider for a
|
|
25
|
+
* different trusted environment.
|
|
26
|
+
*/
|
|
27
|
+
functional_request_signing?: AgentRequestSigningConfig;
|
|
18
28
|
/**
|
|
19
29
|
* AdCP protocol version the test client should speak. Storyboard runners
|
|
20
30
|
* set this from the compliance cache version instead of relying on the
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import type { FormatReferenceStructuredObject as FormatID } from '../types/core.generated';
|
|
5
5
|
import type { AdcpVersion } from '../version';
|
|
6
6
|
import type { TransportOptions, VersionEnvelopeMode } from '../protocols';
|
|
7
|
+
import type { AgentRequestSigningConfig } from '../types/adcp';
|
|
7
8
|
export type TestScenario = 'health_check' | 'discovery' | 'create_media_buy' | 'full_sales_flow' | 'reporting_flow' | 'creative_sync' | 'creative_inline' | 'creative_reference' | 'pricing_models' | 'creative_flow' | 'creative_lifecycle' | 'signals_flow' | 'error_handling' | 'validation' | 'pricing_edge_cases' | 'temporal_validation' | 'behavior_analysis' | 'response_consistency' | 'governance_property_lists' | 'governance_content_standards' | 'property_list_filters' | 'campaign_governance' | 'campaign_governance_denied' | 'campaign_governance_conditions' | 'campaign_governance_delivery' | 'seller_governance_context' | 'si_session_lifecycle' | 'si_availability' | 'si_handoff' | 'capability_discovery' | 'schema_compliance' | 'sync_audiences' | 'media_buy_lifecycle' | 'terminal_state_enforcement' | 'package_lifecycle' | 'error_codes' | 'error_structure' | 'error_transport' | 'deterministic_creative' | 'deterministic_media_buy' | 'deterministic_account' | 'deterministic_session' | 'deterministic_delivery' | 'deterministic_budget' | 'controller_validation' | 'brand_identity' | 'brand_rights_flow' | 'creative_approval';
|
|
8
9
|
export interface TestOptions {
|
|
9
10
|
protocol?: 'mcp' | 'a2a';
|
|
@@ -15,6 +16,15 @@ export interface TestOptions {
|
|
|
15
16
|
* enforce a request-scoped network policy without replacing global fetch.
|
|
16
17
|
*/
|
|
17
18
|
transport?: TransportOptions;
|
|
19
|
+
/**
|
|
20
|
+
* RFC 9421 identity used for ordinary functional test/storyboard calls.
|
|
21
|
+
* Distinct from `StoryboardRunOptions.request_signing`, which configures
|
|
22
|
+
* the dedicated verifier-vector grader. Compliance runners populate this
|
|
23
|
+
* automatically from `signed-requests-runner.yaml > functional_dispatch`
|
|
24
|
+
* only for sandbox runs; callers may provide their own key/provider for a
|
|
25
|
+
* different trusted environment.
|
|
26
|
+
*/
|
|
27
|
+
functional_request_signing?: AgentRequestSigningConfig;
|
|
18
28
|
/**
|
|
19
29
|
* AdCP protocol version the test client should speak. Storyboard runners
|
|
20
30
|
* set this from the compliance cache version instead of relying on the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/lib/testing/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,+BAA+B,IAAI,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAG3F,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC9C,OAAO,KAAK,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/lib/testing/types.ts"],"names":[],"mappings":"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"}
|