@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
@@ -24,21 +24,28 @@ __export(web_bot_auth_exports, {
24
24
  WEB_BOT_AUTH_DRAFT_REVISION: () => WEB_BOT_AUTH_DRAFT_REVISION,
25
25
  WEB_BOT_AUTH_PROFILE: () => WEB_BOT_AUTH_PROFILE,
26
26
  WebBotAuthError: () => WebBotAuthError,
27
+ WebBotAuthUnavailableError: () => WebBotAuthUnavailableError,
27
28
  buildWebBotAuthSignatureBase: () => buildWebBotAuthSignatureBase,
28
29
  createSignedSignatureDirectory: () => createSignedSignatureDirectory,
29
30
  createWebBotAuthAuthorizationFailure: () => createWebBotAuthAuthorizationFailure,
30
31
  createWebBotAuthFetch: () => createWebBotAuthFetch,
32
+ deferWebBotAuthPrincipalFailure: () => deferWebBotAuthPrincipalFailure,
31
33
  getWebBotAuthAuthorizationFailure: () => getWebBotAuthAuthorizationFailure,
32
34
  getWebBotAuthLegacyOriginGate: () => getWebBotAuthLegacyOriginGate,
33
35
  isVerifiedWebBotSigner: () => isVerifiedWebBotSigner,
36
+ isWebBotAuthRevocationChecked: () => isWebBotAuthRevocationChecked,
34
37
  jwkThumbprint: () => jwkThumbprint,
35
38
  receivedAgentOrigin: () => receivedAgentOrigin,
36
39
  registerWebBotAuthLegacyOriginGate: () => registerWebBotAuthLegacyOriginGate,
40
+ registerWebBotAuthRevocationCheck: () => registerWebBotAuthRevocationCheck,
37
41
  selectSignatureProfile: () => selectSignatureProfile,
38
42
  signWebBotAuth: () => signWebBotAuth,
43
+ verifyKeyDirectory: () => verifyKeyDirectory,
39
44
  verifySignatureDirectory: () => verifySignatureDirectory,
40
45
  verifyWebBotAuth: () => verifyWebBotAuth,
41
- webBotAuthToolFailure: () => webBotAuthToolFailure
46
+ webBotAuthPrincipalFailureError: () => webBotAuthPrincipalFailureError,
47
+ webBotAuthToolFailure: () => webBotAuthToolFailure,
48
+ webBotAuthUnavailableResponse: () => webBotAuthUnavailableResponse
42
49
  });
43
50
  module.exports = __toCommonJS(web_bot_auth_exports);
44
51
  var import_version = require('../version.js');
@@ -58,12 +65,14 @@ const WEB_BOT_AUTH_DRAFT_REVISION = "draft-ietf-webbotauth-httpsig-protocol-00";
58
65
  const SIGNATURE_DIRECTORY_PATH = "/.well-known/http-message-signatures-directory";
59
66
  const SIGNATURE_DIRECTORY_MEDIA_TYPE = "application/http-message-signatures-directory+json";
60
67
  const DIRECTORY_TAG = "http-message-signatures-directory";
68
+ const DIRECTORY_CLOCK_SKEW_SECONDS = 60;
61
69
  const TEST_KIDS = /* @__PURE__ */ new Set([
62
70
  "6WsKMCObba49V3uN0vIENnk1BKAk7c9zjBJcWKyQ9_I",
63
71
  "hHurKlVBnPJ4AGiPyl6HWMhxgkIcRBzaC8qWwEtnuVI",
64
72
  "ZtaFf5jj-EzorJmZn6mX3YnmK_QyZKVLbbinzIQKOjQ",
65
73
  "1XlJ3EIte7bPOEqKQtKrKNTNy5hywTyrwFXWrMbXKqE"
66
74
  ]);
75
+ const NO_CHALLENGE_REASONS = /* @__PURE__ */ new Set(["key_revoked", "identity_not_bound", "profile_disabled", "authorization_failed"]);
67
76
  class WebBotAuthError extends Error {
68
77
  constructor(status, reason) {
69
78
  super(
@@ -72,13 +81,7 @@ class WebBotAuthError extends Error {
72
81
  this.status = status;
73
82
  this.reason = reason;
74
83
  this.name = "WebBotAuthError";
75
- this.headers = status === 403 && ![
76
- "key_revoked",
77
- "identity_not_bound",
78
- "revocation_unavailable",
79
- "revocation_persistence_failed",
80
- "profile_disabled"
81
- ].includes(reason) ? {
84
+ this.headers = status === 403 && !NO_CHALLENGE_REASONS.has(reason) ? {
82
85
  "Accept-Signature": 'sig1=("@method" "@target-uri" "@authority" "content-type" "content-digest" "signature-agent";key="sig1");created;expires;nonce;keyid;tag="web-bot-auth"'
83
86
  } : status === 429 ? { "Retry-After": "60" } : {};
84
87
  }
@@ -86,18 +89,29 @@ class WebBotAuthError extends Error {
86
89
  reason;
87
90
  headers;
88
91
  get body() {
89
- if (["key_revoked", "identity_not_bound", "revocation_unavailable", "revocation_persistence_failed"].includes(
90
- this.reason
91
- ))
92
+ if (this.reason === "authorization_failed" || this.reason === "identity_not_bound")
92
93
  return {
93
- adcp_error: {
94
- code: this.reason.startsWith("revocation_") ? "SERVICE_UNAVAILABLE" : "PERMISSION_DENIED",
95
- message: this.message,
96
- recovery: this.reason.startsWith("revocation_") ? "transient" : "correctable"
97
- }
94
+ adcp_error: { code: "PERMISSION_DENIED", message: "Agent authorization failed", recovery: "correctable" }
98
95
  };
99
- return { error: this.message };
96
+ return { adcp_error: { code: "AUTH_INVALID", message: this.message, recovery: "terminal" } };
97
+ }
98
+ /**
99
+ * The signer's identity could not be established because key discovery
100
+ * failed (WBA §6.10) or its revocation status is unknown. The request is
101
+ * unverified; servers apply their unverified-request policy.
102
+ */
103
+ get unverified() {
104
+ return this.reason.startsWith("directory_") || this.reason === "identity_unverified";
105
+ }
106
+ }
107
+ class WebBotAuthUnavailableError extends Error {
108
+ constructor(reason, cause) {
109
+ super("Web Bot Auth verification dependency is unavailable");
110
+ this.reason = reason;
111
+ this.name = "WebBotAuthUnavailableError";
112
+ if (cause !== void 0) Object.defineProperty(this, "cause", { value: cause });
100
113
  }
114
+ reason;
101
115
  }
102
116
  function bad(reason) {
103
117
  throw new WebBotAuthError(400, reason);
@@ -106,7 +120,8 @@ function denied(reason) {
106
120
  throw new WebBotAuthError(403, reason);
107
121
  }
108
122
  function dictionary(value, field) {
109
- if (!value || value.length > 64 * 1024) return bad("headers");
123
+ if (value === void 0 || value === "") return denied("headers_missing");
124
+ if (value.length > 64 * 1024) return bad("headers");
110
125
  try {
111
126
  (0, import_parser.rejectDuplicateDictionaryKeys)(value, field);
112
127
  return (0, import_structured_headers.parseDictionary)(value);
@@ -126,12 +141,18 @@ function jwkThumbprint(jwk) {
126
141
  fields = { crv: jwk.crv, kty: jwk.kty, x: jwk.x };
127
142
  else if (jwk.kty === "EC" && typeof jwk.crv === "string" && typeof jwk.x === "string" && typeof jwk.y === "string")
128
143
  fields = { crv: jwk.crv, kty: jwk.kty, x: jwk.x, y: jwk.y };
144
+ else if (jwk.kty === "RSA" && typeof jwk.e === "string" && typeof jwk.n === "string")
145
+ fields = { e: jwk.e, kty: jwk.kty, n: jwk.n };
129
146
  else return denied("unsupported_key");
130
147
  return (0, import_crypto.createHash)("sha256").update(JSON.stringify(fields)).digest("base64url");
131
148
  }
132
149
  function algorithm(jwk) {
133
150
  if (jwk.kty === "OKP" && jwk.crv === "Ed25519") return "ed25519";
134
151
  if (jwk.kty === "EC" && jwk.crv === "P-256") return "ecdsa-p256-sha256";
152
+ if (jwk.kty === "RSA" && typeof jwk.n === "string" && jwk.e === "AQAB") {
153
+ const bytes = Buffer.from(jwk.n, "base64url").length;
154
+ if (bytes >= 256 && bytes <= 512) return "rsa-pss-sha512";
155
+ }
135
156
  return denied("unsupported_key");
136
157
  }
137
158
  function inputs(headers) {
@@ -175,8 +196,8 @@ function buildWebBotAuthSignatureBase(input, request, response) {
175
196
  if (params.has("key")) return bad("derived_dictionary");
176
197
  if (response && bare !== "@status" && !params.has("req")) return bad("response_component");
177
198
  if (bare === "@method") value = request.method.toUpperCase();
178
- else if (bare === "@target-uri") value = (0, import_canonicalize2.canonicalTargetUri)(request.url, "3.2");
179
- else if (bare === "@authority") value = (0, import_canonicalize2.canonicalAuthority)(request.url, "3.2");
199
+ else if (bare === "@target-uri") value = receivedTargetUri(request.url);
200
+ else if (bare === "@authority") value = receivedAuthority(request.url);
180
201
  else if (bare === "@status" && response && !params.has("req")) value = String(response.status);
181
202
  else return bad("derived_component");
182
203
  } else {
@@ -196,6 +217,21 @@ function buildWebBotAuthSignatureBase(input, request, response) {
196
217
  });
197
218
  return [...lines, `"@signature-params": ${input.signatureParamsValue}`].join("\n");
198
219
  }
220
+ function receivedTargetUri(url) {
221
+ try {
222
+ new URL(url);
223
+ } catch {
224
+ return bad("target_uri");
225
+ }
226
+ return url;
227
+ }
228
+ function receivedAuthority(url) {
229
+ try {
230
+ return new URL(url).host.toLowerCase();
231
+ } catch {
232
+ return bad("target_uri");
233
+ }
234
+ }
199
235
  function assertDigest(headers, body) {
200
236
  const digest = (0, import_canonicalize2.getHeaderValue)(headers, "content-digest");
201
237
  if (!digest || !(0, import_content_digest.contentDigestUsesEncoding)(digest, "rfc8941-base64") || !(0, import_content_digest.contentDigestMatches)(digest, body))
@@ -205,6 +241,7 @@ function verifyBytes(input, request, jwk, response) {
205
241
  const alg = algorithm(jwk);
206
242
  if (input.params.alg !== void 0 && input.params.alg !== alg) denied("algorithm");
207
243
  const signature = (0, import_canonicalize2.getHeaderValue)(response?.headers ?? request.headers, "signature");
244
+ if (signature === void 0 || signature === "") denied("headers_missing");
208
245
  let bytes;
209
246
  try {
210
247
  bytes = (0, import_parser.parseSignature)(signature ?? "", input.label, "rfc8941-base64").bytes;
@@ -231,16 +268,63 @@ function registerWebBotAuthLegacyOriginGate(resolver, gate) {
231
268
  function getWebBotAuthLegacyOriginGate(resolver) {
232
269
  return resolver && legacyOriginGates.get(resolver);
233
270
  }
271
+ const revocationCheckingResolvers = /* @__PURE__ */ new WeakSet();
272
+ function registerWebBotAuthRevocationCheck(resolver) {
273
+ revocationCheckingResolvers.add(resolver);
274
+ }
275
+ function isWebBotAuthRevocationChecked(resolver) {
276
+ return !!resolver && revocationCheckingResolvers.has(resolver);
277
+ }
278
+ function webBotAuthUnavailableResponse() {
279
+ return {
280
+ status: 503,
281
+ headers: { "Retry-After": "60" },
282
+ body: {
283
+ adcp_error: {
284
+ code: "SERVICE_UNAVAILABLE",
285
+ message: "Agent identity verification is temporarily unavailable",
286
+ recovery: "transient"
287
+ }
288
+ }
289
+ };
290
+ }
234
291
  const authorizationFailures = /* @__PURE__ */ new WeakSet();
292
+ const AUTHORIZATION_FAILURE_MESSAGES = {
293
+ key_revoked: "Agent identity key is revoked",
294
+ identity_not_bound: "Agent identity is not onboarded for this operation",
295
+ revocation_unavailable: "Identity revocation information is unavailable",
296
+ authorization_failed: "Agent authorization failed",
297
+ authorization_unavailable: "Agent authorization is temporarily unavailable"
298
+ };
235
299
  function createWebBotAuthAuthorizationFailure(reason) {
300
+ const transient = reason === "revocation_unavailable" || reason === "authorization_unavailable";
236
301
  const failure = Object.freeze({
237
- code: reason === "revocation_unavailable" ? "SERVICE_UNAVAILABLE" : "PERMISSION_DENIED",
238
- 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",
239
- recovery: reason === "revocation_unavailable" ? "transient" : "correctable"
302
+ code: transient ? "SERVICE_UNAVAILABLE" : "PERMISSION_DENIED",
303
+ message: AUTHORIZATION_FAILURE_MESSAGES[reason],
304
+ recovery: transient ? "transient" : "correctable"
240
305
  });
241
306
  authorizationFailures.add(failure);
242
307
  return failure;
243
308
  }
309
+ function principalFailureIsTransient(error) {
310
+ const value = error;
311
+ if (value?.transient === true || value?.recovery === "transient" || value?.code === "SERVICE_UNAVAILABLE")
312
+ return true;
313
+ return typeof value?.code !== "string";
314
+ }
315
+ function webBotAuthPrincipalFailureError(error) {
316
+ return principalFailureIsTransient(error) ? new WebBotAuthUnavailableError("authorization_unavailable", error) : new WebBotAuthError(403, "authorization_failed");
317
+ }
318
+ function deferWebBotAuthPrincipalFailure(signers, error) {
319
+ const signer = signers[signers.length - 1];
320
+ const authorizationFailure = createWebBotAuthAuthorizationFailure(
321
+ principalFailureIsTransient(error) ? "authorization_unavailable" : "authorization_failed"
322
+ );
323
+ return {
324
+ principal: `anonymous:${(0, import_crypto.createHash)("sha256").update(signer.origin).digest("hex")}`,
325
+ authorizationFailure
326
+ };
327
+ }
244
328
  function getWebBotAuthAuthorizationFailure(extra) {
245
329
  for (const value of [extra?.webBotAuthAuthorization, extra?.identity33]) {
246
330
  const claim = value;
@@ -306,7 +390,8 @@ async function verifyWebBotAuth(request, options) {
306
390
  const memberKey = component[1].get("key");
307
391
  if (typeof memberKey !== "string" || memberKey !== input.label) bad("agent_member");
308
392
  const member = agents.get(memberKey);
309
- if (!member || typeof member[0] !== "string") bad("signature_agent");
393
+ if (!member) denied("agent_not_covered");
394
+ if (typeof member[0] !== "string") bad("signature_agent");
310
395
  const type = member[1].get("type");
311
396
  if (type !== void 0 && type !== "directory") denied("agent_type");
312
397
  const origin = receivedAgentOrigin(member[0]);
@@ -326,27 +411,16 @@ async function verifyWebBotAuth(request, options) {
326
411
  assertDigest(request.headers, request.body);
327
412
  }
328
413
  await options.directory.resolve(keyid, origin);
329
- const proof = options.directory.getAuditProof(origin);
330
- if (!proof || new URL(proof.request.url).origin !== origin) denied("directory_origin");
331
- const checkedDirectory = verifySignatureDirectory(
332
- proof,
333
- now,
334
- options.allowTestKeys ?? options.directory.allowsTestKeys
335
- );
414
+ const checkedDirectory = options.directory.getVerifiedDirectory(origin);
415
+ if (!checkedDirectory || new URL(checkedDirectory.proof.request.url).origin !== origin)
416
+ denied("directory_origin");
417
+ if (checkedDirectory.expires <= now) denied("directory_expired");
418
+ if (!testKeysAllowed(options.allowTestKeys ?? options.directory.allowsTestKeys) && checkedDirectory.keys.some((key2) => TEST_KIDS.has(key2.kid)))
419
+ denied("directory_key");
336
420
  const key = checkedDirectory.keys.find((key2) => key2.kid === keyid);
337
421
  if (!key || jwkThumbprint(key) !== keyid) denied("key");
338
422
  verifyBytes(input, request, key);
339
- let authorizationFailure;
340
- try {
341
- if (await options.isRevoked(origin, keyid)) denied("key_revoked");
342
- } catch (error) {
343
- if (options.requireFreshRevocation === false && error instanceof WebBotAuthError && error.reason === "revocation_unavailable") {
344
- } else if (options.deferAuthorizationErrors && error instanceof WebBotAuthError && ["key_revoked", "revocation_unavailable", "revocation_persistence_failed"].includes(error.reason))
345
- authorizationFailure = createWebBotAuthAuthorizationFailure(
346
- error.reason === "revocation_persistence_failed" ? "revocation_unavailable" : error.reason
347
- );
348
- else throw error;
349
- }
423
+ if (options.isRevoked) await legacyRevocationCheck(options, origin, keyid);
350
424
  const nonceTuple = JSON.stringify([origin, nonce]);
351
425
  if (nonces.has(nonceTuple)) throw new WebBotAuthError(options.replayStatus ?? 429, "nonce_replayed");
352
426
  nonces.add(nonceTuple);
@@ -355,20 +429,25 @@ async function verifyWebBotAuth(request, options) {
355
429
  origin,
356
430
  keyid,
357
431
  label: input.label,
358
- verified_at: now,
359
- ...authorizationFailure && { authorizationFailure }
432
+ verified_at: now
360
433
  });
361
434
  verified.push(signer);
362
435
  }
363
436
  for (const signer of verified) {
364
437
  const input = selected.find((i) => i.label === signer.label);
365
- const result = await options.replayStore.insert(
366
- signer.origin,
367
- WEB_BOT_AUTH_PROFILE,
368
- input.params.nonce,
369
- Math.max(1, input.params.expires + 60 - now),
370
- now
371
- );
438
+ let result;
439
+ try {
440
+ result = await options.replayStore.insert(
441
+ signer.origin,
442
+ WEB_BOT_AUTH_PROFILE,
443
+ input.params.nonce,
444
+ // Acceptance is inclusive of expires + 60, so the nonce must outlive it.
445
+ input.params.expires + 60 - now + 1,
446
+ now
447
+ );
448
+ } catch (cause) {
449
+ throw new WebBotAuthUnavailableError("replay_unavailable", cause);
450
+ }
372
451
  if (result !== "ok")
373
452
  throw new WebBotAuthError(result === "replayed" ? options.replayStatus ?? 429 : 429, result);
374
453
  }
@@ -377,48 +456,109 @@ async function verifyWebBotAuth(request, options) {
377
456
  } catch (error) {
378
457
  if (error instanceof WebBotAuthError) {
379
458
  if (error.headers["Accept-Signature"]) {
380
- const label = inputs(request.headers)[0]?.label ?? "sig1";
459
+ let label = "sig1";
460
+ try {
461
+ label = inputs(request.headers)[0]?.label ?? "sig1";
462
+ } catch {
463
+ }
381
464
  const bodyComponents = request.body ? '"content-type" "content-digest" ' : "";
382
465
  error.headers["Accept-Signature"] = `${label}=("@method" "@target-uri" "@authority" ${bodyComponents}"signature-agent";key="${label}");created;expires;nonce;keyid;tag="web-bot-auth"`;
383
466
  }
384
467
  throw error;
385
468
  }
469
+ if (error instanceof WebBotAuthUnavailableError) throw error;
386
470
  return denied("verification_unavailable");
387
471
  }
388
472
  }
389
- function verifySignatureDirectory(response, now = Date.now() / 1e3, allowTestKeys = false) {
473
+ async function legacyRevocationCheck(options, origin, keyid) {
474
+ let revoked;
475
+ try {
476
+ revoked = await options.isRevoked(origin, keyid);
477
+ } catch (error) {
478
+ if (error instanceof WebBotAuthUnavailableError) throw error;
479
+ if (error instanceof WebBotAuthError && error.reason === "revocation_persistence_failed")
480
+ throw new WebBotAuthUnavailableError("revocation_persistence_failed", error);
481
+ if (error instanceof WebBotAuthError && error.reason === "key_revoked") return denied("key_revoked");
482
+ if (options.requireFreshRevocation === false && error instanceof WebBotAuthError && (error.reason === "revocation_unavailable" || error.reason === "revocation_absent"))
483
+ return;
484
+ return denied("identity_unverified");
485
+ }
486
+ if (revoked) denied("key_revoked");
487
+ }
488
+ function testKeysAllowed(allowTestKeys) {
489
+ return allowTestKeys === true && ["test", "development"].includes(process.env.NODE_ENV ?? "");
490
+ }
491
+ const UNSIGNED_DIRECTORY_MEDIA_TYPES = /* @__PURE__ */ new Set([
492
+ SIGNATURE_DIRECTORY_MEDIA_TYPE,
493
+ "application/jwk-set+json",
494
+ "application/json"
495
+ ]);
496
+ function verifyKeyDirectory(response, now = Date.now() / 1e3, options = {}) {
390
497
  try {
391
498
  if (!Number.isFinite(now)) denied("directory_invalid");
392
499
  const origin = (0, import_canonicalize.canonicalAgentOrigin)(response.request.url);
393
- if (response.request.url !== origin + SIGNATURE_DIRECTORY_PATH || response.status !== 200 || (0, import_canonicalize2.getHeaderValue)(response.headers, "content-type")?.split(";")[0]?.trim().toLowerCase() !== SIGNATURE_DIRECTORY_MEDIA_TYPE)
500
+ if (response.request.url !== origin + SIGNATURE_DIRECTORY_PATH || response.status !== 200)
501
+ denied("directory_response");
502
+ const signed = (0, import_canonicalize2.getHeaderValue)(response.headers, "signature-input") !== void 0;
503
+ if (!signed && options.requireSigned) denied("directory_unsigned");
504
+ const mediaType = (0, import_canonicalize2.getHeaderValue)(response.headers, "content-type")?.split(";")[0]?.trim().toLowerCase();
505
+ if (signed ? mediaType !== SIGNATURE_DIRECTORY_MEDIA_TYPE : !UNSIGNED_DIRECTORY_MEDIA_TYPES.has(mediaType ?? ""))
394
506
  denied("directory_response");
395
507
  const body = response.body ?? "";
396
508
  if (Buffer.byteLength(body) > 256 * 1024) denied("directory_size");
397
- assertDigest(response.headers, body);
509
+ if (signed || (0, import_canonicalize2.getHeaderValue)(response.headers, "content-digest") !== void 0)
510
+ assertDirectoryDigest(response.headers, body);
398
511
  const document = (0, import_strict_json.parseStrictJson)(body, 32);
399
512
  if (!Array.isArray(document.keys) || !document.keys.length || document.keys.length > 64) denied("directory_keys");
400
- const signatures = inputs(response.headers);
401
- const keys = /* @__PURE__ */ new Set();
513
+ const signatures = signed ? inputs(response.headers) : [];
514
+ const kids = /* @__PURE__ */ new Set();
515
+ const keys = [];
402
516
  let expires = Infinity;
403
- for (const key of document.keys) {
404
- 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))
517
+ for (const raw of document.keys) {
518
+ if (!raw || typeof raw !== "object") denied("directory_key");
519
+ const thumbprint = jwkThumbprint(raw);
520
+ const key = { ...raw, kid: raw.kid ?? thumbprint };
521
+ 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))
405
522
  denied("directory_key");
406
- keys.add(key.kid);
523
+ algorithm(key);
524
+ kids.add(key.kid);
525
+ keys.push(key);
526
+ if (!signed) continue;
407
527
  const proof = signatures.find((input) => input.params.keyid === key.kid && input.params.tag === DIRECTORY_TAG);
408
- 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)
528
+ 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)
409
529
  denied("directory_proof");
410
530
  verifyBytes(proof, { ...response.request, headers: response.request.headers ?? {} }, key, response);
411
531
  expires = Math.min(expires, proof.params.expires);
412
532
  }
413
- return { keys: document.keys, expires, proof: response };
533
+ if (!signed) expires = now + unsignedAge(options.maxUnsignedAgeSeconds);
534
+ return { keys, expires, signed, proof: response };
414
535
  } catch (error) {
415
- if (error instanceof WebBotAuthError) throw error;
536
+ if (error instanceof WebBotAuthError) {
537
+ throw error.reason.startsWith("directory_") ? error : new WebBotAuthError(403, `directory_${error.reason}`);
538
+ }
416
539
  return denied("directory_invalid");
417
540
  }
418
541
  }
542
+ function unsignedAge(seconds) {
543
+ const value = seconds ?? 86400;
544
+ if (!Number.isFinite(value) || value < 60)
545
+ throw new TypeError("Unsigned directory age must be a finite number of seconds, at least 60");
546
+ return value;
547
+ }
548
+ function verifySignatureDirectory(response, now = Date.now() / 1e3, allowTestKeys = false) {
549
+ return verifyKeyDirectory(response, now, { allowTestKeys, requireSigned: true });
550
+ }
551
+ function assertDirectoryDigest(headers, body) {
552
+ try {
553
+ assertDigest(headers, body);
554
+ } catch {
555
+ denied("directory_digest");
556
+ }
557
+ }
419
558
  class SignatureDirectoryResolver {
420
559
  constructor(options = {}) {
421
560
  this.options = options;
561
+ unsignedAge(options.maxUnsignedDirectoryAgeSeconds);
422
562
  }
423
563
  options;
424
564
  cache = /* @__PURE__ */ new Map();
@@ -430,19 +570,22 @@ class SignatureDirectoryResolver {
430
570
  const origin = receivedAgentOrigin(signerOrigin);
431
571
  const now = (this.options.now ?? (() => Date.now() / 1e3))();
432
572
  const cached = this.cache.get(origin);
573
+ const usable = () => cached && cached.directory.expires > now ? cached.directory : void 0;
433
574
  if (cached && cached.freshUntil > now && cached.directory.expires > now) {
434
575
  const key = cached.directory.keys.find((k) => k.kid === keyid);
435
576
  if (key || (this.keyMissCooldown.get(origin) ?? 0) > now) return key ?? null;
436
577
  this.keyMissCooldown.set(origin, now + 60);
437
578
  }
438
- if ((this.failures.get(origin) ?? 0) > now) {
439
- if (cached && cached.directory.expires > now) return cached.directory.keys.find((k) => k.kid === keyid) ?? null;
440
- return denied("directory_unavailable");
441
- }
579
+ const fromFallback = () => {
580
+ const key = usable()?.keys.find((k) => k.kid === keyid);
581
+ return key ?? denied("directory_unavailable");
582
+ };
583
+ if ((this.failures.get(origin) ?? 0) > now) return fromFallback();
442
584
  let pending = this.pending.get(origin);
443
585
  if (!pending) {
444
586
  if ([...this.pending.keys()].filter((pendingOrigin) => (0, import_etld.eTldPlusOne)(pendingOrigin) === (0, import_etld.eTldPlusOne)(origin)).length >= 2 || this.pending.size >= (this.options.maxConcurrentFetches ?? 16)) {
445
- if (cached && cached.directory.expires > now) return cached.directory.keys.find((k) => k.kid === keyid) ?? null;
587
+ const key = usable()?.keys.find((k) => k.kid === keyid);
588
+ if (key) return key;
446
589
  throw new WebBotAuthError(429, "directory_fetch_limit");
447
590
  }
448
591
  pending = this.fetch(origin, now);
@@ -451,7 +594,7 @@ class SignatureDirectoryResolver {
451
594
  try {
452
595
  return (await pending).keys.find((k) => k.kid === keyid) ?? null;
453
596
  } catch (error) {
454
- if (cached && cached.directory.expires > now) return cached.directory.keys.find((k) => k.kid === keyid) ?? null;
597
+ if (usable()) return fromFallback();
455
598
  throw error;
456
599
  } finally {
457
600
  if (this.pending.get(origin) === pending) this.pending.delete(origin);
@@ -462,15 +605,22 @@ class SignatureDirectoryResolver {
462
605
  const url = origin + SIGNATURE_DIRECTORY_PATH;
463
606
  const result = await (0, import_net.ssrfSafeFetch)(url, {
464
607
  ...this.options.fetchOptions,
465
- headers: { accept: SIGNATURE_DIRECTORY_MEDIA_TYPE },
608
+ headers: {
609
+ accept: `${SIGNATURE_DIRECTORY_MEDIA_TYPE}, application/jwk-set+json;q=0.9, application/json;q=0.8`
610
+ },
466
611
  timeoutMs: 1e4,
467
612
  maxBodyBytes: 256 * 1024
468
613
  });
469
- const body = new TextDecoder("utf-8", { fatal: true }).decode(result.body);
470
- const directory = verifySignatureDirectory(
614
+ if (result.status !== 200) denied("directory_unavailable");
615
+ const body = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(result.body);
616
+ const directory = verifyKeyDirectory(
471
617
  { status: result.status, headers: result.headers, body, request: { method: "GET", url } },
472
618
  now,
473
- this.options.allowTestKeys
619
+ {
620
+ allowTestKeys: this.options.allowTestKeys,
621
+ requireSigned: this.options.requireSignedDirectories,
622
+ maxUnsignedAgeSeconds: this.options.maxUnsignedDirectoryAgeSeconds
623
+ }
474
624
  );
475
625
  const cacheControl = result.headers["cache-control"] ?? "";
476
626
  const maxAge = /(?:^|,)\s*(?:no-cache|no-store)(?:,|$)/i.test(cacheControl) ? 0 : Number(cacheControl.match(/(?:^|,)\s*max-age=(\d+)/i)?.[1] ?? 300);
@@ -495,10 +645,14 @@ class SignatureDirectoryResolver {
495
645
  }
496
646
  }
497
647
  get allowsTestKeys() {
498
- return this.options.allowTestKeys === true && ["test", "development"].includes(process.env.NODE_ENV ?? "");
648
+ return testKeysAllowed(this.options.allowTestKeys);
649
+ }
650
+ /** The directory currently used to verify `origin`, including one kept through a failed refresh. */
651
+ getVerifiedDirectory(origin) {
652
+ return this.cache.get(receivedAgentOrigin(origin))?.directory;
499
653
  }
500
654
  getAuditProof(origin) {
501
- return this.cache.get(receivedAgentOrigin(origin))?.directory.proof;
655
+ return this.getVerifiedDirectory(origin)?.proof;
502
656
  }
503
657
  }
504
658
  async function signBase(base, key) {
@@ -512,6 +666,12 @@ async function signBase(base, key) {
512
666
  return key.provider.sign(Buffer.from(base));
513
667
  }
514
668
  const privateKey = (0, import_crypto.createPrivateKey)({ key: jwk, format: "jwk" });
669
+ if (alg === "rsa-pss-sha512")
670
+ return (0, import_crypto.sign)("sha512", Buffer.from(base), {
671
+ key: privateKey,
672
+ padding: import_crypto.constants.RSA_PKCS1_PSS_PADDING,
673
+ saltLength: 64
674
+ });
515
675
  return alg === "ed25519" ? (0, import_crypto.sign)(null, Buffer.from(base), privateKey) : (0, import_crypto.sign)("sha256", Buffer.from(base), { key: privateKey, dsaEncoding: "ieee-p1363" });
516
676
  }
517
677
  async function signWebBotAuth(request, key, options) {
@@ -521,9 +681,15 @@ async function signWebBotAuth(request, key, options) {
521
681
  const jwk = "privateKey" in key ? key.privateKey : key.publicKey;
522
682
  const now = Math.floor((options.now ?? (() => Date.now() / 1e3))());
523
683
  const window = options.windowSeconds ?? 300;
524
- const nonce = options.nonce ?? (0, import_crypto.randomBytes)(32).toString("base64url");
684
+ const nonce = options.nonce ?? (0, import_crypto.randomBytes)(64).toString("base64url");
525
685
  if (!Number.isInteger(window) || window < 1 || window > 300 || !/^[A-Za-z0-9_+\/-]{22,256}={0,2}$/.test(nonce))
526
686
  throw new TypeError("Invalid Web Bot Auth lifetime or nonce");
687
+ if (!options.relay && ["signature", "signature-input", "signature-agent"].some(
688
+ (name) => (0, import_canonicalize2.getHeaderValue)(request.headers, name) !== void 0
689
+ ))
690
+ throw new TypeError(
691
+ "Request already carries signature fields; pass relay: true to add a covering signature instead of replacing them"
692
+ );
527
693
  const headers = {};
528
694
  for (const [name, value] of Object.entries(request.headers))
529
695
  if (value !== void 0 && !["signature", "signature-input", "signature-agent", "content-digest"].includes(name.toLowerCase()))
@@ -535,6 +701,8 @@ async function signWebBotAuth(request, key, options) {
535
701
  headers["content-digest"] = (0, import_content_digest.computeContentDigest)(request.body, "rfc8941-base64");
536
702
  components.push('"content-type"', '"content-digest"');
537
703
  }
704
+ if (options.relay && ["signature", "signature-input", "signature-agent"].some((name) => !(0, import_canonicalize2.getHeaderValue)(request.headers, name)))
705
+ throw new TypeError("Relay requires the existing signature fields");
538
706
  const earlier = options.relay ? inputs(request.headers) : [];
539
707
  if (earlier.some((input2) => input2.label === label || input2.params.tag !== WEB_BOT_AUTH_PROFILE))
540
708
  throw new TypeError("Relay requires existing Web Bot Auth signatures and a fresh label");
@@ -557,7 +725,10 @@ async function signWebBotAuth(request, key, options) {
557
725
  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"`;
558
726
  const input = (0, import_parser.parseSignatureInput)(headers["signature-input"], label);
559
727
  if (priorSignature) headers.signature = priorSignature;
560
- const bytes = await signBase(buildWebBotAuthSignatureBase(input, { ...request, headers }), key);
728
+ const bytes = await signBase(
729
+ buildWebBotAuthSignatureBase(input, { ...request, url: new URL(request.url).href, headers }),
730
+ key
731
+ );
561
732
  headers.signature = `${priorSignature ? priorSignature + ", " : ""}${label}=:${Buffer.from(bytes).toString("base64")}:`;
562
733
  return headers;
563
734
  }
@@ -573,8 +744,7 @@ async function createSignedSignatureDirectory(originUrl, signingKeys, options =
573
744
  const jwk = "privateKey" in key ? key.privateKey : key.publicKey;
574
745
  return {
575
746
  kty: jwk.kty,
576
- crv: jwk.crv,
577
- x: jwk.x,
747
+ ...jwk.kty === "RSA" ? { n: jwk.n, e: jwk.e } : { crv: jwk.crv, x: jwk.x },
578
748
  ...jwk.y ? { y: jwk.y } : {},
579
749
  kid: jwkThumbprint(jwk),
580
750
  use: "sig"
@@ -595,7 +765,7 @@ async function createSignedSignatureDirectory(originUrl, signingKeys, options =
595
765
  const signatureInputs = [], signatures = [];
596
766
  for (let i = 0; i < keys.length; i++) {
597
767
  const label = `key${i + 1}`;
598
- const value = `${label}=("@authority";req "content-digest");created=${now};expires=${now + lifetime};keyid="${keys[i].kid}";alg="${algorithm(keys[i])}";tag="${DIRECTORY_TAG}"`;
768
+ 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}"`;
599
769
  const input = (0, import_parser.parseSignatureInput)(value, label);
600
770
  const bytes = await signBase(
601
771
  buildWebBotAuthSignatureBase(input, { method: "GET", url: response.request.url, headers: {} }, response),
@@ -621,7 +791,7 @@ function createWebBotAuthFetch(upstream, key, options) {
621
791
  if (init?.body != null) {
622
792
  if (typeof init.body === "string") body = init.body;
623
793
  else if (init.body instanceof Uint8Array || init.body instanceof ArrayBuffer)
624
- body = new TextDecoder("utf-8", { fatal: true }).decode(init.body);
794
+ body = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(init.body);
625
795
  else throw new TypeError("Web Bot Auth fetch requires a UTF-8 string or byte body");
626
796
  }
627
797
  const headers = {};
@@ -642,19 +812,26 @@ function createWebBotAuthFetch(upstream, key, options) {
642
812
  WEB_BOT_AUTH_DRAFT_REVISION,
643
813
  WEB_BOT_AUTH_PROFILE,
644
814
  WebBotAuthError,
815
+ WebBotAuthUnavailableError,
645
816
  buildWebBotAuthSignatureBase,
646
817
  createSignedSignatureDirectory,
647
818
  createWebBotAuthAuthorizationFailure,
648
819
  createWebBotAuthFetch,
820
+ deferWebBotAuthPrincipalFailure,
649
821
  getWebBotAuthAuthorizationFailure,
650
822
  getWebBotAuthLegacyOriginGate,
651
823
  isVerifiedWebBotSigner,
824
+ isWebBotAuthRevocationChecked,
652
825
  jwkThumbprint,
653
826
  receivedAgentOrigin,
654
827
  registerWebBotAuthLegacyOriginGate,
828
+ registerWebBotAuthRevocationCheck,
655
829
  selectSignatureProfile,
656
830
  signWebBotAuth,
831
+ verifyKeyDirectory,
657
832
  verifySignatureDirectory,
658
833
  verifyWebBotAuth,
659
- webBotAuthToolFailure
834
+ webBotAuthPrincipalFailureError,
835
+ webBotAuthToolFailure,
836
+ webBotAuthUnavailableResponse
660
837
  });