@adcp/sdk 14.0.0-rc.52 → 14.0.0-rc.53

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/lib/registry/types.generated.d.mts +12 -0
  2. package/dist/lib/registry/types.generated.d.ts +12 -0
  3. package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
  4. package/dist/lib/server/auth-signature.d.mts +5 -2
  5. package/dist/lib/server/auth-signature.d.ts +5 -2
  6. package/dist/lib/signing/content-digest.d.mts +8 -0
  7. package/dist/lib/signing/content-digest.d.ts +8 -0
  8. package/dist/lib/signing/content-digest.js +13 -4
  9. package/dist/lib/signing/content-digest.mjs +11 -4
  10. package/dist/lib/signing/parser.d.mts +1 -1
  11. package/dist/lib/signing/parser.d.ts +1 -1
  12. package/dist/lib/signing/parser.js +3 -2
  13. package/dist/lib/signing/parser.mjs +3 -2
  14. package/dist/lib/signing/verifier.d.mts +6 -2
  15. package/dist/lib/signing/verifier.d.ts +6 -2
  16. package/dist/lib/signing/verifier.js +14 -10
  17. package/dist/lib/signing/verifier.mjs +15 -10
  18. package/dist/lib/testing/compliance/comply.d.mts +107 -1
  19. package/dist/lib/testing/compliance/comply.d.ts +107 -1
  20. package/dist/lib/testing/compliance/comply.js +313 -1
  21. package/dist/lib/testing/compliance/comply.mjs +320 -2
  22. package/dist/lib/testing/compliance/index.d.mts +1 -1
  23. package/dist/lib/testing/compliance/index.d.ts +1 -1
  24. package/dist/lib/testing/index.d.mts +1 -1
  25. package/dist/lib/testing/index.d.ts +1 -1
  26. package/dist/lib/testing/storyboard/runner.js +10 -0
  27. package/dist/lib/testing/storyboard/runner.mjs +8 -0
  28. package/dist/lib/testing/storyboard/validations.d.mts +1 -1
  29. package/dist/lib/testing/storyboard/validations.d.ts +1 -1
  30. package/dist/lib/version.d.mts +3 -3
  31. package/dist/lib/version.d.ts +3 -3
  32. package/dist/lib/version.js +3 -3
  33. package/dist/lib/version.mjs +3 -3
  34. package/docs/TYPE-SUMMARY.md +1 -1
  35. package/docs/guides/VALIDATE-YOUR-AGENT.md +49 -0
  36. package/docs/llms.txt +1 -1
  37. package/docs/migration-13-to-14.md +14 -7
  38. package/docs/migration-14.x-rc-worksheet.md +4 -4
  39. package/package.json +1 -1
@@ -7877,6 +7877,18 @@ export interface operations {
7877
7877
  "application/json": components["schemas"]["Error"];
7878
7878
  };
7879
7879
  };
7880
+ /** @description Agent does not offer public product browsing (no wholesale buying mode, or a buyer account is required) */
7881
+ 422: {
7882
+ headers: {
7883
+ [name: string]: unknown;
7884
+ };
7885
+ content: {
7886
+ "application/json": {
7887
+ error: string;
7888
+ message: string;
7889
+ };
7890
+ };
7891
+ };
7880
7892
  /** @description Rate limit exceeded */
7881
7893
  429: {
7882
7894
  headers: {
@@ -7877,6 +7877,18 @@ export interface operations {
7877
7877
  "application/json": components["schemas"]["Error"];
7878
7878
  };
7879
7879
  };
7880
+ /** @description Agent does not offer public product browsing (no wholesale buying mode, or a buyer account is required) */
7881
+ 422: {
7882
+ headers: {
7883
+ [name: string]: unknown;
7884
+ };
7885
+ content: {
7886
+ "application/json": {
7887
+ error: string;
7888
+ message: string;
7889
+ };
7890
+ };
7891
+ };
7880
7892
  /** @description Rate limit exceeded */
7881
7893
  429: {
7882
7894
  headers: {
@@ -4,5 +4,5 @@
4
4
  "source_sha": "4e553ad955f83b49c7d221ab5c3ff78237ad02e3",
5
5
  "source_tarball_sha256": "580656d6466ef9f0d1119985e6726c2efea718dc671e2ad30957fcb2fd54af0f",
6
6
  "upstream_adcp_version": "2.5.3",
7
- "synced_at": "2026-09-30T06:48:33.322Z"
7
+ "synced_at": "2026-09-30T09:55:20.074Z"
8
8
  }
@@ -69,8 +69,11 @@ export interface VerifySignatureAsAuthenticatorOptions {
69
69
  now?: () => number;
70
70
  /**
71
71
  * Trusted endpoint release pin used to select the 3.0/3.1 or 3.2 signature
72
- * profile. When omitted, verification accepts both encodings for SDK 13
73
- * compatibility while digest coverage follows `capability`.
72
+ * profile. Set it on every endpoint that advertises a 3.2 release: a 3.2
73
+ * pin rejects Base64URL `Signature` / `Content-Digest` values as
74
+ * `request_signature_header_malformed` whatever `capability` says. When
75
+ * omitted, verification accepts both encodings for SDK 13 compatibility
76
+ * while digest coverage follows `capability`.
74
77
  */
75
78
  adcpVersion?: string;
76
79
  /**
@@ -69,8 +69,11 @@ export interface VerifySignatureAsAuthenticatorOptions {
69
69
  now?: () => number;
70
70
  /**
71
71
  * Trusted endpoint release pin used to select the 3.0/3.1 or 3.2 signature
72
- * profile. When omitted, verification accepts both encodings for SDK 13
73
- * compatibility while digest coverage follows `capability`.
72
+ * profile. Set it on every endpoint that advertises a 3.2 release: a 3.2
73
+ * pin rejects Base64URL `Signature` / `Content-Digest` values as
74
+ * `request_signature_header_malformed` whatever `capability` says. When
75
+ * omitted, verification accepts both encodings for SDK 13 compatibility
76
+ * while digest coverage follows `capability`.
74
77
  */
75
78
  adcpVersion?: string;
76
79
  /**
@@ -8,5 +8,13 @@ export declare function computeContentDigest(body: string | Uint8Array, encoding
8
8
  * up the `sha-256` member without requiring any particular position.
9
9
  */
10
10
  export declare function parseContentDigest(header: string): Buffer | null;
11
+ /** Raw text of the `sha-256` byte-sequence token in a Content-Digest header, if present. */
12
+ export declare function contentDigestSha256Token(header: string): string | undefined;
13
+ /**
14
+ * True when `encoded` is canonical padded standard Base64 (RFC 4648 §4), the
15
+ * RFC 8941 `sf-binary` form AdCP 3.2 requires: `+`/`/` alphabet, length a
16
+ * multiple of four, and only the `=` padding the byte length requires.
17
+ */
18
+ export declare function isPaddedStandardBase64(encoded: string): boolean;
11
19
  export declare function contentDigestUsesEncoding(header: string, encoding: SfBinaryEncoding): boolean;
12
20
  export declare function contentDigestMatches(header: string, body: string | Uint8Array): boolean;
@@ -8,5 +8,13 @@ export declare function computeContentDigest(body: string | Uint8Array, encoding
8
8
  * up the `sha-256` member without requiring any particular position.
9
9
  */
10
10
  export declare function parseContentDigest(header: string): Buffer | null;
11
+ /** Raw text of the `sha-256` byte-sequence token in a Content-Digest header, if present. */
12
+ export declare function contentDigestSha256Token(header: string): string | undefined;
13
+ /**
14
+ * True when `encoded` is canonical padded standard Base64 (RFC 4648 §4), the
15
+ * RFC 8941 `sf-binary` form AdCP 3.2 requires: `+`/`/` alphabet, length a
16
+ * multiple of four, and only the `=` padding the byte length requires.
17
+ */
18
+ export declare function isPaddedStandardBase64(encoded: string): boolean;
11
19
  export declare function contentDigestUsesEncoding(header: string, encoding: SfBinaryEncoding): boolean;
12
20
  export declare function contentDigestMatches(header: string, body: string | Uint8Array): boolean;
@@ -20,7 +20,9 @@ var content_digest_exports = {};
20
20
  __export(content_digest_exports, {
21
21
  computeContentDigest: () => computeContentDigest,
22
22
  contentDigestMatches: () => contentDigestMatches,
23
+ contentDigestSha256Token: () => contentDigestSha256Token,
23
24
  contentDigestUsesEncoding: () => contentDigestUsesEncoding,
25
+ isPaddedStandardBase64: () => isPaddedStandardBase64,
24
26
  parseContentDigest: () => parseContentDigest,
25
27
  requestSigningEncodingForVersion: () => requestSigningEncodingForVersion
26
28
  });
@@ -47,12 +49,17 @@ function parseContentDigest(header) {
47
49
  const m = header.match(SHA256_MEMBER_RE);
48
50
  return m && m[1] ? Buffer.from(m[1], "base64") : null;
49
51
  }
52
+ function contentDigestSha256Token(header) {
53
+ return header.match(/(?:^|[,\s])sha-256=:([^:]+):/)?.[1];
54
+ }
55
+ function isPaddedStandardBase64(encoded) {
56
+ return encoded.length > 0 && encoded.length % 4 === 0 && /^[A-Za-z0-9+/]+={0,2}$/.test(encoded);
57
+ }
50
58
  function contentDigestUsesEncoding(header, encoding) {
51
- const match = header.match(/(?:^|[,\s])sha-256=:([^:]+):/);
52
- if (!match?.[1]) return false;
53
- const encoded = match[1];
59
+ const encoded = contentDigestSha256Token(header);
60
+ if (!encoded) return false;
54
61
  if (encoding === "legacy-base64url") return /^[A-Za-z0-9_-]+$/.test(encoded);
55
- return encoded.length % 4 === 0 && /^[A-Za-z0-9+/]+={1,2}$/.test(encoded);
62
+ return isPaddedStandardBase64(encoded);
56
63
  }
57
64
  function contentDigestMatches(header, body) {
58
65
  const expected = parseContentDigest(header);
@@ -70,7 +77,9 @@ function toBuffer(body) {
70
77
  0 && (module.exports = {
71
78
  computeContentDigest,
72
79
  contentDigestMatches,
80
+ contentDigestSha256Token,
73
81
  contentDigestUsesEncoding,
82
+ isPaddedStandardBase64,
74
83
  parseContentDigest,
75
84
  requestSigningEncodingForVersion
76
85
  });
@@ -20,12 +20,17 @@ function parseContentDigest(header) {
20
20
  const m = header.match(SHA256_MEMBER_RE);
21
21
  return m && m[1] ? Buffer.from(m[1], "base64") : null;
22
22
  }
23
+ function contentDigestSha256Token(header) {
24
+ return header.match(/(?:^|[,\s])sha-256=:([^:]+):/)?.[1];
25
+ }
26
+ function isPaddedStandardBase64(encoded) {
27
+ return encoded.length > 0 && encoded.length % 4 === 0 && /^[A-Za-z0-9+/]+={0,2}$/.test(encoded);
28
+ }
23
29
  function contentDigestUsesEncoding(header, encoding) {
24
- const match = header.match(/(?:^|[,\s])sha-256=:([^:]+):/);
25
- if (!match?.[1]) return false;
26
- const encoded = match[1];
30
+ const encoded = contentDigestSha256Token(header);
31
+ if (!encoded) return false;
27
32
  if (encoding === "legacy-base64url") return /^[A-Za-z0-9_-]+$/.test(encoded);
28
- return encoded.length % 4 === 0 && /^[A-Za-z0-9+/]+={1,2}$/.test(encoded);
33
+ return isPaddedStandardBase64(encoded);
29
34
  }
30
35
  function contentDigestMatches(header, body) {
31
36
  const expected = parseContentDigest(header);
@@ -42,7 +47,9 @@ function toBuffer(body) {
42
47
  export {
43
48
  computeContentDigest,
44
49
  contentDigestMatches,
50
+ contentDigestSha256Token,
45
51
  contentDigestUsesEncoding,
52
+ isPaddedStandardBase64,
46
53
  parseContentDigest,
47
54
  requestSigningEncodingForVersion
48
55
  };
@@ -1,4 +1,4 @@
1
- import type { SfBinaryEncoding } from './content-digest.mjs';
1
+ import { type SfBinaryEncoding } from './content-digest.mjs';
2
2
  export interface ParsedSignatureInput {
3
3
  label: string;
4
4
  components: string[];
@@ -1,4 +1,4 @@
1
- import type { SfBinaryEncoding } from './content-digest';
1
+ import { type SfBinaryEncoding } from './content-digest';
2
2
  export interface ParsedSignatureInput {
3
3
  label: string;
4
4
  components: string[];
@@ -25,6 +25,7 @@ __export(parser_exports, {
25
25
  module.exports = __toCommonJS(parser_exports);
26
26
  var import_structured_headers = require("structured-headers");
27
27
  var import_errors = require('./errors.js');
28
+ var import_content_digest = require('./content-digest.js');
28
29
  const STRING_PARAMS = /* @__PURE__ */ new Set(["nonce", "keyid", "alg", "tag"]);
29
30
  const INTEGER_PARAMS = /* @__PURE__ */ new Set(["created", "expires"]);
30
31
  function malformed(message) {
@@ -79,7 +80,7 @@ function parseSignature(headerValue, expectedLabel, encoding = "legacy-base64url
79
80
  const rawBytes = headerValue.match(new RegExp(`(?:^|,\\s*)${escapedLabel}\\s*=\\s*:([^:]+):`))?.[1];
80
81
  if (!rawBytes) malformed(`Signature header does not contain a byte sequence for label "${expectedLabel}"`);
81
82
  if (encoding === "rfc8941-base64") {
82
- if (rawBytes.length % 4 !== 0 || !/^[A-Za-z0-9+/]+={1,2}$/.test(rawBytes)) {
83
+ if (!(0, import_content_digest.isPaddedStandardBase64)(rawBytes)) {
83
84
  malformed("Signature value must use padded standard Base64 for AdCP 3.2+");
84
85
  }
85
86
  } else if (!/^[A-Za-z0-9_-]+$/.test(rawBytes)) {
@@ -87,7 +88,7 @@ function parseSignature(headerValue, expectedLabel, encoding = "legacy-base64url
87
88
  }
88
89
  let dict;
89
90
  try {
90
- dict = (0, import_structured_headers.parseDictionary)(normalizeByteSequenceBase64(headerValue));
91
+ dict = (0, import_structured_headers.parseDictionary)(encoding === "rfc8941-base64" ? headerValue : normalizeByteSequenceBase64(headerValue));
91
92
  } catch (e) {
92
93
  if (e instanceof import_structured_headers.ParseError) {
93
94
  if (/base64/i.test(e.message)) malformed("Signature value contains non-base64 characters");
@@ -1,5 +1,6 @@
1
1
  import { ParseError, parseDictionary, serializeInnerList } from "structured-headers";
2
2
  import { RequestSignatureError } from "./errors.mjs";
3
+ import { isPaddedStandardBase64 } from "./content-digest.mjs";
3
4
  const STRING_PARAMS = /* @__PURE__ */ new Set(["nonce", "keyid", "alg", "tag"]);
4
5
  const INTEGER_PARAMS = /* @__PURE__ */ new Set(["created", "expires"]);
5
6
  function malformed(message) {
@@ -54,7 +55,7 @@ function parseSignature(headerValue, expectedLabel, encoding = "legacy-base64url
54
55
  const rawBytes = headerValue.match(new RegExp(`(?:^|,\\s*)${escapedLabel}\\s*=\\s*:([^:]+):`))?.[1];
55
56
  if (!rawBytes) malformed(`Signature header does not contain a byte sequence for label "${expectedLabel}"`);
56
57
  if (encoding === "rfc8941-base64") {
57
- if (rawBytes.length % 4 !== 0 || !/^[A-Za-z0-9+/]+={1,2}$/.test(rawBytes)) {
58
+ if (!isPaddedStandardBase64(rawBytes)) {
58
59
  malformed("Signature value must use padded standard Base64 for AdCP 3.2+");
59
60
  }
60
61
  } else if (!/^[A-Za-z0-9_-]+$/.test(rawBytes)) {
@@ -62,7 +63,7 @@ function parseSignature(headerValue, expectedLabel, encoding = "legacy-base64url
62
63
  }
63
64
  let dict;
64
65
  try {
65
- dict = parseDictionary(normalizeByteSequenceBase64(headerValue));
66
+ dict = parseDictionary(encoding === "rfc8941-base64" ? headerValue : normalizeByteSequenceBase64(headerValue));
66
67
  } catch (e) {
67
68
  if (e instanceof ParseError) {
68
69
  if (/base64/i.test(e.message)) malformed("Signature value contains non-base64 characters");
@@ -19,8 +19,12 @@ export interface VerifyRequestOptions {
19
19
  operation?: string;
20
20
  /**
21
21
  * Trusted endpoint release pin; never inferred from request payload data.
22
- * When omitted, the verifier accepts both the legacy 3.0/3.1 Base64URL
23
- * representation and the 3.2+ RFC 8941 Base64 representation. Digest
22
+ * A 3.2+ pin parses `Signature` and `Content-Digest` strictly as RFC 8941
23
+ * padded standard Base64 and rejects Base64URL as
24
+ * `request_signature_header_malformed`, regardless of
25
+ * `capability.covers_content_digest`. When omitted, the verifier accepts
26
+ * both the legacy 3.0/3.1 Base64URL representation and the 3.2+ RFC 8941
27
+ * Base64 representation, so it cannot grade as a 3.2 verifier. Digest
24
28
  * coverage still follows `capability.covers_content_digest`.
25
29
  */
26
30
  adcpVersion?: string;
@@ -19,8 +19,12 @@ export interface VerifyRequestOptions {
19
19
  operation?: string;
20
20
  /**
21
21
  * Trusted endpoint release pin; never inferred from request payload data.
22
- * When omitted, the verifier accepts both the legacy 3.0/3.1 Base64URL
23
- * representation and the 3.2+ RFC 8941 Base64 representation. Digest
22
+ * A 3.2+ pin parses `Signature` and `Content-Digest` strictly as RFC 8941
23
+ * padded standard Base64 and rejects Base64URL as
24
+ * `request_signature_header_malformed`, regardless of
25
+ * `capability.covers_content_digest`. When omitted, the verifier accepts
26
+ * both the legacy 3.0/3.1 Base64URL representation and the 3.2+ RFC 8941
27
+ * Base64 representation, so it cannot grade as a 3.2 verifier. Digest
24
28
  * coverage still follows `capability.covers_content_digest`.
25
29
  */
26
30
  adcpVersion?: string;
@@ -92,7 +92,7 @@ async function verifyRequestSignature(request, options) {
92
92
  parsedEncoding = candidate;
93
93
  break;
94
94
  } catch (error) {
95
- parseError ??= error;
95
+ if (parseError === void 0 || candidate === (pinnedBinaryEncoding ?? "rfc8941-base64")) parseError = error;
96
96
  }
97
97
  }
98
98
  if (!parsedSig) throw parseError;
@@ -192,9 +192,16 @@ async function verifyRequestSignature(request, options) {
192
192
  }
193
193
  if (parsedInput.components.includes("content-digest")) {
194
194
  const digestHeader = (0, import_canonicalize.getHeaderValue)(request.headers, "Content-Digest");
195
- const encodingMatches = !!digestHeader && contentDigestEncodingCandidates(pinnedBinaryEncoding, options.capability.covers_content_digest).some(
195
+ const encodingMatches = !!digestHeader && contentDigestEncodingCandidates(pinnedBinaryEncoding).some(
196
196
  (candidate) => (0, import_content_digest.contentDigestUsesEncoding)(digestHeader, candidate)
197
197
  );
198
+ if (digestHeader && !encodingMatches && pinnedBinaryEncoding === "rfc8941-base64" && (0, import_content_digest.contentDigestSha256Token)(digestHeader) !== void 0) {
199
+ throw new import_errors.RequestSignatureError(
200
+ "request_signature_header_malformed",
201
+ 11,
202
+ "Content-Digest sha-256 value must use padded standard Base64 for AdCP 3.2+"
203
+ );
204
+ }
198
205
  if (!digestHeader || !encodingMatches || !(0, import_content_digest.contentDigestMatches)(digestHeader, request.body ?? "")) {
199
206
  throw new import_errors.RequestSignatureError(
200
207
  "request_signature_digest_mismatch",
@@ -236,17 +243,14 @@ async function verifyRequestSignature(request, options) {
236
243
  return { status: "verified", keyid: jwk.kid, agent_url, verified_at: now };
237
244
  }
238
245
  function signatureEncodingCandidates(pinned, digestPolicy) {
239
- if (!pinned) return ["rfc8941-base64", "legacy-base64url"];
240
- if (digestPolicy !== "either") return [pinned];
241
- return [pinned, alternateBinaryEncoding(pinned)];
246
+ if (!pinned) return ["legacy-base64url", "rfc8941-base64"];
247
+ if (pinned === "rfc8941-base64" || digestPolicy !== "either") return [pinned];
248
+ return ["legacy-base64url", "rfc8941-base64"];
242
249
  }
243
- function contentDigestEncodingCandidates(pinned, digestPolicy) {
244
- if (pinned === "rfc8941-base64" && digestPolicy !== "either") return [pinned];
250
+ function contentDigestEncodingCandidates(pinned) {
251
+ if (pinned === "rfc8941-base64") return [pinned];
245
252
  return ["rfc8941-base64", "legacy-base64url"];
246
253
  }
247
- function alternateBinaryEncoding(encoding) {
248
- return encoding === "rfc8941-base64" ? "legacy-base64url" : "rfc8941-base64";
249
- }
250
254
  function jsonRpcProtocolMethods(body) {
251
255
  if (!body) return [];
252
256
  if (exceedsUnsignedBodyInspectionCap(body)) return [];
@@ -7,6 +7,7 @@ import {
7
7
  } from "./canonicalize.mjs";
8
8
  import {
9
9
  contentDigestMatches,
10
+ contentDigestSha256Token,
10
11
  contentDigestUsesEncoding,
11
12
  requestSigningEncodingForVersion
12
13
  } from "./content-digest.mjs";
@@ -84,7 +85,7 @@ async function verifyRequestSignature(request, options) {
84
85
  parsedEncoding = candidate;
85
86
  break;
86
87
  } catch (error) {
87
- parseError ??= error;
88
+ if (parseError === void 0 || candidate === (pinnedBinaryEncoding ?? "rfc8941-base64")) parseError = error;
88
89
  }
89
90
  }
90
91
  if (!parsedSig) throw parseError;
@@ -184,9 +185,16 @@ async function verifyRequestSignature(request, options) {
184
185
  }
185
186
  if (parsedInput.components.includes("content-digest")) {
186
187
  const digestHeader = getHeaderValue(request.headers, "Content-Digest");
187
- const encodingMatches = !!digestHeader && contentDigestEncodingCandidates(pinnedBinaryEncoding, options.capability.covers_content_digest).some(
188
+ const encodingMatches = !!digestHeader && contentDigestEncodingCandidates(pinnedBinaryEncoding).some(
188
189
  (candidate) => contentDigestUsesEncoding(digestHeader, candidate)
189
190
  );
191
+ if (digestHeader && !encodingMatches && pinnedBinaryEncoding === "rfc8941-base64" && contentDigestSha256Token(digestHeader) !== void 0) {
192
+ throw new RequestSignatureError(
193
+ "request_signature_header_malformed",
194
+ 11,
195
+ "Content-Digest sha-256 value must use padded standard Base64 for AdCP 3.2+"
196
+ );
197
+ }
190
198
  if (!digestHeader || !encodingMatches || !contentDigestMatches(digestHeader, request.body ?? "")) {
191
199
  throw new RequestSignatureError(
192
200
  "request_signature_digest_mismatch",
@@ -228,17 +236,14 @@ async function verifyRequestSignature(request, options) {
228
236
  return { status: "verified", keyid: jwk.kid, agent_url, verified_at: now };
229
237
  }
230
238
  function signatureEncodingCandidates(pinned, digestPolicy) {
231
- if (!pinned) return ["rfc8941-base64", "legacy-base64url"];
232
- if (digestPolicy !== "either") return [pinned];
233
- return [pinned, alternateBinaryEncoding(pinned)];
239
+ if (!pinned) return ["legacy-base64url", "rfc8941-base64"];
240
+ if (pinned === "rfc8941-base64" || digestPolicy !== "either") return [pinned];
241
+ return ["legacy-base64url", "rfc8941-base64"];
234
242
  }
235
- function contentDigestEncodingCandidates(pinned, digestPolicy) {
236
- if (pinned === "rfc8941-base64" && digestPolicy !== "either") return [pinned];
243
+ function contentDigestEncodingCandidates(pinned) {
244
+ if (pinned === "rfc8941-base64") return [pinned];
237
245
  return ["rfc8941-base64", "legacy-base64url"];
238
246
  }
239
- function alternateBinaryEncoding(encoding) {
240
- return encoding === "rfc8941-base64" ? "legacy-base64url" : "rfc8941-base64";
241
- }
242
247
  function jsonRpcProtocolMethods(body) {
243
248
  if (!body) return [];
244
249
  if (exceedsUnsignedBodyInspectionCap(body)) return [];
@@ -9,7 +9,7 @@
9
9
  */
10
10
  import type { TestOptions, TestResult, AgentProfile } from '../types.mjs';
11
11
  import type { NotApplicableStoryboard, ResolvedBundle } from '../storyboard/compliance.mjs';
12
- import type { Storyboard, StoryboardResult, StoryboardRunOptions } from '../storyboard/types.mjs';
12
+ import type { AgentEntry, Storyboard, StoryboardContext, StoryboardResult, StoryboardRunOptions } from '../storyboard/types.mjs';
13
13
  import type { ComplianceBundleResult, ComplianceTrack, ComplianceFailure, ComplianceResult, ComplianceSummary, AdvisoryObservation, OverallStatus } from './types.mjs';
14
14
  import type { VersionEnvelopeMode } from '../../protocols/index.mjs';
15
15
  /**
@@ -98,7 +98,113 @@ export interface ComplyOptions extends TestOptions {
98
98
  testKitPath?: string;
99
99
  /** Scoped hosted stable-line alias for prerelease-backed compliance caches. */
100
100
  hostedStableLineAlias?: string;
101
+ /**
102
+ * Per-storyboard routing hook. `comply()` grades one agent, so storyboards
103
+ * that need a second agent (`requires: [multi_agent]`, e.g. the
104
+ * governance-aware seller scenarios) otherwise skip with
105
+ * `requirement_unmet`. The hook lets a caller that can supply the other
106
+ * agents route those storyboards while every other storyboard keeps the
107
+ * ordinary single-URL run.
108
+ *
109
+ * Called once per selected storyboard, after capability discovery,
110
+ * `required_tools` partitioning and the `timeout_ms` budget check, and
111
+ * before the storyboard runs. Storyboards whose root capability predicate
112
+ * (`requires_capability` / `requires_all_capabilities`) the agent under
113
+ * test does not satisfy are not passed to the hook: they keep their
114
+ * `not_applicable` verdict. Not consulted in the degraded auth-rejected
115
+ * path, which runs only storyboards with no `required_tools`. An error the
116
+ * hook throws propagates out of `comply()`.
117
+ *
118
+ * `context.profile` is the agent under test's own `get_adcp_capabilities`
119
+ * answer: untrusted, agent-controlled input. Do not derive agent URLs or
120
+ * credentials from it.
121
+ *
122
+ * Return:
123
+ * - `undefined`: run the storyboard as usual against `agentUrl`.
124
+ * - a {@link ComplyStoryboardRoute}: run it as
125
+ * `runStoryboard('', storyboard, { ...runOptions, agents, default_agent, context })`
126
+ * and grade the result against the agent under test only (below). The
127
+ * result lands in `tracks`, `summary`, `failures`, `storyboards_executed`
128
+ * and `bundle_results` like any other run.
129
+ * - a {@link ComplyStoryboardSkip}: do not run it. `comply()` records the
130
+ * same whole-storyboard `requirement_unmet` row the runner emits for an
131
+ * unmet `requires:` gate, with `skip.detail` set to the reason (control
132
+ * and bidi characters stripped, max 1000 chars) and `skip.requirement:
133
+ * 'multi_agent'` when the storyboard declares it. Like an unrouted
134
+ * `multi_agent` storyboard it is listed in `storyboards_executed` and caps
135
+ * its bundle at `partial`.
136
+ *
137
+ * Grading of routed results. The agent under test is only credited or
138
+ * blamed for what it served:
139
+ * - A failed step served by another routed agent, including that agent's
140
+ * discovery failure (routed runs use `discovery_resilient`), becomes a
141
+ * `prerequisite_failed` skip: a coverage gap (`partial`), never `failing`.
142
+ * - If no step served by the agent under test passed, a synthetic
143
+ * `agent_under_test_coverage` gap row keeps the bundle from `passing`.
144
+ * - `failures[].fix_command` for a routed storyboard is a single-URL
145
+ * command; re-running it reproduces only the unrouted skip.
146
+ *
147
+ * Credential and network isolation, enforced by `comply()`. Caller
148
+ * configuration errors throw, failing the run:
149
+ * - `agents[default_agent].url` must be the agent under test (`agentUrl`).
150
+ * - Every entry that sets `auth` must set a real credential object (a known
151
+ * `type` with its secret fields); `null`, `''`, `{}` and the like throw.
152
+ * Only an agent-under-test entry may omit `auth`; it then gets the
153
+ * run-level `auth` pinned onto it (or the test-kit default if there is
154
+ * none). Run-level `auth` is dropped from the routed options, so the
155
+ * runner's `entry.auth ?? options.auth` fallback cannot hand it to
156
+ * another agent.
157
+ * - Routing is refused while run-level `headers` are set, because the runner
158
+ * sends them to every routed agent.
159
+ * - A replacement `route.storyboard` must keep the same `id` and grading
160
+ * shape (phases, steps, tasks, agent pins, validations, expectations and
161
+ * gates); only request payloads and context may change.
162
+ * - Run-level `transport` (including `trustedFetchFn` / SSRF guards) is
163
+ * shared by every routed agent. The hook cannot override it; only `url`,
164
+ * `auth` and `transport` (wire protocol) are read from each entry.
165
+ *
166
+ * Storyboard content that would forward test-kit credentials is refused as
167
+ * a skip rather than a throw: `$test_kit.auth` references in storyboard
168
+ * context, or a step not pinned to an agent-under-test key (unpinned steps
169
+ * route by protocol) that uses a `from_test_kit` auth directive or a
170
+ * `$test_kit.auth` reference.
171
+ *
172
+ * Known limit: version negotiation (`adcpVersion`, `wireAdcpVersion`,
173
+ * `versionEnvelope`) is done once against the agent under test and shared
174
+ * by every routed agent.
175
+ *
176
+ * Do not mutate `storyboard`; return a patched copy in `route.storyboard`.
177
+ */
178
+ routeStoryboard?: (storyboard: Storyboard, context: ComplyRouteStoryboardContext) => ComplyStoryboardRouting | Promise<ComplyStoryboardRouting>;
179
+ }
180
+ /** Second argument to {@link ComplyOptions.routeStoryboard}. */
181
+ export interface ComplyRouteStoryboardContext {
182
+ /** The agent under test, as passed to `comply()`. */
183
+ agent_url: string;
184
+ /** Capability profile `comply()` discovered for the agent under test. */
185
+ profile: AgentProfile;
186
+ }
187
+ /** Route one storyboard across several agents. See {@link ComplyOptions.routeStoryboard}. */
188
+ export interface ComplyStoryboardRoute {
189
+ /** Agents map for `runStoryboard`. `agents[default_agent].url` must be the agent under test. */
190
+ agents: Record<string, AgentEntry>;
191
+ /** Key of the agent under test in `agents`. */
192
+ default_agent: string;
193
+ /** Initial-context overrides, merged over any run-level context. */
194
+ context?: StoryboardContext;
195
+ /**
196
+ * Storyboard to run in place of the selected one (e.g. a patched copy).
197
+ * Must keep the same `id` so the result is attributed to the same bundle.
198
+ */
199
+ storyboard?: Storyboard;
200
+ }
201
+ /** Record a storyboard as not runnable. See {@link ComplyOptions.routeStoryboard}. */
202
+ export interface ComplyStoryboardSkip {
203
+ /** Human-readable reason, reported as `skip.detail`. */
204
+ skip: string;
101
205
  }
206
+ /** Return type of {@link ComplyOptions.routeStoryboard}. */
207
+ export type ComplyStoryboardRouting = ComplyStoryboardRoute | ComplyStoryboardSkip | undefined;
102
208
  /**
103
209
  * Run compliance assessment against an agent.
104
210
  *
@@ -9,7 +9,7 @@
9
9
  */
10
10
  import type { TestOptions, TestResult, AgentProfile } from '../types';
11
11
  import type { NotApplicableStoryboard, ResolvedBundle } from '../storyboard/compliance';
12
- import type { Storyboard, StoryboardResult, StoryboardRunOptions } from '../storyboard/types';
12
+ import type { AgentEntry, Storyboard, StoryboardContext, StoryboardResult, StoryboardRunOptions } from '../storyboard/types';
13
13
  import type { ComplianceBundleResult, ComplianceTrack, ComplianceFailure, ComplianceResult, ComplianceSummary, AdvisoryObservation, OverallStatus } from './types';
14
14
  import type { VersionEnvelopeMode } from '../../protocols';
15
15
  /**
@@ -98,7 +98,113 @@ export interface ComplyOptions extends TestOptions {
98
98
  testKitPath?: string;
99
99
  /** Scoped hosted stable-line alias for prerelease-backed compliance caches. */
100
100
  hostedStableLineAlias?: string;
101
+ /**
102
+ * Per-storyboard routing hook. `comply()` grades one agent, so storyboards
103
+ * that need a second agent (`requires: [multi_agent]`, e.g. the
104
+ * governance-aware seller scenarios) otherwise skip with
105
+ * `requirement_unmet`. The hook lets a caller that can supply the other
106
+ * agents route those storyboards while every other storyboard keeps the
107
+ * ordinary single-URL run.
108
+ *
109
+ * Called once per selected storyboard, after capability discovery,
110
+ * `required_tools` partitioning and the `timeout_ms` budget check, and
111
+ * before the storyboard runs. Storyboards whose root capability predicate
112
+ * (`requires_capability` / `requires_all_capabilities`) the agent under
113
+ * test does not satisfy are not passed to the hook: they keep their
114
+ * `not_applicable` verdict. Not consulted in the degraded auth-rejected
115
+ * path, which runs only storyboards with no `required_tools`. An error the
116
+ * hook throws propagates out of `comply()`.
117
+ *
118
+ * `context.profile` is the agent under test's own `get_adcp_capabilities`
119
+ * answer: untrusted, agent-controlled input. Do not derive agent URLs or
120
+ * credentials from it.
121
+ *
122
+ * Return:
123
+ * - `undefined`: run the storyboard as usual against `agentUrl`.
124
+ * - a {@link ComplyStoryboardRoute}: run it as
125
+ * `runStoryboard('', storyboard, { ...runOptions, agents, default_agent, context })`
126
+ * and grade the result against the agent under test only (below). The
127
+ * result lands in `tracks`, `summary`, `failures`, `storyboards_executed`
128
+ * and `bundle_results` like any other run.
129
+ * - a {@link ComplyStoryboardSkip}: do not run it. `comply()` records the
130
+ * same whole-storyboard `requirement_unmet` row the runner emits for an
131
+ * unmet `requires:` gate, with `skip.detail` set to the reason (control
132
+ * and bidi characters stripped, max 1000 chars) and `skip.requirement:
133
+ * 'multi_agent'` when the storyboard declares it. Like an unrouted
134
+ * `multi_agent` storyboard it is listed in `storyboards_executed` and caps
135
+ * its bundle at `partial`.
136
+ *
137
+ * Grading of routed results. The agent under test is only credited or
138
+ * blamed for what it served:
139
+ * - A failed step served by another routed agent, including that agent's
140
+ * discovery failure (routed runs use `discovery_resilient`), becomes a
141
+ * `prerequisite_failed` skip: a coverage gap (`partial`), never `failing`.
142
+ * - If no step served by the agent under test passed, a synthetic
143
+ * `agent_under_test_coverage` gap row keeps the bundle from `passing`.
144
+ * - `failures[].fix_command` for a routed storyboard is a single-URL
145
+ * command; re-running it reproduces only the unrouted skip.
146
+ *
147
+ * Credential and network isolation, enforced by `comply()`. Caller
148
+ * configuration errors throw, failing the run:
149
+ * - `agents[default_agent].url` must be the agent under test (`agentUrl`).
150
+ * - Every entry that sets `auth` must set a real credential object (a known
151
+ * `type` with its secret fields); `null`, `''`, `{}` and the like throw.
152
+ * Only an agent-under-test entry may omit `auth`; it then gets the
153
+ * run-level `auth` pinned onto it (or the test-kit default if there is
154
+ * none). Run-level `auth` is dropped from the routed options, so the
155
+ * runner's `entry.auth ?? options.auth` fallback cannot hand it to
156
+ * another agent.
157
+ * - Routing is refused while run-level `headers` are set, because the runner
158
+ * sends them to every routed agent.
159
+ * - A replacement `route.storyboard` must keep the same `id` and grading
160
+ * shape (phases, steps, tasks, agent pins, validations, expectations and
161
+ * gates); only request payloads and context may change.
162
+ * - Run-level `transport` (including `trustedFetchFn` / SSRF guards) is
163
+ * shared by every routed agent. The hook cannot override it; only `url`,
164
+ * `auth` and `transport` (wire protocol) are read from each entry.
165
+ *
166
+ * Storyboard content that would forward test-kit credentials is refused as
167
+ * a skip rather than a throw: `$test_kit.auth` references in storyboard
168
+ * context, or a step not pinned to an agent-under-test key (unpinned steps
169
+ * route by protocol) that uses a `from_test_kit` auth directive or a
170
+ * `$test_kit.auth` reference.
171
+ *
172
+ * Known limit: version negotiation (`adcpVersion`, `wireAdcpVersion`,
173
+ * `versionEnvelope`) is done once against the agent under test and shared
174
+ * by every routed agent.
175
+ *
176
+ * Do not mutate `storyboard`; return a patched copy in `route.storyboard`.
177
+ */
178
+ routeStoryboard?: (storyboard: Storyboard, context: ComplyRouteStoryboardContext) => ComplyStoryboardRouting | Promise<ComplyStoryboardRouting>;
179
+ }
180
+ /** Second argument to {@link ComplyOptions.routeStoryboard}. */
181
+ export interface ComplyRouteStoryboardContext {
182
+ /** The agent under test, as passed to `comply()`. */
183
+ agent_url: string;
184
+ /** Capability profile `comply()` discovered for the agent under test. */
185
+ profile: AgentProfile;
186
+ }
187
+ /** Route one storyboard across several agents. See {@link ComplyOptions.routeStoryboard}. */
188
+ export interface ComplyStoryboardRoute {
189
+ /** Agents map for `runStoryboard`. `agents[default_agent].url` must be the agent under test. */
190
+ agents: Record<string, AgentEntry>;
191
+ /** Key of the agent under test in `agents`. */
192
+ default_agent: string;
193
+ /** Initial-context overrides, merged over any run-level context. */
194
+ context?: StoryboardContext;
195
+ /**
196
+ * Storyboard to run in place of the selected one (e.g. a patched copy).
197
+ * Must keep the same `id` so the result is attributed to the same bundle.
198
+ */
199
+ storyboard?: Storyboard;
200
+ }
201
+ /** Record a storyboard as not runnable. See {@link ComplyOptions.routeStoryboard}. */
202
+ export interface ComplyStoryboardSkip {
203
+ /** Human-readable reason, reported as `skip.detail`. */
204
+ skip: string;
101
205
  }
206
+ /** Return type of {@link ComplyOptions.routeStoryboard}. */
207
+ export type ComplyStoryboardRouting = ComplyStoryboardRoute | ComplyStoryboardSkip | undefined;
102
208
  /**
103
209
  * Run compliance assessment against an agent.
104
210
  *