@agentlogin/issuer 0.0.0-stage → 0.2.0-beta.1

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 (58) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +237 -2
  3. package/dist/agents.d.ts +40 -0
  4. package/dist/agents.js +257 -0
  5. package/dist/agents.js.map +1 -0
  6. package/dist/app.d.ts +3 -0
  7. package/dist/app.js +652 -0
  8. package/dist/app.js.map +1 -0
  9. package/dist/clients.d.ts +20 -0
  10. package/dist/clients.js +272 -0
  11. package/dist/clients.js.map +1 -0
  12. package/dist/constants.d.ts +30 -0
  13. package/dist/constants.js +32 -0
  14. package/dist/constants.js.map +1 -0
  15. package/dist/context.d.ts +37 -0
  16. package/dist/context.js +2 -0
  17. package/dist/context.js.map +1 -0
  18. package/dist/errors.d.ts +10 -0
  19. package/dist/errors.js +17 -0
  20. package/dist/errors.js.map +1 -0
  21. package/dist/index.d.ts +45 -0
  22. package/dist/index.js +107 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/owner-address.d.ts +2 -0
  25. package/dist/owner-address.js +24 -0
  26. package/dist/owner-address.js.map +1 -0
  27. package/dist/safefetch.d.ts +38 -0
  28. package/dist/safefetch.js +164 -0
  29. package/dist/safefetch.js.map +1 -0
  30. package/dist/server.d.ts +1 -0
  31. package/dist/server.js +89 -0
  32. package/dist/server.js.map +1 -0
  33. package/dist/signing-configuration.d.ts +14 -0
  34. package/dist/signing-configuration.js +35 -0
  35. package/dist/signing-configuration.js.map +1 -0
  36. package/dist/signing-state.d.ts +22 -0
  37. package/dist/signing-state.js +14 -0
  38. package/dist/signing-state.js.map +1 -0
  39. package/dist/sqlite-store.d.ts +24 -0
  40. package/dist/sqlite-store.js +81 -0
  41. package/dist/sqlite-store.js.map +1 -0
  42. package/dist/state.d.ts +30 -0
  43. package/dist/state.js +77 -0
  44. package/dist/state.js.map +1 -0
  45. package/dist/store.d.ts +693 -0
  46. package/dist/store.js +883 -0
  47. package/dist/store.js.map +1 -0
  48. package/dist/tokens.d.ts +37 -0
  49. package/dist/tokens.js +167 -0
  50. package/dist/tokens.js.map +1 -0
  51. package/dist/transactions.d.ts +90 -0
  52. package/dist/transactions.js +449 -0
  53. package/dist/transactions.js.map +1 -0
  54. package/dist/util.d.ts +29 -0
  55. package/dist/util.js +113 -0
  56. package/dist/util.js.map +1 -0
  57. package/package.json +37 -4
  58. package/static/wait.js +42 -0
package/dist/store.js ADDED
@@ -0,0 +1,883 @@
1
+ import { LEGACY_SIGNING_CACHE_SECONDS, LEGACY_SIGNING_OVERLAP_SECONDS, samePublicSigningKey, validateSigningPublication } from './signing-state.js';
2
+ import { ownerAddressKey } from './owner-address.js';
3
+ export const TERMINAL = new Set(['completed', 'denied', 'expired']);
4
+ /** Section 6.4: scopes may accumulate only under the same material disclosure terms. */
5
+ export function sameConsentMetadata(a, b) {
6
+ return a.redirectHost === b.redirectHost && a.clientName === b.clientName && a.tosUri === b.tosUri && a.policyUri === b.policyUri;
7
+ }
8
+ /** The material consent preconditions, shared by gate planning and atomic acceptance. */
9
+ export function consentRecordReason(record, expected, expireBefore) {
10
+ if (!record)
11
+ return 'first_visit';
12
+ if (!expected.scopes.every((scope) => record.scopes.includes(scope)) || !sameConsentMetadata(record, expected))
13
+ return 'material_change';
14
+ if (expireBefore !== undefined && record.lastUsedAt < expireBefore)
15
+ return 'expired';
16
+ return undefined;
17
+ }
18
+ /** Section 5.1: equality is allowed; zero requests fresh authentication, not instantaneous delivery. */
19
+ export function isAuthenticationTooOld(maxAge, authTime, now) {
20
+ if (maxAge === undefined)
21
+ return false; // Existing records predate this optional request constraint.
22
+ if (!Number.isSafeInteger(maxAge) || maxAge < 0 || !Number.isSafeInteger(authTime) || authTime < 0)
23
+ return true;
24
+ return maxAge > 0 && now - authTime > maxAge;
25
+ }
26
+ export class AssertionWindowError extends Error {
27
+ code = 'ASSERTION_WINDOW';
28
+ constructor() { super('The signed assertion is outside its validity window'); this.name = 'AssertionWindowError'; }
29
+ }
30
+ /** Stable across separate copies of this package loaded by an external adapter. */
31
+ export function isAssertionWindowError(error) {
32
+ return error instanceof Error && 'code' in error && error.code === 'ASSERTION_WINDOW';
33
+ }
34
+ /** Step 2 authority is rechecked atomically before consuming a first assertion. */
35
+ export class AssertionSubjectError extends Error {
36
+ error;
37
+ code = 'ASSERTION_SUBJECT';
38
+ constructor(error) {
39
+ super('The asserting key or Agent is no longer authorized');
40
+ this.error = error;
41
+ this.name = 'AssertionSubjectError';
42
+ }
43
+ }
44
+ export function isAssertionSubjectError(error) {
45
+ return error instanceof Error && 'code' in error && error.code === 'ASSERTION_SUBJECT' &&
46
+ 'error' in error && (error.error === 'unknown_key' || error.error === 'key_revoked');
47
+ }
48
+ /** First assertions cannot take effect at or after the transaction deadline. */
49
+ export class TransactionExpiredError extends Error {
50
+ code = 'TRANSACTION_EXPIRED';
51
+ constructor() { super('The transaction expired before assertion acceptance'); this.name = 'TransactionExpiredError'; }
52
+ }
53
+ export function isTransactionExpiredError(error) {
54
+ return error instanceof Error && 'code' in error && error.code === 'TRANSACTION_EXPIRED';
55
+ }
56
+ /** A previously identified transaction bearer no longer has authority. */
57
+ export class TransactionTokenError extends Error {
58
+ code = 'TRANSACTION_TOKEN';
59
+ constructor() { super('The transaction bearer is no longer valid'); this.name = 'TransactionTokenError'; }
60
+ }
61
+ export function isTransactionTokenError(error) {
62
+ return error instanceof Error && 'code' in error && error.code === 'TRANSACTION_TOKEN';
63
+ }
64
+ /** The public key is already active for another Agent; pending enrollments confer no exclusivity. */
65
+ export class KeyActivationConflictError extends Error {
66
+ code = 'KEY_ACTIVATION_CONFLICT';
67
+ constructor() { super('The public key is active for another Agent'); this.name = 'KeyActivationConflictError'; }
68
+ }
69
+ /** Stable across separate copies of this package loaded by an external adapter. */
70
+ export function isKeyActivationConflictError(error) {
71
+ return error instanceof Error && 'code' in error && error.code === 'KEY_ACTIVATION_CONFLICT';
72
+ }
73
+ /** Public operations only: internal reservation/count helpers cannot be called outside their atomic transition. */
74
+ export const STORE_METHODS = Object.freeze(Object.keys({
75
+ currentTime: true,
76
+ publishSigningKeys: true,
77
+ listSigningKeys: true,
78
+ activateSigningKey: true,
79
+ removeSigningKey: true,
80
+ reserveSigningKey: true,
81
+ putOwner: true,
82
+ deleteOwner: true,
83
+ getOwner: true,
84
+ getOwnerByEmail: true,
85
+ putOwnerCode: true,
86
+ takeOwnerCode: true,
87
+ putAgent: true,
88
+ getAgent: true,
89
+ setAgentEnabled: true,
90
+ deleteAgent: true,
91
+ listAgentsByOwner: true,
92
+ putCredential: true,
93
+ getCredential: true,
94
+ listCredentials: true,
95
+ deleteCredential: true,
96
+ resolveManagement: true,
97
+ readManagementKeys: true,
98
+ readManagementAccounts: true,
99
+ getKey: true,
100
+ listKeys: true,
101
+ insertKey: true,
102
+ activateKey: true,
103
+ revokeKey: true,
104
+ revokeAllKeys: true,
105
+ patchKey: true,
106
+ deleteKey: true,
107
+ wasRevoked: true,
108
+ putClient: true,
109
+ getClient: true,
110
+ insertTransaction: true,
111
+ getTransaction: true,
112
+ readTransaction: true,
113
+ updateTransaction: true,
114
+ listOpenTransactions: true,
115
+ listLegacyTransactions: true,
116
+ completeTransaction: true,
117
+ getCode: true,
118
+ redeemCode: true,
119
+ voidCodes: true,
120
+ insertAccessToken: true,
121
+ getAccessToken: true,
122
+ revokeAccessTokens: true,
123
+ getConsent: true,
124
+ listConsents: true,
125
+ deleteConsent: true,
126
+ getAccount: true,
127
+ listAccounts: true,
128
+ setAccountStatus: true,
129
+ deleteAccount: true,
130
+ signupsEver: true,
131
+ }));
132
+ /** Reads never rewrite state, including polling an effectively expired transaction. */
133
+ export const READ_METHODS = new Set([
134
+ 'currentTime', 'listSigningKeys', 'getOwner', 'getOwnerByEmail', 'getAgent', 'listAgentsByOwner', 'getCredential',
135
+ 'listCredentials', 'getKey', 'listKeys', 'wasRevoked', 'getClient', 'getTransaction', 'readTransaction',
136
+ 'resolveManagement', 'readManagementKeys', 'readManagementAccounts',
137
+ 'listOpenTransactions', 'listLegacyTransactions', 'getCode', 'getAccessToken', 'getConsent', 'listConsents', 'getAccount',
138
+ 'listAccounts', 'signupsEver',
139
+ ]);
140
+ const copy = (v) => (v === undefined ? v : structuredClone(v));
141
+ const matches = (x, f) => (f.kid === undefined || x.kid === f.kid) &&
142
+ (f.keyEnrollment === undefined || x.keyEnrollment === f.keyEnrollment) &&
143
+ (f.handle === undefined || x.handle === f.handle) &&
144
+ (f.clientId === undefined || x.clientId === f.clientId) &&
145
+ (f.ownerId === undefined || x.ownerId === f.ownerId) &&
146
+ (f.txnId === undefined || x.txnId === f.txnId);
147
+ export class StoreState {
148
+ signingKeys = new Map();
149
+ activeSigningKid;
150
+ legacySigningHistory = false;
151
+ deletedIdentities = new Map();
152
+ owners = new Map();
153
+ agents = new Map();
154
+ ownerCodes = new Map();
155
+ credentials = new Map();
156
+ keys = new Map();
157
+ revoked = new Set();
158
+ clients = new Map();
159
+ transactions = new Map();
160
+ legacyTransactions = new Set();
161
+ codes = new Map();
162
+ tokens = new Map();
163
+ consents = new Map();
164
+ accounts = new Map();
165
+ signups = new Map(); // legacy snapshot migration only
166
+ signupEvents = new Map();
167
+ deletedOwners = new Map();
168
+ reservations = new Map();
169
+ constructor(snapshot) {
170
+ if (snapshot !== undefined) {
171
+ const parsed = JSON.parse(snapshot);
172
+ if ((parsed.version !== 1 && parsed.version !== 2 && parsed.version !== 3) || !parsed.maps || !Array.isArray(parsed.revoked))
173
+ throw new Error('Unsupported issuer store snapshot');
174
+ for (const name of ['owners', 'agents', 'ownerCodes', 'credentials', 'keys', 'clients', 'transactions', 'codes', 'tokens', 'consents', 'accounts', 'signups']) {
175
+ if (!Array.isArray(parsed.maps[name]))
176
+ throw new Error(`Invalid store map: ${name}`);
177
+ this[name] = new Map(parsed.maps[name]);
178
+ }
179
+ // Version 1 originally indexed all keys by kid. Derive the scoped index from each value so existing SQLite
180
+ // snapshots and the newer scoped form both load without losing identity, revocation or transaction history.
181
+ this.keys = new Map([...this.keys.values()].map((key) => [this.keyIndex(key.agentSub, key.kid), key]));
182
+ this.signingKeys = new Map(parsed.maps.signingKeys ?? []);
183
+ this.activeSigningKid = parsed.activeSigningKid;
184
+ this.legacySigningHistory = parsed.legacySigningHistory ?? parsed.maps.signingKeys === undefined;
185
+ this.deletedIdentities = new Map(parsed.maps.deletedIdentities ?? []);
186
+ this.signupEvents = new Map(parsed.maps.signupEvents ?? []);
187
+ if (!parsed.maps.signupEvents) {
188
+ for (const tx of this.transactions.values())
189
+ if (tx.status === 'completed' && tx.ownerId && tx.agentSub) {
190
+ const key = `${tx.ownerId}\u0000${tx.clientId}`;
191
+ this.signupEvents.set(key, [...new Set([...(this.signupEvents.get(key) ?? []), `${tx.agentSub}\u0000${tx.clientId}`])]);
192
+ }
193
+ for (const [key, count] of this.signups) {
194
+ const known = this.signupEvents.get(key) ?? [];
195
+ this.signupEvents.set(key, [...known, ...Array.from({ length: Math.max(0, count - known.length) }, (_, i) => `legacy:${key}:${i}`)]);
196
+ }
197
+ }
198
+ this.deletedOwners = new Map(parsed.maps.deletedOwners ?? []);
199
+ this.revoked = new Set(parsed.revoked);
200
+ this.reservations = new Map(parsed.reservations.map(([key, entries]) => [key, new Map(entries)]));
201
+ for (const tx of this.transactions.values())
202
+ if (!tx.clientPresentation && !TERMINAL.has(tx.status))
203
+ this.legacyTransactions.add(tx.id);
204
+ }
205
+ }
206
+ exportSnapshot() {
207
+ const maps = Object.fromEntries(['owners', 'agents', 'ownerCodes', 'credentials', 'keys', 'clients', 'transactions', 'codes', 'tokens', 'consents', 'accounts', 'signups', 'signupEvents', 'deletedOwners', 'signingKeys', 'deletedIdentities'].map((name) => [name, [...this[name].entries()]]));
208
+ return JSON.stringify({ version: 3, maps, activeSigningKid: this.activeSigningKid, legacySigningHistory: this.legacySigningHistory, revoked: [...this.revoked], reservations: [...this.reservations].map(([key, entries]) => [key, [...entries]]) });
209
+ }
210
+ currentTime(fallbackNow) { return fallbackNow; }
211
+ publishSigningKeys(keys, bootstrapKid, now, cacheLifetime) {
212
+ if (!Number.isSafeInteger(cacheLifetime) || cacheLifetime < 0 || !keys.some((k) => k.kid === bootstrapKid))
213
+ throw new TypeError('Invalid signing publication');
214
+ for (const key of keys) {
215
+ validateSigningPublication(key);
216
+ const previous = this.signingKeys.get(key.kid);
217
+ if (previous && !samePublicSigningKey(previous.publicJwk, key.publicJwk))
218
+ throw new Error('Signing key identifier cannot be reused');
219
+ if (previous && previous.status !== 'published')
220
+ throw new Error('Removed signing key cannot be republished');
221
+ }
222
+ const bootstrap = this.signingKeys.size === 0;
223
+ const retireNotBefore = this.legacySigningHistory ? now + LEGACY_SIGNING_OVERLAP_SECONDS : undefined;
224
+ // Every replica serves the shared key ring. A replica with a longer cache policy
225
+ // may serve keys another replica stages later, without reloading its own ring.
226
+ const sharedCacheLifetime = Math.max(cacheLifetime, ...[...this.signingKeys.values()].map((key) => key.cacheLifetime));
227
+ for (const existing of this.signingKeys.values())
228
+ if (existing.status === 'published')
229
+ existing.cacheLifetime = sharedCacheLifetime;
230
+ for (const key of keys) {
231
+ const previous = this.signingKeys.get(key.kid);
232
+ this.signingKeys.set(key.kid, previous ? { ...previous, cacheLifetime: sharedCacheLifetime } : { ...copy(key), status: 'published', publishedAt: now, cacheLifetime: sharedCacheLifetime, retireNotBefore });
233
+ }
234
+ if (bootstrap)
235
+ this.activeSigningKid = bootstrapKid;
236
+ this.legacySigningHistory = false;
237
+ }
238
+ listSigningKeys() { return copy([...this.signingKeys.values()].filter((k) => k.status === 'published')); }
239
+ activateSigningKey(kid, now) {
240
+ const key = this.signingKeys.get(kid);
241
+ if (!key || key.status !== 'published' || now <= key.publishedAt)
242
+ return false;
243
+ this.activeSigningKid = kid;
244
+ return true;
245
+ }
246
+ removeSigningKey(kid, compromised, now) {
247
+ const key = this.signingKeys.get(kid);
248
+ if (!key)
249
+ return false;
250
+ if (key.status !== 'published')
251
+ return true;
252
+ if (!compromised && (this.activeSigningKid === kid || now < Math.max((key.lastIssuedExpiresAt ?? key.publishedAt) + key.cacheLifetime, key.retireNotBefore ?? 0)))
253
+ return false;
254
+ key.status = compromised ? 'compromised' : 'retired';
255
+ if (this.activeSigningKid === kid)
256
+ this.activeSigningKid = undefined;
257
+ return true;
258
+ }
259
+ reserveSigningKey(expiresAt, now) {
260
+ const key = this.activeSigningKid ? this.signingKeys.get(this.activeSigningKid) : undefined;
261
+ if (!key || key.status !== 'published' || !Number.isSafeInteger(expiresAt) || expiresAt <= now)
262
+ return undefined;
263
+ key.lastIssuedExpiresAt = Math.max(key.lastIssuedExpiresAt ?? 0, expiresAt);
264
+ if (key.retireNotBefore !== undefined)
265
+ key.retireNotBefore = Math.max(key.retireNotBefore, expiresAt + LEGACY_SIGNING_CACHE_SECONDS);
266
+ return copy(key);
267
+ }
268
+ quotaOwners(ownerId) {
269
+ const owner = this.owners.get(ownerId);
270
+ if (!owner?.emailVerified)
271
+ return [ownerId];
272
+ const address = ownerAddressKey(owner.email);
273
+ return [...this.owners.values()].filter((o) => o.emailVerified && ownerAddressKey(o.email) === address).map((o) => o.id);
274
+ }
275
+ expireOwnerLinks(now) {
276
+ for (const [id, link] of this.deletedOwners)
277
+ if (now >= link.expiresAt) {
278
+ this.deletedOwners.delete(id);
279
+ for (const key of this.signupEvents.keys())
280
+ if (key.startsWith(`${id}\u0000`))
281
+ this.signupEvents.delete(key);
282
+ }
283
+ }
284
+ putOwner(o, now = o.createdAt) {
285
+ const address = ownerAddressKey(o.email);
286
+ const previous = this.owners.get(o.id);
287
+ if (previous && previous.ownerSub !== o.ownerSub)
288
+ throw new Error('Owner subject is immutable');
289
+ if (this.deletedIdentities.has(o.id) || [...this.deletedIdentities.values()].includes(o.ownerSub))
290
+ throw new Error('Deleted Owner identity cannot be reused');
291
+ if ([...this.owners.values()].some((owner) => owner.id !== o.id && owner.ownerSub === o.ownerSub))
292
+ throw new Error('Owner subject already exists');
293
+ this.expireOwnerLinks(now);
294
+ this.owners.set(o.id, copy(o));
295
+ if (o.emailVerified) {
296
+ const peers = this.quotaOwners(o.id);
297
+ const sources = new Set([...peers, ...[...this.deletedOwners].filter(([, link]) => link.address === address).map(([id]) => id)]);
298
+ const totals = new Map();
299
+ for (const [key, events] of this.signupEvents) {
300
+ const [id, client] = key.split('\u0000');
301
+ if (!sources.has(id))
302
+ continue;
303
+ const all = totals.get(client) ?? new Set();
304
+ for (const event of events)
305
+ all.add(event);
306
+ totals.set(client, all);
307
+ }
308
+ for (const peer of peers)
309
+ for (const [client, events] of totals)
310
+ this.signupEvents.set(`${peer}\u0000${client}`, [...events]);
311
+ }
312
+ }
313
+ deleteOwner(id, ownerSub, now, retentionSeconds) {
314
+ if (!Number.isSafeInteger(retentionSeconds) || retentionSeconds < 0 || !Number.isSafeInteger(now + retentionSeconds))
315
+ throw new TypeError('Invalid Owner retention period');
316
+ const owner = this.owners.get(id);
317
+ if (!owner || owner.ownerSub !== ownerSub)
318
+ return false;
319
+ this.expireOwnerLinks(now);
320
+ for (const agent of [...this.agents.values()])
321
+ if (agent.ownerId === id)
322
+ this.deleteAgent(agent.handle, agent.sub, now);
323
+ for (const [hash, credential] of this.credentials)
324
+ if (credential.ownerId === id)
325
+ this.credentials.delete(hash);
326
+ this.ownerCodes.delete(id);
327
+ this.owners.delete(id);
328
+ this.deletedIdentities.set(id, ownerSub);
329
+ if (owner.emailVerified && retentionSeconds > 0)
330
+ this.deletedOwners.set(id, { address: ownerAddressKey(owner.email), expiresAt: now + retentionSeconds });
331
+ else
332
+ for (const key of this.signupEvents.keys())
333
+ if (key.startsWith(`${id}\u0000`))
334
+ this.signupEvents.delete(key);
335
+ return true;
336
+ }
337
+ getOwner(id) { return copy(this.owners.get(id)); }
338
+ getOwnerByEmail(email) {
339
+ const wanted = ownerAddressKey(email);
340
+ return copy([...this.owners.values()].filter((o) => ownerAddressKey(o.email) === wanted).sort((a, b) => b.createdAt - a.createdAt)[0]);
341
+ }
342
+ putOwnerCode(c) { this.ownerCodes.set(c.ownerId, copy(c)); }
343
+ takeOwnerCode(ownerId) {
344
+ const c = this.ownerCodes.get(ownerId);
345
+ this.ownerCodes.delete(ownerId);
346
+ return c;
347
+ }
348
+ putAgent(a) {
349
+ if (this.agents.has(a.handle) || [...this.agents.values()].some((agent) => agent.sub === a.sub))
350
+ return false;
351
+ this.agents.set(a.handle, copy(a));
352
+ return true;
353
+ }
354
+ getAgent(handle) { return copy(this.agents.get(handle)); }
355
+ setAgentEnabled(handle, enabled, agentSub, now) {
356
+ const a = this.agents.get(handle);
357
+ if (!a || a.sub !== agentSub)
358
+ return undefined;
359
+ a.enabled = enabled;
360
+ if (!enabled) {
361
+ a.authorizationVersion += 1;
362
+ this.invalidate({ handle }, 'access_denied', now);
363
+ }
364
+ return copy(a);
365
+ }
366
+ deleteAgent(handle, agentSub, now) {
367
+ const a = this.agents.get(handle);
368
+ if (!a || a.sub !== agentSub)
369
+ return false;
370
+ this.invalidate({ handle }, 'access_denied', now);
371
+ for (const [index, k] of this.keys)
372
+ if (k.handle === handle && k.agentSub === agentSub) {
373
+ if (k.status === 'revoked')
374
+ this.revokeTransactionTokens(k.kid, k.enrollment);
375
+ this.revoked.add(`${handle}\u0000${k.kid}`);
376
+ this.keys.delete(index);
377
+ }
378
+ for (const [id, c] of this.consents)
379
+ if (c.handle === handle)
380
+ this.consents.delete(id);
381
+ for (const [id, a] of this.accounts)
382
+ if (a.handle === handle)
383
+ this.accounts.delete(id);
384
+ for (const [hash, c] of this.credentials)
385
+ if (c.handle === handle && c.kind === 'delegated')
386
+ this.credentials.delete(hash);
387
+ this.agents.delete(handle);
388
+ return true;
389
+ }
390
+ managementCurrent(handle, auth) {
391
+ const agent = this.agents.get(handle);
392
+ const credential = this.credentials.get(auth.hash);
393
+ return agent?.sub === auth.agentSub && agent.ownerId === auth.ownerId && credential?.ownerId === auth.ownerId &&
394
+ credential.kind === auth.kind && credential.sessionAuth === auth.sessionAuth && credential.createdAt === auth.createdAt &&
395
+ credential.handle === auth.handle && (credential.kind === 'owner' || credential.handle === handle);
396
+ }
397
+ resolveManagement(hash, handle) {
398
+ const credential = this.credentials.get(hash);
399
+ if (!credential)
400
+ return 'invalid_token';
401
+ const agent = this.agents.get(handle);
402
+ if (!agent || agent.ownerId !== credential.ownerId || (credential.kind === 'delegated' && credential.handle !== handle))
403
+ return 'not_found';
404
+ return copy({ ...credential, agentSub: agent.sub });
405
+ }
406
+ readManagementKeys(handle, authorization, kid) {
407
+ if (!this.managementCurrent(handle, authorization))
408
+ return undefined;
409
+ return copy([...this.keys.values()].filter((key) => key.agentSub === authorization.agentSub && key.handle === handle && (kid === undefined || key.kid === kid)));
410
+ }
411
+ readManagementAccounts(handle, authorization) {
412
+ if (!this.managementCurrent(handle, authorization))
413
+ return undefined;
414
+ return this.listAccounts(handle);
415
+ }
416
+ invalidate(filter, reason, now) {
417
+ for (const t of this.transactions.values())
418
+ if (!TERMINAL.has(t.status) && matches({ ...t, txnId: t.id }, filter)) {
419
+ // Expiry may not have been materialized yet. A later lifecycle event must not restart its retention window,
420
+ // including when the asserting key expired before the transaction's own deadline.
421
+ const expired = this.expiredTransaction(t, now);
422
+ t.status = expired?.status ?? 'denied';
423
+ t.reason = expired ? expired.reason : reason;
424
+ t.terminalAt = expired?.terminalAt ?? now;
425
+ t.challenge = undefined;
426
+ t.version += 1;
427
+ if (t.ownerId)
428
+ this.releaseSignup({ ownerId: t.ownerId, clientId: t.clientId, txnId: t.id });
429
+ }
430
+ this.voidCodes(filter);
431
+ this.revokeAccessTokens(filter);
432
+ }
433
+ expiredTransaction(t, now) {
434
+ const key = t.kid && t.keyEnrollment ? this.enrollmentKey(t.kid, t.keyEnrollment) : undefined;
435
+ const keyExpiry = key && key.enrollment === t.keyEnrollment && key.agentSub === t.agentSub && key.status !== 'pending'
436
+ ? key.expiresAt : Infinity;
437
+ if (now >= t.expiresAt && t.expiresAt <= keyExpiry) {
438
+ return { ...t, status: 'expired', terminalAt: t.expiresAt, reason: undefined, challenge: undefined };
439
+ }
440
+ if (now >= keyExpiry)
441
+ return { ...t, status: 'denied', reason: 'key_revoked', terminalAt: keyExpiry, challenge: undefined };
442
+ return undefined;
443
+ }
444
+ assertionSubjectReason(tx, now) {
445
+ const agent = tx.handle && this.agents.get(tx.handle);
446
+ if (!agent || agent.sub !== tx.agentSub || !agent.enabled || agent.authorizationVersion !== tx.agentVersion)
447
+ return 'unknown_key';
448
+ const key = tx.kid && tx.keyEnrollment ? this.enrollmentKey(tx.kid, tx.keyEnrollment) : undefined;
449
+ if (!key || key.handle !== tx.handle || key.agentSub !== tx.agentSub)
450
+ return 'unknown_key';
451
+ if (key.status !== 'active' || now >= key.expiresAt)
452
+ return 'key_revoked';
453
+ return undefined;
454
+ }
455
+ subjectReason(s, now) {
456
+ const key = s.kid && s.keyEnrollment && this.enrollmentKey(s.kid, s.keyEnrollment);
457
+ if (!key || key.handle !== s.handle || key.enrollment !== s.keyEnrollment || key.agentSub !== s.agentSub || key.status !== 'active' || now >= key.expiresAt)
458
+ return 'key_revoked';
459
+ const agent = s.handle && this.agents.get(s.handle);
460
+ if (!agent || agent.sub !== s.agentSub || !agent.enabled || agent.authorizationVersion !== s.agentVersion)
461
+ return 'access_denied';
462
+ const account = this.accounts.get(`${s.handle}\u0000${s.clientId}`);
463
+ if (account?.status === 'disabled' || (account?.authorizationVersion ?? 0) !== s.accountVersion)
464
+ return 'access_denied';
465
+ return undefined;
466
+ }
467
+ listAgentsByOwner(ownerId) { return copy([...this.agents.values()].filter((a) => a.ownerId === ownerId)); }
468
+ putCredential(c) { this.credentials.set(c.hash, copy(c)); }
469
+ getCredential(hash) { return copy(this.credentials.get(hash)); }
470
+ listCredentials(f) {
471
+ return copy([...this.credentials.values()].filter((c) => (f.ownerId === undefined || c.ownerId === f.ownerId) && (f.handle === undefined || c.handle === f.handle) && (f.kind === undefined || c.kind === f.kind)));
472
+ }
473
+ deleteCredential(hash) { this.credentials.delete(hash); }
474
+ keyIndex(agentSub, kid) { return `${agentSub}\u0000${kid}`; }
475
+ enrollmentKey(kid, enrollment) {
476
+ return [...this.keys.values()].find((key) => key.kid === kid && key.enrollment === enrollment);
477
+ }
478
+ getKey(kid, handle) {
479
+ if (handle !== undefined) {
480
+ const agent = this.agents.get(handle);
481
+ return copy(agent ? this.keys.get(this.keyIndex(agent.sub, kid)) : undefined);
482
+ }
483
+ const keys = [...this.keys.values()].filter((key) => key.kid === kid);
484
+ const active = keys.filter((key) => key.status === 'active');
485
+ return copy(active.length === 1 ? active[0] : keys.length === 1 ? keys[0] : undefined);
486
+ }
487
+ listKeys(handle) { return copy([...this.keys.values()].filter((k) => k.handle === handle)); }
488
+ insertKey(k, now, authorization) {
489
+ if (k.agentSub !== authorization.agentSub || !this.managementCurrent(k.handle, authorization))
490
+ return 'unauthorized';
491
+ if (this.revoked.has(`${k.handle}\u0000${k.kid}`))
492
+ return 'revoked';
493
+ const index = this.keyIndex(k.agentSub, k.kid);
494
+ const cur = this.keys.get(index);
495
+ const enrolled = cur !== undefined && ((cur.status === 'pending' && cur.challenge !== undefined && now < cur.challenge.expiresAt && now < cur.expiresAt) || (cur.status === 'active' && now < cur.expiresAt));
496
+ if (enrolled)
497
+ return 'enrolled';
498
+ if (k.status !== 'pending' || k.expiresAt <= now || !k.challenge || k.challenge.expiresAt <= now)
499
+ return 'unauthorized';
500
+ if ([...this.keys.values()].some((key) => key.enrollment === k.enrollment))
501
+ throw new Error('Key enrollment already exists');
502
+ // Preserve the original expiry outcome before this Agent's current-key index points at a new enrollment.
503
+ if (cur?.status === 'active' && now >= cur.expiresAt)
504
+ this.invalidate({ kid: cur.kid, keyEnrollment: cur.enrollment }, 'key_revoked', cur.expiresAt);
505
+ this.keys.set(index, copy(k));
506
+ return 'ok';
507
+ }
508
+ activateKey(kid, enrollment, challenge, now, assertionWindow) {
509
+ const k = this.enrollmentKey(kid, enrollment);
510
+ if (!k || this.agents.get(k.handle)?.sub !== k.agentSub || k.status !== 'pending' || !k.challenge || k.challenge.value !== challenge || now >= k.challenge.expiresAt || now >= k.expiresAt)
511
+ return false;
512
+ if (assertionWindow && (now < assertionWindow.notBefore || now >= assertionWindow.expiresAt))
513
+ throw new AssertionWindowError();
514
+ const active = [...this.keys.values()].filter((key) => key.kid === kid && key.status === 'active');
515
+ if (active.some((key) => now < key.expiresAt))
516
+ throw new KeyActivationConflictError();
517
+ for (const expired of active) {
518
+ this.invalidate({ kid, keyEnrollment: expired.enrollment }, 'key_revoked', expired.expiresAt);
519
+ expired.status = 'expired';
520
+ }
521
+ k.status = 'active';
522
+ k.challenge = undefined;
523
+ return true;
524
+ }
525
+ revokeKey(kid, enrollment, now, authorization) {
526
+ const k = this.enrollmentKey(kid, enrollment);
527
+ if (authorization && (!k || k.agentSub !== authorization.agentSub || !this.managementCurrent(k.handle, authorization)))
528
+ return undefined;
529
+ // Individual revocation also forbids reuse after expiry, including an enrollment retired by another Agent.
530
+ if (!k)
531
+ return undefined;
532
+ if (k.status === 'revoked') {
533
+ this.revokeTransactionTokens(kid, enrollment);
534
+ return authorization ? copy(k) : undefined;
535
+ }
536
+ k.status = 'revoked';
537
+ k.revokedAt = now;
538
+ k.challenge = undefined;
539
+ this.revoked.add(`${k.handle}\u0000${k.kid}`);
540
+ this.invalidate({ kid, keyEnrollment: enrollment }, 'key_revoked', now);
541
+ this.revokeTransactionTokens(kid, enrollment);
542
+ return copy(k);
543
+ }
544
+ revokeTransactionTokens(kid, enrollment) {
545
+ // Explicit revocation ends bearer authority even for retained terminal transactions. Natural expiry does not.
546
+ for (const tx of this.transactions.values())
547
+ if (tx.kid === kid && tx.keyEnrollment === enrollment && tx.transactionTokenHash !== undefined) {
548
+ delete tx.transactionTokenHash;
549
+ tx.version += 1;
550
+ }
551
+ }
552
+ transactionTokenRevoked(tx) {
553
+ const key = tx.kid && tx.keyEnrollment ? this.enrollmentKey(tx.kid, tx.keyEnrollment) : undefined;
554
+ return key?.status === 'revoked' && key.handle === tx.handle && key.agentSub === tx.agentSub;
555
+ }
556
+ revokeAllKeys(handle, authorization, now) {
557
+ if (!this.managementCurrent(handle, authorization))
558
+ return false;
559
+ for (const k of this.keys.values())
560
+ if (k.handle === handle && k.agentSub === authorization.agentSub && (k.status === 'pending' || k.status === 'active'))
561
+ this.revokeKey(k.kid, k.enrollment, now);
562
+ if (authorization.kind === 'owner')
563
+ for (const [hash, c] of this.credentials)
564
+ if (c.handle === handle && c.kind === 'delegated')
565
+ this.credentials.delete(hash);
566
+ return true;
567
+ }
568
+ patchKey(kid, enrollment, patch, authorization) {
569
+ const k = this.enrollmentKey(kid, enrollment);
570
+ if (!k)
571
+ return undefined;
572
+ if (authorization && (k.agentSub !== authorization.agentSub || !this.managementCurrent(k.handle, authorization) ||
573
+ (patch.shareOwner !== undefined && (authorization.kind !== 'owner' || !['passkey', 'oidc', 'domain', 'email_factor'].includes(authorization.sessionAuth ?? '')))))
574
+ return undefined;
575
+ if (patch.name !== undefined)
576
+ k.name = patch.name;
577
+ if (patch.shareOwner !== undefined)
578
+ k.permissions.share_owner = patch.shareOwner;
579
+ return copy(k);
580
+ }
581
+ deleteKey(kid, enrollment, pendingExpiredAt) {
582
+ const k = enrollment === undefined ? this.getKey(kid) : this.enrollmentKey(kid, enrollment);
583
+ if (!k || (pendingExpiredAt !== undefined && (k.status !== 'pending' || (k.challenge && pendingExpiredAt < k.challenge.expiresAt))))
584
+ return;
585
+ // Persist legacy revocation before its key record disappears; missing/expired enrollments alone revoke no bearer.
586
+ if (k.status === 'revoked')
587
+ this.revokeTransactionTokens(k.kid, k.enrollment);
588
+ this.keys.delete(this.keyIndex(k.agentSub, kid));
589
+ }
590
+ wasRevoked(handle, kid) { return this.revoked.has(`${handle}\u0000${kid}`); }
591
+ putClient(c) { this.clients.set(c.clientId, copy(c)); }
592
+ getClient(clientId) { return copy(this.clients.get(clientId)); }
593
+ insertTransaction(t) {
594
+ if (this.transactions.has(t.id))
595
+ throw new Error('Transaction already exists');
596
+ this.transactions.set(t.id, copy(t));
597
+ if (!t.clientPresentation && !TERMINAL.has(t.status))
598
+ this.legacyTransactions.add(t.id);
599
+ }
600
+ getTransaction(id) { return copy(this.transactions.get(id)); }
601
+ readTransaction(id, { now, retainFor }) {
602
+ const stored = this.transactions.get(id);
603
+ if (!stored)
604
+ return undefined;
605
+ let tx = copy(stored);
606
+ // Older snapshots may retain hashes after revocation; deny that exact enrollment without a polling write.
607
+ if (this.transactionTokenRevoked(tx))
608
+ delete tx.transactionTokenHash;
609
+ if (!TERMINAL.has(tx.status)) {
610
+ const expired = this.expiredTransaction(tx, now);
611
+ if (expired)
612
+ tx = expired;
613
+ else if (tx.kid) {
614
+ const reason = this.subjectReason(tx, now);
615
+ if (reason)
616
+ tx = { ...tx, status: 'denied', reason, terminalAt: now, challenge: undefined };
617
+ }
618
+ }
619
+ return TERMINAL.has(tx.status) && tx.terminalAt !== undefined && now > tx.terminalAt + retainFor ? undefined : tx;
620
+ }
621
+ updateTransaction(t, expectedVersion, effects) {
622
+ const cur = this.transactions.get(t.id);
623
+ if (cur && effects.transactionTokenHash !== undefined && (effects.transactionTokenHash !== cur.transactionTokenHash || this.transactionTokenRevoked(cur)))
624
+ throw new TransactionTokenError();
625
+ if (!cur || cur.version !== expectedVersion || TERMINAL.has(cur.status) || t.status === 'completed')
626
+ return undefined;
627
+ if (!cur.clientPresentation && t.status !== 'denied')
628
+ throw new TransactionExpiredError();
629
+ const expired = this.expiredTransaction(cur, effects.now);
630
+ if (cur.status === 'awaiting_agent' && t.kid) {
631
+ if (expired)
632
+ throw new TransactionExpiredError();
633
+ const reason = this.assertionSubjectReason(t, effects.now);
634
+ if (reason)
635
+ throw new AssertionSubjectError(reason);
636
+ }
637
+ if (effects.assertionWindow && (effects.now < effects.assertionWindow.notBefore || effects.now >= effects.assertionWindow.expiresAt))
638
+ throw new AssertionWindowError();
639
+ if (cur.status === 'awaiting_agent' && t.kid && effects.expectedShareOwner !== undefined &&
640
+ this.enrollmentKey(t.kid, t.keyEnrollment)?.permissions.share_owner !== effects.expectedShareOwner)
641
+ return undefined;
642
+ const next = { ...copy(expired ?? t), clientPresentation: copy(cur.clientPresentation), maxAge: cur.maxAge, assertedAt: cur.assertedAt };
643
+ const deny = (reason) => { next.status = 'denied'; next.reason = reason; next.terminalAt = effects.now; next.challenge = undefined; };
644
+ if (!expired && next.kid) {
645
+ const reason = this.subjectReason(next, effects.now);
646
+ if (reason)
647
+ deny(reason);
648
+ else if (cur.status === 'awaiting_agent' && !this.accounts.has(`${next.handle}\u0000${next.clientId}`) && effects.maxSignups !== undefined) {
649
+ // Assertion acceptance owns the reservation: a losing CAS never creates or releases it.
650
+ const reserved = this.reserveSignup({ ownerId: next.ownerId, clientId: next.clientId, handle: next.handle, txnId: next.id, expiresAt: next.expiresAt, max: effects.maxSignups, now: effects.now });
651
+ if (!reserved)
652
+ deny('signup_limit_reached');
653
+ else
654
+ next.passedCap = true;
655
+ }
656
+ if (cur.status === 'awaiting_agent' && !reason)
657
+ next.assertedAt = effects.now;
658
+ }
659
+ const consentKey = `${next.handle}\u0000${next.clientId}`;
660
+ const previousConsent = this.consents.get(consentKey);
661
+ if (!TERMINAL.has(next.status) && effects.touchConsent && !effects.consent) {
662
+ const reason = consentRecordReason(previousConsent, effects.consentContext ?? { scopes: next.scopes, redirectHost: new URL(next.redirectUri).host }, effects.expireConsentBefore);
663
+ if (reason) {
664
+ next.consentRelied = false;
665
+ next.reviewOutstanding = true;
666
+ next.consentReason = reason;
667
+ if (next.prompt === 'none')
668
+ deny('consent_required');
669
+ else
670
+ next.status = next.ownerApprovalOutstanding ? 'awaiting_owner' : 'awaiting_consent';
671
+ }
672
+ }
673
+ if (TERMINAL.has(next.status) && !expired)
674
+ next.terminalAt = effects.now;
675
+ next.version = expectedVersion + 1;
676
+ this.transactions.set(next.id, next);
677
+ if (TERMINAL.has(next.status) && next.ownerId)
678
+ this.releaseSignup({ ownerId: next.ownerId, clientId: next.clientId, txnId: next.id });
679
+ else {
680
+ if (next.passedCap && next.ownerId)
681
+ this.extendSignup({ ownerId: next.ownerId, clientId: next.clientId, txnId: next.id, expiresAt: next.expiresAt });
682
+ if (previousConsent && effects.expireConsentBefore !== undefined && previousConsent.lastUsedAt < effects.expireConsentBefore)
683
+ this.consents.delete(consentKey);
684
+ if (effects.consent)
685
+ this.grantConsent(effects.consent, effects.now);
686
+ else if (effects.touchConsent && next.consentRelied && next.status === 'ready' && next.handle)
687
+ this.touchConsent(next.handle, next.clientId, effects.now);
688
+ }
689
+ return copy(next);
690
+ }
691
+ completeTransaction(t, expectedVersion, code, now, transactionTokenHash) {
692
+ const cur = this.transactions.get(t.id);
693
+ if (cur && transactionTokenHash !== undefined && (transactionTokenHash !== cur.transactionTokenHash || this.transactionTokenRevoked(cur)))
694
+ throw new TransactionTokenError();
695
+ if (!cur || cur.version !== expectedVersion || cur.status !== 'ready')
696
+ return undefined;
697
+ const dead = !cur.clientPresentation ? 'access_denied' : this.subjectReason(cur, now);
698
+ const reservation = cur.ownerId && this.reservations.get(`${cur.ownerId}\u0000${cur.clientId}`)?.get(cur.id);
699
+ const hasAccount = this.accounts.has(`${cur.handle}\u0000${cur.clientId}`);
700
+ const needsReservation = cur.passedCap && !hasAccount;
701
+ const cap = this.clients.get(cur.clientId)?.maxSignupsPerOwner;
702
+ const capExceeded = !hasAccount && cap !== undefined && this.signupsEver(cur.ownerId, cur.clientId) >= cap;
703
+ const staleAuthentication = isAuthenticationTooOld(cur.maxAge, cur.assertedAt, now);
704
+ if (now >= cur.expiresAt || dead || staleAuthentication || capExceeded || (needsReservation && (!reservation || reservation.expiresAt <= now))) {
705
+ return this.updateTransaction({ ...cur, status: 'denied', reason: dead ?? (staleAuthentication ? 'login_required' : 'signup_limit_reached'), terminalAt: now, challenge: undefined }, expectedVersion, { now });
706
+ }
707
+ if (this.codes.has(code.hash))
708
+ throw new Error('Authorization code already exists');
709
+ // Every Account creation needs current capacity, even with an owned reservation:
710
+ // sibling completion/deletion or an address merge may have changed other claims.
711
+ if (!hasAccount && cap !== undefined && !this.reserveSignup({
712
+ ownerId: cur.ownerId, clientId: cur.clientId, handle: cur.handle, txnId: cur.id,
713
+ expiresAt: cur.expiresAt, max: cap, now,
714
+ }))
715
+ return this.updateTransaction({ ...cur, status: 'denied', reason: 'signup_limit_reached', terminalAt: now, challenge: undefined }, expectedVersion, { now });
716
+ const next = { ...copy(cur), version: expectedVersion + 1, status: 'completed', terminalAt: now, challenge: undefined, codeIssued: true };
717
+ const issued = { ...copy(code), txnId: cur.id, clientId: cur.clientId, redirectUri: cur.redirectUri,
718
+ codeChallenge: cur.codeChallenge, nonce: cur.nonce, scopes: copy(cur.scopes), handle: cur.handle, kid: cur.kid,
719
+ keyEnrollment: cur.keyEnrollment, agentSub: cur.agentSub, agentVersion: cur.agentVersion, accountVersion: cur.accountVersion,
720
+ authTime: cur.assertedAt, maxAge: cur.maxAge, used: false, voided: false, replayed: undefined };
721
+ this.transactions.set(next.id, next);
722
+ this.codes.set(code.hash, issued);
723
+ this.recordSignIn({ handle: next.handle, clientId: next.clientId, ownerId: next.ownerId, txnId: next.id, now });
724
+ return copy(next);
725
+ }
726
+ listOpenTransactions(f) {
727
+ return copy([...this.transactions.values()].filter((t) => !TERMINAL.has(t.status) && matches({ ...t, txnId: t.id }, f)));
728
+ }
729
+ listLegacyTransactions(limit) {
730
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 256)
731
+ throw new RangeError('Legacy transaction batch must be between 1 and 256');
732
+ const result = [];
733
+ for (const id of this.legacyTransactions) {
734
+ const tx = this.transactions.get(id);
735
+ if (!tx || tx.clientPresentation || TERMINAL.has(tx.status)) {
736
+ this.legacyTransactions.delete(id);
737
+ continue;
738
+ }
739
+ result.push(tx);
740
+ if (result.length === limit)
741
+ break;
742
+ }
743
+ return copy(result);
744
+ }
745
+ getCode(hash) { return copy(this.codes.get(hash)); }
746
+ redeemCode(hash, now) {
747
+ const c = this.codes.get(hash);
748
+ if (!c)
749
+ return 'unknown';
750
+ if (c.used) {
751
+ c.replayed = true;
752
+ this.revokeAccessTokens({ txnId: c.txnId });
753
+ return 'used';
754
+ }
755
+ if (c.voided || this.subjectReason(c, now))
756
+ return 'void';
757
+ if (now >= c.expiresAt || isAuthenticationTooOld(c.maxAge, c.authTime, now))
758
+ return 'expired';
759
+ c.used = true;
760
+ return 'ok';
761
+ }
762
+ voidCodes(f) { for (const c of this.codes.values())
763
+ if (!c.used && matches(c, f))
764
+ c.voided = true; }
765
+ insertAccessToken(t, now) {
766
+ const code = [...this.codes.values()].find((c) => c.txnId === t.txnId);
767
+ if (!code || !code.used || code.replayed || isAuthenticationTooOld(code.maxAge, code.authTime, now) || this.subjectReason(code, now) || this.subjectReason(t, now))
768
+ return false;
769
+ for (const field of ['handle', 'kid', 'agentSub', 'keyEnrollment', 'agentVersion', 'accountVersion', 'clientId']) {
770
+ if (t[field] !== code[field])
771
+ return false;
772
+ }
773
+ if (JSON.stringify(t.scopes) !== JSON.stringify(code.scopes))
774
+ return false;
775
+ if (this.tokens.has(t.hash) || [...this.tokens.values()].some((token) => token.txnId === t.txnId))
776
+ return false;
777
+ this.tokens.set(t.hash, { ...copy(t), issuedAt: now });
778
+ return true;
779
+ }
780
+ getAccessToken(hash) { return copy(this.tokens.get(hash)); }
781
+ revokeAccessTokens(f) { for (const t of this.tokens.values())
782
+ if (matches(t, f))
783
+ t.revoked = true; }
784
+ getConsent(handle, clientId) { return copy(this.consents.get(`${handle}\u0000${clientId}`)); }
785
+ listConsents(handle) { return copy([...this.consents.values()].filter((c) => c.handle === handle)); }
786
+ grantConsent(c, now) {
787
+ const key = `${c.handle}\u0000${c.clientId}`;
788
+ const cur = this.consents.get(key);
789
+ // Accepting a subset under new terms does not authorize the old scopes under those terms.
790
+ const previous = cur && sameConsentMetadata(cur, c) ? cur : undefined;
791
+ this.consents.set(key, { ...copy(c), scopes: [...new Set([...(previous?.scopes ?? []), ...c.scopes])], createdAt: previous?.createdAt ?? now, lastUsedAt: now });
792
+ }
793
+ touchConsent(handle, clientId, now) {
794
+ const cur = this.consents.get(`${handle}\u0000${clientId}`);
795
+ if (cur)
796
+ cur.lastUsedAt = now;
797
+ }
798
+ deleteConsent(handle, clientId, authorization) {
799
+ if (!this.managementCurrent(handle, authorization))
800
+ return false;
801
+ this.consents.delete(`${handle}\u0000${clientId}`);
802
+ return true;
803
+ }
804
+ getAccount(handle, clientId) { return copy(this.accounts.get(`${handle}\u0000${clientId}`)); }
805
+ listAccounts(handle) { return copy([...this.accounts.values()].filter((a) => a.handle === handle)); }
806
+ setAccountStatus(handle, clientId, status, authorization, now) {
807
+ if (!this.managementCurrent(handle, authorization))
808
+ return undefined;
809
+ const a = this.accounts.get(`${handle}\u0000${clientId}`);
810
+ if (a) {
811
+ a.status = status;
812
+ if (status === 'disabled') {
813
+ a.authorizationVersion += 1;
814
+ this.invalidate({ handle, clientId }, 'access_denied', now);
815
+ }
816
+ }
817
+ return copy(a);
818
+ }
819
+ deleteAccount(handle, clientId) { this.accounts.delete(`${handle}\u0000${clientId}`); }
820
+ signupsEver(ownerId, clientId) { return new Set(this.quotaOwners(ownerId).flatMap((id) => this.signupEvents.get(`${id}\u0000${clientId}`) ?? [])).size; }
821
+ reserveSignup(p) {
822
+ const key = `${p.ownerId}\u0000${p.clientId}`;
823
+ const held = this.reservations.get(key) ?? new Map();
824
+ const peers = this.quotaOwners(p.ownerId);
825
+ const others = new Set();
826
+ for (const owner of peers)
827
+ for (const [txn, r] of this.reservations.get(`${owner}\u0000${p.clientId}`) ?? []) {
828
+ const t = this.transactions.get(txn);
829
+ if (r.expiresAt <= p.now || !t || TERMINAL.has(t.status) || (t.kid && this.subjectReason(t, p.now))) {
830
+ this.reservations.get(`${owner}\u0000${p.clientId}`)?.delete(txn);
831
+ }
832
+ else if (r.handle !== p.handle && !this.accounts.has(`${r.handle}\u0000${p.clientId}`))
833
+ others.add(t.agentSub);
834
+ }
835
+ const admitted = this.signupsEver(p.ownerId, p.clientId) + others.size < p.max;
836
+ if (admitted)
837
+ held.set(p.txnId, { handle: p.handle, expiresAt: p.expiresAt });
838
+ this.reservations.set(key, held);
839
+ return admitted;
840
+ }
841
+ extendSignup(p) {
842
+ const r = this.reservations.get(`${p.ownerId}\u0000${p.clientId}`)?.get(p.txnId);
843
+ if (r)
844
+ r.expiresAt = p.expiresAt;
845
+ }
846
+ releaseSignup(p) {
847
+ this.reservations.get(`${p.ownerId}\u0000${p.clientId}`)?.delete(p.txnId);
848
+ }
849
+ recordSignIn(p) {
850
+ const key = `${p.ownerId}\u0000${p.clientId}`;
851
+ this.reservations.get(key)?.delete(p.txnId);
852
+ const accountKey = `${p.handle}\u0000${p.clientId}`;
853
+ const existing = this.accounts.get(accountKey);
854
+ if (existing) {
855
+ existing.lastSignedInAt = p.now;
856
+ existing.signInCount += 1;
857
+ return { created: false };
858
+ }
859
+ this.accounts.set(accountKey, { handle: p.handle, clientId: p.clientId, status: 'enabled', authorizationVersion: 0, firstSignedInAt: p.now, lastSignedInAt: p.now, signInCount: 1 });
860
+ // A deleted Account can be created again for the same Agent/client pair.
861
+ // Identify this creation, while inherited memberships retain the same event.
862
+ const event = `created:${p.txnId}`;
863
+ for (const owner of this.quotaOwners(p.ownerId)) {
864
+ const membership = `${owner}\u0000${p.clientId}`;
865
+ this.signupEvents.set(membership, [...new Set([...(this.signupEvents.get(membership) ?? []), event])]);
866
+ }
867
+ return { created: true };
868
+ }
869
+ }
870
+ export class MemoryStore {
871
+ constructor() {
872
+ const state = new StoreState();
873
+ return new Proxy(this, {
874
+ get(target, prop, receiver) {
875
+ const method = Reflect.get(state, prop);
876
+ if (typeof prop !== 'string' || !STORE_METHODS.includes(prop) || typeof method !== 'function')
877
+ return Reflect.get(target, prop, receiver);
878
+ return async (...args) => Reflect.apply(method, state, args);
879
+ },
880
+ });
881
+ }
882
+ }
883
+ //# sourceMappingURL=store.js.map