@cortexkit/anthropic-auth-core 1.22.0 → 1.23.0

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 (48) hide show
  1. package/dist/accounts.d.ts +59 -2
  2. package/dist/accounts.js +666 -115
  3. package/dist/auth.js +3 -0
  4. package/dist/cachekeep.d.ts +18 -1
  5. package/dist/cachekeep.js +108 -82
  6. package/dist/cch.d.ts +11 -1
  7. package/dist/cch.js +48 -3
  8. package/dist/claude-code.d.ts +7 -1
  9. package/dist/claude-code.js +18 -3
  10. package/dist/claustrum-enrollment-registry.d.ts +17 -0
  11. package/dist/claustrum-enrollment-registry.js +194 -0
  12. package/dist/claustrum-enrollment.d.ts +70 -0
  13. package/dist/claustrum-enrollment.js +485 -0
  14. package/dist/claustrum-scoped-roster.d.ts +23 -0
  15. package/dist/claustrum-scoped-roster.js +194 -0
  16. package/dist/claustrum-scoped-runtime.d.ts +28 -0
  17. package/dist/claustrum-scoped-runtime.js +230 -0
  18. package/dist/claustrum-scoped.d.ts +39 -0
  19. package/dist/claustrum-scoped.js +193 -0
  20. package/dist/claustrum.d.ts +149 -6
  21. package/dist/claustrum.js +990 -48
  22. package/dist/commands/account.d.ts +40 -12
  23. package/dist/commands/account.js +138 -9
  24. package/dist/constants.d.ts +3 -2
  25. package/dist/constants.js +3 -2
  26. package/dist/custom-headers.d.ts +3 -0
  27. package/dist/custom-headers.js +102 -0
  28. package/dist/fast.js +1 -1
  29. package/dist/index.d.ts +7 -0
  30. package/dist/index.js +7 -0
  31. package/dist/logger.js +3 -1
  32. package/dist/model-remap.d.ts +27 -0
  33. package/dist/model-remap.js +72 -0
  34. package/dist/models.d.ts +19 -0
  35. package/dist/models.js +48 -12
  36. package/dist/oauth-profile.d.ts +2 -1
  37. package/dist/oauth-profile.js +6 -0
  38. package/dist/prime.d.ts +11 -3
  39. package/dist/prime.js +25 -7
  40. package/dist/quota-header-feed.d.ts +13 -0
  41. package/dist/quota-header-feed.js +60 -8
  42. package/dist/quota-manager.d.ts +15 -1
  43. package/dist/quota-manager.js +73 -22
  44. package/dist/relay.d.ts +8 -0
  45. package/dist/relay.js +72 -12
  46. package/dist/sticky-routing.d.ts +2 -0
  47. package/dist/sticky-routing.js +19 -1
  48. package/package.json +3 -2
package/dist/models.js CHANGED
@@ -9,6 +9,10 @@ export const CLAUDE_MYTHOS_5_1_MODEL_ID = 'claude-mythos-5-1';
9
9
  * above so per-million-token cost estimation does not conflate families.
10
10
  */
11
11
  export const CLAUDE_HAIKU_4_5_MODEL_ID = 'claude-haiku-4-5';
12
+ /** Remove OpenCode's context-window qualifier before model-family checks. */
13
+ export function normalizeAnthropicModelId(model) {
14
+ return model.endsWith('[1m]') ? model.slice(0, -4) : model;
15
+ }
12
16
  /**
13
17
  * Per-million-token USD pricing for Haiku 4.5. Used to project the cumulative
14
18
  * cost of prime requests from persisted usage counters — never persisted on
@@ -74,18 +78,23 @@ export const CLAUDE_FABLE_MYTHOS_5_1_MODEL_SPECS = {
74
78
  [CLAUDE_MYTHOS_5_1_MODEL_ID]: CLAUDE_FABLE_MYTHOS_5_MODEL_SPECS[CLAUDE_MYTHOS_5_1_MODEL_ID],
75
79
  };
76
80
  export function isClaudeFableOrMythos5Model(model) {
77
- return (typeof model === 'string' &&
78
- CLAUDE_FABLE_MYTHOS_5_MODEL_IDS.some((id) => model === id || model.startsWith(`${id}-`)));
81
+ if (typeof model !== 'string')
82
+ return false;
83
+ const normalized = normalizeAnthropicModelId(model);
84
+ return CLAUDE_FABLE_MYTHOS_5_MODEL_IDS.some((id) => normalized === id || normalized.startsWith(`${id}-`));
79
85
  }
80
86
  export function isClaudeFableOrMythos51Model(model) {
81
- return (typeof model === 'string' &&
82
- CLAUDE_FABLE_MYTHOS_5_1_MODEL_IDS.some((id) => model === id || model.startsWith(`${id}-`)));
87
+ if (typeof model !== 'string')
88
+ return false;
89
+ const normalized = normalizeAnthropicModelId(model);
90
+ return CLAUDE_FABLE_MYTHOS_5_1_MODEL_IDS.some((id) => normalized === id || normalized.startsWith(`${id}-`));
83
91
  }
84
92
  export function isClaudeFable51Model(model) {
85
93
  if (typeof model !== 'string')
86
94
  return false;
87
- return (model === CLAUDE_FABLE_5_1_MODEL_ID ||
88
- model.startsWith(`${CLAUDE_FABLE_5_1_MODEL_ID}-`));
95
+ const normalized = normalizeAnthropicModelId(model);
96
+ return (normalized === CLAUDE_FABLE_5_1_MODEL_ID ||
97
+ normalized.startsWith(`${CLAUDE_FABLE_5_1_MODEL_ID}-`));
89
98
  }
90
99
  export function getClaudeFableMythos5ReleaseDate(model) {
91
100
  return isClaudeFableOrMythos51Model(model)
@@ -101,9 +110,11 @@ export const CLAUDE_SONNET_5_MODEL_ID = 'claude-sonnet-5';
101
110
  */
102
111
  export const CLAUDE_SONNET_5_ADAPTIVE_THINKING = CLAUDE_FABLE_MYTHOS_5_SUMMARIZED_THINKING;
103
112
  export function isClaudeSonnet5Model(model) {
104
- return (typeof model === 'string' &&
105
- (model === CLAUDE_SONNET_5_MODEL_ID ||
106
- model.startsWith(`${CLAUDE_SONNET_5_MODEL_ID}-`)));
113
+ if (typeof model !== 'string')
114
+ return false;
115
+ const normalized = normalizeAnthropicModelId(model);
116
+ return (normalized === CLAUDE_SONNET_5_MODEL_ID ||
117
+ normalized.startsWith(`${CLAUDE_SONNET_5_MODEL_ID}-`));
107
118
  }
108
119
  export const CLAUDE_OPUS_5_MODEL_ID = 'claude-opus-5';
109
120
  /**
@@ -116,9 +127,34 @@ export const CLAUDE_OPUS_5_MODEL_ID = 'claude-opus-5';
116
127
  */
117
128
  export const CLAUDE_OPUS_5_ADAPTIVE_THINKING = CLAUDE_FABLE_MYTHOS_5_SUMMARIZED_THINKING;
118
129
  export function isClaudeOpus5Model(model) {
119
- return (typeof model === 'string' &&
120
- (model === CLAUDE_OPUS_5_MODEL_ID ||
121
- model.startsWith(`${CLAUDE_OPUS_5_MODEL_ID}-`)));
130
+ if (typeof model !== 'string')
131
+ return false;
132
+ const normalized = normalizeAnthropicModelId(model);
133
+ return (!isClaudeOpus55Model(normalized) &&
134
+ (normalized === CLAUDE_OPUS_5_MODEL_ID ||
135
+ normalized.startsWith(`${CLAUDE_OPUS_5_MODEL_ID}-`)));
136
+ }
137
+ export const CLAUDE_OPUS_5_5_MODEL_ID = 'claude-opus-5-5';
138
+ export const CLAUDE_OPUS_5_5_RELEASE_DATE = '2026-09-18';
139
+ export const CLAUDE_OPUS_5_5_CONTEXT_WINDOW = 1_000_000;
140
+ export const CLAUDE_OPUS_5_5_MAX_OUTPUT_TOKENS = 128_000;
141
+ export const CLAUDE_OPUS_5_5_PRICING = {
142
+ input: 4,
143
+ output: 20,
144
+ cacheRead: 0.2,
145
+ cacheWrite5m: 5,
146
+ cacheWrite1h: 8,
147
+ };
148
+ export const CLAUDE_OPUS_5_5_ADAPTIVE_THINKING = CLAUDE_FABLE_MYTHOS_5_SUMMARIZED_THINKING;
149
+ export function isClaudeOpus55Model(model) {
150
+ if (typeof model !== 'string')
151
+ return false;
152
+ const normalized = normalizeAnthropicModelId(model);
153
+ return (normalized === CLAUDE_OPUS_5_5_MODEL_ID ||
154
+ normalized.startsWith(`${CLAUDE_OPUS_5_5_MODEL_ID}-`));
155
+ }
156
+ export function isClaudeOpus5FamilyModel(model) {
157
+ return isClaudeOpus5Model(model) || isClaudeOpus55Model(model);
122
158
  }
123
159
  export function isOpenAIReasoningSignature(value) {
124
160
  if (typeof value !== 'string')
@@ -1,8 +1,9 @@
1
- import type { OAuthAccountProfile } from './accounts.ts';
1
+ import type { OAuthAccountProfile, ProviderAccountUuid } from './accounts.ts';
2
2
  export declare const PROFILE_TTL_MS: number;
3
3
  export declare function fetchOAuthAccountProfile(input: {
4
4
  accessToken: string;
5
5
  accountIdentity?: string;
6
+ providerAccountUuid?: ProviderAccountUuid;
6
7
  fetchImpl?: typeof fetch;
7
8
  now?: () => number;
8
9
  signal?: AbortSignal;
@@ -1,6 +1,9 @@
1
+ import { assertNotCustodyTombstone } from './claustrum.js';
1
2
  const PROFILE_URL = 'https://api.anthropic.com/api/oauth/profile';
2
3
  export const PROFILE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
3
4
  export async function fetchOAuthAccountProfile(input) {
5
+ // The bearer value is guarded where it becomes a header, like every other builder.
6
+ assertNotCustodyTombstone(input.accessToken, 'anthropic');
4
7
  const response = await (input.fetchImpl ?? fetch)(PROFILE_URL, {
5
8
  method: 'GET',
6
9
  signal: input.signal,
@@ -29,6 +32,9 @@ export async function fetchOAuthAccountProfile(input) {
29
32
  ...(input.accountIdentity !== undefined && {
30
33
  accountIdentity: input.accountIdentity,
31
34
  }),
35
+ ...(input.providerAccountUuid !== undefined && {
36
+ providerAccountUuid: input.providerAccountUuid,
37
+ }),
32
38
  };
33
39
  }
34
40
  export function oauthProfileMatchesIdentity(profile, accountIdentity) {
package/dist/prime.d.ts CHANGED
@@ -30,7 +30,7 @@ export type PrimeAccountStatus = {
30
30
  label: string;
31
31
  nextDueAt?: number;
32
32
  lastPrimedAt?: number | null;
33
- lastResult?: 'ok' | 'error';
33
+ lastResult?: 'ok' | 'error' | 'skipped';
34
34
  usage?: import('./accounts.ts').PrimeUsageCounters;
35
35
  estimatedCostUsd?: number;
36
36
  };
@@ -47,7 +47,7 @@ export type PrimeSendResult = {
47
47
  status?: number;
48
48
  ms?: number;
49
49
  error: string;
50
- reason?: 'token-refresh' | 'send';
50
+ reason?: 'token-refresh' | 'vault-cold' | 'send';
51
51
  };
52
52
  /**
53
53
  * Pure command parser — accepts `''` / `on` / `off` and treats anything else
@@ -72,7 +72,7 @@ export declare function buildPrimeAccountStatuses(storage: AccountStorage | null
72
72
  now?: number;
73
73
  transient?: ReadonlyMap<string, {
74
74
  lastPrimedAt?: number;
75
- lastResult?: 'ok' | 'error';
75
+ lastResult?: 'ok' | 'error' | 'skipped';
76
76
  }>;
77
77
  }): PrimeAccountStatus[];
78
78
  /**
@@ -182,9 +182,17 @@ export type PrimeManagerOptions = {
182
182
  now?: () => number;
183
183
  markerDir?: string;
184
184
  storagePath: string;
185
+ setIntervalImpl?: typeof globalThis.setInterval;
186
+ clearIntervalImpl?: typeof globalThis.clearInterval;
187
+ setTimeoutImpl?: typeof globalThis.setTimeout;
188
+ clearTimeoutImpl?: typeof globalThis.clearTimeout;
185
189
  };
186
190
  export declare class PrimeManager {
187
191
  private timer;
192
+ private readonly setIntervalImpl;
193
+ private readonly clearIntervalImpl;
194
+ private readonly setTimeoutImpl;
195
+ private readonly clearTimeoutImpl;
188
196
  private postFireRefreshTimers;
189
197
  private lastForcedCheck;
190
198
  private stopped;
package/dist/prime.js CHANGED
@@ -155,6 +155,8 @@ function formatPrimed(lastPrimedAt, lastResult) {
155
155
  });
156
156
  if (lastResult === 'error')
157
157
  return `primed ${time} err`;
158
+ if (lastResult === 'skipped')
159
+ return `primed ${time} skipped`;
158
160
  return `primed ${time} \u2713`;
159
161
  }
160
162
  function formatUsd(value) {
@@ -346,6 +348,10 @@ function storedResetMs(window) {
346
348
  }
347
349
  export class PrimeManager {
348
350
  timer = null;
351
+ setIntervalImpl;
352
+ clearIntervalImpl;
353
+ setTimeoutImpl;
354
+ clearTimeoutImpl;
349
355
  postFireRefreshTimers = new Set();
350
356
  lastForcedCheck = new Map();
351
357
  // Set true by `stop()` so a tick already in flight can short-circuit
@@ -373,6 +379,11 @@ export class PrimeManager {
373
379
  windowActive = new Map();
374
380
  constructor(options) {
375
381
  this.options = options;
382
+ this.setIntervalImpl = options.setIntervalImpl ?? globalThis.setInterval;
383
+ this.clearIntervalImpl =
384
+ options.clearIntervalImpl ?? globalThis.clearInterval;
385
+ this.setTimeoutImpl = options.setTimeoutImpl ?? globalThis.setTimeout;
386
+ this.clearTimeoutImpl = options.clearTimeoutImpl ?? globalThis.clearTimeout;
376
387
  }
377
388
  updateOptions(options) {
378
389
  this.options = options;
@@ -382,7 +393,7 @@ export class PrimeManager {
382
393
  return;
383
394
  this.stopped = false;
384
395
  logger.trace('prime', 'manager started');
385
- this.timer = setInterval(() => {
396
+ this.timer = this.setIntervalImpl(() => {
386
397
  void this.tick().catch((error) => {
387
398
  logger.warn('prime', 'tick failed', {
388
399
  error: error instanceof Error ? error.message : String(error),
@@ -399,11 +410,12 @@ export class PrimeManager {
399
410
  const hadPostFireRefresh = this.postFireRefreshTimers.size > 0;
400
411
  this.stopped = true;
401
412
  if (this.timer) {
402
- clearInterval(this.timer);
413
+ this.clearIntervalImpl(this.timer);
403
414
  this.timer = null;
404
415
  }
405
- for (const timer of this.postFireRefreshTimers)
406
- clearTimeout(timer);
416
+ for (const timer of this.postFireRefreshTimers) {
417
+ this.clearTimeoutImpl(timer);
418
+ }
407
419
  this.postFireRefreshTimers.clear();
408
420
  this.lastForcedCheck.clear();
409
421
  if (!hadTimer && !hadPostFireRefresh)
@@ -687,7 +699,7 @@ export class PrimeManager {
687
699
  if (this.stopped)
688
700
  return;
689
701
  if (!result.ok) {
690
- // Spec Logging table: two distinct warn events.
702
+ // `vault-cold` is a cache-warm skip; it is not a provider failure.
691
703
  // - `prime token refresh failed` — token-refresh failure
692
704
  // before the request fires (reason: 'token-refresh').
693
705
  // - `prime fire failed` — HTTP error / fetch throw /
@@ -698,6 +710,12 @@ export class PrimeManager {
698
710
  error: result.error,
699
711
  });
700
712
  }
713
+ else if (result.reason === 'vault-cold') {
714
+ logger.debug('prime', 'prime vault credential cold', {
715
+ account: evaluation.label,
716
+ reason: result.reason,
717
+ });
718
+ }
701
719
  else {
702
720
  logger.warn('prime', 'prime fire failed', {
703
721
  account: evaluation.label,
@@ -707,7 +725,7 @@ export class PrimeManager {
707
725
  }
708
726
  this.transient.set(evaluation.id, {
709
727
  lastPrimedAt: now,
710
- lastResult: 'error',
728
+ lastResult: result.reason === 'vault-cold' ? 'skipped' : 'error',
711
729
  });
712
730
  return;
713
731
  }
@@ -757,7 +775,7 @@ export class PrimeManager {
757
775
  // Unified fire-response headers can arm the window instantly on newer
758
776
  // trees. This branch predates header harvesting, so a delayed poll captures
759
777
  // the reset after the usage API's observed propagation lag.
760
- const timer = setTimeout(() => {
778
+ const timer = this.setTimeoutImpl(() => {
761
779
  this.postFireRefreshTimers.delete(timer);
762
780
  if (this.stopped)
763
781
  return;
@@ -1,6 +1,13 @@
1
1
  import { type AccountStorage, type OAuthQuotaSnapshot, type QuotaFieldName, type QuotaFieldSource, type QuotaFieldSources } from './accounts.ts';
2
2
  export declare const QUOTA_HEADER_FEED_SCHEMA_VERSION = 3;
3
3
  export declare const QUOTA_HEADER_FEED_LEASE_MS = 180000;
4
+ /**
5
+ * Lease files are per process; each carries only the accounts whose response
6
+ * headers THAT process harvested. Consumers MUST union entries across all files
7
+ * inside `lease_horizon_ms`, deduplicating by account; "newest file wins" is wrong.
8
+ * `anthropic_account_uuid` is always present: null is unresolvable, while absence
9
+ * identifies an old producer. A fallback `account_ref` is store-local.
10
+ */
4
11
  export type QuotaHeaderFeedIdentity = {
5
12
  identity_source: 'credential_id';
6
13
  credential_id: string;
@@ -22,9 +29,12 @@ type QuotaHeaderFeedMetadata = {
22
29
  observed_at_ms: number;
23
30
  };
24
31
  export type QuotaHeaderFeedEntry = QuotaHeaderFeedIdentity & QuotaHeaderFeedMetadata & {
32
+ /** Always present: null means UUID resolution failed; absence denotes an old producer. */
33
+ anthropic_account_uuid: string | null;
25
34
  quota: QuotaHeaderFeedQuota;
26
35
  };
27
36
  export type QuotaHeaderFeedPublishEntry = QuotaHeaderFeedIdentity & QuotaHeaderFeedMetadata & {
37
+ anthropic_account_uuid: string | null;
28
38
  quota: Omit<QuotaHeaderFeedQuota, 'provenance'> & {
29
39
  fieldSources?: QuotaFieldSources;
30
40
  };
@@ -44,8 +54,11 @@ export declare class QuotaHeaderFeedRegistry {
44
54
  now?: () => number;
45
55
  leaseMs?: number;
46
56
  instanceId?: string;
57
+ removeFile?: (path: string) => Promise<void>;
58
+ beforeRemoveFile?: (path: string) => Promise<void>;
47
59
  });
48
60
  publish(entry: QuotaHeaderFeedPublishEntry): Promise<void>;
61
+ private reapStaleSiblingLeases;
49
62
  list(): Promise<QuotaHeaderFeedEntry[]>;
50
63
  dispose(): Promise<void>;
51
64
  }
@@ -1,5 +1,5 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import { chmod, mkdir, readdir, readFile, rename, rm, writeFile, } from 'node:fs/promises';
2
+ import { chmod, lstat, mkdir, readdir, readFile, rename, rm, stat, writeFile, } from 'node:fs/promises';
3
3
  import { tmpdir } from 'node:os';
4
4
  import { join } from 'node:path';
5
5
  import { isOAuthAccount, QUOTA_FIELD_NAMES, } from './accounts.js';
@@ -22,6 +22,10 @@ function validIdentity(entry) {
22
22
  return false;
23
23
  if (!entry.quota || typeof entry.quota !== 'object')
24
24
  return false;
25
+ if (!Object.hasOwn(entry, 'anthropic_account_uuid') ||
26
+ (entry.anthropic_account_uuid !== null &&
27
+ typeof entry.anthropic_account_uuid !== 'string'))
28
+ return false;
25
29
  if (entry.identity_source === 'none') {
26
30
  return !('credential_id' in entry) && !('account_ref' in entry);
27
31
  }
@@ -192,17 +196,33 @@ export class QuotaHeaderFeedRegistry {
192
196
  this.filePath = join(options.directory ?? getDefaultQuotaHeaderFeedDirectory(), `${instanceId}.json`);
193
197
  }
194
198
  publish(entry) {
195
- const { accountKey, quota, ...entryWithoutQuota } = entry;
196
- const cleanEntry = {
197
- ...entryWithoutQuota,
198
- quota: projectQuota(quota),
199
- };
199
+ const { accountKey, quota } = entry;
200
200
  try {
201
- validatePublishEntry(cleanEntry);
201
+ validatePublishEntry(entry);
202
202
  }
203
203
  catch (error) {
204
204
  return Promise.reject(error);
205
205
  }
206
+ const identity = entry.identity_source === 'credential_id'
207
+ ? {
208
+ identity_source: 'credential_id',
209
+ credential_id: entry.credential_id,
210
+ }
211
+ : entry.identity_source === 'account_ref'
212
+ ? {
213
+ identity_source: 'account_ref',
214
+ account_ref: entry.account_ref,
215
+ }
216
+ : { identity_source: 'none' };
217
+ const cleanEntry = {
218
+ ...identity,
219
+ schema_version: entry.schema_version,
220
+ provider: entry.provider,
221
+ configured_account_count: entry.configured_account_count,
222
+ observed_at_ms: entry.observed_at_ms,
223
+ anthropic_account_uuid: entry.anthropic_account_uuid,
224
+ quota: projectQuota(quota),
225
+ };
206
226
  if (!accountKey)
207
227
  return Promise.reject(new Error('Invalid quota header feed account key'));
208
228
  this.writeChain = this.writeChain
@@ -211,6 +231,7 @@ export class QuotaHeaderFeedRegistry {
211
231
  const directory = this.options.directory ?? getDefaultQuotaHeaderFeedDirectory();
212
232
  await mkdir(directory, { recursive: true, mode: 0o700 });
213
233
  await chmod(directory, 0o700);
234
+ await this.reapStaleSiblingLeases(directory);
214
235
  let entries = {};
215
236
  try {
216
237
  const record = JSON.parse(await readFile(this.filePath, 'utf8'));
@@ -223,7 +244,7 @@ export class QuotaHeaderFeedRegistry {
223
244
  entries[accountKey] = cleanEntry;
224
245
  const tempPath = `${this.filePath}.${randomUUID()}.tmp`;
225
246
  try {
226
- await writeFile(tempPath, `${JSON.stringify({ version: QUOTA_HEADER_FEED_SCHEMA_VERSION, entries })}\n`, { mode: 0o600 });
247
+ await writeFile(tempPath, `${JSON.stringify({ version: QUOTA_HEADER_FEED_SCHEMA_VERSION, lease_horizon_ms: this.options.leaseMs ?? QUOTA_HEADER_FEED_LEASE_MS, entries })}\n`, { mode: 0o600 });
227
248
  await chmod(tempPath, 0o600);
228
249
  await rename(tempPath, this.filePath);
229
250
  }
@@ -233,6 +254,37 @@ export class QuotaHeaderFeedRegistry {
233
254
  });
234
255
  return this.writeChain;
235
256
  }
257
+ async reapStaleSiblingLeases(directory) {
258
+ const now = this.options.now?.() ?? Date.now();
259
+ const leaseMs = this.options.leaseMs ?? QUOTA_HEADER_FEED_LEASE_MS;
260
+ let names;
261
+ try {
262
+ names = await readdir(directory);
263
+ }
264
+ catch {
265
+ return;
266
+ }
267
+ await Promise.all(names
268
+ .filter((name) => /^\d+-[0-9a-f-]+\.json$/i.test(name))
269
+ .map(async (name) => {
270
+ const path = join(directory, name);
271
+ if (path === this.filePath)
272
+ return;
273
+ try {
274
+ const file = await stat(path);
275
+ if (file.mtimeMs > now || now - file.mtimeMs < leaseMs)
276
+ return;
277
+ await this.options.beforeRemoveFile?.(path);
278
+ const current = await lstat(path);
279
+ if (current.ino !== file.ino || current.mtimeMs !== file.mtimeMs)
280
+ return;
281
+ await (this.options.removeFile ?? ((target) => rm(target)))(path);
282
+ }
283
+ catch {
284
+ // A missed cleanup must not prevent this process from refreshing its lease.
285
+ }
286
+ }));
287
+ }
236
288
  async list() {
237
289
  await this.writeChain.catch(() => { });
238
290
  const directory = this.options.directory ?? getDefaultQuotaHeaderFeedDirectory();
@@ -20,6 +20,12 @@ export type QuotaRefreshResult = {
20
20
  export type QuotaManagerOptions = {
21
21
  storage: AccountStorage | null;
22
22
  fetchImpl?: typeof fetch;
23
+ /** Resolve custody credentials at the actual HTTP boundary, after quota gates. */
24
+ fetchQuotaSnapshot?: (request: {
25
+ kind: 'main' | 'fallback';
26
+ accountId: string | undefined;
27
+ accessToken: string;
28
+ }) => Promise<OAuthQuotaSnapshot>;
23
29
  now?: () => number;
24
30
  onMainQuotaFetched?: (quota: OAuthQuotaSnapshot, checkedAt: number, tokenFingerprint: string, fetchStartedAt: number, quotaErrorGeneration: number, quotaErrorClearedAt: number | undefined) => void;
25
31
  onApiError?: (error: AccountOperationError, quotaErrorGeneration: number) => void;
@@ -39,6 +45,8 @@ export declare class QuotaManager {
39
45
  private fallbackGenerations;
40
46
  private inflightMain;
41
47
  private inflightMainAccountId;
48
+ private inflightMainAccessToken;
49
+ private inflightMainToken;
42
50
  private inflightFallbacks;
43
51
  private mainLastApiError;
44
52
  private fallbackApiErrors;
@@ -46,6 +54,7 @@ export declare class QuotaManager {
46
54
  private lastApiCallAt;
47
55
  private storage;
48
56
  private readonly fetchImpl;
57
+ private readonly fetchQuotaSnapshot;
49
58
  private readonly now;
50
59
  private readonly onMainQuotaFetched;
51
60
  private readonly onApiError;
@@ -61,10 +70,13 @@ export declare class QuotaManager {
61
70
  * Cached fallback quota entry, scoped to the configured account id.
62
71
  */
63
72
  getFallback(accountId: string, account?: Pick<OAuthAccount, 'authLineageId'>): QuotaEntry | null;
73
+ /** A host transport resolves scoped credentials at dispatch time. */
74
+ canFetchWithoutAccessToken(): boolean;
64
75
  getAllFallbacks(): Map<string, QuotaEntry>;
65
76
  setMain(mainAccountId: string | undefined, entry: QuotaEntry): void;
77
+ clearMain(): void;
66
78
  /** Bind quota state to the account behind the current main credential. */
67
- setMainQuotaAccountIdentity(accountIdentity: string | undefined, strict?: boolean): number | undefined;
79
+ setMainQuotaAccountIdentity(accountIdentity: string | undefined, strict?: boolean, force?: boolean): number | undefined;
68
80
  setFallback(accountId: string, entry: QuotaEntry, account: Pick<OAuthAccount, 'authLineageId'> | undefined): void;
69
81
  clearFallback(accountId: string): void;
70
82
  pushMainFromHeaders(mainAccountId: string | undefined, incoming: OAuthQuotaSnapshot): QuotaEntry;
@@ -122,6 +134,8 @@ export declare class QuotaManager {
122
134
  private static quotaLockName;
123
135
  private bindFallbackLineage;
124
136
  private acceptFallbackQuotaObservation;
137
+ private isClosed;
138
+ close(): void;
125
139
  /**
126
140
  * Serialize API calls through a shared gate so only one
127
141
  * quota API request runs at a time, with a minimum gap