@adcp/sdk 14.0.0-rc.51 → 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.
- package/README.md +1 -1
- package/dist/lib/registry/types.generated.d.mts +12 -0
- package/dist/lib/registry/types.generated.d.ts +12 -0
- package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
- package/dist/lib/server/auth-signature.d.mts +5 -2
- package/dist/lib/server/auth-signature.d.ts +5 -2
- package/dist/lib/signing/content-digest.d.mts +8 -0
- package/dist/lib/signing/content-digest.d.ts +8 -0
- package/dist/lib/signing/content-digest.js +13 -4
- package/dist/lib/signing/content-digest.mjs +11 -4
- package/dist/lib/signing/parser.d.mts +1 -1
- package/dist/lib/signing/parser.d.ts +1 -1
- package/dist/lib/signing/parser.js +3 -2
- package/dist/lib/signing/parser.mjs +3 -2
- package/dist/lib/signing/verifier.d.mts +6 -2
- package/dist/lib/signing/verifier.d.ts +6 -2
- package/dist/lib/signing/verifier.js +14 -10
- package/dist/lib/signing/verifier.mjs +15 -10
- package/dist/lib/testing/compliance/comply.d.mts +107 -1
- package/dist/lib/testing/compliance/comply.d.ts +107 -1
- package/dist/lib/testing/compliance/comply.js +313 -1
- package/dist/lib/testing/compliance/comply.mjs +320 -2
- package/dist/lib/testing/compliance/index.d.mts +1 -1
- package/dist/lib/testing/compliance/index.d.ts +1 -1
- package/dist/lib/testing/index.d.mts +1 -1
- package/dist/lib/testing/index.d.ts +1 -1
- package/dist/lib/testing/storyboard/runner.js +10 -0
- package/dist/lib/testing/storyboard/runner.mjs +8 -0
- package/dist/lib/testing/storyboard/validations.d.mts +1 -1
- package/dist/lib/testing/storyboard/validations.d.ts +1 -1
- package/dist/lib/version.d.mts +3 -3
- package/dist/lib/version.d.ts +3 -3
- package/dist/lib/version.js +3 -3
- package/dist/lib/version.mjs +3 -3
- package/docs/TYPE-SUMMARY.md +2 -2
- package/docs/guides/VALIDATE-YOUR-AGENT.md +49 -0
- package/docs/llms.txt +2 -2
- package/docs/migration-13-to-14.md +14 -7
- package/docs/migration-14.x-rc-worksheet.md +4 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
[](https://adcontextprotocol.github.io/adcp-client/api/)
|
|
8
8
|
[](https://github.com/adcontextprotocol/adcp-client/actions)
|
|
9
9
|
|
|
10
|
-
Official TypeScript/JavaScript
|
|
10
|
+
Official TypeScript/JavaScript SDK for the **Ad Context Protocol (AdCP)**. Use its buyer client to call agents, server framework to build them, and conformance tools to test them. The SDK supports MCP and A2A, with both immediate and asynchronous task results.
|
|
11
11
|
|
|
12
12
|
## For AI Agents
|
|
13
13
|
|
|
@@ -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-
|
|
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.
|
|
73
|
-
*
|
|
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.
|
|
73
|
-
*
|
|
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
|
|
52
|
-
if (!
|
|
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
|
|
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
|
|
25
|
-
if (!
|
|
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
|
|
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
|
};
|
|
@@ -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 (
|
|
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 (
|
|
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
|
-
*
|
|
23
|
-
*
|
|
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
|
-
*
|
|
23
|
-
*
|
|
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
|
|
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
|
|
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 ["
|
|
240
|
-
if (digestPolicy !== "either") return [pinned];
|
|
241
|
-
return [
|
|
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
|
|
244
|
-
if (pinned === "rfc8941-base64"
|
|
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
|
|
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
|
|
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 ["
|
|
232
|
-
if (digestPolicy !== "either") return [pinned];
|
|
233
|
-
return [
|
|
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
|
|
236
|
-
if (pinned === "rfc8941-base64"
|
|
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
|
*
|