@cortexkit/antigravity-auth-core 3.0.8 → 3.0.10

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 (37) hide show
  1. package/dist/common-auth-embedded/auth-menu/login.d.ts +6 -10
  2. package/dist/common-auth-embedded/auth-menu/login.js +14 -11
  3. package/dist/common-auth-embedded/claustrum/consumer.d.ts +8 -2
  4. package/dist/common-auth-embedded/claustrum/consumer.js +64 -21
  5. package/dist/common-auth-embedded/claustrum/index.d.ts +2 -1
  6. package/dist/common-auth-embedded/claustrum/index.js +1 -0
  7. package/dist/common-auth-embedded/claustrum/insula.d.ts +23 -0
  8. package/dist/common-auth-embedded/claustrum/insula.js +64 -2
  9. package/dist/common-auth-embedded/claustrum/quota-writer.d.ts +18 -0
  10. package/dist/common-auth-embedded/claustrum/quota-writer.js +253 -0
  11. package/dist/common-auth-embedded/claustrum/roster.d.ts +18 -2
  12. package/dist/common-auth-embedded/claustrum/roster.js +100 -43
  13. package/dist/common-auth-embedded/fs/with-lock.d.ts +1 -0
  14. package/dist/common-auth-embedded/fs/with-lock.js +2 -0
  15. package/dist/common-auth-embedded/quota/format.d.ts +19 -0
  16. package/dist/common-auth-embedded/quota/format.js +50 -5
  17. package/dist/common-auth-embedded/quota/index.d.ts +3 -3
  18. package/dist/common-auth-embedded/quota/index.js +2 -2
  19. package/dist/common-auth-embedded/quota/map.d.ts +24 -1
  20. package/dist/common-auth-embedded/quota/map.js +31 -6
  21. package/dist/common-auth-embedded/quota/merge.d.ts +6 -1
  22. package/dist/common-auth-embedded/quota/merge.js +17 -3
  23. package/dist/common-auth-embedded/quota/projection.d.ts +3 -1
  24. package/dist/common-auth-embedded/quota/projection.js +3 -1
  25. package/dist/common-auth-embedded/source-output.json +89 -73
  26. package/dist/constants.d.ts +5 -0
  27. package/dist/constants.d.ts.map +1 -1
  28. package/dist/constants.js +5 -0
  29. package/dist/constants.js.map +1 -1
  30. package/dist/pre-response-deadline.d.ts.map +1 -1
  31. package/dist/pre-response-deadline.js +5 -1
  32. package/dist/pre-response-deadline.js.map +1 -1
  33. package/dist/vault-account-source.d.ts +14 -2
  34. package/dist/vault-account-source.d.ts.map +1 -1
  35. package/dist/vault-account-source.js +38 -0
  36. package/dist/vault-account-source.js.map +1 -1
  37. package/package.json +1 -1
@@ -26,28 +26,24 @@ export interface MenuLogin {
26
26
  signal?: AbortSignal;
27
27
  }): Promise<LoginFlow>;
28
28
  /**
29
- * Opens the URL in a browser; false or a throw means no browser could be
30
- * opened. Defaults to `openBrowserForMenu`.
29
+ * Opens the URL in a browser; false or a throw means its opener could not be
30
+ * started. Defaults to `openBrowserForMenu`.
31
31
  */
32
32
  openBrowser?(url: string): boolean | undefined | Promise<boolean | undefined>;
33
33
  }
34
- type BrowserExec = (file: string, args: string[], options: {
35
- stdio: 'ignore';
36
- timeout: number;
37
- }) => unknown;
38
34
  type BrowserSpawn = (file: string, args: string[], options: {
39
35
  stdio: 'ignore';
40
- windowsHide: true;
36
+ windowsHide?: true;
41
37
  }) => {
42
38
  pid?: number;
43
39
  unref(): void;
44
40
  on(event: 'error', listener: () => void): unknown;
45
41
  };
46
- /** Opens a URL with the platform's opener; false when that fails. */
47
- export declare function openBrowserForMenu(url: string, platform?: NodeJS.Platform, execFileSync?: BrowserExec, spawn?: BrowserSpawn): boolean;
42
+ /** Opens a URL with the platform's opener; false when it could not start. */
43
+ export declare function openBrowserForMenu(url: string, platform?: NodeJS.Platform, spawn?: BrowserSpawn): boolean;
48
44
  /**
49
45
  * Runs a login for the menu: a browser login first and, when no browser can
50
- * be opened, a device-code login instead, so a headless machine can still
46
+ * be started, a device-code login instead, so a headless machine can still
51
47
  * add an account. The URL is always printed, so an operator can open it by
52
48
  * hand when the opener reports success but nothing appears.
53
49
  */
@@ -1,6 +1,6 @@
1
- import { execFileSync as defaultExecFileSync, spawn as defaultSpawn, } from 'node:child_process';
2
- /** Opens a URL with the platform's opener; false when that fails. */
3
- export function openBrowserForMenu(url, platform = process.platform, execFileSync = defaultExecFileSync, spawn = defaultSpawn) {
1
+ import { spawn as defaultSpawn } from 'node:child_process';
2
+ /** Opens a URL with the platform's opener; false when it could not start. */
3
+ export function openBrowserForMenu(url, platform = process.platform, spawn = defaultSpawn) {
4
4
  try {
5
5
  if (platform === 'win32') {
6
6
  // Start-Process opens the URL with its associated app. Base64 UTF-16LE
@@ -28,15 +28,18 @@ export function openBrowserForMenu(url, platform = process.platform, execFileSyn
28
28
  // Node leaves pid undefined when the process could not be started.
29
29
  return child.pid !== undefined;
30
30
  }
31
- else {
32
- execFileSync(platform === 'darwin' ? 'open' : 'xdg-open', [url], {
33
- stdio: 'ignore',
34
- timeout: 3000,
35
- });
36
- }
37
- return true;
31
+ const child = spawn(platform === 'darwin' ? 'open' : 'xdg-open', [url], {
32
+ stdio: 'ignore',
33
+ });
34
+ // A failed launch reports asynchronously; without a listener it would
35
+ // become an uncaught exception after this function has returned.
36
+ child.on('error', () => { });
37
+ child.unref();
38
+ // The URL handler's process may outlive the caller; a pid means launch began.
39
+ return child.pid !== undefined;
38
40
  }
39
41
  catch {
42
+ // Browser launch is best effort; the printed URL remains the fallback.
40
43
  return false;
41
44
  }
42
45
  }
@@ -53,7 +56,7 @@ function printFlow(context, flow) {
53
56
  }
54
57
  /**
55
58
  * Runs a login for the menu: a browser login first and, when no browser can
56
- * be opened, a device-code login instead, so a headless machine can still
59
+ * be started, a device-code login instead, so a headless machine can still
57
60
  * add an account. The URL is always printed, so an operator can open it by
58
61
  * hand when the opener reports success but nothing appears.
59
62
  */
@@ -3,7 +3,7 @@ import type { QuotaObservation } from '../quota/index.js';
3
3
  import type { RoutingRow } from '../routing/index.js';
4
4
  import { type ClaustrumFamily, type ClaustrumScopedAttempt, type ClaustrumScopedClient, type IdentityParser, type KeptReceiptState, type VaultGrantReach } from './custody.js';
5
5
  import { type ClaustrumLogger } from './errors.js';
6
- import { type InsulaQuotaOptions } from './insula.js';
6
+ import { type InsulaQuotaOptions, type InsulaQuotaResult } from './insula.js';
7
7
  import { type AccountMapper, type QuotaReceipt, type VaultRosterFile } from './roster.js';
8
8
  import { type WatchMode } from './watch.js';
9
9
  export interface ClaustrumConsumerOptions<Q = QuotaObservation> {
@@ -134,6 +134,10 @@ export declare class ClaustrumConsumer<Q = QuotaObservation> {
134
134
  * A route without a known provider account cannot join an insula entry.
135
135
  */
136
136
  cachedInsulaQuotaForRoute(routeId: string): Promise<Q | undefined>;
137
+ /** Cache-only background lookup, preserving the reason a route has no reading. */
138
+ lookupInsulaQuotaForRoute(routeId: string): Promise<InsulaQuotaResult<Q>>;
139
+ /** Skip the fallback poll while insula is cold or its upstream fetch failed. */
140
+ pollInsulaQuotaForRoute(routeId: string, fallback: () => Promise<Q | undefined>): Promise<Q | undefined>;
137
141
  /**
138
142
  * Authorize one physical send on a vault route. The roster file is re-read
139
143
  * first, so a decline committed by another process applies before the next
@@ -165,8 +169,10 @@ export declare class ClaustrumConsumer<Q = QuotaObservation> {
165
169
  * a served credential (an insula reading from `cachedInsulaQuotaForRoute`).
166
170
  * It lands only while the route is still bound to that known account, so no
167
171
  * credential fetch is needed and a replaced account's reading is dropped.
168
- * The in-memory roster is reloaded from the file, without a vault call.
172
+ * Memory updates immediately; durable publication is detached and coalesced.
169
173
  */
170
174
  recordAccountQuota(routeId: string, accountIdentity: string, observation: QuotaObservation): Promise<boolean>;
171
175
  close(): void;
176
+ /** Close immediately; the last consumer also waits for the writer's bounded drain. */
177
+ dispose(): Promise<void>;
172
178
  }
@@ -3,8 +3,9 @@ import { combineAbortSignals } from '../util/any-signal.js';
3
3
  import { ClaustrumScopedCustody, decideScopedRetryAfter401, } from './custody.js';
4
4
  import { readClaustrumEnrollmentToken } from './enrollment.js';
5
5
  import { ClaustrumConsumerError } from './errors.js';
6
- import { acquireInsulaQuotaSource } from './insula.js';
7
- import { acceptVaultRoute, declineVaultRoute, readVaultRoster, recordVaultAccountQuota, recordVaultQuota, refreshVaultRoster, vaultRoutingRows, } from './roster.js';
6
+ import { acquireInsulaQuotaSource, } from './insula.js';
7
+ import { pendingVaultQuota, retainVaultQuotaWriter } from './quota-writer.js';
8
+ import { acceptVaultRoute, applyVaultQuota, declineVaultRoute, forgetPendingVaultDiscovery, readVaultRoster, recordVaultAccountQuota, recordVaultQuota, refreshVaultRoster, vaultRoutingRows, } from './roster.js';
8
9
  import { SAFETY_POLL_MS } from './watch.js';
9
10
  /**
10
11
  * One plugin host's view of its vault accounts. It turns vault credentials
@@ -32,11 +33,16 @@ export class ClaustrumConsumer {
32
33
  #insula;
33
34
  #vaultWatch;
34
35
  #releaseInsulaWatch;
36
+ #insulaSkips = new Map();
35
37
  /** When this instance last completed a discovery, by `now`. */
36
38
  #discoveredAt;
39
+ #releaseQuotaWriter;
37
40
  constructor(options) {
38
41
  this.#options = options;
39
42
  this.#logger = options.logger ?? createLogger('claustrum');
43
+ this.#releaseQuotaWriter = retainVaultQuotaWriter(options.rosterPath, (publication) => {
44
+ this.#roster = applyVaultQuota(this.#roster, publication).roster;
45
+ });
40
46
  }
41
47
  #assertOpen(signal) {
42
48
  this.#shutdown.signal.throwIfAborted();
@@ -175,11 +181,9 @@ export class ClaustrumConsumer {
175
181
  let discovered;
176
182
  const roster = await refreshVaultRoster({
177
183
  path: this.#options.rosterPath,
178
- custody: {
179
- discover: async (signal) => {
180
- discovered = await custody.discover(signal);
181
- return discovered;
182
- },
184
+ custody,
185
+ onInventory: (inventory) => {
186
+ discovered = inventory;
183
187
  },
184
188
  isActive: () => this.#active(),
185
189
  signal: this.#shutdown.signal,
@@ -192,7 +196,7 @@ export class ClaustrumConsumer {
192
196
  });
193
197
  this.#assertOpen();
194
198
  const changed = roster?.view !== this.#roster?.view;
195
- this.#roster = roster;
199
+ this.#roster = pendingVaultQuota(this.#options.rosterPath, roster);
196
200
  custody.retain(roster?.rows ?? []);
197
201
  // Each discovery replaces the previous value, including with unknown. A
198
202
  // refresh that served a peer's roster discovered nothing and leaves it;
@@ -348,7 +352,7 @@ export class ClaustrumConsumer {
348
352
  return undefined;
349
353
  try {
350
354
  await readClaustrumEnrollmentToken(this.#options.tokenPath);
351
- const roster = await readVaultRoster(this.#options.rosterPath);
355
+ const roster = pendingVaultQuota(this.#options.rosterPath, await readVaultRoster(this.#options.rosterPath));
352
356
  const row = roster?.rows.find((entry) => entry.routeId === routeId);
353
357
  return this.#normalizeInsulaQuota(row?.accountIdentity);
354
358
  }
@@ -356,6 +360,44 @@ export class ClaustrumConsumer {
356
360
  return undefined;
357
361
  }
358
362
  }
363
+ /** Cache-only background lookup, preserving the reason a route has no reading. */
364
+ async lookupInsulaQuotaForRoute(routeId) {
365
+ const options = this.#options.insula;
366
+ if (!options || this.#shutdown.signal.aborted || !(await this.#active()))
367
+ return { miss: 'unreachable' };
368
+ try {
369
+ await readClaustrumEnrollmentToken(this.#options.tokenPath);
370
+ const roster = pendingVaultQuota(this.#options.rosterPath, await readVaultRoster(this.#options.rosterPath));
371
+ const account = roster?.rows.find((row) => row.routeId === routeId)?.accountIdentity;
372
+ if (!account || !this.#insula)
373
+ return { miss: 'unreachable' };
374
+ const result = this.#insula.source.lookup(options.provider, account, (this.#options.now ?? Date.now)());
375
+ const reading = result.reading &&
376
+ options.normalize(result.reading.entry, result.reading.context);
377
+ if (result.miss)
378
+ return { miss: result.miss, ...(reading !== undefined && { reading }) };
379
+ // Refusing an echo or unsupported window leaves the plugin's poll in charge.
380
+ return reading === undefined ? { miss: 'unreachable' } : { reading };
381
+ }
382
+ catch {
383
+ return { miss: 'unreachable' };
384
+ }
385
+ }
386
+ /** Skip the fallback poll while insula is cold or its upstream fetch failed. */
387
+ async pollInsulaQuotaForRoute(routeId, fallback) {
388
+ const result = await this.lookupInsulaQuotaForRoute(routeId);
389
+ if (result.miss === 'cold' || result.miss === 'upstream-failed') {
390
+ if (this.#insulaSkips.get(routeId) !== result.miss) {
391
+ this.#insulaSkips.set(routeId, result.miss);
392
+ this.#logger.warn('skipped insula provider poll', {
393
+ reason: result.miss,
394
+ });
395
+ }
396
+ return undefined;
397
+ }
398
+ this.#insulaSkips.delete(routeId);
399
+ return result.miss ? fallback() : result.reading;
400
+ }
359
401
  #normalizeInsulaQuota(accountIdentity) {
360
402
  if (!this.#options.insula || !accountIdentity)
361
403
  return undefined;
@@ -380,7 +422,7 @@ export class ClaustrumConsumer {
380
422
  this.#insula = undefined;
381
423
  throw new ClaustrumConsumerError('not-active', 'Claustrum scoped custody is not active');
382
424
  }
383
- const current = await readVaultRoster(this.#options.rosterPath);
425
+ const current = pendingVaultQuota(this.#options.rosterPath, await readVaultRoster(this.#options.rosterPath));
384
426
  this.#assertOpen(signal);
385
427
  const observed = roster.rows.find((row) => row.routeId === routeId);
386
428
  const configured = current?.rows.find((row) => row.routeId === routeId);
@@ -472,13 +514,13 @@ export class ClaustrumConsumer {
472
514
  async decline(routeId) {
473
515
  this.#assertOpen();
474
516
  await declineVaultRoute(this.#options.rosterPath, routeId);
475
- this.#roster = await readVaultRoster(this.#options.rosterPath);
517
+ this.#roster = pendingVaultQuota(this.#options.rosterPath, await readVaultRoster(this.#options.rosterPath));
476
518
  this.#custody?.retain(this.#roster?.rows ?? []);
477
519
  }
478
520
  async accept(routeId) {
479
521
  this.#assertOpen();
480
522
  await acceptVaultRoute(this.#options.rosterPath, routeId);
481
- this.#roster = await readVaultRoster(this.#options.rosterPath);
523
+ this.#roster = pendingVaultQuota(this.#options.rosterPath, await readVaultRoster(this.#options.rosterPath));
482
524
  }
483
525
  /**
484
526
  * Store a quota or profile observation for a vault route. The receipt the
@@ -488,7 +530,7 @@ export class ClaustrumConsumer {
488
530
  */
489
531
  async recordQuota(routeId, observation, attempt) {
490
532
  this.#assertOpen();
491
- const kept = await recordVaultQuota(this.#options.rosterPath, {
533
+ return recordVaultQuota(this.#options.rosterPath, {
492
534
  routeId,
493
535
  observation,
494
536
  credentialId: attempt.credentialId,
@@ -500,27 +542,21 @@ export class ClaustrumConsumer {
500
542
  expectedAccountIdentity: attempt.expectedAccountIdentity,
501
543
  }),
502
544
  });
503
- if (kept)
504
- this.#roster = await readVaultRoster(this.#options.rosterPath);
505
- return kept;
506
545
  }
507
546
  /**
508
547
  * Store a background observation that belongs to an account rather than to
509
548
  * a served credential (an insula reading from `cachedInsulaQuotaForRoute`).
510
549
  * It lands only while the route is still bound to that known account, so no
511
550
  * credential fetch is needed and a replaced account's reading is dropped.
512
- * The in-memory roster is reloaded from the file, without a vault call.
551
+ * Memory updates immediately; durable publication is detached and coalesced.
513
552
  */
514
553
  async recordAccountQuota(routeId, accountIdentity, observation) {
515
554
  this.#assertOpen();
516
- const kept = await recordVaultAccountQuota(this.#options.rosterPath, {
555
+ return recordVaultAccountQuota(this.#options.rosterPath, {
517
556
  routeId,
518
557
  accountIdentity,
519
558
  observation,
520
559
  });
521
- if (kept)
522
- this.#roster = await readVaultRoster(this.#options.rosterPath);
523
- return kept;
524
560
  }
525
561
  close() {
526
562
  if (this.#shutdown.signal.aborted)
@@ -535,5 +571,12 @@ export class ClaustrumConsumer {
535
571
  this.#insula = undefined;
536
572
  this.#roster = undefined;
537
573
  this.#grantReach = undefined;
574
+ void this.#releaseQuotaWriter();
575
+ forgetPendingVaultDiscovery(this.#options.rosterPath);
576
+ }
577
+ /** Close immediately; the last consumer also waits for the writer's bounded drain. */
578
+ async dispose() {
579
+ this.close();
580
+ await this.#releaseQuotaWriter();
538
581
  }
539
582
  }
@@ -7,8 +7,9 @@ export { type AccountIdentitySource, type ClaustrumFamily, type ClaustrumRosterW
7
7
  export { type ClaustrumEnrollmentClient, type ClaustrumEnrollmentConnection, type ClaustrumEnrollmentDisconnectResult, ClaustrumEnrollmentManager, type ClaustrumEnrollmentPaths, type ClaustrumEnrollmentResetResult, type ClaustrumEnrollmentStatus, classifyEnrollmentError, connectClaustrumEnrollmentClient, disconnectClaustrumEnrollment, type EnrollmentDisposition, enrollmentAuthority, enrollmentName, getClaustrumEnrollmentPaths, hostEnrollmentPaths, RETRYABLE_ENROLLMENT_CODES, readClaustrumEnrollmentStatus, readClaustrumEnrollmentToken, resetClaustrumEnrollmentState, TERMINAL_ENROLLMENT_CODES, } from './enrollment.js';
8
8
  export { ClaustrumConsumerError, type ClaustrumConsumerFailureKind, type ClaustrumLogger, } from './errors.js';
9
9
  export { assertHostSlotMatchesMode, assertNotCustodyPlaceholder, CUSTODY_PLACEHOLDER_PREFIX, classifyHostSlot, custodyPlaceholder, custodyPlaceholderKey, type HostSlotContent, isCustodyPlaceholder, isCustodyPlaceholderValue, } from './host-slot.js';
10
- export { acquireInsulaQuotaSource, type InsulaQuotaContext, type InsulaQuotaOptions, type InsulaQuotaSource, type InsulaUsageEntry, type InsulaUsageTransport, type InsulaUsageWatch, type InsulaUsageWindow, type InsulaWatchOptions, } from './insula.js';
10
+ export { acquireInsulaQuotaSource, type InsulaQuotaContext, type InsulaQuotaLookup, type InsulaQuotaMiss, type InsulaQuotaOptions, type InsulaQuotaReading, type InsulaQuotaResult, type InsulaQuotaSource, type InsulaUsageEntry, type InsulaUsageTransport, type InsulaUsageWatch, type InsulaUsageWindow, type InsulaWatchOptions, } from './insula.js';
11
11
  export { acceptAccount, type DeclinedAccount, declineAccount, isDeclined, } from './interlock.js';
12
+ export { drainVaultQuota } from './quota-writer.js';
12
13
  export { type AccountMapper, acceptVaultRoute, DEFAULT_ROUTE_PREFIX, declineVaultRoute, type MutateVaultRosterOptions, mutateVaultRoster, type ProjectionOptions, projectVaultRoster, type QuotaReceipt, readVaultRoster, recordVaultAccountQuota, recordVaultQuota, refreshVaultRoster, resolveVaultPrimary, type VaultPrimary, type VaultPrimaryBinding, type VaultPrimaryUnavailableReason, type VaultRosterFile, type VaultRosterLockContext, type VaultRosterRow, vaultRoutingRows, } from './roster.js';
13
14
  export { type ChangeNotice, ChangeNoticeReader, RESUBSCRIBE_BACKOFF_MS, SAFETY_POLL_MS, ScopedRosterWatch, type ScopedRosterWatchOptions, type ScopedWatchEnd, type ScopedWatchHandle, type ScopedWatchNoticeShape, type ScopedWatchSource, WATCH_RETRY_MS, type WatchMode, } from './watch.js';
14
15
  /**
@@ -14,6 +14,7 @@ export { ClaustrumConsumerError, } from './errors.js';
14
14
  export { assertHostSlotMatchesMode, assertNotCustodyPlaceholder, CUSTODY_PLACEHOLDER_PREFIX, classifyHostSlot, custodyPlaceholder, custodyPlaceholderKey, isCustodyPlaceholder, isCustodyPlaceholderValue, } from './host-slot.js';
15
15
  export { acquireInsulaQuotaSource, } from './insula.js';
16
16
  export { acceptAccount, declineAccount, isDeclined, } from './interlock.js';
17
+ export { drainVaultQuota } from './quota-writer.js';
17
18
  export { acceptVaultRoute, DEFAULT_ROUTE_PREFIX, declineVaultRoute, mutateVaultRoster, projectVaultRoster, readVaultRoster, recordVaultAccountQuota, recordVaultQuota, refreshVaultRoster, resolveVaultPrimary, vaultRoutingRows, } from './roster.js';
18
19
  export { ChangeNoticeReader, RESUBSCRIBE_BACKOFF_MS, SAFETY_POLL_MS, ScopedRosterWatch, WATCH_RETRY_MS, } from './watch.js';
19
20
  /**
@@ -34,6 +34,27 @@ export interface InsulaQuotaContext {
34
34
  fetchedAt: number;
35
35
  source?: string;
36
36
  }
37
+ export type InsulaQuotaMiss = 'cold' | 'stale' | 'absent' | 'upstream-failed' | 'credential' | 'unreachable';
38
+ export interface InsulaQuotaReading {
39
+ entry: InsulaUsageEntry;
40
+ context: InsulaQuotaContext;
41
+ }
42
+ /** A miss may retain a dated reading; it is not a current quota observation. */
43
+ export type InsulaQuotaLookup = {
44
+ reading: InsulaQuotaReading;
45
+ miss?: never;
46
+ } | {
47
+ miss: InsulaQuotaMiss;
48
+ reading?: InsulaQuotaReading;
49
+ };
50
+ /** The normalized equivalent of a source lookup, for a plugin's quota type. */
51
+ export type InsulaQuotaResult<Q> = {
52
+ reading: Q;
53
+ miss?: never;
54
+ } | {
55
+ miss: InsulaQuotaMiss;
56
+ reading?: Q;
57
+ };
37
58
  export interface InsulaQuotaOptions<Q> {
38
59
  provider: 'codex' | 'claude' | 'antigravity';
39
60
  /**
@@ -110,6 +131,8 @@ export declare class InsulaQuotaSource {
110
131
  entry: InsulaUsageEntry;
111
132
  context: InsulaQuotaContext;
112
133
  } | undefined;
134
+ /** Cache-only background lookup; current() retains its original miss behavior. */
135
+ lookup(provider: string, accountId: string, now: number): InsulaQuotaLookup;
113
136
  }
114
137
  export declare function acquireInsulaQuotaSource(transport: InsulaUsageTransport, tokenPath: string): {
115
138
  source: InsulaQuotaSource;
@@ -128,6 +128,9 @@ export function insulaUsageTransport(options) {
128
128
  }
129
129
  const MINUTE = 60_000;
130
130
  const MAX_AGE = 4 * MINUTE;
131
+ // consumer-contract.md:331 refetches a slot after 60 s. Two such intervals
132
+ // bound our willingness to wait, not a claim that insula completed its sweep.
133
+ const COLD_WAIT = 2 * MINUTE;
131
134
  const sources = new Map();
132
135
  // Keep only the minute budget after the last owner closes, so project churn
133
136
  // cannot reset the bound. Expired budgets are pruned on the next tick.
@@ -170,6 +173,9 @@ export class InsulaQuotaSource {
170
173
  transport;
171
174
  key;
172
175
  #entries = [];
176
+ #available = false;
177
+ #completeProviders = new Set();
178
+ #coldSince = new Map();
173
179
  /** Every usage.get, polled or watch-driven, shares this one slot. */
174
180
  #reads = new CoalescedRead(async () => {
175
181
  try {
@@ -178,13 +184,25 @@ export class InsulaQuotaSource {
178
184
  !Array.isArray(reply.result) ||
179
185
  reply.error !== undefined)
180
186
  throw new Error('unavailable insula usage');
181
- // Each response is a snapshot, not a delta. Missing accounts fall back;
182
- // neither pending nor the entry count asserts provider completeness.
187
+ // Each response is a snapshot, not a delta. Only completeProviders
188
+ // asserts completeness; neither pending nor the entry count does.
183
189
  this.#entries = reply.result.filter((entry) => record(entry) && typeof entry.provider === 'string');
190
+ this.#available =
191
+ this.#entries.length === reply.result.length &&
192
+ (reply.completeProviders === undefined ||
193
+ (Array.isArray(reply.completeProviders) &&
194
+ reply.completeProviders.every((value) => typeof value === 'string')));
195
+ this.#completeProviders = new Set(this.#available && Array.isArray(reply.completeProviders)
196
+ ? reply.completeProviders
197
+ : []);
198
+ for (const provider of this.#completeProviders)
199
+ this.#coldSince.delete(provider);
184
200
  }
185
201
  catch {
186
202
  // Refusal is absence, even if the previous reading was still young.
187
203
  this.#entries = [];
204
+ this.#available = false;
205
+ this.#completeProviders.clear();
188
206
  }
189
207
  }, () => { });
190
208
  #reader = new ChangeNoticeReader();
@@ -391,6 +409,50 @@ export class InsulaQuotaSource {
391
409
  }
392
410
  return undefined;
393
411
  }
412
+ /** Cache-only background lookup; current() retains its original miss behavior. */
413
+ lookup(provider, accountId, now) {
414
+ if (!this.#available)
415
+ return { miss: 'unreachable' };
416
+ const entry = this.#entries.find((value) => value.provider === provider &&
417
+ typeof value.account === 'string' &&
418
+ value.account.trim() === accountId);
419
+ // An unlabelled error describes the provider's credential path, not an
420
+ // unrelated labelled account. Never borrow another account's verdict.
421
+ const errorClass = (entry ??
422
+ this.#entries.find((value) => value.provider === provider &&
423
+ value.account === undefined &&
424
+ value.errorClass !== undefined))?.errorClass;
425
+ if (errorClass === 'upstream_failed')
426
+ return { miss: 'upstream-failed' };
427
+ if (errorClass === 'credential_absent' ||
428
+ errorClass === 'credential_unusable' ||
429
+ errorClass === 'credential_rejected')
430
+ return { miss: 'credential' };
431
+ if (errorClass !== undefined)
432
+ return { miss: 'unreachable' };
433
+ if (entry) {
434
+ const fetchedAt = typeof entry.fetchedAt === 'string'
435
+ ? Date.parse(entry.fetchedAt)
436
+ : Number.NaN;
437
+ if (!record(entry.usage) || !Number.isFinite(fetchedAt))
438
+ return { miss: 'unreachable' };
439
+ const reading = {
440
+ entry,
441
+ context: {
442
+ fetchedAt,
443
+ ...(typeof entry.source === 'string' && { source: entry.source }),
444
+ },
445
+ };
446
+ return now >= fetchedAt + MAX_AGE || entry.stale !== undefined
447
+ ? { miss: 'stale', reading }
448
+ : { reading };
449
+ }
450
+ if (this.#completeProviders.has(provider))
451
+ return { miss: 'absent' };
452
+ const since = this.#coldSince.get(provider) ?? now;
453
+ this.#coldSince.set(provider, since);
454
+ return { miss: now - since >= COLD_WAIT ? 'unreachable' : 'cold' };
455
+ }
394
456
  }
395
457
  export function acquireInsulaQuotaSource(transport, tokenPath) {
396
458
  const key = JSON.stringify([
@@ -0,0 +1,18 @@
1
+ import { type QuotaObservation } from '../quota/index.js';
2
+ import { type QuotaReceipt, type VaultRosterFile } from './roster.js';
3
+ export type VaultQuotaPublication = {
4
+ routeId: string;
5
+ accountIdentity?: string;
6
+ receipt?: QuotaReceipt;
7
+ observation: QuotaObservation;
8
+ };
9
+ type Listener = (publication: VaultQuotaPublication) => void;
10
+ /** Retain the shared writer; the last consumer's close drains it. */
11
+ export declare function retainVaultQuotaWriter(path: string, listener: Listener): () => Promise<void>;
12
+ /** Enqueue without filesystem work; each quota field retains its own receipt fence. */
13
+ export declare function enqueueVaultQuota(path: string, publication: VaultQuotaPublication): Promise<boolean>;
14
+ /** Overlay uncommitted observations when a consumer reloads the durable roster. */
15
+ export declare function pendingVaultQuota(path: string, roster: VaultRosterFile | undefined): VaultRosterFile | undefined;
16
+ /** Wait at most timeoutMs for detached publication; shutdown loss is acceptable. */
17
+ export declare function drainVaultQuota(path: string, timeoutMs?: number): Promise<void>;
18
+ export {};