@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
@@ -14,17 +14,33 @@ export interface MonotonicOriginRevocationStore {
14
14
  merge(origin: string, snapshot: OriginRevocationSnapshot): Promise<void>;
15
15
  get(origin: string, keyid: string): Promise<RevocationDetail | undefined>;
16
16
  }
17
+ /**
18
+ * Process-local revocation history for development and tests. Public
19
+ * deployments need durable history (`createIdentity33StateStores`) so a
20
+ * restart never forgets a revocation.
21
+ *
22
+ * Capacity is bounded per origin, per operator (registrable domain) and in
23
+ * total. The operator budget stops one registrable domain's wildcard
24
+ * subdomains from filling the store alone; it does not reserve capacity for
25
+ * anyone else, so several operators together can still exhaust the total.
26
+ * Facts are never evicted to regain capacity; a refused merge fails closed.
27
+ */
17
28
  export declare class InMemoryOriginRevocationStore implements MonotonicOriginRevocationStore {
18
- private readonly limits;
19
29
  readonly durability: "process-local";
20
30
  private readonly revoked;
31
+ private readonly originKeys;
32
+ private readonly operatorKeys;
33
+ private readonly limits;
21
34
  constructor(limits?: {
22
- perOrigin: number;
23
- total: number;
35
+ perOrigin?: number;
36
+ perOperator?: number;
37
+ total?: number;
24
38
  });
25
39
  merge(origin: string, snapshot: OriginRevocationSnapshot): Promise<void>;
26
40
  get(origin: string, keyid: string): Promise<RevocationDetail | undefined>;
27
41
  }
42
+ /** See {@link OriginRevocationResolver.status}. */
43
+ export type OriginRevocationStatus = 'revoked' | 'current' | 'absent' | 'unknown';
28
44
  export declare class OriginRevocationResolver {
29
45
  private readonly options;
30
46
  private readonly freshness;
@@ -33,6 +49,7 @@ export declare class OriginRevocationResolver {
33
49
  private readonly pending;
34
50
  private readonly failures;
35
51
  private readonly persistenceFailures;
52
+ private readonly absentUntil;
36
53
  constructor(options: {
37
54
  directory: import('../signing/web-bot-auth.mjs').SignatureDirectoryResolver;
38
55
  store: MonotonicOriginRevocationStore;
@@ -41,7 +58,26 @@ export declare class OriginRevocationResolver {
41
58
  fetchOptions?: Pick<SsrfFetchOptions, 'lookup' | 'trustedFetchFn'>;
42
59
  maxOrigins?: number;
43
60
  });
61
+ /**
62
+ * Compatibility wrapper over {@link status}: `true` when revoked, `false`
63
+ * when the list is current, and `revocation_unavailable` when the identity
64
+ * publishes no list (`absent`) or its status is not known (`unknown`).
65
+ */
44
66
  isRevoked(originUrl: string, keyid: string): Promise<boolean>;
67
+ /**
68
+ * Revocation status of `keyid` at `origin`:
69
+ * - `revoked`: a stored (permanent) revocation covers the key.
70
+ * - `current`: a signed list is current, or within `next_update` + grace.
71
+ * - `absent`: the origin serves no list (404/410). The identity is still
72
+ * established but cannot be bound for spend.
73
+ * - `unknown`: no list could be verified and grace has passed (or there was
74
+ * never a list). The identity is not established.
75
+ * Throws {@link WebBotAuthUnavailableError} (`revocation_persistence_failed`)
76
+ * when the history store fails: an infrastructure outage, never a
77
+ * revocation outcome.
78
+ */
79
+ status(originUrl: string, keyid: string): Promise<OriginRevocationStatus>;
80
+ private currentStatus;
45
81
  private readStored;
46
82
  private refresh;
47
83
  private now;
@@ -14,17 +14,33 @@ export interface MonotonicOriginRevocationStore {
14
14
  merge(origin: string, snapshot: OriginRevocationSnapshot): Promise<void>;
15
15
  get(origin: string, keyid: string): Promise<RevocationDetail | undefined>;
16
16
  }
17
+ /**
18
+ * Process-local revocation history for development and tests. Public
19
+ * deployments need durable history (`createIdentity33StateStores`) so a
20
+ * restart never forgets a revocation.
21
+ *
22
+ * Capacity is bounded per origin, per operator (registrable domain) and in
23
+ * total. The operator budget stops one registrable domain's wildcard
24
+ * subdomains from filling the store alone; it does not reserve capacity for
25
+ * anyone else, so several operators together can still exhaust the total.
26
+ * Facts are never evicted to regain capacity; a refused merge fails closed.
27
+ */
17
28
  export declare class InMemoryOriginRevocationStore implements MonotonicOriginRevocationStore {
18
- private readonly limits;
19
29
  readonly durability: "process-local";
20
30
  private readonly revoked;
31
+ private readonly originKeys;
32
+ private readonly operatorKeys;
33
+ private readonly limits;
21
34
  constructor(limits?: {
22
- perOrigin: number;
23
- total: number;
35
+ perOrigin?: number;
36
+ perOperator?: number;
37
+ total?: number;
24
38
  });
25
39
  merge(origin: string, snapshot: OriginRevocationSnapshot): Promise<void>;
26
40
  get(origin: string, keyid: string): Promise<RevocationDetail | undefined>;
27
41
  }
42
+ /** See {@link OriginRevocationResolver.status}. */
43
+ export type OriginRevocationStatus = 'revoked' | 'current' | 'absent' | 'unknown';
28
44
  export declare class OriginRevocationResolver {
29
45
  private readonly options;
30
46
  private readonly freshness;
@@ -33,6 +49,7 @@ export declare class OriginRevocationResolver {
33
49
  private readonly pending;
34
50
  private readonly failures;
35
51
  private readonly persistenceFailures;
52
+ private readonly absentUntil;
36
53
  constructor(options: {
37
54
  directory: import('../signing/web-bot-auth').SignatureDirectoryResolver;
38
55
  store: MonotonicOriginRevocationStore;
@@ -41,7 +58,26 @@ export declare class OriginRevocationResolver {
41
58
  fetchOptions?: Pick<SsrfFetchOptions, 'lookup' | 'trustedFetchFn'>;
42
59
  maxOrigins?: number;
43
60
  });
61
+ /**
62
+ * Compatibility wrapper over {@link status}: `true` when revoked, `false`
63
+ * when the list is current, and `revocation_unavailable` when the identity
64
+ * publishes no list (`absent`) or its status is not known (`unknown`).
65
+ */
44
66
  isRevoked(originUrl: string, keyid: string): Promise<boolean>;
67
+ /**
68
+ * Revocation status of `keyid` at `origin`:
69
+ * - `revoked`: a stored (permanent) revocation covers the key.
70
+ * - `current`: a signed list is current, or within `next_update` + grace.
71
+ * - `absent`: the origin serves no list (404/410). The identity is still
72
+ * established but cannot be bound for spend.
73
+ * - `unknown`: no list could be verified and grace has passed (or there was
74
+ * never a list). The identity is not established.
75
+ * Throws {@link WebBotAuthUnavailableError} (`revocation_persistence_failed`)
76
+ * when the history store fails: an infrastructure outage, never a
77
+ * revocation outcome.
78
+ */
79
+ status(originUrl: string, keyid: string): Promise<OriginRevocationStatus>;
80
+ private currentStatus;
45
81
  private readStored;
46
82
  private refresh;
47
83
  private now;
@@ -45,28 +45,51 @@ const REASONS = [
45
45
  "keyCompromise"
46
46
  ];
47
47
  class InMemoryOriginRevocationStore {
48
- constructor(limits = { perOrigin: 16384, total: 1e5 }) {
49
- this.limits = limits;
50
- }
51
- limits;
52
48
  durability = "process-local";
53
49
  revoked = /* @__PURE__ */ new Map();
50
+ originKeys = /* @__PURE__ */ new Map();
51
+ operatorKeys = /* @__PURE__ */ new Map();
52
+ limits;
53
+ constructor(limits = {}) {
54
+ this.limits = {
55
+ perOrigin: limits.perOrigin ?? 16384,
56
+ perOperator: limits.perOperator ?? 16384,
57
+ total: limits.total ?? 1e5
58
+ };
59
+ if (Object.values(this.limits).some((value) => !Number.isSafeInteger(value) || value < 1))
60
+ throw new TypeError("Revocation limits must be positive safe integers");
61
+ }
54
62
  async merge(origin, snapshot) {
55
63
  origin = (0, import_canonicalize.canonicalAgentOrigin)(origin);
64
+ const operator = operatorOf(origin);
56
65
  const additions = new Set(snapshot.revoked_kids.filter((kid) => !this.revoked.has(JSON.stringify([origin, kid]))));
57
- const count = [...this.revoked.keys()].filter((key) => JSON.parse(key)[0] === origin).length;
58
- if (count + additions.size > this.limits.perOrigin || this.revoked.size + additions.size > this.limits.total)
66
+ const originCount = this.originKeys.get(origin) ?? 0, operatorCount = this.operatorKeys.get(operator) ?? 0;
67
+ if (originCount + additions.size > this.limits.perOrigin || operatorCount + additions.size > this.limits.perOperator || this.revoked.size + additions.size > this.limits.total)
59
68
  throw new import_web_bot_auth.WebBotAuthError(403, "revocation_storage_limit");
60
69
  for (const kid of snapshot.revoked_kids) {
61
70
  const key = JSON.stringify([origin, kid]), prior = this.revoked.get(key), detail = snapshot.revocation_details?.[kid] ?? {};
62
71
  this.revoked.set(key, (0, import_revocation2.mergeRevocationDetails)(prior, detail));
63
72
  }
73
+ if (additions.size) {
74
+ this.originKeys.set(origin, originCount + additions.size);
75
+ this.operatorKeys.set(operator, operatorCount + additions.size);
76
+ }
64
77
  }
65
78
  async get(origin, keyid) {
66
79
  const entry = this.revoked.get(JSON.stringify([(0, import_canonicalize.canonicalAgentOrigin)(origin), keyid]));
67
80
  return entry && { ...entry };
68
81
  }
69
82
  }
83
+ function operatorOf(origin) {
84
+ try {
85
+ return (0, import_etld.eTldPlusOne)(origin);
86
+ } catch {
87
+ return new URL(origin).hostname;
88
+ }
89
+ }
90
+ const MAX_POLLING_SECONDS = 1800;
91
+ class RevocationListAbsentError extends Error {
92
+ }
70
93
  class OriginRevocationResolver {
71
94
  constructor(options) {
72
95
  this.options = options;
@@ -82,31 +105,73 @@ class OriginRevocationResolver {
82
105
  pending = /* @__PURE__ */ new Map();
83
106
  failures = /* @__PURE__ */ new Map();
84
107
  persistenceFailures = /* @__PURE__ */ new Set();
108
+ absentUntil = /* @__PURE__ */ new Map();
109
+ /**
110
+ * Compatibility wrapper over {@link status}: `true` when revoked, `false`
111
+ * when the list is current, and `revocation_unavailable` when the identity
112
+ * publishes no list (`absent`) or its status is not known (`unknown`).
113
+ */
85
114
  async isRevoked(originUrl, keyid) {
115
+ const state = await this.status(originUrl, keyid);
116
+ if (state === "revoked") return true;
117
+ if (state === "current") return false;
118
+ throw new import_web_bot_auth.WebBotAuthError(403, state === "absent" ? "revocation_absent" : "revocation_unavailable");
119
+ }
120
+ /**
121
+ * Revocation status of `keyid` at `origin`:
122
+ * - `revoked`: a stored (permanent) revocation covers the key.
123
+ * - `current`: a signed list is current, or within `next_update` + grace.
124
+ * - `absent`: the origin serves no list (404/410). The identity is still
125
+ * established but cannot be bound for spend.
126
+ * - `unknown`: no list could be verified and grace has passed (or there was
127
+ * never a list). The identity is not established.
128
+ * Throws {@link WebBotAuthUnavailableError} (`revocation_persistence_failed`)
129
+ * when the history store fails: an infrastructure outage, never a
130
+ * revocation outcome.
131
+ */
132
+ async status(originUrl, keyid) {
133
+ try {
134
+ return await this.currentStatus(originUrl, keyid);
135
+ } catch (error) {
136
+ if (error instanceof import_web_bot_auth.WebBotAuthError && error.reason === "revocation_persistence_failed")
137
+ throw new import_web_bot_auth.WebBotAuthUnavailableError("revocation_persistence_failed", error);
138
+ throw error;
139
+ }
140
+ }
141
+ async currentStatus(originUrl, keyid) {
86
142
  const origin = (0, import_canonicalize.canonicalAgentOrigin)(originUrl), now = this.now();
87
- if (await this.readStored(origin, keyid)) return true;
143
+ if (await this.readStored(origin, keyid)) return "revoked";
144
+ const settled = async () => {
145
+ if (await this.readStored(origin, keyid)) return "revoked";
146
+ if ((this.absentUntil.get(origin) ?? 0) > now) return "absent";
147
+ return (this.freshness.get(origin) ?? 0) > now || (this.graceUntil.get(origin) ?? 0) > now ? "current" : "unknown";
148
+ };
88
149
  if ((this.freshness.get(origin) ?? 0) <= now) {
89
150
  if ((this.failures.get(origin) ?? 0) > now) {
90
151
  if (this.persistenceFailures.has(origin)) throw new import_web_bot_auth.WebBotAuthError(403, "revocation_persistence_failed");
91
- if ((this.graceUntil.get(origin) ?? 0) > now) return false;
92
- throw new import_web_bot_auth.WebBotAuthError(403, "revocation_unavailable");
152
+ return settled();
93
153
  }
94
154
  let promise = this.pending.get(origin);
95
155
  if (!promise) {
96
- if (this.pending.size >= 16 || [...this.pending.keys()].filter((pendingOrigin) => (0, import_etld.eTldPlusOne)(pendingOrigin) === (0, import_etld.eTldPlusOne)(origin)).length >= 2) {
97
- throw new import_web_bot_auth.WebBotAuthError(403, "revocation_unavailable");
156
+ if (this.pending.size >= 16 || [...this.pending.keys()].filter((pendingOrigin) => operatorOf(pendingOrigin) === operatorOf(origin)).length >= 2) {
157
+ return settled();
98
158
  }
99
159
  promise = this.refresh(origin).catch((error) => {
100
160
  if (this.failures.size >= (this.options.maxOrigins ?? 1e3)) {
101
161
  const oldest = this.failures.keys().next().value;
102
162
  this.failures.delete(oldest);
103
163
  this.persistenceFailures.delete(oldest);
164
+ this.absentUntil.delete(oldest);
104
165
  }
105
166
  this.failures.set(origin, now + 60);
106
167
  if (error instanceof import_web_bot_auth.WebBotAuthError && error.reason === "revocation_persistence_failed") {
107
168
  this.persistenceFailures.add(origin);
108
169
  this.graceUntil.delete(origin);
109
170
  }
171
+ if (error.cause instanceof RevocationListAbsentError) {
172
+ this.absentUntil.set(origin, now + 60);
173
+ this.graceUntil.delete(origin);
174
+ }
110
175
  throw error;
111
176
  });
112
177
  this.pending.set(origin, promise);
@@ -115,12 +180,11 @@ class OriginRevocationResolver {
115
180
  await promise;
116
181
  } catch (error) {
117
182
  if (error instanceof import_web_bot_auth.WebBotAuthError && error.reason === "revocation_persistence_failed") throw error;
118
- if ((this.graceUntil.get(origin) ?? 0) <= now) throw error;
119
183
  } finally {
120
184
  if (this.pending.get(origin) === promise) this.pending.delete(origin);
121
185
  }
122
186
  }
123
- return !!await this.readStored(origin, keyid);
187
+ return settled();
124
188
  }
125
189
  async readStored(origin, keyid) {
126
190
  try {
@@ -138,6 +202,7 @@ class OriginRevocationResolver {
138
202
  maxBodyBytes: 256 * 1024,
139
203
  timeoutMs: 1e4
140
204
  });
205
+ if (response.status === 404 || response.status === 410) throw new RevocationListAbsentError();
141
206
  if (response.status !== 200) throw new Error("Unavailable revocation list");
142
207
  const body = new TextDecoder("utf-8", { fatal: true }).decode(response.body);
143
208
  let tokens;
@@ -163,9 +228,9 @@ class OriginRevocationResolver {
163
228
  if (header.typ !== "adcp-gov-revocation+jws" || typeof header.kid !== "string" || typeof header.alg !== "string" || !["EdDSA", "ES256"].includes(header.alg))
164
229
  continue;
165
230
  await this.options.directory.resolve(header.kid, origin);
166
- const proof = this.options.directory.getAuditProof(origin);
167
- if (!proof || new URL(proof.request.url).origin !== origin) continue;
168
- const checked = (0, import_web_bot_auth.verifySignatureDirectory)(proof, this.now(), this.options.directory.allowsTestKeys);
231
+ const checked = this.options.directory.getVerifiedDirectory(origin);
232
+ if (!checked || new URL(checked.proof.request.url).origin !== origin || checked.expires <= this.now())
233
+ continue;
169
234
  const jwk = checked.keys.find((key) => key.kid === header.kid);
170
235
  if (!jwk) continue;
171
236
  const result = await (0, import_jose.compactVerify)(token, await (0, import_jose.importJWK)(jwk, header.alg), {
@@ -182,7 +247,8 @@ class OriginRevocationResolver {
182
247
  32
183
248
  );
184
249
  const updated = Date.parse(snapshot.updated) / 1e3, next = Date.parse(snapshot.next_update) / 1e3, now = this.now();
185
- if (snapshot.issuer !== origin || !Number.isFinite(updated) || !Number.isFinite(next) || updated > now || next <= now || next <= updated || next > now + 1800 || !Array.isArray(snapshot.revoked_kids) || snapshot.revoked_kids.length > 4096 || snapshot.revoked_kids.some((k) => typeof k !== "string" || k.length > 256) || updated < (this.updated.get(origin) ?? 0))
250
+ if (snapshot.issuer !== origin || !Number.isFinite(updated) || !Number.isFinite(next) || // Tolerate a publisher clock slightly ahead, as for request `created`.
251
+ updated > now + 60 || next <= now || next <= updated || !Array.isArray(snapshot.revoked_kids) || snapshot.revoked_kids.length > 4096 || snapshot.revoked_kids.some((k) => typeof k !== "string" || k.length > 256) || updated < (this.updated.get(origin) ?? 0))
186
252
  throw new Error("Invalid revocation snapshot");
187
253
  for (const [kid, detail] of Object.entries(snapshot.revocation_details ?? {}))
188
254
  if (!snapshot.revoked_kids.includes(kid) || !detail || detail.reason && !REASONS.includes(detail.reason) || detail.invalidity_date && !Number.isFinite(Date.parse(detail.invalidity_date)))
@@ -200,11 +266,15 @@ class OriginRevocationResolver {
200
266
  this.updated.delete(oldest);
201
267
  this.graceUntil.delete(oldest);
202
268
  }
203
- this.graceUntil.set(origin, next + 4 * (this.options.pollingIntervalSeconds ?? 300));
269
+ this.graceUntil.set(
270
+ origin,
271
+ Math.min(next, now + MAX_POLLING_SECONDS) + 4 * (this.options.pollingIntervalSeconds ?? 300)
272
+ );
204
273
  this.updated.set(origin, updated);
205
274
  this.freshness.set(origin, Math.min(next, now + (this.options.pollingIntervalSeconds ?? 300)));
206
275
  this.failures.delete(origin);
207
276
  this.persistenceFailures.delete(origin);
277
+ this.absentUntil.delete(origin);
208
278
  } catch (cause) {
209
279
  if (cause instanceof import_web_bot_auth.WebBotAuthError && cause.reason === "revocation_persistence_failed") throw cause;
210
280
  const error = new import_web_bot_auth.WebBotAuthError(403, "revocation_unavailable");
@@ -6,7 +6,7 @@ import { parseStrictJson } from "../signing/agent-resolver/strict-json.mjs";
6
6
  import {
7
7
  SignatureDirectoryResolver,
8
8
  WebBotAuthError,
9
- verifySignatureDirectory,
9
+ WebBotAuthUnavailableError,
10
10
  jwkThumbprint
11
11
  } from "../signing/web-bot-auth.mjs";
12
12
  const REASONS = [
@@ -24,28 +24,51 @@ const REASONS = [
24
24
  import { mergeRevocationDetails } from "../signing/revocation.mjs";
25
25
  import { mergeRevocationDetails as mergeRevocationDetail } from "../signing/revocation.mjs";
26
26
  class InMemoryOriginRevocationStore {
27
- constructor(limits = { perOrigin: 16384, total: 1e5 }) {
28
- this.limits = limits;
29
- }
30
- limits;
31
27
  durability = "process-local";
32
28
  revoked = /* @__PURE__ */ new Map();
29
+ originKeys = /* @__PURE__ */ new Map();
30
+ operatorKeys = /* @__PURE__ */ new Map();
31
+ limits;
32
+ constructor(limits = {}) {
33
+ this.limits = {
34
+ perOrigin: limits.perOrigin ?? 16384,
35
+ perOperator: limits.perOperator ?? 16384,
36
+ total: limits.total ?? 1e5
37
+ };
38
+ if (Object.values(this.limits).some((value) => !Number.isSafeInteger(value) || value < 1))
39
+ throw new TypeError("Revocation limits must be positive safe integers");
40
+ }
33
41
  async merge(origin, snapshot) {
34
42
  origin = canonicalAgentOrigin(origin);
43
+ const operator = operatorOf(origin);
35
44
  const additions = new Set(snapshot.revoked_kids.filter((kid) => !this.revoked.has(JSON.stringify([origin, kid]))));
36
- const count = [...this.revoked.keys()].filter((key) => JSON.parse(key)[0] === origin).length;
37
- if (count + additions.size > this.limits.perOrigin || this.revoked.size + additions.size > this.limits.total)
45
+ const originCount = this.originKeys.get(origin) ?? 0, operatorCount = this.operatorKeys.get(operator) ?? 0;
46
+ if (originCount + additions.size > this.limits.perOrigin || operatorCount + additions.size > this.limits.perOperator || this.revoked.size + additions.size > this.limits.total)
38
47
  throw new WebBotAuthError(403, "revocation_storage_limit");
39
48
  for (const kid of snapshot.revoked_kids) {
40
49
  const key = JSON.stringify([origin, kid]), prior = this.revoked.get(key), detail = snapshot.revocation_details?.[kid] ?? {};
41
50
  this.revoked.set(key, mergeRevocationDetail(prior, detail));
42
51
  }
52
+ if (additions.size) {
53
+ this.originKeys.set(origin, originCount + additions.size);
54
+ this.operatorKeys.set(operator, operatorCount + additions.size);
55
+ }
43
56
  }
44
57
  async get(origin, keyid) {
45
58
  const entry = this.revoked.get(JSON.stringify([canonicalAgentOrigin(origin), keyid]));
46
59
  return entry && { ...entry };
47
60
  }
48
61
  }
62
+ function operatorOf(origin) {
63
+ try {
64
+ return eTldPlusOne(origin);
65
+ } catch {
66
+ return new URL(origin).hostname;
67
+ }
68
+ }
69
+ const MAX_POLLING_SECONDS = 1800;
70
+ class RevocationListAbsentError extends Error {
71
+ }
49
72
  class OriginRevocationResolver {
50
73
  constructor(options) {
51
74
  this.options = options;
@@ -61,31 +84,73 @@ class OriginRevocationResolver {
61
84
  pending = /* @__PURE__ */ new Map();
62
85
  failures = /* @__PURE__ */ new Map();
63
86
  persistenceFailures = /* @__PURE__ */ new Set();
87
+ absentUntil = /* @__PURE__ */ new Map();
88
+ /**
89
+ * Compatibility wrapper over {@link status}: `true` when revoked, `false`
90
+ * when the list is current, and `revocation_unavailable` when the identity
91
+ * publishes no list (`absent`) or its status is not known (`unknown`).
92
+ */
64
93
  async isRevoked(originUrl, keyid) {
94
+ const state = await this.status(originUrl, keyid);
95
+ if (state === "revoked") return true;
96
+ if (state === "current") return false;
97
+ throw new WebBotAuthError(403, state === "absent" ? "revocation_absent" : "revocation_unavailable");
98
+ }
99
+ /**
100
+ * Revocation status of `keyid` at `origin`:
101
+ * - `revoked`: a stored (permanent) revocation covers the key.
102
+ * - `current`: a signed list is current, or within `next_update` + grace.
103
+ * - `absent`: the origin serves no list (404/410). The identity is still
104
+ * established but cannot be bound for spend.
105
+ * - `unknown`: no list could be verified and grace has passed (or there was
106
+ * never a list). The identity is not established.
107
+ * Throws {@link WebBotAuthUnavailableError} (`revocation_persistence_failed`)
108
+ * when the history store fails: an infrastructure outage, never a
109
+ * revocation outcome.
110
+ */
111
+ async status(originUrl, keyid) {
112
+ try {
113
+ return await this.currentStatus(originUrl, keyid);
114
+ } catch (error) {
115
+ if (error instanceof WebBotAuthError && error.reason === "revocation_persistence_failed")
116
+ throw new WebBotAuthUnavailableError("revocation_persistence_failed", error);
117
+ throw error;
118
+ }
119
+ }
120
+ async currentStatus(originUrl, keyid) {
65
121
  const origin = canonicalAgentOrigin(originUrl), now = this.now();
66
- if (await this.readStored(origin, keyid)) return true;
122
+ if (await this.readStored(origin, keyid)) return "revoked";
123
+ const settled = async () => {
124
+ if (await this.readStored(origin, keyid)) return "revoked";
125
+ if ((this.absentUntil.get(origin) ?? 0) > now) return "absent";
126
+ return (this.freshness.get(origin) ?? 0) > now || (this.graceUntil.get(origin) ?? 0) > now ? "current" : "unknown";
127
+ };
67
128
  if ((this.freshness.get(origin) ?? 0) <= now) {
68
129
  if ((this.failures.get(origin) ?? 0) > now) {
69
130
  if (this.persistenceFailures.has(origin)) throw new WebBotAuthError(403, "revocation_persistence_failed");
70
- if ((this.graceUntil.get(origin) ?? 0) > now) return false;
71
- throw new WebBotAuthError(403, "revocation_unavailable");
131
+ return settled();
72
132
  }
73
133
  let promise = this.pending.get(origin);
74
134
  if (!promise) {
75
- if (this.pending.size >= 16 || [...this.pending.keys()].filter((pendingOrigin) => eTldPlusOne(pendingOrigin) === eTldPlusOne(origin)).length >= 2) {
76
- throw new WebBotAuthError(403, "revocation_unavailable");
135
+ if (this.pending.size >= 16 || [...this.pending.keys()].filter((pendingOrigin) => operatorOf(pendingOrigin) === operatorOf(origin)).length >= 2) {
136
+ return settled();
77
137
  }
78
138
  promise = this.refresh(origin).catch((error) => {
79
139
  if (this.failures.size >= (this.options.maxOrigins ?? 1e3)) {
80
140
  const oldest = this.failures.keys().next().value;
81
141
  this.failures.delete(oldest);
82
142
  this.persistenceFailures.delete(oldest);
143
+ this.absentUntil.delete(oldest);
83
144
  }
84
145
  this.failures.set(origin, now + 60);
85
146
  if (error instanceof WebBotAuthError && error.reason === "revocation_persistence_failed") {
86
147
  this.persistenceFailures.add(origin);
87
148
  this.graceUntil.delete(origin);
88
149
  }
150
+ if (error.cause instanceof RevocationListAbsentError) {
151
+ this.absentUntil.set(origin, now + 60);
152
+ this.graceUntil.delete(origin);
153
+ }
89
154
  throw error;
90
155
  });
91
156
  this.pending.set(origin, promise);
@@ -94,12 +159,11 @@ class OriginRevocationResolver {
94
159
  await promise;
95
160
  } catch (error) {
96
161
  if (error instanceof WebBotAuthError && error.reason === "revocation_persistence_failed") throw error;
97
- if ((this.graceUntil.get(origin) ?? 0) <= now) throw error;
98
162
  } finally {
99
163
  if (this.pending.get(origin) === promise) this.pending.delete(origin);
100
164
  }
101
165
  }
102
- return !!await this.readStored(origin, keyid);
166
+ return settled();
103
167
  }
104
168
  async readStored(origin, keyid) {
105
169
  try {
@@ -117,6 +181,7 @@ class OriginRevocationResolver {
117
181
  maxBodyBytes: 256 * 1024,
118
182
  timeoutMs: 1e4
119
183
  });
184
+ if (response.status === 404 || response.status === 410) throw new RevocationListAbsentError();
120
185
  if (response.status !== 200) throw new Error("Unavailable revocation list");
121
186
  const body = new TextDecoder("utf-8", { fatal: true }).decode(response.body);
122
187
  let tokens;
@@ -142,9 +207,9 @@ class OriginRevocationResolver {
142
207
  if (header.typ !== "adcp-gov-revocation+jws" || typeof header.kid !== "string" || typeof header.alg !== "string" || !["EdDSA", "ES256"].includes(header.alg))
143
208
  continue;
144
209
  await this.options.directory.resolve(header.kid, origin);
145
- const proof = this.options.directory.getAuditProof(origin);
146
- if (!proof || new URL(proof.request.url).origin !== origin) continue;
147
- const checked = verifySignatureDirectory(proof, this.now(), this.options.directory.allowsTestKeys);
210
+ const checked = this.options.directory.getVerifiedDirectory(origin);
211
+ if (!checked || new URL(checked.proof.request.url).origin !== origin || checked.expires <= this.now())
212
+ continue;
148
213
  const jwk = checked.keys.find((key) => key.kid === header.kid);
149
214
  if (!jwk) continue;
150
215
  const result = await compactVerify(token, await importJWK(jwk, header.alg), {
@@ -161,7 +226,8 @@ class OriginRevocationResolver {
161
226
  32
162
227
  );
163
228
  const updated = Date.parse(snapshot.updated) / 1e3, next = Date.parse(snapshot.next_update) / 1e3, now = this.now();
164
- if (snapshot.issuer !== origin || !Number.isFinite(updated) || !Number.isFinite(next) || updated > now || next <= now || next <= updated || next > now + 1800 || !Array.isArray(snapshot.revoked_kids) || snapshot.revoked_kids.length > 4096 || snapshot.revoked_kids.some((k) => typeof k !== "string" || k.length > 256) || updated < (this.updated.get(origin) ?? 0))
229
+ if (snapshot.issuer !== origin || !Number.isFinite(updated) || !Number.isFinite(next) || // Tolerate a publisher clock slightly ahead, as for request `created`.
230
+ updated > now + 60 || next <= now || next <= updated || !Array.isArray(snapshot.revoked_kids) || snapshot.revoked_kids.length > 4096 || snapshot.revoked_kids.some((k) => typeof k !== "string" || k.length > 256) || updated < (this.updated.get(origin) ?? 0))
165
231
  throw new Error("Invalid revocation snapshot");
166
232
  for (const [kid, detail] of Object.entries(snapshot.revocation_details ?? {}))
167
233
  if (!snapshot.revoked_kids.includes(kid) || !detail || detail.reason && !REASONS.includes(detail.reason) || detail.invalidity_date && !Number.isFinite(Date.parse(detail.invalidity_date)))
@@ -179,11 +245,15 @@ class OriginRevocationResolver {
179
245
  this.updated.delete(oldest);
180
246
  this.graceUntil.delete(oldest);
181
247
  }
182
- this.graceUntil.set(origin, next + 4 * (this.options.pollingIntervalSeconds ?? 300));
248
+ this.graceUntil.set(
249
+ origin,
250
+ Math.min(next, now + MAX_POLLING_SECONDS) + 4 * (this.options.pollingIntervalSeconds ?? 300)
251
+ );
183
252
  this.updated.set(origin, updated);
184
253
  this.freshness.set(origin, Math.min(next, now + (this.options.pollingIntervalSeconds ?? 300)));
185
254
  this.failures.delete(origin);
186
255
  this.persistenceFailures.delete(origin);
256
+ this.absentUntil.delete(origin);
187
257
  } catch (cause) {
188
258
  if (cause instanceof WebBotAuthError && cause.reason === "revocation_persistence_failed") throw cause;
189
259
  const error = new WebBotAuthError(403, "revocation_unavailable");
@@ -1,3 +1,4 @@
1
+ import type { OriginRevocationStatus } from './revocation.mjs';
1
2
  import type { AuthPrincipal } from '../server/auth.mjs';
2
3
  import type { HandlerContext, ProtocolHandlers } from '../server/create-adcp-server.mjs';
3
4
  import { type VerifiedWebBotSigner } from '../signing/web-bot-auth.mjs';
@@ -10,6 +11,17 @@ export declare function createIdentity33PrincipalResolver(options: {
10
11
  lifecycle: OriginIdentityLifecycle;
11
12
  anonymousPrincipal?: string;
12
13
  anonymousTools?: readonly string[];
14
+ /**
15
+ * Origin revocation, checked here (the identity layer), not during signature
16
+ * verification. A revoked key is refused without a challenge; unknown
17
+ * status leaves the identity unverified; an origin serving no list stays
18
+ * identity-established and cannot be bound or spend. Usually an
19
+ * `OriginRevocationResolver`. Without it, the server must supply the
20
+ * deprecated `webBotAuth.isRevoked`.
21
+ */
22
+ revocation?: {
23
+ status(origin: string, keyid: string): Promise<OriginRevocationStatus>;
24
+ };
13
25
  }): (signers: readonly VerifiedWebBotSigner[], operation: ResolvedOperation) => Promise<AuthPrincipal>;
14
26
  /** Pass to createAdcpServer.identity33.authorize; framework runs it before its replay cache. */
15
27
  export declare function createIdentity33Authorization<TAccount>(options: {
@@ -1,3 +1,4 @@
1
+ import type { OriginRevocationStatus } from './revocation';
1
2
  import type { AuthPrincipal } from '../server/auth';
2
3
  import type { HandlerContext, ProtocolHandlers } from '../server/create-adcp-server';
3
4
  import { type VerifiedWebBotSigner } from '../signing/web-bot-auth';
@@ -10,6 +11,17 @@ export declare function createIdentity33PrincipalResolver(options: {
10
11
  lifecycle: OriginIdentityLifecycle;
11
12
  anonymousPrincipal?: string;
12
13
  anonymousTools?: readonly string[];
14
+ /**
15
+ * Origin revocation, checked here (the identity layer), not during signature
16
+ * verification. A revoked key is refused without a challenge; unknown
17
+ * status leaves the identity unverified; an origin serving no list stays
18
+ * identity-established and cannot be bound or spend. Usually an
19
+ * `OriginRevocationResolver`. Without it, the server must supply the
20
+ * deprecated `webBotAuth.isRevoked`.
21
+ */
22
+ revocation?: {
23
+ status(origin: string, keyid: string): Promise<OriginRevocationStatus>;
24
+ };
13
25
  }): (signers: readonly VerifiedWebBotSigner[], operation: ResolvedOperation) => Promise<AuthPrincipal>;
14
26
  /** Pass to createAdcpServer.identity33.authorize; framework runs it before its replay cache. */
15
27
  export declare function createIdentity33Authorization<TAccount>(options: {