@adcp/sdk 16.0.0-beta.1 → 16.0.0-beta.2

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 (97) hide show
  1. package/dist/lib/core/SingleAgentClient.d.mts +10 -1
  2. package/dist/lib/core/SingleAgentClient.d.ts +10 -1
  3. package/dist/lib/core/SingleAgentClient.js +29 -8
  4. package/dist/lib/core/SingleAgentClient.mjs +29 -8
  5. package/dist/lib/identity-3.3/generated-compat.d.mts +34 -0
  6. package/dist/lib/identity-3.3/generated-compat.d.ts +34 -0
  7. package/dist/lib/identity-3.3/generated-compat.js +16 -0
  8. package/dist/lib/identity-3.3/generated-compat.mjs +0 -0
  9. package/dist/lib/identity-3.3/index.d.mts +2 -2
  10. package/dist/lib/identity-3.3/index.d.ts +2 -2
  11. package/dist/lib/identity-3.3/index.js +4 -0
  12. package/dist/lib/identity-3.3/index.mjs +5 -1
  13. package/dist/lib/identity-3.3/lifecycle.d.mts +17 -5
  14. package/dist/lib/identity-3.3/lifecycle.d.ts +17 -5
  15. package/dist/lib/identity-3.3/lifecycle.js +32 -5
  16. package/dist/lib/identity-3.3/lifecycle.mjs +38 -6
  17. package/dist/lib/identity-3.3/revocation.d.mts +39 -3
  18. package/dist/lib/identity-3.3/revocation.d.ts +39 -3
  19. package/dist/lib/identity-3.3/revocation.js +88 -18
  20. package/dist/lib/identity-3.3/revocation.mjs +89 -19
  21. package/dist/lib/identity-3.3/server.d.mts +12 -0
  22. package/dist/lib/identity-3.3/server.d.ts +12 -0
  23. package/dist/lib/identity-3.3/server.js +103 -43
  24. package/dist/lib/identity-3.3/server.mjs +111 -45
  25. package/dist/lib/identity-3.3/trust.d.mts +62 -1
  26. package/dist/lib/identity-3.3/trust.d.ts +62 -1
  27. package/dist/lib/identity-3.3/trust.js +126 -21
  28. package/dist/lib/identity-3.3/trust.mjs +124 -21
  29. package/dist/lib/identity-3.3/types.d.mts +39 -11
  30. package/dist/lib/identity-3.3/types.d.ts +39 -11
  31. package/dist/lib/index.d.mts +1 -1
  32. package/dist/lib/index.d.ts +1 -1
  33. package/dist/lib/index.js +2 -0
  34. package/dist/lib/index.mjs +2 -0
  35. package/dist/lib/protocols/index.d.mts +1 -1
  36. package/dist/lib/protocols/index.d.ts +1 -1
  37. package/dist/lib/protocols/index.js +2 -2
  38. package/dist/lib/protocols/index.mjs +2 -2
  39. package/dist/lib/reporting/bigquery/index.js +8 -0
  40. package/dist/lib/reporting/bigquery/index.mjs +8 -0
  41. package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
  42. package/dist/lib/server/auth-signature.d.mts +6 -1
  43. package/dist/lib/server/auth-signature.d.ts +6 -1
  44. package/dist/lib/server/auth-signature.js +24 -13
  45. package/dist/lib/server/auth-signature.mjs +29 -13
  46. package/dist/lib/server/auth.d.mts +2 -2
  47. package/dist/lib/server/auth.d.ts +2 -2
  48. package/dist/lib/server/create-adcp-server.js +31 -10
  49. package/dist/lib/server/create-adcp-server.mjs +37 -11
  50. package/dist/lib/signing/crypto.js +8 -0
  51. package/dist/lib/signing/crypto.mjs +9 -1
  52. package/dist/lib/signing/fetch-async.js +1 -1
  53. package/dist/lib/signing/fetch-async.mjs +1 -1
  54. package/dist/lib/signing/fetch.js +1 -1
  55. package/dist/lib/signing/fetch.mjs +1 -1
  56. package/dist/lib/signing/middleware.js +9 -0
  57. package/dist/lib/signing/middleware.mjs +14 -0
  58. package/dist/lib/signing/parser.d.mts +8 -5
  59. package/dist/lib/signing/parser.d.ts +8 -5
  60. package/dist/lib/signing/parser.js +77 -61
  61. package/dist/lib/signing/parser.mjs +77 -61
  62. package/dist/lib/signing/revocation.js +3 -0
  63. package/dist/lib/signing/revocation.mjs +3 -0
  64. package/dist/lib/signing/verifier.js +1 -1
  65. package/dist/lib/signing/verifier.mjs +1 -1
  66. package/dist/lib/signing/web-bot-auth.d.mts +84 -10
  67. package/dist/lib/signing/web-bot-auth.d.ts +84 -10
  68. package/dist/lib/signing/web-bot-auth.js +259 -82
  69. package/dist/lib/signing/web-bot-auth.mjs +259 -87
  70. package/dist/lib/signing/webhook-verifier.js +1 -1
  71. package/dist/lib/signing/webhook-verifier.mjs +1 -1
  72. package/dist/lib/testing/storyboard/validations.d.mts +1 -1
  73. package/dist/lib/testing/storyboard/validations.d.ts +1 -1
  74. package/dist/lib/v2/projection/catalog-snapshot.js +3 -1
  75. package/dist/lib/v2/projection/catalog-snapshot.mjs +3 -1
  76. package/dist/lib/v2/projection/creative-delivery.d.mts +31 -9
  77. package/dist/lib/v2/projection/creative-delivery.d.ts +31 -9
  78. package/dist/lib/v2/projection/index.d.mts +1 -1
  79. package/dist/lib/v2/projection/index.d.ts +1 -1
  80. package/dist/lib/v2/projection/index.js +2 -0
  81. package/dist/lib/v2/projection/index.mjs +2 -0
  82. package/dist/lib/v2/projection/legacy-routes.d.mts +10 -0
  83. package/dist/lib/v2/projection/legacy-routes.d.ts +10 -0
  84. package/dist/lib/v2/projection/legacy-routes.js +7 -0
  85. package/dist/lib/v2/projection/legacy-routes.mjs +6 -0
  86. package/dist/lib/version.d.mts +3 -3
  87. package/dist/lib/version.d.ts +3 -3
  88. package/dist/lib/version.js +3 -3
  89. package/dist/lib/version.mjs +3 -3
  90. package/docs/TYPE-SUMMARY.md +2 -2
  91. package/docs/guides/REPORTING-LEDGER.md +15 -5
  92. package/docs/guides/REPORTING-OPERATIONS.md +17 -11
  93. package/docs/llms.txt +2 -2
  94. package/docs/migration-12-to-13.md +8 -0
  95. package/docs/migration-14.x-rc-worksheet.md +4 -4
  96. package/docs/migration-agent-identity-3.3.md +150 -11
  97. package/package.json +3 -1
@@ -1,16 +1,18 @@
1
1
  import { toReleasePrecisionVersion } from "../version.mjs";
2
- import { createHash, createPrivateKey, randomBytes, sign as nodeSign } from "crypto";
2
+ import {
3
+ constants as cryptoConstants,
4
+ createHash,
5
+ createPrivateKey,
6
+ randomBytes,
7
+ sign as nodeSign
8
+ } from "crypto";
3
9
  import { parseDictionary, parseItem, serializeItem } from "structured-headers";
4
10
  import { ssrfSafeFetch } from "../net/index.mjs";
5
11
  import { eTldPlusOne } from "./agent-resolver/etld.mjs";
6
12
  import { canonicalAgentOrigin } from "./agent-resolver/canonicalize.mjs";
7
13
  import { canonicalAgentUrl } from "./agent-resolver/select-agent.mjs";
8
14
  import { parseStrictJson } from "./agent-resolver/strict-json.mjs";
9
- import {
10
- canonicalAuthority,
11
- canonicalTargetUri,
12
- getHeaderValue
13
- } from "./canonicalize.mjs";
15
+ import { getHeaderValue } from "./canonicalize.mjs";
14
16
  import { computeContentDigest, contentDigestMatches, contentDigestUsesEncoding } from "./content-digest.mjs";
15
17
  import { jwkToPublicKey, verifySignature } from "./crypto.mjs";
16
18
  import {
@@ -23,12 +25,14 @@ const WEB_BOT_AUTH_DRAFT_REVISION = "draft-ietf-webbotauth-httpsig-protocol-00";
23
25
  const SIGNATURE_DIRECTORY_PATH = "/.well-known/http-message-signatures-directory";
24
26
  const SIGNATURE_DIRECTORY_MEDIA_TYPE = "application/http-message-signatures-directory+json";
25
27
  const DIRECTORY_TAG = "http-message-signatures-directory";
28
+ const DIRECTORY_CLOCK_SKEW_SECONDS = 60;
26
29
  const TEST_KIDS = /* @__PURE__ */ new Set([
27
30
  "6WsKMCObba49V3uN0vIENnk1BKAk7c9zjBJcWKyQ9_I",
28
31
  "hHurKlVBnPJ4AGiPyl6HWMhxgkIcRBzaC8qWwEtnuVI",
29
32
  "ZtaFf5jj-EzorJmZn6mX3YnmK_QyZKVLbbinzIQKOjQ",
30
33
  "1XlJ3EIte7bPOEqKQtKrKNTNy5hywTyrwFXWrMbXKqE"
31
34
  ]);
35
+ const NO_CHALLENGE_REASONS = /* @__PURE__ */ new Set(["key_revoked", "identity_not_bound", "profile_disabled", "authorization_failed"]);
32
36
  class WebBotAuthError extends Error {
33
37
  constructor(status, reason) {
34
38
  super(
@@ -37,13 +41,7 @@ class WebBotAuthError extends Error {
37
41
  this.status = status;
38
42
  this.reason = reason;
39
43
  this.name = "WebBotAuthError";
40
- this.headers = status === 403 && ![
41
- "key_revoked",
42
- "identity_not_bound",
43
- "revocation_unavailable",
44
- "revocation_persistence_failed",
45
- "profile_disabled"
46
- ].includes(reason) ? {
44
+ this.headers = status === 403 && !NO_CHALLENGE_REASONS.has(reason) ? {
47
45
  "Accept-Signature": 'sig1=("@method" "@target-uri" "@authority" "content-type" "content-digest" "signature-agent";key="sig1");created;expires;nonce;keyid;tag="web-bot-auth"'
48
46
  } : status === 429 ? { "Retry-After": "60" } : {};
49
47
  }
@@ -51,19 +49,30 @@ class WebBotAuthError extends Error {
51
49
  reason;
52
50
  headers;
53
51
  get body() {
54
- if (["key_revoked", "identity_not_bound", "revocation_unavailable", "revocation_persistence_failed"].includes(
55
- this.reason
56
- ))
52
+ if (this.reason === "authorization_failed" || this.reason === "identity_not_bound")
57
53
  return {
58
- adcp_error: {
59
- code: this.reason.startsWith("revocation_") ? "SERVICE_UNAVAILABLE" : "PERMISSION_DENIED",
60
- message: this.message,
61
- recovery: this.reason.startsWith("revocation_") ? "transient" : "correctable"
62
- }
54
+ adcp_error: { code: "PERMISSION_DENIED", message: "Agent authorization failed", recovery: "correctable" }
63
55
  };
64
- return { error: this.message };
56
+ return { adcp_error: { code: "AUTH_INVALID", message: this.message, recovery: "terminal" } };
57
+ }
58
+ /**
59
+ * The signer's identity could not be established because key discovery
60
+ * failed (WBA §6.10) or its revocation status is unknown. The request is
61
+ * unverified; servers apply their unverified-request policy.
62
+ */
63
+ get unverified() {
64
+ return this.reason.startsWith("directory_") || this.reason === "identity_unverified";
65
65
  }
66
66
  }
67
+ class WebBotAuthUnavailableError extends Error {
68
+ constructor(reason, cause) {
69
+ super("Web Bot Auth verification dependency is unavailable");
70
+ this.reason = reason;
71
+ this.name = "WebBotAuthUnavailableError";
72
+ if (cause !== void 0) Object.defineProperty(this, "cause", { value: cause });
73
+ }
74
+ reason;
75
+ }
67
76
  function bad(reason) {
68
77
  throw new WebBotAuthError(400, reason);
69
78
  }
@@ -71,7 +80,8 @@ function denied(reason) {
71
80
  throw new WebBotAuthError(403, reason);
72
81
  }
73
82
  function dictionary(value, field) {
74
- if (!value || value.length > 64 * 1024) return bad("headers");
83
+ if (value === void 0 || value === "") return denied("headers_missing");
84
+ if (value.length > 64 * 1024) return bad("headers");
75
85
  try {
76
86
  rejectDuplicateDictionaryKeys(value, field);
77
87
  return parseDictionary(value);
@@ -91,12 +101,18 @@ function jwkThumbprint(jwk) {
91
101
  fields = { crv: jwk.crv, kty: jwk.kty, x: jwk.x };
92
102
  else if (jwk.kty === "EC" && typeof jwk.crv === "string" && typeof jwk.x === "string" && typeof jwk.y === "string")
93
103
  fields = { crv: jwk.crv, kty: jwk.kty, x: jwk.x, y: jwk.y };
104
+ else if (jwk.kty === "RSA" && typeof jwk.e === "string" && typeof jwk.n === "string")
105
+ fields = { e: jwk.e, kty: jwk.kty, n: jwk.n };
94
106
  else return denied("unsupported_key");
95
107
  return createHash("sha256").update(JSON.stringify(fields)).digest("base64url");
96
108
  }
97
109
  function algorithm(jwk) {
98
110
  if (jwk.kty === "OKP" && jwk.crv === "Ed25519") return "ed25519";
99
111
  if (jwk.kty === "EC" && jwk.crv === "P-256") return "ecdsa-p256-sha256";
112
+ if (jwk.kty === "RSA" && typeof jwk.n === "string" && jwk.e === "AQAB") {
113
+ const bytes = Buffer.from(jwk.n, "base64url").length;
114
+ if (bytes >= 256 && bytes <= 512) return "rsa-pss-sha512";
115
+ }
100
116
  return denied("unsupported_key");
101
117
  }
102
118
  function inputs(headers) {
@@ -140,8 +156,8 @@ function buildWebBotAuthSignatureBase(input, request, response) {
140
156
  if (params.has("key")) return bad("derived_dictionary");
141
157
  if (response && bare !== "@status" && !params.has("req")) return bad("response_component");
142
158
  if (bare === "@method") value = request.method.toUpperCase();
143
- else if (bare === "@target-uri") value = canonicalTargetUri(request.url, "3.2");
144
- else if (bare === "@authority") value = canonicalAuthority(request.url, "3.2");
159
+ else if (bare === "@target-uri") value = receivedTargetUri(request.url);
160
+ else if (bare === "@authority") value = receivedAuthority(request.url);
145
161
  else if (bare === "@status" && response && !params.has("req")) value = String(response.status);
146
162
  else return bad("derived_component");
147
163
  } else {
@@ -161,6 +177,21 @@ function buildWebBotAuthSignatureBase(input, request, response) {
161
177
  });
162
178
  return [...lines, `"@signature-params": ${input.signatureParamsValue}`].join("\n");
163
179
  }
180
+ function receivedTargetUri(url) {
181
+ try {
182
+ new URL(url);
183
+ } catch {
184
+ return bad("target_uri");
185
+ }
186
+ return url;
187
+ }
188
+ function receivedAuthority(url) {
189
+ try {
190
+ return new URL(url).host.toLowerCase();
191
+ } catch {
192
+ return bad("target_uri");
193
+ }
194
+ }
164
195
  function assertDigest(headers, body) {
165
196
  const digest = getHeaderValue(headers, "content-digest");
166
197
  if (!digest || !contentDigestUsesEncoding(digest, "rfc8941-base64") || !contentDigestMatches(digest, body))
@@ -170,6 +201,7 @@ function verifyBytes(input, request, jwk, response) {
170
201
  const alg = algorithm(jwk);
171
202
  if (input.params.alg !== void 0 && input.params.alg !== alg) denied("algorithm");
172
203
  const signature = getHeaderValue(response?.headers ?? request.headers, "signature");
204
+ if (signature === void 0 || signature === "") denied("headers_missing");
173
205
  let bytes;
174
206
  try {
175
207
  bytes = parseSignature(signature ?? "", input.label, "rfc8941-base64").bytes;
@@ -196,16 +228,63 @@ function registerWebBotAuthLegacyOriginGate(resolver, gate) {
196
228
  function getWebBotAuthLegacyOriginGate(resolver) {
197
229
  return resolver && legacyOriginGates.get(resolver);
198
230
  }
231
+ const revocationCheckingResolvers = /* @__PURE__ */ new WeakSet();
232
+ function registerWebBotAuthRevocationCheck(resolver) {
233
+ revocationCheckingResolvers.add(resolver);
234
+ }
235
+ function isWebBotAuthRevocationChecked(resolver) {
236
+ return !!resolver && revocationCheckingResolvers.has(resolver);
237
+ }
238
+ function webBotAuthUnavailableResponse() {
239
+ return {
240
+ status: 503,
241
+ headers: { "Retry-After": "60" },
242
+ body: {
243
+ adcp_error: {
244
+ code: "SERVICE_UNAVAILABLE",
245
+ message: "Agent identity verification is temporarily unavailable",
246
+ recovery: "transient"
247
+ }
248
+ }
249
+ };
250
+ }
199
251
  const authorizationFailures = /* @__PURE__ */ new WeakSet();
252
+ const AUTHORIZATION_FAILURE_MESSAGES = {
253
+ key_revoked: "Agent identity key is revoked",
254
+ identity_not_bound: "Agent identity is not onboarded for this operation",
255
+ revocation_unavailable: "Identity revocation information is unavailable",
256
+ authorization_failed: "Agent authorization failed",
257
+ authorization_unavailable: "Agent authorization is temporarily unavailable"
258
+ };
200
259
  function createWebBotAuthAuthorizationFailure(reason) {
260
+ const transient = reason === "revocation_unavailable" || reason === "authorization_unavailable";
201
261
  const failure = Object.freeze({
202
- code: reason === "revocation_unavailable" ? "SERVICE_UNAVAILABLE" : "PERMISSION_DENIED",
203
- message: reason === "revocation_unavailable" ? "Identity revocation information is unavailable" : reason === "key_revoked" ? "Agent identity key is revoked" : "Agent identity is not onboarded for this operation",
204
- recovery: reason === "revocation_unavailable" ? "transient" : "correctable"
262
+ code: transient ? "SERVICE_UNAVAILABLE" : "PERMISSION_DENIED",
263
+ message: AUTHORIZATION_FAILURE_MESSAGES[reason],
264
+ recovery: transient ? "transient" : "correctable"
205
265
  });
206
266
  authorizationFailures.add(failure);
207
267
  return failure;
208
268
  }
269
+ function principalFailureIsTransient(error) {
270
+ const value = error;
271
+ if (value?.transient === true || value?.recovery === "transient" || value?.code === "SERVICE_UNAVAILABLE")
272
+ return true;
273
+ return typeof value?.code !== "string";
274
+ }
275
+ function webBotAuthPrincipalFailureError(error) {
276
+ return principalFailureIsTransient(error) ? new WebBotAuthUnavailableError("authorization_unavailable", error) : new WebBotAuthError(403, "authorization_failed");
277
+ }
278
+ function deferWebBotAuthPrincipalFailure(signers, error) {
279
+ const signer = signers[signers.length - 1];
280
+ const authorizationFailure = createWebBotAuthAuthorizationFailure(
281
+ principalFailureIsTransient(error) ? "authorization_unavailable" : "authorization_failed"
282
+ );
283
+ return {
284
+ principal: `anonymous:${createHash("sha256").update(signer.origin).digest("hex")}`,
285
+ authorizationFailure
286
+ };
287
+ }
209
288
  function getWebBotAuthAuthorizationFailure(extra) {
210
289
  for (const value of [extra?.webBotAuthAuthorization, extra?.identity33]) {
211
290
  const claim = value;
@@ -271,7 +350,8 @@ async function verifyWebBotAuth(request, options) {
271
350
  const memberKey = component[1].get("key");
272
351
  if (typeof memberKey !== "string" || memberKey !== input.label) bad("agent_member");
273
352
  const member = agents.get(memberKey);
274
- if (!member || typeof member[0] !== "string") bad("signature_agent");
353
+ if (!member) denied("agent_not_covered");
354
+ if (typeof member[0] !== "string") bad("signature_agent");
275
355
  const type = member[1].get("type");
276
356
  if (type !== void 0 && type !== "directory") denied("agent_type");
277
357
  const origin = receivedAgentOrigin(member[0]);
@@ -291,27 +371,16 @@ async function verifyWebBotAuth(request, options) {
291
371
  assertDigest(request.headers, request.body);
292
372
  }
293
373
  await options.directory.resolve(keyid, origin);
294
- const proof = options.directory.getAuditProof(origin);
295
- if (!proof || new URL(proof.request.url).origin !== origin) denied("directory_origin");
296
- const checkedDirectory = verifySignatureDirectory(
297
- proof,
298
- now,
299
- options.allowTestKeys ?? options.directory.allowsTestKeys
300
- );
374
+ const checkedDirectory = options.directory.getVerifiedDirectory(origin);
375
+ if (!checkedDirectory || new URL(checkedDirectory.proof.request.url).origin !== origin)
376
+ denied("directory_origin");
377
+ if (checkedDirectory.expires <= now) denied("directory_expired");
378
+ if (!testKeysAllowed(options.allowTestKeys ?? options.directory.allowsTestKeys) && checkedDirectory.keys.some((key2) => TEST_KIDS.has(key2.kid)))
379
+ denied("directory_key");
301
380
  const key = checkedDirectory.keys.find((key2) => key2.kid === keyid);
302
381
  if (!key || jwkThumbprint(key) !== keyid) denied("key");
303
382
  verifyBytes(input, request, key);
304
- let authorizationFailure;
305
- try {
306
- if (await options.isRevoked(origin, keyid)) denied("key_revoked");
307
- } catch (error) {
308
- if (options.requireFreshRevocation === false && error instanceof WebBotAuthError && error.reason === "revocation_unavailable") {
309
- } else if (options.deferAuthorizationErrors && error instanceof WebBotAuthError && ["key_revoked", "revocation_unavailable", "revocation_persistence_failed"].includes(error.reason))
310
- authorizationFailure = createWebBotAuthAuthorizationFailure(
311
- error.reason === "revocation_persistence_failed" ? "revocation_unavailable" : error.reason
312
- );
313
- else throw error;
314
- }
383
+ if (options.isRevoked) await legacyRevocationCheck(options, origin, keyid);
315
384
  const nonceTuple = JSON.stringify([origin, nonce]);
316
385
  if (nonces.has(nonceTuple)) throw new WebBotAuthError(options.replayStatus ?? 429, "nonce_replayed");
317
386
  nonces.add(nonceTuple);
@@ -320,20 +389,25 @@ async function verifyWebBotAuth(request, options) {
320
389
  origin,
321
390
  keyid,
322
391
  label: input.label,
323
- verified_at: now,
324
- ...authorizationFailure && { authorizationFailure }
392
+ verified_at: now
325
393
  });
326
394
  verified.push(signer);
327
395
  }
328
396
  for (const signer of verified) {
329
397
  const input = selected.find((i) => i.label === signer.label);
330
- const result = await options.replayStore.insert(
331
- signer.origin,
332
- WEB_BOT_AUTH_PROFILE,
333
- input.params.nonce,
334
- Math.max(1, input.params.expires + 60 - now),
335
- now
336
- );
398
+ let result;
399
+ try {
400
+ result = await options.replayStore.insert(
401
+ signer.origin,
402
+ WEB_BOT_AUTH_PROFILE,
403
+ input.params.nonce,
404
+ // Acceptance is inclusive of expires + 60, so the nonce must outlive it.
405
+ input.params.expires + 60 - now + 1,
406
+ now
407
+ );
408
+ } catch (cause) {
409
+ throw new WebBotAuthUnavailableError("replay_unavailable", cause);
410
+ }
337
411
  if (result !== "ok")
338
412
  throw new WebBotAuthError(result === "replayed" ? options.replayStatus ?? 429 : 429, result);
339
413
  }
@@ -342,48 +416,109 @@ async function verifyWebBotAuth(request, options) {
342
416
  } catch (error) {
343
417
  if (error instanceof WebBotAuthError) {
344
418
  if (error.headers["Accept-Signature"]) {
345
- const label = inputs(request.headers)[0]?.label ?? "sig1";
419
+ let label = "sig1";
420
+ try {
421
+ label = inputs(request.headers)[0]?.label ?? "sig1";
422
+ } catch {
423
+ }
346
424
  const bodyComponents = request.body ? '"content-type" "content-digest" ' : "";
347
425
  error.headers["Accept-Signature"] = `${label}=("@method" "@target-uri" "@authority" ${bodyComponents}"signature-agent";key="${label}");created;expires;nonce;keyid;tag="web-bot-auth"`;
348
426
  }
349
427
  throw error;
350
428
  }
429
+ if (error instanceof WebBotAuthUnavailableError) throw error;
351
430
  return denied("verification_unavailable");
352
431
  }
353
432
  }
354
- function verifySignatureDirectory(response, now = Date.now() / 1e3, allowTestKeys = false) {
433
+ async function legacyRevocationCheck(options, origin, keyid) {
434
+ let revoked;
435
+ try {
436
+ revoked = await options.isRevoked(origin, keyid);
437
+ } catch (error) {
438
+ if (error instanceof WebBotAuthUnavailableError) throw error;
439
+ if (error instanceof WebBotAuthError && error.reason === "revocation_persistence_failed")
440
+ throw new WebBotAuthUnavailableError("revocation_persistence_failed", error);
441
+ if (error instanceof WebBotAuthError && error.reason === "key_revoked") return denied("key_revoked");
442
+ if (options.requireFreshRevocation === false && error instanceof WebBotAuthError && (error.reason === "revocation_unavailable" || error.reason === "revocation_absent"))
443
+ return;
444
+ return denied("identity_unverified");
445
+ }
446
+ if (revoked) denied("key_revoked");
447
+ }
448
+ function testKeysAllowed(allowTestKeys) {
449
+ return allowTestKeys === true && ["test", "development"].includes(process.env.NODE_ENV ?? "");
450
+ }
451
+ const UNSIGNED_DIRECTORY_MEDIA_TYPES = /* @__PURE__ */ new Set([
452
+ SIGNATURE_DIRECTORY_MEDIA_TYPE,
453
+ "application/jwk-set+json",
454
+ "application/json"
455
+ ]);
456
+ function verifyKeyDirectory(response, now = Date.now() / 1e3, options = {}) {
355
457
  try {
356
458
  if (!Number.isFinite(now)) denied("directory_invalid");
357
459
  const origin = canonicalAgentOrigin(response.request.url);
358
- if (response.request.url !== origin + SIGNATURE_DIRECTORY_PATH || response.status !== 200 || getHeaderValue(response.headers, "content-type")?.split(";")[0]?.trim().toLowerCase() !== SIGNATURE_DIRECTORY_MEDIA_TYPE)
460
+ if (response.request.url !== origin + SIGNATURE_DIRECTORY_PATH || response.status !== 200)
461
+ denied("directory_response");
462
+ const signed = getHeaderValue(response.headers, "signature-input") !== void 0;
463
+ if (!signed && options.requireSigned) denied("directory_unsigned");
464
+ const mediaType = getHeaderValue(response.headers, "content-type")?.split(";")[0]?.trim().toLowerCase();
465
+ if (signed ? mediaType !== SIGNATURE_DIRECTORY_MEDIA_TYPE : !UNSIGNED_DIRECTORY_MEDIA_TYPES.has(mediaType ?? ""))
359
466
  denied("directory_response");
360
467
  const body = response.body ?? "";
361
468
  if (Buffer.byteLength(body) > 256 * 1024) denied("directory_size");
362
- assertDigest(response.headers, body);
469
+ if (signed || getHeaderValue(response.headers, "content-digest") !== void 0)
470
+ assertDirectoryDigest(response.headers, body);
363
471
  const document = parseStrictJson(body, 32);
364
472
  if (!Array.isArray(document.keys) || !document.keys.length || document.keys.length > 64) denied("directory_keys");
365
- const signatures = inputs(response.headers);
366
- const keys = /* @__PURE__ */ new Set();
473
+ const signatures = signed ? inputs(response.headers) : [];
474
+ const kids = /* @__PURE__ */ new Set();
475
+ const keys = [];
367
476
  let expires = Infinity;
368
- for (const key of document.keys) {
369
- if (!key || typeof key !== "object" || typeof key.kid !== "string" || key.kid !== jwkThumbprint(key) || keys.has(key.kid) || key.d !== void 0 || Object.keys(key).some((k) => k.startsWith("_private")) || key.adcp_use !== void 0 || (!allowTestKeys || !["test", "development"].includes(process.env.NODE_ENV ?? "")) && TEST_KIDS.has(key.kid))
477
+ for (const raw of document.keys) {
478
+ if (!raw || typeof raw !== "object") denied("directory_key");
479
+ const thumbprint = jwkThumbprint(raw);
480
+ const key = { ...raw, kid: raw.kid ?? thumbprint };
481
+ if (key.kid !== thumbprint || kids.has(key.kid) || key.d !== void 0 || Object.keys(key).some((k) => k.startsWith("_private")) || key.adcp_use !== void 0 || !testKeysAllowed(options.allowTestKeys) && TEST_KIDS.has(key.kid))
370
482
  denied("directory_key");
371
- keys.add(key.kid);
483
+ algorithm(key);
484
+ kids.add(key.kid);
485
+ keys.push(key);
486
+ if (!signed) continue;
372
487
  const proof = signatures.find((input) => input.params.keyid === key.kid && input.params.tag === DIRECTORY_TAG);
373
- 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 || proof.params.expires <= now || proof.params.expires <= proof.params.created)
488
+ 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)
374
489
  denied("directory_proof");
375
490
  verifyBytes(proof, { ...response.request, headers: response.request.headers ?? {} }, key, response);
376
491
  expires = Math.min(expires, proof.params.expires);
377
492
  }
378
- return { keys: document.keys, expires, proof: response };
493
+ if (!signed) expires = now + unsignedAge(options.maxUnsignedAgeSeconds);
494
+ return { keys, expires, signed, proof: response };
379
495
  } catch (error) {
380
- if (error instanceof WebBotAuthError) throw error;
496
+ if (error instanceof WebBotAuthError) {
497
+ throw error.reason.startsWith("directory_") ? error : new WebBotAuthError(403, `directory_${error.reason}`);
498
+ }
381
499
  return denied("directory_invalid");
382
500
  }
383
501
  }
502
+ function unsignedAge(seconds) {
503
+ const value = seconds ?? 86400;
504
+ if (!Number.isFinite(value) || value < 60)
505
+ throw new TypeError("Unsigned directory age must be a finite number of seconds, at least 60");
506
+ return value;
507
+ }
508
+ function verifySignatureDirectory(response, now = Date.now() / 1e3, allowTestKeys = false) {
509
+ return verifyKeyDirectory(response, now, { allowTestKeys, requireSigned: true });
510
+ }
511
+ function assertDirectoryDigest(headers, body) {
512
+ try {
513
+ assertDigest(headers, body);
514
+ } catch {
515
+ denied("directory_digest");
516
+ }
517
+ }
384
518
  class SignatureDirectoryResolver {
385
519
  constructor(options = {}) {
386
520
  this.options = options;
521
+ unsignedAge(options.maxUnsignedDirectoryAgeSeconds);
387
522
  }
388
523
  options;
389
524
  cache = /* @__PURE__ */ new Map();
@@ -395,19 +530,22 @@ class SignatureDirectoryResolver {
395
530
  const origin = receivedAgentOrigin(signerOrigin);
396
531
  const now = (this.options.now ?? (() => Date.now() / 1e3))();
397
532
  const cached = this.cache.get(origin);
533
+ const usable = () => cached && cached.directory.expires > now ? cached.directory : void 0;
398
534
  if (cached && cached.freshUntil > now && cached.directory.expires > now) {
399
535
  const key = cached.directory.keys.find((k) => k.kid === keyid);
400
536
  if (key || (this.keyMissCooldown.get(origin) ?? 0) > now) return key ?? null;
401
537
  this.keyMissCooldown.set(origin, now + 60);
402
538
  }
403
- if ((this.failures.get(origin) ?? 0) > now) {
404
- if (cached && cached.directory.expires > now) return cached.directory.keys.find((k) => k.kid === keyid) ?? null;
405
- return denied("directory_unavailable");
406
- }
539
+ const fromFallback = () => {
540
+ const key = usable()?.keys.find((k) => k.kid === keyid);
541
+ return key ?? denied("directory_unavailable");
542
+ };
543
+ if ((this.failures.get(origin) ?? 0) > now) return fromFallback();
407
544
  let pending = this.pending.get(origin);
408
545
  if (!pending) {
409
546
  if ([...this.pending.keys()].filter((pendingOrigin) => eTldPlusOne(pendingOrigin) === eTldPlusOne(origin)).length >= 2 || this.pending.size >= (this.options.maxConcurrentFetches ?? 16)) {
410
- if (cached && cached.directory.expires > now) return cached.directory.keys.find((k) => k.kid === keyid) ?? null;
547
+ const key = usable()?.keys.find((k) => k.kid === keyid);
548
+ if (key) return key;
411
549
  throw new WebBotAuthError(429, "directory_fetch_limit");
412
550
  }
413
551
  pending = this.fetch(origin, now);
@@ -416,7 +554,7 @@ class SignatureDirectoryResolver {
416
554
  try {
417
555
  return (await pending).keys.find((k) => k.kid === keyid) ?? null;
418
556
  } catch (error) {
419
- if (cached && cached.directory.expires > now) return cached.directory.keys.find((k) => k.kid === keyid) ?? null;
557
+ if (usable()) return fromFallback();
420
558
  throw error;
421
559
  } finally {
422
560
  if (this.pending.get(origin) === pending) this.pending.delete(origin);
@@ -427,15 +565,22 @@ class SignatureDirectoryResolver {
427
565
  const url = origin + SIGNATURE_DIRECTORY_PATH;
428
566
  const result = await ssrfSafeFetch(url, {
429
567
  ...this.options.fetchOptions,
430
- headers: { accept: SIGNATURE_DIRECTORY_MEDIA_TYPE },
568
+ headers: {
569
+ accept: `${SIGNATURE_DIRECTORY_MEDIA_TYPE}, application/jwk-set+json;q=0.9, application/json;q=0.8`
570
+ },
431
571
  timeoutMs: 1e4,
432
572
  maxBodyBytes: 256 * 1024
433
573
  });
434
- const body = new TextDecoder("utf-8", { fatal: true }).decode(result.body);
435
- const directory = verifySignatureDirectory(
574
+ if (result.status !== 200) denied("directory_unavailable");
575
+ const body = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(result.body);
576
+ const directory = verifyKeyDirectory(
436
577
  { status: result.status, headers: result.headers, body, request: { method: "GET", url } },
437
578
  now,
438
- this.options.allowTestKeys
579
+ {
580
+ allowTestKeys: this.options.allowTestKeys,
581
+ requireSigned: this.options.requireSignedDirectories,
582
+ maxUnsignedAgeSeconds: this.options.maxUnsignedDirectoryAgeSeconds
583
+ }
439
584
  );
440
585
  const cacheControl = result.headers["cache-control"] ?? "";
441
586
  const maxAge = /(?:^|,)\s*(?:no-cache|no-store)(?:,|$)/i.test(cacheControl) ? 0 : Number(cacheControl.match(/(?:^|,)\s*max-age=(\d+)/i)?.[1] ?? 300);
@@ -460,10 +605,14 @@ class SignatureDirectoryResolver {
460
605
  }
461
606
  }
462
607
  get allowsTestKeys() {
463
- return this.options.allowTestKeys === true && ["test", "development"].includes(process.env.NODE_ENV ?? "");
608
+ return testKeysAllowed(this.options.allowTestKeys);
609
+ }
610
+ /** The directory currently used to verify `origin`, including one kept through a failed refresh. */
611
+ getVerifiedDirectory(origin) {
612
+ return this.cache.get(receivedAgentOrigin(origin))?.directory;
464
613
  }
465
614
  getAuditProof(origin) {
466
- return this.cache.get(receivedAgentOrigin(origin))?.directory.proof;
615
+ return this.getVerifiedDirectory(origin)?.proof;
467
616
  }
468
617
  }
469
618
  async function signBase(base, key) {
@@ -477,6 +626,12 @@ async function signBase(base, key) {
477
626
  return key.provider.sign(Buffer.from(base));
478
627
  }
479
628
  const privateKey = createPrivateKey({ key: jwk, format: "jwk" });
629
+ if (alg === "rsa-pss-sha512")
630
+ return nodeSign("sha512", Buffer.from(base), {
631
+ key: privateKey,
632
+ padding: cryptoConstants.RSA_PKCS1_PSS_PADDING,
633
+ saltLength: 64
634
+ });
480
635
  return alg === "ed25519" ? nodeSign(null, Buffer.from(base), privateKey) : nodeSign("sha256", Buffer.from(base), { key: privateKey, dsaEncoding: "ieee-p1363" });
481
636
  }
482
637
  async function signWebBotAuth(request, key, options) {
@@ -486,9 +641,15 @@ async function signWebBotAuth(request, key, options) {
486
641
  const jwk = "privateKey" in key ? key.privateKey : key.publicKey;
487
642
  const now = Math.floor((options.now ?? (() => Date.now() / 1e3))());
488
643
  const window = options.windowSeconds ?? 300;
489
- const nonce = options.nonce ?? randomBytes(32).toString("base64url");
644
+ const nonce = options.nonce ?? randomBytes(64).toString("base64url");
490
645
  if (!Number.isInteger(window) || window < 1 || window > 300 || !/^[A-Za-z0-9_+\/-]{22,256}={0,2}$/.test(nonce))
491
646
  throw new TypeError("Invalid Web Bot Auth lifetime or nonce");
647
+ if (!options.relay && ["signature", "signature-input", "signature-agent"].some(
648
+ (name) => getHeaderValue(request.headers, name) !== void 0
649
+ ))
650
+ throw new TypeError(
651
+ "Request already carries signature fields; pass relay: true to add a covering signature instead of replacing them"
652
+ );
492
653
  const headers = {};
493
654
  for (const [name, value] of Object.entries(request.headers))
494
655
  if (value !== void 0 && !["signature", "signature-input", "signature-agent", "content-digest"].includes(name.toLowerCase()))
@@ -500,6 +661,8 @@ async function signWebBotAuth(request, key, options) {
500
661
  headers["content-digest"] = computeContentDigest(request.body, "rfc8941-base64");
501
662
  components.push('"content-type"', '"content-digest"');
502
663
  }
664
+ if (options.relay && ["signature", "signature-input", "signature-agent"].some((name) => !getHeaderValue(request.headers, name)))
665
+ throw new TypeError("Relay requires the existing signature fields");
503
666
  const earlier = options.relay ? inputs(request.headers) : [];
504
667
  if (earlier.some((input2) => input2.label === label || input2.params.tag !== WEB_BOT_AUTH_PROFILE))
505
668
  throw new TypeError("Relay requires existing Web Bot Auth signatures and a fresh label");
@@ -522,7 +685,10 @@ async function signWebBotAuth(request, key, options) {
522
685
  headers["signature-input"] = `${priorInput ? priorInput + ", " : ""}${label}=(${components.join(" ")});created=${now};expires=${now + window};nonce="${nonce}";keyid="${jwkThumbprint(jwk)}";alg="${algorithm(jwk)}";tag="web-bot-auth"`;
523
686
  const input = parseSignatureInput(headers["signature-input"], label);
524
687
  if (priorSignature) headers.signature = priorSignature;
525
- const bytes = await signBase(buildWebBotAuthSignatureBase(input, { ...request, headers }), key);
688
+ const bytes = await signBase(
689
+ buildWebBotAuthSignatureBase(input, { ...request, url: new URL(request.url).href, headers }),
690
+ key
691
+ );
526
692
  headers.signature = `${priorSignature ? priorSignature + ", " : ""}${label}=:${Buffer.from(bytes).toString("base64")}:`;
527
693
  return headers;
528
694
  }
@@ -538,8 +704,7 @@ async function createSignedSignatureDirectory(originUrl, signingKeys, options =
538
704
  const jwk = "privateKey" in key ? key.privateKey : key.publicKey;
539
705
  return {
540
706
  kty: jwk.kty,
541
- crv: jwk.crv,
542
- x: jwk.x,
707
+ ...jwk.kty === "RSA" ? { n: jwk.n, e: jwk.e } : { crv: jwk.crv, x: jwk.x },
543
708
  ...jwk.y ? { y: jwk.y } : {},
544
709
  kid: jwkThumbprint(jwk),
545
710
  use: "sig"
@@ -560,7 +725,7 @@ async function createSignedSignatureDirectory(originUrl, signingKeys, options =
560
725
  const signatureInputs = [], signatures = [];
561
726
  for (let i = 0; i < keys.length; i++) {
562
727
  const label = `key${i + 1}`;
563
- const value = `${label}=("@authority";req "content-digest");created=${now};expires=${now + lifetime};keyid="${keys[i].kid}";alg="${algorithm(keys[i])}";tag="${DIRECTORY_TAG}"`;
728
+ const value = `${label}=("@authority";req "content-digest");created=${now - DIRECTORY_CLOCK_SKEW_SECONDS};expires=${now + lifetime};keyid="${keys[i].kid}";alg="${algorithm(keys[i])}";tag="${DIRECTORY_TAG}"`;
564
729
  const input = parseSignatureInput(value, label);
565
730
  const bytes = await signBase(
566
731
  buildWebBotAuthSignatureBase(input, { method: "GET", url: response.request.url, headers: {} }, response),
@@ -586,7 +751,7 @@ function createWebBotAuthFetch(upstream, key, options) {
586
751
  if (init?.body != null) {
587
752
  if (typeof init.body === "string") body = init.body;
588
753
  else if (init.body instanceof Uint8Array || init.body instanceof ArrayBuffer)
589
- body = new TextDecoder("utf-8", { fatal: true }).decode(init.body);
754
+ body = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(init.body);
590
755
  else throw new TypeError("Web Bot Auth fetch requires a UTF-8 string or byte body");
591
756
  }
592
757
  const headers = {};
@@ -606,19 +771,26 @@ export {
606
771
  WEB_BOT_AUTH_DRAFT_REVISION,
607
772
  WEB_BOT_AUTH_PROFILE,
608
773
  WebBotAuthError,
774
+ WebBotAuthUnavailableError,
609
775
  buildWebBotAuthSignatureBase,
610
776
  createSignedSignatureDirectory,
611
777
  createWebBotAuthAuthorizationFailure,
612
778
  createWebBotAuthFetch,
779
+ deferWebBotAuthPrincipalFailure,
613
780
  getWebBotAuthAuthorizationFailure,
614
781
  getWebBotAuthLegacyOriginGate,
615
782
  isVerifiedWebBotSigner,
783
+ isWebBotAuthRevocationChecked,
616
784
  jwkThumbprint,
617
785
  receivedAgentOrigin,
618
786
  registerWebBotAuthLegacyOriginGate,
787
+ registerWebBotAuthRevocationCheck,
619
788
  selectSignatureProfile,
620
789
  signWebBotAuth,
790
+ verifyKeyDirectory,
621
791
  verifySignatureDirectory,
622
792
  verifyWebBotAuth,
623
- webBotAuthToolFailure
793
+ webBotAuthPrincipalFailureError,
794
+ webBotAuthToolFailure,
795
+ webBotAuthUnavailableResponse
624
796
  };
@@ -209,7 +209,7 @@ async function verifyWebhookSignature(request, options) {
209
209
  if (keyResolution.operatorAuthorizationValidUntil !== void 0) {
210
210
  assertDelegatedOperatorAuthorizationActive(keyResolution.operatorAuthorizationValidUntil, currentTime());
211
211
  }
212
- const remaining = parsedInput.params.expires - now + import_types.CLOCK_SKEW_TOLERANCE_SECONDS;
212
+ const remaining = parsedInput.params.expires - now + import_types.CLOCK_SKEW_TOLERANCE_SECONDS + 1;
213
213
  const ttl = Math.max(remaining, import_types.MAX_SIGNATURE_WINDOW_SECONDS + import_types.CLOCK_SKEW_TOLERANCE_SECONDS);
214
214
  const insertResult = await options.replayStore.insert(jwk.kid, replayScope, parsedInput.params.nonce, ttl, now);
215
215
  if (insertResult === "replayed") {
@@ -182,7 +182,7 @@ async function verifyWebhookSignature(request, options) {
182
182
  if (keyResolution.operatorAuthorizationValidUntil !== void 0) {
183
183
  assertDelegatedOperatorAuthorizationActive(keyResolution.operatorAuthorizationValidUntil, currentTime());
184
184
  }
185
- const remaining = parsedInput.params.expires - now + CLOCK_SKEW_TOLERANCE_SECONDS;
185
+ const remaining = parsedInput.params.expires - now + CLOCK_SKEW_TOLERANCE_SECONDS + 1;
186
186
  const ttl = Math.max(remaining, MAX_SIGNATURE_WINDOW_SECONDS + CLOCK_SKEW_TOLERANCE_SECONDS);
187
187
  const insertResult = await options.replayStore.insert(jwk.kid, replayScope, parsedInput.params.nonce, ttl, now);
188
188
  if (insertResult === "replayed") {
@@ -197,7 +197,7 @@ export interface UpstreamTrafficQueryResult {
197
197
  */
198
198
  export declare function runValidations(validations: StoryboardValidation[], context: ValidationContext): ValidationResult[];
199
199
  /** Capability semver emitted in run summaries and used for advisory expiry. */
200
- export declare const RUNNER_CAPABILITY_VERSION = "16.0.0-beta.1";
200
+ export declare const RUNNER_CAPABILITY_VERSION = "16.0.0-beta.2";
201
201
  /** True when a failed validation contributes to the owning step's grade. */
202
202
  export declare function validationFailsStep(result: ValidationResult): boolean;
203
203
  /**
@@ -197,7 +197,7 @@ export interface UpstreamTrafficQueryResult {
197
197
  */
198
198
  export declare function runValidations(validations: StoryboardValidation[], context: ValidationContext): ValidationResult[];
199
199
  /** Capability semver emitted in run summaries and used for advisory expiry. */
200
- export declare const RUNNER_CAPABILITY_VERSION = "16.0.0-beta.1";
200
+ export declare const RUNNER_CAPABILITY_VERSION = "16.0.0-beta.2";
201
201
  /** True when a failed validation contributes to the owning step's grade. */
202
202
  export declare function validationFailsStep(result: ValidationResult): boolean;
203
203
  /**