@adcp/sdk 16.0.0-beta.6 → 16.0.0-beta.8

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 (137) hide show
  1. package/README.md +17 -0
  2. package/dist/lib/conformance/index.d.mts +2 -0
  3. package/dist/lib/conformance/index.d.ts +2 -0
  4. package/dist/lib/conformance/index.js +3 -0
  5. package/dist/lib/conformance/index.mjs +2 -0
  6. package/dist/lib/conformance/property-lists.d.mts +14 -0
  7. package/dist/lib/conformance/property-lists.d.ts +14 -0
  8. package/dist/lib/conformance/property-lists.js +45 -0
  9. package/dist/lib/conformance/property-lists.mjs +21 -0
  10. package/dist/lib/conformance/runConformance.js +20 -1
  11. package/dist/lib/conformance/runConformance.mjs +20 -1
  12. package/dist/lib/conformance/types.d.mts +3 -0
  13. package/dist/lib/conformance/types.d.ts +3 -0
  14. package/dist/lib/core/SingleAgentClient.d.mts +13 -3
  15. package/dist/lib/core/SingleAgentClient.d.ts +13 -3
  16. package/dist/lib/core/SingleAgentClient.js +109 -21
  17. package/dist/lib/core/SingleAgentClient.mjs +112 -22
  18. package/dist/lib/core/TaskExecutor.d.mts +1 -0
  19. package/dist/lib/core/TaskExecutor.d.ts +1 -0
  20. package/dist/lib/core/TaskExecutor.js +48 -34
  21. package/dist/lib/core/TaskExecutor.mjs +49 -35
  22. package/dist/lib/identifiers.d.mts +53 -0
  23. package/dist/lib/identifiers.d.ts +53 -0
  24. package/dist/lib/identifiers.js +212 -0
  25. package/dist/lib/identifiers.mjs +163 -0
  26. package/dist/lib/identity-3.3/revocation.js +5 -4
  27. package/dist/lib/identity-3.3/revocation.mjs +5 -4
  28. package/dist/lib/index.d.mts +3 -1
  29. package/dist/lib/index.d.ts +3 -1
  30. package/dist/lib/index.js +2 -0
  31. package/dist/lib/index.mjs +1 -0
  32. package/dist/lib/media-buy/index.d.mts +1 -1
  33. package/dist/lib/media-buy/index.d.ts +1 -1
  34. package/dist/lib/media-buy/property-policy.d.mts +15 -1
  35. package/dist/lib/media-buy/property-policy.d.ts +15 -1
  36. package/dist/lib/media-buy/property-policy.js +135 -3
  37. package/dist/lib/media-buy/property-policy.mjs +134 -3
  38. package/dist/lib/registry/index.d.mts +12 -0
  39. package/dist/lib/registry/index.d.ts +12 -0
  40. package/dist/lib/registry/index.js +159 -5
  41. package/dist/lib/registry/index.mjs +156 -4
  42. package/dist/lib/registry/types.d.mts +23 -0
  43. package/dist/lib/registry/types.d.ts +23 -0
  44. package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
  45. package/dist/lib/server/auth-signature.js +23 -2
  46. package/dist/lib/server/auth-signature.mjs +26 -2
  47. package/dist/lib/server/auth.d.mts +6 -0
  48. package/dist/lib/server/auth.d.ts +6 -0
  49. package/dist/lib/server/auth.js +6 -0
  50. package/dist/lib/server/auth.mjs +6 -0
  51. package/dist/lib/server/create-adcp-server.js +19 -3
  52. package/dist/lib/server/create-adcp-server.mjs +23 -4
  53. package/dist/lib/server/decisioning/runtime/from-platform.js +6 -2
  54. package/dist/lib/server/decisioning/runtime/from-platform.mjs +6 -2
  55. package/dist/lib/server/decisioning/specialisms/lists.d.mts +4 -4
  56. package/dist/lib/server/decisioning/specialisms/lists.d.ts +4 -4
  57. package/dist/lib/server/index.d.mts +3 -0
  58. package/dist/lib/server/index.d.ts +3 -0
  59. package/dist/lib/server/index.js +8 -1
  60. package/dist/lib/server/index.mjs +4 -0
  61. package/dist/lib/server/property-lists.d.mts +36 -0
  62. package/dist/lib/server/property-lists.d.ts +36 -0
  63. package/dist/lib/server/property-lists.js +154 -0
  64. package/dist/lib/server/property-lists.mjs +129 -0
  65. package/dist/lib/server/serve.js +2 -1
  66. package/dist/lib/server/serve.mjs +2 -1
  67. package/dist/lib/server/targeting-helpers.d.mts +2 -0
  68. package/dist/lib/server/targeting-helpers.d.ts +2 -0
  69. package/dist/lib/server/targeting-helpers.js +28 -1
  70. package/dist/lib/server/targeting-helpers.mjs +18 -1
  71. package/dist/lib/server/webhook-emitter.js +1 -0
  72. package/dist/lib/server/webhook-emitter.mjs +2 -1
  73. package/dist/lib/signing/crypto.d.mts +1 -1
  74. package/dist/lib/signing/crypto.d.ts +1 -1
  75. package/dist/lib/signing/crypto.js +37 -2
  76. package/dist/lib/signing/crypto.mjs +37 -2
  77. package/dist/lib/signing/parser.d.mts +6 -0
  78. package/dist/lib/signing/parser.d.ts +6 -0
  79. package/dist/lib/signing/parser.js +7 -3
  80. package/dist/lib/signing/parser.mjs +6 -3
  81. package/dist/lib/signing/server.d.mts +1 -1
  82. package/dist/lib/signing/server.d.ts +1 -1
  83. package/dist/lib/signing/server.mjs +5 -1
  84. package/dist/lib/signing/web-bot-auth.d.mts +61 -22
  85. package/dist/lib/signing/web-bot-auth.d.ts +61 -22
  86. package/dist/lib/signing/web-bot-auth.js +286 -266
  87. package/dist/lib/signing/web-bot-auth.mjs +288 -277
  88. package/dist/lib/signing/webhook-verifier.d.mts +11 -0
  89. package/dist/lib/signing/webhook-verifier.d.ts +11 -0
  90. package/dist/lib/signing/webhook-verifier.js +11 -3
  91. package/dist/lib/signing/webhook-verifier.mjs +11 -3
  92. package/dist/lib/supply-path/verify.js +12 -2
  93. package/dist/lib/supply-path/verify.mjs +2 -2
  94. package/dist/lib/testing/client.js +3 -5
  95. package/dist/lib/testing/client.mjs +4 -6
  96. package/dist/lib/testing/compliance/comply.js +7 -2
  97. package/dist/lib/testing/compliance/comply.mjs +8 -3
  98. package/dist/lib/testing/index.d.mts +1 -1
  99. package/dist/lib/testing/index.d.ts +1 -1
  100. package/dist/lib/testing/index.js +4 -2
  101. package/dist/lib/testing/index.mjs +8 -2
  102. package/dist/lib/testing/storyboard/runner.js +12 -12
  103. package/dist/lib/testing/storyboard/runner.mjs +14 -13
  104. package/dist/lib/testing/storyboard/validations.d.mts +1 -1
  105. package/dist/lib/testing/storyboard/validations.d.ts +1 -1
  106. package/dist/lib/testing/storyboard/validations.js +23 -1
  107. package/dist/lib/testing/storyboard/validations.mjs +28 -2
  108. package/dist/lib/testing/types.d.mts +39 -0
  109. package/dist/lib/testing/types.d.ts +39 -0
  110. package/dist/lib/v2/projection/augment-response.js +6 -3
  111. package/dist/lib/v2/projection/augment-response.mjs +8 -5
  112. package/dist/lib/v2/projection/creative-delivery.js +29 -17
  113. package/dist/lib/v2/projection/creative-delivery.mjs +31 -18
  114. package/dist/lib/v2/projection/v1-to-v2.d.mts +7 -0
  115. package/dist/lib/v2/projection/v1-to-v2.d.ts +7 -0
  116. package/dist/lib/v2/projection/v1-to-v2.js +70 -2
  117. package/dist/lib/v2/projection/v1-to-v2.mjs +72 -1
  118. package/dist/lib/validation/index.d.mts +1 -1
  119. package/dist/lib/validation/index.d.ts +1 -1
  120. package/dist/lib/validation/index.js +4 -2
  121. package/dist/lib/validation/index.mjs +4 -2
  122. package/dist/lib/validation/schema-loader.d.mts +26 -0
  123. package/dist/lib/validation/schema-loader.d.ts +26 -0
  124. package/dist/lib/validation/schema-loader.js +98 -14
  125. package/dist/lib/validation/schema-loader.mjs +95 -13
  126. package/dist/lib/version.d.mts +3 -3
  127. package/dist/lib/version.d.ts +3 -3
  128. package/dist/lib/version.js +3 -3
  129. package/dist/lib/version.mjs +3 -3
  130. package/docs/TYPE-SUMMARY.md +2 -2
  131. package/docs/guides/PROPERTY-LISTS.md +264 -0
  132. package/docs/guides/SIGNING-GUIDE.md +25 -0
  133. package/docs/guides/VALIDATE-YOUR-AGENT.md +34 -0
  134. package/docs/llms.txt +2 -2
  135. package/docs/migration-14.x-rc-worksheet.md +4 -4
  136. package/docs/migration-agent-identity-3.3.md +60 -23
  137. package/package.json +1 -1
@@ -1,17 +1,13 @@
1
1
  import { toReleasePrecisionVersion } from "../version.mjs";
2
- import {
3
- constants as cryptoConstants,
4
- createHash,
5
- createPrivateKey,
6
- randomBytes,
7
- sign as nodeSign
8
- } from "crypto";
9
- import { parseDictionary, parseItem, serializeItem } from "structured-headers";
2
+ import { createHash, randomBytes } from "crypto";
3
+ import { parseDictionary, parseItem, serializeInnerList, serializeItem } from "structured-headers";
10
4
  import {
11
5
  component,
12
6
  componentIdentity,
13
7
  createSignature,
14
- isSignatureError
8
+ isSignatureError,
9
+ SignatureErrorCode,
10
+ verifySignature as verifyHttpMessageSignature
15
11
  } from "http-message-sig";
16
12
  import { ssrfSafeFetch } from "../net/index.mjs";
17
13
  import { eTldPlusOne } from "./agent-resolver/etld.mjs";
@@ -20,36 +16,52 @@ import { canonicalAgentUrl } from "./agent-resolver/select-agent.mjs";
20
16
  import { parseStrictJson } from "./agent-resolver/strict-json.mjs";
21
17
  import { getHeaderValue } from "./canonicalize.mjs";
22
18
  import { computeContentDigest, contentDigestMatches, contentDigestUsesEncoding } from "./content-digest.mjs";
23
- import { jwkToPublicKey, signWithPrivateJwkAsync, verifySignature, verifySignatureAsync } from "./crypto.mjs";
19
+ import { signWithPrivateJwkAsync, verifySignatureAsync } from "./crypto.mjs";
24
20
  import {
25
21
  parseSignature,
26
- parseSignatureInput,
22
+ parseSignatureInputMember,
27
23
  rejectDuplicateDictionaryKeys
28
24
  } from "./parser.mjs";
29
25
  const WEB_BOT_AUTH_PROFILE = "web-bot-auth";
30
26
  const WEB_BOT_AUTH_DRAFT_REVISION = "draft-ietf-webbotauth-httpsig-protocol-00";
31
27
  const SIGNATURE_DIRECTORY_PATH = "/.well-known/http-message-signatures-directory";
32
28
  const SIGNATURE_DIRECTORY_MEDIA_TYPE = "application/http-message-signatures-directory+json";
29
+ const WEB_BOT_AUTH_ALGORITHMS = Object.freeze(["ed25519", "rsa-pss-sha512"]);
33
30
  const DIRECTORY_TAG = "http-message-signatures-directory";
34
- const DIRECTORY_CLOCK_SKEW_SECONDS = 60;
31
+ const CLOCK_SKEW_SECONDS = 60;
32
+ const MAX_LIFETIME_SECONDS = 300;
33
+ const REQUIRED_PARAMETERS = ["created", "expires", "nonce", "keyid", "alg", "tag"];
35
34
  const TEST_KIDS = /* @__PURE__ */ new Set([
36
35
  "6WsKMCObba49V3uN0vIENnk1BKAk7c9zjBJcWKyQ9_I",
37
36
  "hHurKlVBnPJ4AGiPyl6HWMhxgkIcRBzaC8qWwEtnuVI",
38
37
  "ZtaFf5jj-EzorJmZn6mX3YnmK_QyZKVLbbinzIQKOjQ",
39
38
  "1XlJ3EIte7bPOEqKQtKrKNTNy5hywTyrwFXWrMbXKqE"
40
39
  ]);
40
+ function webBotAuthAcceptSignature(label = "sig1", hasBody = true, nonce) {
41
+ const body = hasBody ? '"content-type" "content-digest" ' : "";
42
+ const nonceParam = nonce === void 0 ? "nonce" : `nonce="${nonce}"`;
43
+ return `${label}=("@method" "@target-uri" "@authority" ${body}"signature-agent";key="${label}");created;expires;${nonceParam};keyid;alg;tag="web-bot-auth"`;
44
+ }
45
+ function freshNonce() {
46
+ return randomBytes(64).toString("base64url");
47
+ }
48
+ function nonceBytes(nonce) {
49
+ if (typeof nonce !== "string") return void 0;
50
+ const encoding = /^[A-Za-z0-9_-]{86}$/.test(nonce) ? "base64url" : /^[A-Za-z0-9+/]{86}==$/.test(nonce) ? "base64" : void 0;
51
+ if (!encoding) return void 0;
52
+ const bytes = Buffer.from(nonce, encoding);
53
+ return bytes.length === 64 && bytes.toString(encoding) === nonce ? bytes : void 0;
54
+ }
41
55
  const NO_CHALLENGE_REASONS = /* @__PURE__ */ new Set(["key_revoked", "identity_not_bound", "profile_disabled", "authorization_failed"]);
42
56
  class WebBotAuthError extends Error {
43
57
  constructor(status, reason) {
44
58
  super(
45
- status === 400 ? "Malformed Web Bot Auth message" : status === 429 ? "Web Bot Auth rate limit" : "Web Bot Auth verification failed"
59
+ status === 400 ? "Malformed Web Bot Auth message" : status === 429 ? reason === "replayed" ? "Web Bot Auth signature or nonce reused" : "Web Bot Auth rate limit" : "Web Bot Auth verification failed"
46
60
  );
47
61
  this.status = status;
48
62
  this.reason = reason;
49
63
  this.name = "WebBotAuthError";
50
- this.headers = status === 403 && !NO_CHALLENGE_REASONS.has(reason) ? {
51
- "Accept-Signature": 'sig1=("@method" "@target-uri" "@authority" "content-type" "content-digest" "signature-agent";key="sig1");created;expires;nonce;keyid;tag="web-bot-auth"'
52
- } : status === 429 ? { "Retry-After": "60" } : {};
64
+ this.headers = status === 403 && !NO_CHALLENGE_REASONS.has(reason) ? { "Accept-Signature": webBotAuthAcceptSignature() } : status === 429 && reason === "replayed" ? { "Accept-Signature": webBotAuthAcceptSignature("sig1", true, freshNonce()) } : status === 429 ? { "Retry-After": "60" } : {};
53
65
  }
54
66
  status;
55
67
  reason;
@@ -85,6 +97,20 @@ function bad(reason) {
85
97
  function denied(reason) {
86
98
  throw new WebBotAuthError(403, reason);
87
99
  }
100
+ function challengeFor(error, label, hasBody) {
101
+ const current = error.headers["Accept-Signature"];
102
+ if (current) {
103
+ const nonce = error.status === 429 ? freshNonce() : void 0;
104
+ error.headers["Accept-Signature"] = webBotAuthAcceptSignature(label, hasBody, nonce);
105
+ }
106
+ return error;
107
+ }
108
+ function unsignedWebBotAuthError(hasBody) {
109
+ return challengeFor(new WebBotAuthError(403, "signature_required"), "sig1", hasBody);
110
+ }
111
+ function advertisesOnlyWebBotAuth(profiles) {
112
+ return !!profiles?.includes(WEB_BOT_AUTH_PROFILE) && !profiles.includes("adcp/request-signing/v1");
113
+ }
88
114
  function dictionary(value, field) {
89
115
  if (value === void 0 || value === "") return denied("headers_missing");
90
116
  if (value.length > 64 * 1024) return bad("headers");
@@ -114,18 +140,30 @@ function jwkThumbprint(jwk) {
114
140
  }
115
141
  function algorithm(jwk) {
116
142
  if (jwk.kty === "OKP" && jwk.crv === "Ed25519") return "ed25519";
117
- if (jwk.kty === "EC" && jwk.crv === "P-256") return "ecdsa-p256-sha256";
118
143
  if (jwk.kty === "RSA" && typeof jwk.n === "string" && jwk.e === "AQAB") {
119
144
  const bytes = Buffer.from(jwk.n, "base64url").length;
120
145
  if (bytes >= 256 && bytes <= 512) return "rsa-pss-sha512";
121
146
  }
122
147
  return denied("unsupported_key");
123
148
  }
124
- function inputs(headers) {
125
- const value = getHeaderValue(headers, "signature-input");
126
- return [...dictionary(value, "Signature-Input").keys()].map((label) => {
149
+ function signingAlgorithm(jwk) {
150
+ try {
151
+ return algorithm(jwk);
152
+ } catch {
153
+ throw new TypeError(
154
+ "Web Bot Auth signs with Ed25519 or RSA-PSS-SHA512 (2048-4096 bit, e=65537) keys only; ES256 remains on the AdCP RFC 9421 profile"
155
+ );
156
+ }
157
+ }
158
+ function assertWebBotAuthSigningKey(key) {
159
+ signingAlgorithm("privateKey" in key ? key.privateKey : key.publicKey);
160
+ }
161
+ function inputs(headers, limit) {
162
+ const members = dictionary(getHeaderValue(headers, "signature-input"), "Signature-Input");
163
+ if (members.size > limit) bad("signature_input");
164
+ return [...members].map(([label, member]) => {
127
165
  try {
128
- return parseSignatureInput(value, label);
166
+ return parseSignatureInputMember(label, member);
129
167
  } catch {
130
168
  return bad("signature_input");
131
169
  }
@@ -135,10 +173,10 @@ function receivedAgentOrigin(value) {
135
173
  try {
136
174
  canonicalAgentUrl(value);
137
175
  const origin = canonicalAgentOrigin(value);
138
- if (value !== origin || /[^\x21-\x7e]/.test(value) || value.includes("%")) return bad("agent_origin");
176
+ if (value !== origin || /[^\x21-\x7e]/.test(value) || value.includes("%")) return denied("agent_origin");
139
177
  return origin;
140
178
  } catch {
141
- return bad("agent_origin");
179
+ return denied("agent_origin");
142
180
  }
143
181
  }
144
182
  function buildWebBotAuthSignatureBase(input, request, response) {
@@ -171,10 +209,7 @@ function buildWebBotAuthSignatureBase(input, request, response) {
171
209
  if (params.has("key")) {
172
210
  const member = dictionary(value, bare).get(params.get("key"));
173
211
  if (!member) return denied("missing_member");
174
- if (Array.isArray(member[0])) {
175
- const selected = parseSignatureInput(value, params.get("key"));
176
- value = selected.signatureParamsValue;
177
- } else value = serializeItem(member);
212
+ value = Array.isArray(member[0]) ? serializeInnerList(member) : serializeItem(member);
178
213
  }
179
214
  }
180
215
  if (value === void 0) return denied("missing_component");
@@ -187,7 +222,7 @@ function receivedTargetUri(url) {
187
222
  try {
188
223
  new URL(url);
189
224
  } catch {
190
- return bad("target_uri");
225
+ return denied("target_uri");
191
226
  }
192
227
  return url;
193
228
  }
@@ -195,7 +230,7 @@ function receivedAuthority(url) {
195
230
  try {
196
231
  return new URL(url).host.toLowerCase();
197
232
  } catch {
198
- return bad("target_uri");
233
+ return denied("target_uri");
199
234
  }
200
235
  }
201
236
  function assertDigest(headers, body) {
@@ -203,41 +238,48 @@ function assertDigest(headers, body) {
203
238
  if (!digest || !contentDigestUsesEncoding(digest, "rfc8941-base64") || !contentDigestMatches(digest, body))
204
239
  denied("content_digest");
205
240
  }
206
- function signatureBytes(input, request, jwk, response) {
241
+ function keyVerifier(jwk) {
207
242
  const alg = algorithm(jwk);
208
- if (input.params.alg !== void 0 && input.params.alg !== alg) denied("algorithm");
209
- const signature = getHeaderValue(response?.headers ?? request.headers, "signature");
210
- if (signature === void 0 || signature === "") denied("headers_missing");
211
- try {
212
- return { alg, bytes: parseSignature(signature ?? "", input.label, "rfc8941-base64").bytes };
213
- } catch {
214
- return bad("signature");
215
- }
243
+ return { algorithm: alg, verify: (data, signature) => verifySignatureAsync(alg, jwk, data, signature) };
216
244
  }
217
- async function verifyBytesAsync(input, request, jwk) {
218
- const { alg, bytes } = signatureBytes(input, request, jwk);
219
- let valid = false;
220
- try {
221
- valid = await verifySignatureAsync(alg, jwk, Buffer.from(buildWebBotAuthSignatureBase(input, request)), bytes);
222
- } catch (error) {
223
- if (error instanceof WebBotAuthError) throw error;
224
- }
225
- if (!valid) denied("signature_invalid");
245
+ function componentDescriptor(identifier) {
246
+ const bare = identifier.split(";", 1)[0];
247
+ const [, params] = parseItem(`"${bare}"${identifier.slice(bare.length)}`);
248
+ return component(bare, Object.fromEntries(params));
249
+ }
250
+ function coveredFields(components, headers) {
251
+ const names = new Set(components.filter((c) => !c.name.startsWith("@")).map((c) => c.name));
252
+ return [...names].flatMap((name) => {
253
+ const value = getHeaderValue(headers, name);
254
+ return value === void 0 ? [] : [{ name, value }];
255
+ });
226
256
  }
227
- function verifyBytes(input, request, jwk, response) {
228
- const { alg, bytes } = signatureBytes(input, request, jwk, response);
229
- let valid = false;
257
+ async function verifyWithLibrary(message, label, policy, resolveVerifier) {
230
258
  try {
231
- valid = verifySignature(
232
- alg,
233
- jwkToPublicKey(jwk),
234
- Buffer.from(buildWebBotAuthSignatureBase(input, request, response)),
235
- bytes
236
- );
259
+ await verifyHttpMessageSignature(message, {
260
+ label,
261
+ policy: { algorithms: WEB_BOT_AUTH_ALGORITHMS, clockSkew: CLOCK_SKEW_SECONDS, ...policy },
262
+ resolveVerifier
263
+ });
237
264
  } catch (error) {
238
- if (error instanceof WebBotAuthError) throw error;
265
+ if (!isSignatureError(error)) throw error;
266
+ const cause = error.cause;
267
+ if (cause instanceof WebBotAuthError || cause instanceof WebBotAuthUnavailableError) throw cause;
268
+ switch (error.code) {
269
+ case SignatureErrorCode.VerificationFailed:
270
+ return denied("signature_invalid");
271
+ case SignatureErrorCode.AlgorithmMismatch:
272
+ return denied("algorithm");
273
+ case SignatureErrorCode.PolicyViolation:
274
+ return denied("policy");
275
+ case SignatureErrorCode.MissingField:
276
+ return denied("missing_component");
277
+ case SignatureErrorCode.ResolverFailed:
278
+ return denied("verification_unavailable");
279
+ default:
280
+ return denied("component");
281
+ }
239
282
  }
240
- if (!valid) denied("signature_invalid");
241
283
  }
242
284
  const legacyOriginGates = /* @__PURE__ */ new WeakMap();
243
285
  function registerWebBotAuthLegacyOriginGate(resolver, gate) {
@@ -331,123 +373,127 @@ function isVerifiedWebBotSigner(value) {
331
373
  return typeof value === "object" && value !== null && verifiedSigners.has(value);
332
374
  }
333
375
  async function verifyWebBotAuth(request, options) {
376
+ const hasBody = request.body !== void 0 && request.body !== "";
377
+ let label = "sig1";
334
378
  try {
335
379
  if (!(options.directory instanceof SignatureDirectoryResolver)) denied("directory_unverified");
336
380
  const now = Math.floor((options.now ?? (() => Date.now() / 1e3))());
337
381
  if (!Number.isFinite(now)) denied("verification_unavailable");
338
- const all = inputs(request.headers);
339
- const candidates = all.filter((input) => input.params.tag === WEB_BOT_AUTH_PROFILE);
340
- const selected = [];
341
- const remaining = [...candidates];
342
- while (remaining.length) {
343
- const index = remaining.findIndex(
344
- (input) => candidates.every(
345
- (other) => !input.components.some(
346
- (c) => c === `signature-input;key="${other.label}"` || c === `signature;key="${other.label}"`
347
- ) || selected.some((done) => done.label === other.label)
348
- )
349
- );
350
- if (index < 0) denied("relay_cycle");
351
- selected.push(remaining.splice(index, 1)[0]);
382
+ const inputHeader = getHeaderValue(request.headers, "signature-input");
383
+ const signatureHeader = getHeaderValue(request.headers, "signature");
384
+ const agentHeader = getHeaderValue(request.headers, "signature-agent");
385
+ const present = (value) => value !== void 0 && value !== "";
386
+ const inputMembers = present(inputHeader) ? dictionary(inputHeader, "Signature-Input") : void 0;
387
+ const signatureMembers = present(signatureHeader) ? dictionary(signatureHeader, "Signature") : void 0;
388
+ const agents = present(agentHeader) ? dictionary(agentHeader, "Signature-Agent") : void 0;
389
+ for (const member2 of inputMembers?.values() ?? [])
390
+ if (!Array.isArray(member2[0]) || member2[0].some((item) => typeof item[0] !== "string")) bad("signature_input");
391
+ for (const member2 of signatureMembers?.values() ?? [])
392
+ if (!(member2[0] instanceof ArrayBuffer) || member2[1].size !== 0) bad("signature");
393
+ for (const member2 of agents?.values() ?? []) if (typeof member2[0] !== "string") bad("signature_agent");
394
+ if (!inputMembers || !signatureMembers) return denied("headers_missing");
395
+ if (inputMembers.size !== 1 || signatureMembers.size !== 1) denied("signature_count");
396
+ label = inputMembers.keys().next().value;
397
+ if (!signatureMembers.has(label)) denied("signature_label");
398
+ try {
399
+ parseSignature(signatureHeader, label, "rfc8941-base64");
400
+ } catch {
401
+ return bad("signature");
352
402
  }
353
- if (!selected.length || selected.length > 8) denied("profile");
354
- const agents = dictionary(getHeaderValue(request.headers, "signature-agent"), "Signature-Agent");
355
- const verified = [];
356
- const nonces = /* @__PURE__ */ new Set();
357
- for (const input of selected) {
358
- const { created, expires, nonce, keyid } = input.params;
359
- if ([created, expires, nonce, keyid].some((value) => value === void 0)) denied("parameters_missing");
360
- if (!Number.isInteger(created) || !Number.isInteger(expires) || typeof nonce !== "string" || !/^[A-Za-z0-9_+\/-]{22,256}={0,2}$/.test(nonce) || typeof keyid !== "string" || !/^[A-Za-z0-9_-]{43}$/.test(keyid))
361
- bad("parameters");
362
- if (expires <= created || expires - created > 300 || created > now + 60 || expires < now - 60) denied("window");
363
- const agentComponents = input.components.filter(
364
- (c) => c.startsWith("signature-agent;key=") && !verified.some((s) => selected.find((i) => i.label === s.label).components.includes(c))
365
- );
366
- if (agentComponents.length !== 1) denied("agent_not_covered");
367
- const component2 = parseItem(`"${agentComponents[0].replace("signature-agent;", 'signature-agent";')}`);
368
- const memberKey = component2[1].get("key");
369
- if (typeof memberKey !== "string" || memberKey !== input.label) bad("agent_member");
370
- const member = agents.get(memberKey);
371
- if (!member) denied("agent_not_covered");
372
- if (typeof member[0] !== "string") bad("signature_agent");
373
- const type = member[1].get("type");
374
- if (type !== void 0 && type !== "directory") denied("agent_type");
375
- const origin = receivedAgentOrigin(member[0]);
376
- for (const earlier of verified) {
377
- const inner = selected.find((i) => i.label === earlier.label);
378
- const required = [
379
- ...inner.components,
380
- `signature-input;key="${inner.label}"`,
381
- `signature;key="${inner.label}"`
382
- ];
383
- if (required.some((c) => !input.components.includes(c))) denied("relay_coverage");
384
- }
385
- if (new Set(input.components).size !== input.components.length || ["@method", "@target-uri", "@authority"].some((c) => !input.components.includes(c)))
386
- denied("coverage");
387
- if (request.body !== void 0 && request.body !== "") {
388
- if (["content-type", "content-digest"].some((c) => !input.components.includes(c))) denied("body_coverage");
389
- assertDigest(request.headers, request.body);
390
- }
391
- await options.directory.resolve(keyid, origin);
392
- const checkedDirectory = options.directory.getVerifiedDirectory(origin);
393
- if (!checkedDirectory || new URL(checkedDirectory.proof.request.url).origin !== origin)
394
- denied("directory_origin");
395
- if (checkedDirectory.expires <= now) denied("directory_expired");
396
- if (!testKeysAllowed(options.allowTestKeys ?? options.directory.allowsTestKeys) && checkedDirectory.keys.some((key2) => TEST_KIDS.has(key2.kid)))
397
- denied("directory_key");
398
- const key = checkedDirectory.keys.find((key2) => key2.kid === keyid);
399
- if (!key || jwkThumbprint(key) !== keyid) denied("key");
400
- await verifyBytesAsync(input, request, key);
401
- if (options.isRevoked) await legacyRevocationCheck(options, origin, keyid);
402
- const nonceTuple = JSON.stringify([origin, nonce]);
403
- if (nonces.has(nonceTuple)) throw new WebBotAuthError(options.replayStatus ?? 429, "nonce_replayed");
404
- nonces.add(nonceTuple);
405
- const signer = Object.freeze({
406
- profile: WEB_BOT_AUTH_PROFILE,
407
- origin,
408
- keyid,
409
- label: input.label,
410
- verified_at: now
411
- });
412
- verified.push(signer);
403
+ let input;
404
+ try {
405
+ input = parseSignatureInputMember(label, inputMembers.get(label));
406
+ } catch {
407
+ return denied("parameters");
413
408
  }
414
- for (const signer of verified) {
415
- const input = selected.find((i) => i.label === signer.label);
416
- let result;
417
- try {
418
- result = await options.replayStore.insert(
419
- signer.origin,
420
- WEB_BOT_AUTH_PROFILE,
421
- input.params.nonce,
422
- // Acceptance is inclusive of expires + 60, so the nonce must outlive it.
423
- input.params.expires + 60 - now + 1,
424
- now
425
- );
426
- } catch (cause) {
427
- throw new WebBotAuthUnavailableError("replay_unavailable", cause);
428
- }
429
- if (result !== "ok")
430
- throw new WebBotAuthError(result === "replayed" ? options.replayStatus ?? 429 : 429, result);
409
+ if (input.params.tag !== WEB_BOT_AUTH_PROFILE) denied("profile");
410
+ if (!agents || agents.size !== 1) denied(agents ? "agent_count" : "agent_missing");
411
+ const agentMember = `signature-agent;key="${label}"`;
412
+ const agentComponents = input.components.filter((c) => c.startsWith("signature-agent;"));
413
+ if (agentComponents.length !== 1 || agentComponents[0] !== agentMember) denied("agent_not_covered");
414
+ const member = agents.get(label);
415
+ if (!member) denied("agent_not_covered");
416
+ const type = member[1].get("type");
417
+ if (type !== void 0 && type !== "directory") denied("agent_type");
418
+ const origin = receivedAgentOrigin(member[0]);
419
+ const { created, expires, nonce, keyid, alg } = input.params;
420
+ if ([created, expires, nonce, keyid, alg].some((value) => value === void 0)) denied("parameters_missing");
421
+ const nonceKey = nonceBytes(nonce)?.toString("base64url");
422
+ if (!nonceKey || !Number.isSafeInteger(created) || !Number.isSafeInteger(expires) || typeof keyid !== "string" || !/^[A-Za-z0-9_-]{43}$/.test(keyid) || !WEB_BOT_AUTH_ALGORITHMS.includes(alg))
423
+ denied("parameters");
424
+ if (expires <= created || expires - created > MAX_LIFETIME_SECONDS || created > now + CLOCK_SKEW_SECONDS || expires < now - CLOCK_SKEW_SECONDS)
425
+ denied("window");
426
+ if (new Set(input.components).size !== input.components.length) denied("coverage");
427
+ const required = [component("@method"), component("@target-uri"), component("@authority")];
428
+ if (hasBody) required.push(component("content-type"), component("content-digest"));
429
+ required.push(component("signature-agent", { key: label }));
430
+ const covered = new Set(input.components);
431
+ if (required.some((c) => !covered.has(componentIdentity(c).replace(/^"([^"]*)"/, "$1")))) denied("coverage");
432
+ if (hasBody) assertDigest(request.headers, request.body);
433
+ let components;
434
+ try {
435
+ components = input.components.map(componentDescriptor);
436
+ } catch {
437
+ return denied("component");
431
438
  }
432
- for (const signer of verified) verifiedSigners.add(signer);
433
- return verified;
434
- } catch (error) {
435
- if (error instanceof WebBotAuthError) {
436
- if (error.headers["Accept-Signature"]) {
437
- let label = "sig1";
438
- try {
439
- label = inputs(request.headers)[0]?.label ?? "sig1";
440
- } catch {
441
- }
442
- const bodyComponents = request.body ? '"content-type" "content-digest" ' : "";
443
- error.headers["Accept-Signature"] = `${label}=("@method" "@target-uri" "@authority" ${bodyComponents}"signature-agent";key="${label}");created;expires;nonce;keyid;tag="web-bot-auth"`;
444
- }
445
- throw error;
439
+ const fields = [
440
+ ...coveredFields(components, request.headers),
441
+ { name: "signature-input", value: inputHeader },
442
+ { name: "signature", value: signatureHeader }
443
+ ];
444
+ await verifyWithLibrary(
445
+ {
446
+ kind: "request",
447
+ method: request.method.toUpperCase(),
448
+ targetUri: receivedTargetUri(request.url),
449
+ fields
450
+ },
451
+ label,
452
+ { requiredComponents: required, requiredParameters: REQUIRED_PARAMETERS, maxAge: MAX_LIFETIME_SECONDS, now },
453
+ async () => keyVerifier(await directoryKey(options, origin, keyid, now))
454
+ );
455
+ let result;
456
+ try {
457
+ result = await options.replayStore.insert(
458
+ origin,
459
+ WEB_BOT_AUTH_PROFILE,
460
+ nonceKey,
461
+ // Acceptance is inclusive of expires + skew, so the nonce must outlive it.
462
+ expires + CLOCK_SKEW_SECONDS - now + 1,
463
+ now
464
+ );
465
+ } catch (cause) {
466
+ throw new WebBotAuthUnavailableError("replay_unavailable", cause);
446
467
  }
468
+ if (result === "replayed") throw new WebBotAuthError(429, "replayed");
469
+ if (result !== "ok") throw new WebBotAuthError(429, result);
470
+ if (options.isRevoked) await legacyRevocationCheck(options, origin, keyid);
471
+ const signer = Object.freeze({
472
+ profile: WEB_BOT_AUTH_PROFILE,
473
+ origin,
474
+ keyid,
475
+ label,
476
+ verified_at: now
477
+ });
478
+ verifiedSigners.add(signer);
479
+ return [signer];
480
+ } catch (error) {
481
+ if (error instanceof WebBotAuthError) throw challengeFor(error, label, hasBody);
447
482
  if (error instanceof WebBotAuthUnavailableError) throw error;
448
- return denied("verification_unavailable");
483
+ throw challengeFor(new WebBotAuthError(403, "verification_unavailable"), label, hasBody);
449
484
  }
450
485
  }
486
+ async function directoryKey(options, origin, keyid, now) {
487
+ await options.directory.resolve(keyid, origin);
488
+ const checkedDirectory = options.directory.getVerifiedDirectory(origin);
489
+ if (!checkedDirectory || new URL(checkedDirectory.proof.request.url).origin !== origin) denied("directory_origin");
490
+ if (checkedDirectory.expires <= now) denied("directory_expired");
491
+ if (!testKeysAllowed(options.allowTestKeys ?? options.directory.allowsTestKeys) && checkedDirectory.keys.some((key2) => TEST_KIDS.has(key2.kid)))
492
+ denied("directory_key");
493
+ const key = checkedDirectory.keys.find((key2) => key2.kid === keyid);
494
+ if (!key || jwkThumbprint(key) !== keyid) denied("key");
495
+ return key;
496
+ }
451
497
  async function legacyRevocationCheck(options, origin, keyid) {
452
498
  let revoked;
453
499
  try {
@@ -466,29 +512,27 @@ async function legacyRevocationCheck(options, origin, keyid) {
466
512
  function testKeysAllowed(allowTestKeys) {
467
513
  return allowTestKeys === true && ["test", "development"].includes(process.env.NODE_ENV ?? "");
468
514
  }
469
- const UNSIGNED_DIRECTORY_MEDIA_TYPES = /* @__PURE__ */ new Set([
470
- SIGNATURE_DIRECTORY_MEDIA_TYPE,
471
- "application/jwk-set+json",
472
- "application/json"
473
- ]);
474
- function verifyKeyDirectory(response, now = Date.now() / 1e3, options = {}) {
515
+ async function verifyKeyDirectory(response, now = Date.now() / 1e3, options = {}) {
475
516
  try {
476
517
  if (!Number.isFinite(now)) denied("directory_invalid");
477
518
  const origin = canonicalAgentOrigin(response.request.url);
478
519
  if (response.request.url !== origin + SIGNATURE_DIRECTORY_PATH || response.status !== 200)
479
520
  denied("directory_response");
480
- const signed = getHeaderValue(response.headers, "signature-input") !== void 0;
481
- if (!signed && options.requireSigned) denied("directory_unsigned");
521
+ const inputHeader = getHeaderValue(response.headers, "signature-input");
522
+ const signatureHeader = getHeaderValue(response.headers, "signature");
523
+ if (!inputHeader || !signatureHeader) denied("directory_unsigned");
482
524
  const mediaType = getHeaderValue(response.headers, "content-type")?.split(";")[0]?.trim().toLowerCase();
483
- if (signed ? mediaType !== SIGNATURE_DIRECTORY_MEDIA_TYPE : !UNSIGNED_DIRECTORY_MEDIA_TYPES.has(mediaType ?? ""))
484
- denied("directory_response");
525
+ if (mediaType !== SIGNATURE_DIRECTORY_MEDIA_TYPE) denied("directory_response");
485
526
  const body = response.body ?? "";
486
527
  if (Buffer.byteLength(body) > 256 * 1024) denied("directory_size");
487
- if (signed || getHeaderValue(response.headers, "content-digest") !== void 0)
488
- assertDirectoryDigest(response.headers, body);
528
+ try {
529
+ assertDigest(response.headers, body);
530
+ } catch {
531
+ denied("directory_digest");
532
+ }
489
533
  const document = parseStrictJson(body, 32);
490
534
  if (!Array.isArray(document.keys) || !document.keys.length || document.keys.length > 64) denied("directory_keys");
491
- const signatures = signed ? inputs(response.headers) : [];
535
+ const signatures = inputs(response.headers, 64);
492
536
  const kids = /* @__PURE__ */ new Set();
493
537
  const keys = [];
494
538
  let expires = Infinity;
@@ -501,15 +545,47 @@ function verifyKeyDirectory(response, now = Date.now() / 1e3, options = {}) {
501
545
  algorithm(key);
502
546
  kids.add(key.kid);
503
547
  keys.push(key);
504
- if (!signed) continue;
505
548
  const proof = signatures.find((input) => input.params.keyid === key.kid && input.params.tag === DIRECTORY_TAG);
506
- if (!proof || !proof.components.includes("@authority;req") || !proof.components.includes("content-digest") || !Number.isInteger(proof.params.created) || !Number.isInteger(proof.params.expires) || proof.params.created > now + DIRECTORY_CLOCK_SKEW_SECONDS || proof.params.expires <= now || proof.params.expires <= proof.params.created)
549
+ if (!proof || !proof.components.includes("@authority;req") || !proof.components.includes("content-digest") || !Number.isSafeInteger(proof.params.created) || !Number.isSafeInteger(proof.params.expires) || proof.params.expires <= now || proof.params.expires <= proof.params.created)
507
550
  denied("directory_proof");
508
- verifyBytes(proof, { ...response.request, headers: response.request.headers ?? {} }, key, response);
551
+ const proofComponents = [component("@authority", { req: true }), component("content-digest")];
552
+ let covered;
553
+ try {
554
+ covered = proof.components.map(componentDescriptor);
555
+ } catch {
556
+ return denied("directory_proof");
557
+ }
558
+ const requestHeaders = response.request.headers ?? {};
559
+ await verifyWithLibrary(
560
+ {
561
+ kind: "response",
562
+ status: response.status,
563
+ fields: [
564
+ ...coveredFields(
565
+ covered.filter((c) => c.parameters.req === void 0),
566
+ response.headers
567
+ ),
568
+ { name: "signature-input", value: inputHeader },
569
+ { name: "signature", value: signatureHeader }
570
+ ],
571
+ request: {
572
+ kind: "request",
573
+ method: response.request.method,
574
+ targetUri: response.request.url,
575
+ fields: coveredFields(
576
+ covered.filter((c) => c.parameters.req !== void 0),
577
+ requestHeaders
578
+ )
579
+ }
580
+ },
581
+ proof.label,
582
+ // The library rejects a proof whose `created` is in the future (beyond skew).
583
+ { requiredComponents: proofComponents, requiredParameters: ["created", "expires", "keyid", "tag"], now },
584
+ async () => keyVerifier(key)
585
+ );
509
586
  expires = Math.min(expires, proof.params.expires);
510
587
  }
511
- if (!signed) expires = now + unsignedAge(options.maxUnsignedAgeSeconds);
512
- return { keys, expires, signed, proof: response };
588
+ return { keys, expires, signed: true, proof: response };
513
589
  } catch (error) {
514
590
  if (error instanceof WebBotAuthError) {
515
591
  throw error.reason.startsWith("directory_") ? error : new WebBotAuthError(403, `directory_${error.reason}`);
@@ -517,26 +593,12 @@ function verifyKeyDirectory(response, now = Date.now() / 1e3, options = {}) {
517
593
  return denied("directory_invalid");
518
594
  }
519
595
  }
520
- function unsignedAge(seconds) {
521
- const value = seconds ?? 86400;
522
- if (!Number.isFinite(value) || value < 60)
523
- throw new TypeError("Unsigned directory age must be a finite number of seconds, at least 60");
524
- return value;
525
- }
526
596
  function verifySignatureDirectory(response, now = Date.now() / 1e3, allowTestKeys = false) {
527
- return verifyKeyDirectory(response, now, { allowTestKeys, requireSigned: true });
528
- }
529
- function assertDirectoryDigest(headers, body) {
530
- try {
531
- assertDigest(headers, body);
532
- } catch {
533
- denied("directory_digest");
534
- }
597
+ return verifyKeyDirectory(response, now, { allowTestKeys });
535
598
  }
536
599
  class SignatureDirectoryResolver {
537
600
  constructor(options = {}) {
538
601
  this.options = options;
539
- unsignedAge(options.maxUnsignedDirectoryAgeSeconds);
540
602
  }
541
603
  options;
542
604
  cache = /* @__PURE__ */ new Map();
@@ -583,22 +645,16 @@ class SignatureDirectoryResolver {
583
645
  const url = origin + SIGNATURE_DIRECTORY_PATH;
584
646
  const result = await ssrfSafeFetch(url, {
585
647
  ...this.options.fetchOptions,
586
- headers: {
587
- accept: `${SIGNATURE_DIRECTORY_MEDIA_TYPE}, application/jwk-set+json;q=0.9, application/json;q=0.8`
588
- },
648
+ headers: { accept: SIGNATURE_DIRECTORY_MEDIA_TYPE },
589
649
  timeoutMs: 1e4,
590
650
  maxBodyBytes: 256 * 1024
591
651
  });
592
652
  if (result.status !== 200) denied("directory_unavailable");
593
653
  const body = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(result.body);
594
- const directory = verifyKeyDirectory(
654
+ const directory = await verifyKeyDirectory(
595
655
  { status: result.status, headers: result.headers, body, request: { method: "GET", url } },
596
656
  now,
597
- {
598
- allowTestKeys: this.options.allowTestKeys,
599
- requireSigned: this.options.requireSignedDirectories,
600
- maxUnsignedAgeSeconds: this.options.maxUnsignedDirectoryAgeSeconds
601
- }
657
+ { allowTestKeys: this.options.allowTestKeys }
602
658
  );
603
659
  const cacheControl = result.headers["cache-control"] ?? "";
604
660
  const maxAge = /(?:^|,)\s*(?:no-cache|no-store)(?:,|$)/i.test(cacheControl) ? 0 : Number(cacheControl.match(/(?:^|,)\s*max-age=(\d+)/i)?.[1] ?? 300);
@@ -635,7 +691,7 @@ class SignatureDirectoryResolver {
635
691
  }
636
692
  function webBotSigner(key) {
637
693
  const jwk = "privateKey" in key ? key.privateKey : key.publicKey;
638
- const alg = algorithm(jwk);
694
+ const alg = signingAlgorithm(jwk);
639
695
  if (!["test", "development"].includes(process.env.NODE_ENV ?? "") && TEST_KIDS.has(jwkThumbprint(jwk)))
640
696
  throw new TypeError("Published Web Bot Auth test keys cannot sign in production");
641
697
  if ("provider" in key) {
@@ -643,17 +699,6 @@ function webBotSigner(key) {
643
699
  throw new TypeError("Web Bot Auth provider identity does not match its public key");
644
700
  return { algorithm: alg, sign: async (data) => Buffer.from(await key.provider.sign(Buffer.from(data))) };
645
701
  }
646
- if (alg === "rsa-pss-sha512") {
647
- const privateKey = createPrivateKey({ key: jwk, format: "jwk" });
648
- return {
649
- algorithm: alg,
650
- sign: (data) => nodeSign("sha512", Buffer.from(data), {
651
- key: privateKey,
652
- padding: cryptoConstants.RSA_PKCS1_PSS_PADDING,
653
- saltLength: 64
654
- })
655
- };
656
- }
657
702
  return { algorithm: alg, sign: (data) => signWithPrivateJwkAsync(alg, jwk, Buffer.from(data)) };
658
703
  }
659
704
  async function createWebBotSignature(...args) {
@@ -664,38 +709,28 @@ async function createWebBotSignature(...args) {
664
709
  throw error;
665
710
  }
666
711
  }
667
- function componentDescriptor(identifier) {
668
- const bare = identifier.split(";", 1)[0];
669
- const [, params] = parseItem(`"${bare}"${identifier.slice(bare.length)}`);
670
- return component(bare, Object.fromEntries(params));
671
- }
672
- function coveredFields(components, headers) {
673
- const names = new Set(components.filter((c) => !c.name.startsWith("@")).map((c) => c.name));
674
- return [...names].flatMap((name) => {
675
- const value = getHeaderValue(headers, name);
676
- return value === void 0 ? [] : [{ name, value }];
677
- });
678
- }
679
712
  async function signWebBotAuth(request, key, options) {
713
+ if (options.relay)
714
+ throw new TypeError(
715
+ "Web Bot Auth relays were removed from the profile (adcp#7894): an intermediary signs as itself with one signature"
716
+ );
680
717
  const origin = canonicalAgentOrigin(options.origin);
681
718
  const label = options.label ?? "sig1";
682
719
  if (!/^[a-z][a-z0-9_.*-]*$/.test(label)) throw new TypeError("Invalid signature label");
683
720
  const jwk = "privateKey" in key ? key.privateKey : key.publicKey;
684
721
  const now = Math.floor((options.now ?? (() => Date.now() / 1e3))());
685
- const window = options.windowSeconds ?? 300;
686
- const nonce = options.nonce ?? randomBytes(64).toString("base64url");
722
+ const window = options.windowSeconds ?? MAX_LIFETIME_SECONDS;
723
+ const nonce = options.nonce ?? freshNonce();
687
724
  if (!Number.isSafeInteger(now)) throw new TypeError("Invalid Web Bot Auth clock");
688
- if (!Number.isInteger(window) || window < 1 || window > 300 || !/^[A-Za-z0-9_+\/-]{22,256}={0,2}$/.test(nonce))
689
- throw new TypeError("Invalid Web Bot Auth lifetime or nonce");
690
- if (!options.relay && ["signature", "signature-input", "signature-agent"].some(
725
+ if (!Number.isInteger(window) || window < 1 || window > MAX_LIFETIME_SECONDS || !nonceBytes(nonce))
726
+ throw new TypeError("Invalid Web Bot Auth lifetime or nonce (the nonce is 64 bytes, Base64URL or Base64)");
727
+ if (["signature", "signature-input", "signature-agent"].some(
691
728
  (name) => getHeaderValue(request.headers, name) !== void 0
692
729
  ))
693
- throw new TypeError(
694
- "Request already carries signature fields; pass relay: true to add a covering signature instead of replacing them"
695
- );
730
+ throw new TypeError("Request already carries signature fields; the profile takes exactly one signature");
696
731
  const headers = {};
697
732
  for (const [name, value] of Object.entries(request.headers))
698
- if (value !== void 0 && !["signature", "signature-input", "signature-agent", "content-digest"].includes(name.toLowerCase()))
733
+ if (value !== void 0 && name.toLowerCase() !== "content-digest")
699
734
  headers[name.toLowerCase()] = Array.isArray(value) ? value.join(", ") : value;
700
735
  const components = [component("@method"), component("@target-uri"), component("@authority")];
701
736
  if (request.body !== void 0 && request.body !== "") {
@@ -704,37 +739,8 @@ async function signWebBotAuth(request, key, options) {
704
739
  headers["content-digest"] = computeContentDigest(request.body, "rfc8941-base64");
705
740
  components.push(component("content-type"), component("content-digest"));
706
741
  }
707
- if (options.relay && ["signature", "signature-input", "signature-agent"].some((name) => !getHeaderValue(request.headers, name)))
708
- throw new TypeError("Relay requires the existing signature fields");
709
- const earlier = options.relay ? inputs(request.headers) : [];
710
- if (earlier.some((input) => input.label === label || input.params.tag !== WEB_BOT_AUTH_PROFILE))
711
- throw new TypeError("Relay requires existing Web Bot Auth signatures and a fresh label");
712
- const priorAgent = options.relay ? getHeaderValue(request.headers, "signature-agent") : void 0;
713
- if (priorAgent) dictionary(priorAgent, "Signature-Agent");
714
- headers["signature-agent"] = `${priorAgent ? priorAgent + ", " : ""}${label}="${origin}"`;
715
- if (priorAgent) dictionary(headers["signature-agent"], "Signature-Agent");
742
+ headers["signature-agent"] = `${label}="${origin}"`;
716
743
  components.push(component("signature-agent", { key: label }));
717
- const covered = new Set(components.map(componentIdentity));
718
- const cover = (descriptor) => {
719
- const identity = componentIdentity(descriptor);
720
- if (!covered.has(identity)) {
721
- covered.add(identity);
722
- components.push(descriptor);
723
- }
724
- };
725
- for (const inner of earlier) {
726
- for (const identifier of inner.components) cover(componentDescriptor(identifier));
727
- cover(component("signature-input", { key: inner.label }));
728
- cover(component("signature", { key: inner.label }));
729
- }
730
- const priorInput = options.relay ? getHeaderValue(request.headers, "signature-input") : void 0;
731
- const priorSignature = options.relay ? getHeaderValue(request.headers, "signature") : void 0;
732
- if (options.relay && (!priorInput || !priorSignature || !priorAgent))
733
- throw new TypeError("Relay requires the existing signature fields");
734
- if (priorInput) headers["signature-input"] = priorInput;
735
- if (priorSignature) headers.signature = priorSignature;
736
- for (const inner of earlier)
737
- buildWebBotAuthSignatureBase(inner, { ...request, url: new URL(request.url).href, headers });
738
744
  const targetUri = new URL(request.url);
739
745
  targetUri.hash = "";
740
746
  const signed = await createWebBotSignature(
@@ -752,14 +758,14 @@ async function signWebBotAuth(request, key, options) {
752
758
  expires: now + window,
753
759
  nonce,
754
760
  keyid: jwkThumbprint(jwk),
755
- alg: algorithm(jwk),
761
+ alg: signingAlgorithm(jwk),
756
762
  tag: WEB_BOT_AUTH_PROFILE
757
763
  },
758
764
  signer: webBotSigner(key)
759
765
  }
760
766
  );
761
- headers["signature-input"] = `${priorInput ? priorInput + ", " : ""}${signed.signatureInput}`;
762
- headers.signature = `${priorSignature ? priorSignature + ", " : ""}${signed.signature}`;
767
+ headers["signature-input"] = signed.signatureInput;
768
+ headers.signature = signed.signature;
763
769
  return headers;
764
770
  }
765
771
  async function createSignedSignatureDirectory(originUrl, signingKeys, options = {}) {
@@ -768,14 +774,13 @@ async function createSignedSignatureDirectory(originUrl, signingKeys, options =
768
774
  const lifetime = options.lifetimeSeconds ?? 86400;
769
775
  if (!Number.isFinite(now) || !Number.isSafeInteger(lifetime) || lifetime < 1 || !Number.isSafeInteger(now + lifetime))
770
776
  throw new TypeError("Invalid directory proof clock or lifetime");
771
- if (!signingKeys.length || signingKeys.length > 64 || !Number.isInteger(lifetime) || lifetime < 1)
772
- throw new TypeError("Invalid directory keys or lifetime");
777
+ if (!signingKeys.length || signingKeys.length > 64) throw new TypeError("Invalid directory keys or lifetime");
773
778
  const keys = signingKeys.map((key) => {
774
779
  const jwk = "privateKey" in key ? key.privateKey : key.publicKey;
780
+ signingAlgorithm(jwk);
775
781
  return {
776
782
  kty: jwk.kty,
777
783
  ...jwk.kty === "RSA" ? { n: jwk.n, e: jwk.e } : { crv: jwk.crv, x: jwk.x },
778
- ...jwk.y ? { y: jwk.y } : {},
779
784
  kid: jwkThumbprint(jwk),
780
785
  use: "sig"
781
786
  };
@@ -806,10 +811,10 @@ async function createSignedSignatureDirectory(originUrl, signingKeys, options =
806
811
  components: [component("@authority", { req: true }), component("content-digest")],
807
812
  parameters: {
808
813
  // Backdate created so verifiers whose clocks run slightly behind accept a fresh proof.
809
- created: now - DIRECTORY_CLOCK_SKEW_SECONDS,
814
+ created: now - CLOCK_SKEW_SECONDS,
810
815
  expires: now + lifetime,
811
816
  keyid: keys[i].kid,
812
- alg: algorithm(keys[i]),
817
+ alg: signingAlgorithm(keys[i]),
813
818
  tag: DIRECTORY_TAG
814
819
  },
815
820
  signer: webBotSigner(signingKeys[i])
@@ -823,6 +828,7 @@ async function createSignedSignatureDirectory(originUrl, signingKeys, options =
823
828
  return response;
824
829
  }
825
830
  function createWebBotAuthFetch(upstream, key, options) {
831
+ assertWebBotAuthSigningKey(key);
826
832
  return async (input, init) => {
827
833
  const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
828
834
  if (options.shouldSign && !options.shouldSign(url, init)) return upstream(input, init);
@@ -852,10 +858,13 @@ export {
852
858
  SIGNATURE_DIRECTORY_MEDIA_TYPE,
853
859
  SIGNATURE_DIRECTORY_PATH,
854
860
  SignatureDirectoryResolver,
861
+ WEB_BOT_AUTH_ALGORITHMS,
855
862
  WEB_BOT_AUTH_DRAFT_REVISION,
856
863
  WEB_BOT_AUTH_PROFILE,
857
864
  WebBotAuthError,
858
865
  WebBotAuthUnavailableError,
866
+ advertisesOnlyWebBotAuth,
867
+ assertWebBotAuthSigningKey,
859
868
  buildWebBotAuthSignatureBase,
860
869
  createSignedSignatureDirectory,
861
870
  createWebBotAuthAuthorizationFailure,
@@ -871,9 +880,11 @@ export {
871
880
  registerWebBotAuthRevocationCheck,
872
881
  selectSignatureProfile,
873
882
  signWebBotAuth,
883
+ unsignedWebBotAuthError,
874
884
  verifyKeyDirectory,
875
885
  verifySignatureDirectory,
876
886
  verifyWebBotAuth,
887
+ webBotAuthAcceptSignature,
877
888
  webBotAuthPrincipalFailureError,
878
889
  webBotAuthToolFailure,
879
890
  webBotAuthUnavailableResponse