@cortexkit/anthropic-auth-core 1.22.0 → 2.0.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 (133) hide show
  1. package/README.md +2 -0
  2. package/dist/accounts.d.ts +52 -8
  3. package/dist/cachekeep.d.ts +27 -1
  4. package/dist/cch.d.ts +11 -1
  5. package/dist/claude-code.d.ts +23 -1
  6. package/dist/claustrum-enrollment.d.ts +72 -0
  7. package/dist/claustrum-scoped-roster.d.ts +27 -0
  8. package/dist/claustrum-scoped-runtime.d.ts +31 -0
  9. package/dist/claustrum-scoped.d.ts +53 -0
  10. package/dist/claustrum.d.ts +16 -82
  11. package/dist/commands/account.d.ts +38 -11
  12. package/dist/constants.d.ts +3 -2
  13. package/dist/custom-headers.d.ts +3 -0
  14. package/dist/index.d.ts +50 -0
  15. package/dist/index.js +29700 -31
  16. package/dist/internal-types/LICENSE +21 -0
  17. package/dist/internal-types/commands/builtins.d.ts +71 -0
  18. package/dist/internal-types/commands/index.d.ts +10 -0
  19. package/dist/internal-types/commands/menu.d.ts +61 -0
  20. package/dist/internal-types/commands/model.d.ts +238 -0
  21. package/dist/internal-types/commands/pi.d.ts +23 -0
  22. package/dist/internal-types/commands/seam.d.ts +72 -0
  23. package/dist/internal-types/fs/refresh-file-lock.d.ts +33 -0
  24. package/dist/internal-types/logger/capture-sink.d.ts +14 -0
  25. package/dist/internal-types/logger/engine.d.ts +100 -0
  26. package/dist/internal-types/logger/index.d.ts +6 -0
  27. package/dist/internal-types/logger/redact.d.ts +11 -0
  28. package/dist/internal-types/pi-slot/index.d.ts +3 -0
  29. package/dist/internal-types/pi-slot/slot.d.ts +49 -0
  30. package/dist/internal-types/quota/codec.d.ts +11 -0
  31. package/dist/internal-types/quota/format.d.ts +46 -0
  32. package/dist/internal-types/quota/index.d.ts +10 -0
  33. package/dist/internal-types/quota/map.d.ts +63 -0
  34. package/dist/internal-types/quota/merge.d.ts +48 -0
  35. package/dist/internal-types/quota/projection.d.ts +64 -0
  36. package/dist/internal-types/store/attribution.d.ts +28 -0
  37. package/dist/internal-types/store/errors.d.ts +55 -0
  38. package/dist/internal-types/store/hooks.d.ts +12 -0
  39. package/dist/internal-types/store/identity.d.ts +39 -0
  40. package/dist/internal-types/store/index.d.ts +21 -0
  41. package/dist/internal-types/store/mutate.d.ts +189 -0
  42. package/dist/internal-types/store/pool.d.ts +196 -0
  43. package/dist/internal-types/store/provider-state.d.ts +131 -0
  44. package/dist/internal-types/store/publication.d.ts +47 -0
  45. package/dist/internal-types/store/pull.d.ts +36 -0
  46. package/dist/internal-types/store/refresh-lock.d.ts +63 -0
  47. package/dist/internal-types/store/refresh.d.ts +72 -0
  48. package/dist/internal-types/store/rows.d.ts +313 -0
  49. package/dist/internal-types/store/runtime.d.ts +36 -0
  50. package/dist/internal-types/store/schema.d.ts +443 -0
  51. package/dist/internal-types/store/settings.d.ts +63 -0
  52. package/dist/internal-types/store/torn.d.ts +78 -0
  53. package/dist/migration-source.d.ts +33 -0
  54. package/dist/model-remap.d.ts +27 -0
  55. package/dist/models.d.ts +60 -0
  56. package/dist/native-account-runtime.d.ts +121 -0
  57. package/dist/native-account-view.d.ts +126 -0
  58. package/dist/native-command-effects.d.ts +59 -0
  59. package/dist/native-credential-validation.d.ts +42 -0
  60. package/dist/native-custody-activation.d.ts +45 -0
  61. package/dist/native-custody.d.ts +114 -0
  62. package/dist/native-enrollment-authority.d.ts +2 -0
  63. package/dist/native-host-auth-read.d.ts +3 -0
  64. package/dist/native-host-auth-write.d.ts +51 -0
  65. package/dist/native-host-auth.d.ts +12 -0
  66. package/dist/native-local-credential-service.d.ts +76 -0
  67. package/dist/native-local-runtime-readers.d.ts +22 -0
  68. package/dist/native-lock-observer.d.ts +12 -0
  69. package/dist/native-menu-executor.d.ts +243 -0
  70. package/dist/native-menu-model.d.ts +142 -0
  71. package/dist/native-migration-proof.d.ts +28 -0
  72. package/dist/native-migration-source.d.ts +47 -0
  73. package/dist/native-migration.d.ts +66 -0
  74. package/dist/native-quota-codec.d.ts +32 -0
  75. package/dist/native-refresh-coordinator.d.ts +240 -0
  76. package/dist/native-roster-seed.d.ts +115 -0
  77. package/dist/native-runtime-registry.d.ts +19 -0
  78. package/dist/native-runtime.d.ts +204 -0
  79. package/dist/native-transient-account.d.ts +38 -0
  80. package/dist/native-ui.d.ts +45 -0
  81. package/dist/native-usage-publication.d.ts +11 -0
  82. package/dist/native-vault-admission.d.ts +8 -0
  83. package/dist/native-vault-connection.d.ts +33 -0
  84. package/dist/native-vault-paths.d.ts +13 -0
  85. package/dist/native-vault-runtime.d.ts +102 -0
  86. package/dist/native-vault-seed.d.ts +16 -0
  87. package/dist/oauth-profile.d.ts +2 -1
  88. package/dist/pool-authority.d.ts +159 -0
  89. package/dist/pool-binding.d.ts +32 -0
  90. package/dist/pool-paths.d.ts +21 -0
  91. package/dist/pool-store.d.ts +32 -0
  92. package/dist/prime.d.ts +11 -3
  93. package/dist/provider-error.d.ts +139 -0
  94. package/dist/quota-header-feed.d.ts +13 -0
  95. package/dist/quota-manager.d.ts +30 -5
  96. package/dist/relay.d.ts +22 -1
  97. package/dist/request-history.d.ts +34 -0
  98. package/dist/sse.d.ts +81 -0
  99. package/dist/sticky-routing.d.ts +29 -0
  100. package/package.json +9 -3
  101. package/dist/accounts.js +0 -3330
  102. package/dist/auth.js +0 -182
  103. package/dist/cache1h.js +0 -97
  104. package/dist/cachekeep-registry.js +0 -124
  105. package/dist/cachekeep.js +0 -578
  106. package/dist/cch.js +0 -127
  107. package/dist/claude-code.js +0 -388
  108. package/dist/claustrum.js +0 -529
  109. package/dist/commands/account.js +0 -239
  110. package/dist/constants.js +0 -82
  111. package/dist/dump.js +0 -616
  112. package/dist/fast.js +0 -66
  113. package/dist/json.js +0 -10
  114. package/dist/killswitch.js +0 -132
  115. package/dist/logger.js +0 -286
  116. package/dist/logging.js +0 -59
  117. package/dist/mid-conversation-output-config.js +0 -89
  118. package/dist/models.js +0 -141
  119. package/dist/network-errors.js +0 -27
  120. package/dist/oauth-profile.js +0 -54
  121. package/dist/pkce.js +0 -17
  122. package/dist/prime.js +0 -823
  123. package/dist/provider.js +0 -0
  124. package/dist/quota-header-feed.js +0 -279
  125. package/dist/quota-headers.js +0 -121
  126. package/dist/quota-manager.js +0 -805
  127. package/dist/quotas.js +0 -141
  128. package/dist/relay.js +0 -1243
  129. package/dist/routing.js +0 -94
  130. package/dist/start.js +0 -29
  131. package/dist/sticky-routing.js +0 -523
  132. package/dist/thinking-binding.js +0 -54
  133. package/dist/token-fingerprint.js +0 -8
@@ -1,805 +0,0 @@
1
- /**
2
- * Unified quota cache and API gateway.
3
- *
4
- * Single source of truth for main + fallback quota state. All consumers
5
- * share one QuotaManager instance so they see the same in-memory cache.
6
- * Handles deduplication, rate-limiting (429 backoff), and staleness.
7
- */
8
- import { acquireRefreshFileLock, buildQuotaOperationError, fetchOAuthQuotaSnapshot, getPersistedMainQuota, getQuotaCheckIntervalMs, getQuotaNextRefreshAt, getQuotaRefreshEveryNRequests, getScopedQuotaWindowForModel, isQuotaPolicyAuthError, mergeMainQuotaErrorClearedAt, quotaBackoffActive, quotaSnapshotCheckedAt, } from './accounts.js';
9
- import { logger } from './logger.js';
10
- import { mergeHeaderQuotaSnapshot } from './quota-headers.js';
11
- export { tokenFingerprint } from './token-fingerprint.js';
12
- import { tokenFingerprint } from './token-fingerprint.js';
13
- // Capture real setTimeout before tests can mock globalThis.setTimeout
14
- const nativeSetTimeout = globalThis.setTimeout;
15
- function mergePollCompletionWithNewerHeaders(current, polled) {
16
- if (current?.source !== 'headers')
17
- return polled;
18
- const fiveHourIsNewer = Boolean(current.five_hour &&
19
- current.five_hour.checkedAt > (polled.five_hour?.checkedAt ?? 0));
20
- const sevenDayIsNewer = Boolean(current.seven_day &&
21
- current.seven_day.checkedAt > (polled.seven_day?.checkedAt ?? 0));
22
- if (!fiveHourIsNewer && !sevenDayIsNewer)
23
- return polled;
24
- return mergeHeaderQuotaSnapshot(polled, {
25
- ...(fiveHourIsNewer && { five_hour: current.five_hour }),
26
- ...(sevenDayIsNewer && { seven_day: current.seven_day }),
27
- fallbackAdvised: current.fallbackAdvised,
28
- ...(current.bindingWindowSource === 'headers' && {
29
- bindingWindow: current.bindingWindow,
30
- bindingWindowSource: current.bindingWindowSource,
31
- }),
32
- source: 'headers',
33
- checkedAt: Math.max(fiveHourIsNewer ? (current.five_hour?.checkedAt ?? 0) : 0, sevenDayIsNewer ? (current.seven_day?.checkedAt ?? 0) : 0),
34
- });
35
- }
36
- // ---------------------------------------------------------------------------
37
- // Class
38
- // ---------------------------------------------------------------------------
39
- export class QuotaManager {
40
- // --- State ---
41
- main = null;
42
- mainAccountId;
43
- mainQuotaIdentityKnown = false;
44
- mainQuotaIdentityStrict = false;
45
- mainGeneration = 0;
46
- mainQuotaErrorGeneration = 0;
47
- mainQuotaErrorClearedAt;
48
- fallbacks = new Map();
49
- fallbackAuthLineages = new Map();
50
- fallbackGenerations = new Map();
51
- // --- Inflight deduplication ---
52
- inflightMain = null;
53
- inflightMainAccountId;
54
- inflightFallbacks = new Map();
55
- // --- Rate-limiting (scoped per route so a fallback 429 never backs off the
56
- // main account or vice versa) ---
57
- mainLastApiError = undefined;
58
- fallbackApiErrors = new Map();
59
- // --- Serial API gate (prevents concurrent quota API calls) ---
60
- apiGate = Promise.resolve();
61
- lastApiCallAt = 0;
62
- // --- Config ---
63
- storage;
64
- fetchImpl;
65
- now;
66
- onMainQuotaFetched;
67
- onApiError;
68
- onFallbackQuotaFetched;
69
- onFallbackApiError;
70
- constructor(opts) {
71
- this.storage = opts.storage;
72
- this.fetchImpl = opts.fetchImpl ?? fetch;
73
- this.now = opts.now ?? Date.now;
74
- this.onMainQuotaFetched = opts.onMainQuotaFetched;
75
- this.onApiError = opts.onApiError;
76
- this.onFallbackQuotaFetched = opts.onFallbackQuotaFetched;
77
- this.onFallbackApiError = opts.onFallbackApiError;
78
- this.seedMainFromStorage(opts.storage, opts.storage?.mainAccountId);
79
- this.seedMainBackoffFromStorage(opts.storage);
80
- }
81
- // =========================================================================
82
- // Get (synchronous, from cache)
83
- // =========================================================================
84
- /**
85
- * Cached main quota entry, scoped to the account behind the main credential.
86
- * An unresolved identity keeps a retained cache unverified rather than exposing it.
87
- */
88
- getMain(mainAccountId) {
89
- if (this.mainAccountId !== mainAccountId)
90
- return null;
91
- if (this.mainQuotaIdentityKnown &&
92
- this.mainQuotaIdentityStrict &&
93
- (mainAccountId === undefined ||
94
- this.main?.quota.accountIdentity !== mainAccountId)) {
95
- return null;
96
- }
97
- return this.main;
98
- }
99
- /**
100
- * Cached fallback quota entry, scoped to the configured account id.
101
- */
102
- getFallback(accountId, account) {
103
- this.bindFallbackLineage(accountId, account);
104
- return this.fallbacks.get(accountId) ?? null;
105
- }
106
- getAllFallbacks() {
107
- return this.fallbacks;
108
- }
109
- // =========================================================================
110
- // Set (manual inject — seeding from persisted account.quota on boot)
111
- // =========================================================================
112
- setMain(mainAccountId, entry) {
113
- this.mainAccountId = mainAccountId;
114
- this.main = entry;
115
- }
116
- /** Bind quota state to the account behind the current main credential. */
117
- setMainQuotaAccountIdentity(accountIdentity, strict = true) {
118
- const previousIdentityKnown = this.mainQuotaIdentityKnown;
119
- const previousAccountIdentity = this.mainAccountId;
120
- const changed = !previousIdentityKnown ||
121
- this.mainAccountId !== accountIdentity ||
122
- this.mainQuotaIdentityStrict !== strict;
123
- this.mainQuotaIdentityKnown = true;
124
- this.mainQuotaIdentityStrict = strict;
125
- if (!changed)
126
- return undefined;
127
- this.mainGeneration += 1;
128
- this.mainAccountId = accountIdentity;
129
- if (accountIdentity !== undefined &&
130
- this.main?.quota.accountIdentity !== accountIdentity) {
131
- this.main = null;
132
- }
133
- if (accountIdentity !== undefined &&
134
- this.mainLastApiError &&
135
- (this.mainLastApiError.accountIdentity !== undefined
136
- ? this.mainLastApiError.accountIdentity !== accountIdentity
137
- : previousIdentityKnown &&
138
- previousAccountIdentity !== undefined &&
139
- previousAccountIdentity !== accountIdentity)) {
140
- this.mainLastApiError = undefined;
141
- this.mainQuotaErrorGeneration += 1;
142
- this.mainQuotaErrorClearedAt = mergeMainQuotaErrorClearedAt(this.mainQuotaErrorClearedAt, this.now());
143
- return this.mainQuotaErrorGeneration;
144
- }
145
- return undefined;
146
- }
147
- setFallback(accountId, entry, account) {
148
- this.bindFallbackLineage(accountId, account);
149
- this.fallbacks.set(accountId, entry);
150
- }
151
- clearFallback(accountId) {
152
- this.fallbackGenerations.set(accountId, (this.fallbackGenerations.get(accountId) ?? 0) + 1);
153
- this.fallbacks.delete(accountId);
154
- this.fallbackApiErrors.delete(accountId);
155
- this.inflightFallbacks.delete(QuotaManager.fallbackInflightKey(accountId));
156
- }
157
- pushMainFromHeaders(mainAccountId, incoming) {
158
- const checkedAt = incoming.checkedAt ?? this.now();
159
- const quota = mergeHeaderQuotaSnapshot(this.mainAccountId === mainAccountId &&
160
- (!this.mainQuotaIdentityKnown ||
161
- !this.mainQuotaIdentityStrict ||
162
- this.main?.quota.accountIdentity === mainAccountId)
163
- ? this.main?.quota
164
- : undefined, {
165
- ...incoming,
166
- ...(mainAccountId !== undefined && { accountIdentity: mainAccountId }),
167
- });
168
- const entry = {
169
- quota,
170
- checkedAt,
171
- refreshAfter: getQuotaNextRefreshAt(quota, this.storage, checkedAt),
172
- };
173
- this.setMain(mainAccountId, entry);
174
- return entry;
175
- }
176
- pushFallbackFromHeaders(accountId, incoming, account) {
177
- if (!this.acceptFallbackQuotaObservation(accountId, account))
178
- return null;
179
- const checkedAt = incoming.checkedAt ?? this.now();
180
- const quota = mergeHeaderQuotaSnapshot(this.fallbacks.get(accountId)?.quota, {
181
- ...incoming,
182
- accountIdentity: accountId,
183
- });
184
- const entry = {
185
- quota,
186
- checkedAt,
187
- refreshAfter: getQuotaNextRefreshAt(quota, this.storage, checkedAt),
188
- };
189
- this.fallbacks.set(accountId, entry);
190
- return entry;
191
- }
192
- // =========================================================================
193
- // Refresh (async, deduplicated, rate-limited)
194
- // =========================================================================
195
- async refreshMain(mainAccountIdOrAccessToken, accessToken, expectedIdentityGeneration) {
196
- const mainAccountId = mainAccountIdOrAccessToken;
197
- const credential = accessToken ?? mainAccountIdOrAccessToken;
198
- if (!credential)
199
- throw new Error('Main OAuth access token is unavailable');
200
- return (await this.refreshMainWithMetadata(mainAccountId, credential, expectedIdentityGeneration)).quota;
201
- }
202
- async refreshMainWithMetadata(mainAccountIdOrAccessToken, accessToken, expectedIdentityGeneration) {
203
- const effectiveAccountId = mainAccountIdOrAccessToken;
204
- const credential = accessToken ?? mainAccountIdOrAccessToken;
205
- if (!credential)
206
- throw new Error('Main OAuth access token is unavailable');
207
- if (expectedIdentityGeneration !== undefined &&
208
- (this.mainGeneration !== expectedIdentityGeneration ||
209
- this.mainAccountId !== effectiveAccountId)) {
210
- throw new Error('Main quota identity changed before refresh');
211
- }
212
- if (this.mainAccountId !== effectiveAccountId) {
213
- this.main = null;
214
- this.mainAccountId = effectiveAccountId;
215
- if (this.mainQuotaIdentityKnown)
216
- this.mainGeneration += 1;
217
- }
218
- if (this.inflightMain && this.inflightMainAccountId === effectiveAccountId)
219
- return this.inflightMain;
220
- // Rate-limit — if API recently 429'd, return stale or throw
221
- if (this.isBackedOff()) {
222
- if (this.main && this.mainAccountId === effectiveAccountId) {
223
- return { quota: this.main.quota, fetched: false };
224
- }
225
- throw new Error('Quota API rate-limited — try again later');
226
- }
227
- this.inflightMainAccountId = effectiveAccountId;
228
- const generation = this.mainGeneration;
229
- this.inflightMain = this._fetchMain(effectiveAccountId, credential, generation);
230
- return this.inflightMain;
231
- }
232
- async refreshFallback(accountId, accessToken, account) {
233
- return (await this.refreshFallbackWithMetadata(accountId, accessToken, account)).quota;
234
- }
235
- async refreshFallbackWithMetadata(accountId, accessToken, account) {
236
- this.bindFallbackLineage(accountId, account);
237
- const inflightKey = QuotaManager.fallbackInflightKey(accountId);
238
- const inflight = this.inflightFallbacks.get(inflightKey);
239
- if (inflight)
240
- return inflight;
241
- // Rate-limit — scoped to THIS fallback account only
242
- if (this.isFallbackBackedOff(accountId)) {
243
- const cached = this.getFallback(accountId);
244
- if (cached)
245
- return { quota: cached.quota, fetched: false };
246
- throw new Error('Quota API rate-limited — try again later');
247
- }
248
- const generation = this.fallbackGenerations.get(accountId) ?? 0;
249
- const promise = this._fetchFallback(accountId, accessToken, generation, account);
250
- this.inflightFallbacks.set(inflightKey, promise);
251
- void promise.then(() => {
252
- if (this.inflightFallbacks.get(inflightKey) === promise) {
253
- this.inflightFallbacks.delete(inflightKey);
254
- }
255
- }, () => {
256
- if (this.inflightFallbacks.get(inflightKey) === promise) {
257
- this.inflightFallbacks.delete(inflightKey);
258
- }
259
- });
260
- return promise;
261
- }
262
- async refreshAllFallbacks(accounts, resolveAccessToken) {
263
- const now = this.now();
264
- for (const account of accounts) {
265
- if (account.enabled === false)
266
- continue;
267
- const accessToken = resolveAccessToken?.(account) ??
268
- (account.access &&
269
- account.expires !== undefined &&
270
- account.expires > now
271
- ? account.access
272
- : undefined);
273
- if (!accessToken)
274
- continue;
275
- this.bindFallbackLineage(account.id, account);
276
- const cached = this.getFallback(account.id);
277
- if (cached && now < cached.refreshAfter)
278
- continue;
279
- try {
280
- await this.refreshFallback(account.id, accessToken, account);
281
- }
282
- catch {
283
- // Best-effort — keep stale cache entry if fetch fails
284
- }
285
- }
286
- }
287
- /**
288
- * Fire-and-forget refresh. Does not await, swallows errors.
289
- */
290
- refreshMainInBackground(accessToken) {
291
- if (this.inflightMain)
292
- return;
293
- if (this.isBackedOff())
294
- return;
295
- void this.refreshMain(this.mainAccountId, accessToken).catch(() => { });
296
- }
297
- // =========================================================================
298
- // Staleness queries
299
- // =========================================================================
300
- scopedWindowIsStale(entry, modelId) {
301
- const scoped = getScopedQuotaWindowForModel(entry.quota, modelId);
302
- return Boolean(scoped &&
303
- this.now() - scoped.checkedAt >= getQuotaCheckIntervalMs(this.storage));
304
- }
305
- isMainStale(modelId) {
306
- if (!this.main)
307
- return true;
308
- if (this.mainQuotaIdentityKnown &&
309
- this.mainQuotaIdentityStrict &&
310
- (this.mainAccountId === undefined ||
311
- this.main.quota.accountIdentity !== this.mainAccountId)) {
312
- return true;
313
- }
314
- return (this.now() >= this.main.refreshAfter ||
315
- this.scopedWindowIsStale(this.main, modelId));
316
- }
317
- isFallbackStale(accountId, _accessToken, modelId, account) {
318
- // Keep the unused slot to avoid breaking internal callers that pass modelId
319
- // third; cached quota is scoped to stable account identity.
320
- const entry = this.getFallback(accountId, account);
321
- if (!entry)
322
- return true;
323
- return (this.now() >= entry.refreshAfter ||
324
- this.scopedWindowIsStale(entry, modelId));
325
- }
326
- shouldRefreshOnRequestCount(requestCount) {
327
- const everyN = getQuotaRefreshEveryNRequests(this.storage);
328
- if (everyN <= 0)
329
- return false;
330
- return requestCount > 0 && requestCount % everyN === 0;
331
- }
332
- /**
333
- * Combined check: should a refresh happen right now?
334
- * True if main is stale by time OR triggered by request count.
335
- */
336
- needsRefresh(requestCount, modelId) {
337
- return (this.isMainStale(modelId) ||
338
- this.shouldRefreshOnRequestCount(requestCount));
339
- }
340
- // =========================================================================
341
- // Config
342
- // =========================================================================
343
- updateStorage(storage) {
344
- this.storage = storage;
345
- this.seedMainFromStorage(storage, this.mainQuotaIdentityKnown ? this.mainAccountId : storage?.mainAccountId);
346
- this.seedMainBackoffFromStorage(storage);
347
- }
348
- /**
349
- * Seed/update the main quota cache from persisted state. This is deliberately
350
- * callable after every disk load so another plugin process's fresh quota write
351
- * can stop this process from showing "checking…" or making a redundant quota
352
- * API call.
353
- */
354
- seedMainFromStorage(storage, mainAccountId) {
355
- const persisted = getPersistedMainQuota(storage);
356
- if (!persisted)
357
- return;
358
- const entry = {
359
- quota: {
360
- ...persisted.quota,
361
- ...((!this.mainQuotaIdentityKnown || !this.mainQuotaIdentityStrict) &&
362
- mainAccountId !== undefined &&
363
- persisted.accountIdentity === undefined && {
364
- accountIdentity: mainAccountId,
365
- }),
366
- },
367
- refreshAfter: getQuotaNextRefreshAt(persisted.quota, storage, persisted.checkedAt),
368
- checkedAt: persisted.checkedAt,
369
- };
370
- const current = this.main &&
371
- this.mainAccountId === mainAccountId &&
372
- (!this.mainQuotaIdentityKnown ||
373
- !this.mainQuotaIdentityStrict ||
374
- this.main.quota.accountIdentity === mainAccountId)
375
- ? this.main
376
- : undefined;
377
- if (current &&
378
- current.checkedAt >= entry.checkedAt &&
379
- current.quota.source === 'headers' &&
380
- entry.quota.source === 'poll') {
381
- const mergedQuota = mergeHeaderQuotaSnapshot(entry.quota, current.quota);
382
- this.main = {
383
- quota: mergedQuota,
384
- refreshAfter: getQuotaNextRefreshAt(mergedQuota, storage, current.checkedAt),
385
- checkedAt: current.checkedAt,
386
- };
387
- return;
388
- }
389
- if (this.main &&
390
- this.mainAccountId === mainAccountId &&
391
- this.main.checkedAt >= entry.checkedAt) {
392
- return;
393
- }
394
- this.main = entry;
395
- if (mainAccountId !== undefined)
396
- this.mainAccountId = mainAccountId;
397
- }
398
- seedMainBackoffFromStorage(storage) {
399
- const persistedError = storage?.quota?.mainLastQuotaApiError;
400
- const persistedIdentity = persistedError?.accountIdentity;
401
- const persistedGeneration = typeof storage?.quota?.mainQuotaErrorGeneration === 'number' &&
402
- Number.isSafeInteger(storage.quota.mainQuotaErrorGeneration) &&
403
- storage.quota.mainQuotaErrorGeneration >= 0
404
- ? storage.quota.mainQuotaErrorGeneration
405
- : undefined;
406
- const persistedClearedAt = typeof storage?.quota?.mainQuotaErrorClearedAt === 'number' &&
407
- Number.isFinite(storage.quota.mainQuotaErrorClearedAt) &&
408
- storage.quota.mainQuotaErrorClearedAt >= 0
409
- ? storage.quota.mainQuotaErrorClearedAt
410
- : undefined;
411
- if (persistedClearedAt !== undefined &&
412
- (this.mainQuotaErrorClearedAt === undefined ||
413
- persistedClearedAt > this.mainQuotaErrorClearedAt)) {
414
- this.mainQuotaErrorClearedAt = mergeMainQuotaErrorClearedAt(this.mainQuotaErrorClearedAt, persistedClearedAt);
415
- if ((!persistedError ||
416
- (typeof persistedError.checkedAt === 'number' &&
417
- persistedError.checkedAt <= persistedClearedAt)) &&
418
- (this.mainLastApiError?.checkedAt === undefined ||
419
- this.mainLastApiError.checkedAt <= persistedClearedAt)) {
420
- this.mainLastApiError = undefined;
421
- }
422
- }
423
- if (persistedGeneration !== undefined &&
424
- persistedGeneration > this.mainQuotaErrorGeneration) {
425
- this.mainQuotaErrorGeneration = persistedGeneration;
426
- if (!persistedError &&
427
- (this.mainLastApiError?.checkedAt === undefined ||
428
- this.mainQuotaErrorClearedAt === undefined ||
429
- this.mainLastApiError.checkedAt <= this.mainQuotaErrorClearedAt)) {
430
- this.mainLastApiError = undefined;
431
- }
432
- }
433
- if (persistedError &&
434
- (!this.mainQuotaIdentityKnown ||
435
- persistedIdentity === undefined ||
436
- persistedIdentity === this.mainAccountId) &&
437
- (this.mainQuotaErrorClearedAt === undefined ||
438
- typeof persistedError.checkedAt !== 'number' ||
439
- persistedError.checkedAt > this.mainQuotaErrorClearedAt) &&
440
- (this.mainLastApiError?.checkedAt === undefined ||
441
- persistedError.checkedAt >= this.mainLastApiError.checkedAt) &&
442
- quotaBackoffActive(persistedError, this.now())) {
443
- this.mainLastApiError = persistedError;
444
- if (persistedGeneration !== undefined) {
445
- this.mainQuotaErrorGeneration = persistedGeneration;
446
- }
447
- return;
448
- }
449
- if (this.mainAccountId !== undefined &&
450
- this.mainLastApiError?.accountIdentity !== undefined &&
451
- this.mainLastApiError.accountIdentity !== this.mainAccountId) {
452
- this.mainLastApiError = undefined;
453
- }
454
- }
455
- /**
456
- * Seed fallback cache entries from persisted account.quota data.
457
- * Updates older in-memory entries so a fresh quota write from another plugin
458
- * process prevents redundant checks and stale sidebar writes.
459
- */
460
- seedFallbacksFromAccounts(accounts) {
461
- const checkInterval = getQuotaCheckIntervalMs(this.storage);
462
- const configuredIds = new Set(accounts.map((account) => account.id));
463
- for (const accountId of this.fallbackAuthLineages.keys()) {
464
- if (!configuredIds.has(accountId)) {
465
- this.clearFallback(accountId);
466
- this.fallbackAuthLineages.delete(accountId);
467
- }
468
- }
469
- for (const account of accounts) {
470
- const lineageChanged = this.bindFallbackLineage(account.id, account);
471
- if (lineageChanged)
472
- continue;
473
- if (account.enabled === false)
474
- continue;
475
- const persistedError = account.lastQuotaRefreshError;
476
- const persistedClearAt = quotaSnapshotCheckedAt(account.quota);
477
- const currentError = this.fallbackApiErrors.get(account.id);
478
- if (persistedError &&
479
- quotaBackoffActive(persistedError, this.now()) &&
480
- persistedClearAt <= (persistedError.checkedAt ?? 0) &&
481
- (!currentError || persistedError.checkedAt >= currentError.checkedAt)) {
482
- this.fallbackApiErrors.set(account.id, persistedError);
483
- }
484
- else if (currentError && persistedClearAt >= currentError.checkedAt) {
485
- this.fallbackApiErrors.delete(account.id);
486
- }
487
- if (!account.quota)
488
- continue;
489
- const checkedAt = quotaSnapshotCheckedAt(account.quota);
490
- if (checkedAt <= 0)
491
- continue;
492
- const existing = this.getFallback(account.id);
493
- if (existing && existing.checkedAt >= checkedAt)
494
- continue;
495
- this.setFallback(account.id, {
496
- quota: { ...account.quota, accountIdentity: account.id },
497
- refreshAfter: checkedAt + checkInterval,
498
- checkedAt,
499
- }, account);
500
- }
501
- }
502
- /**
503
- * Whether the MAIN quota API is currently in backoff. Scoped to the main
504
- * account — a fallback account's 429 never reports here.
505
- */
506
- isBackedOff() {
507
- // Unlike cached numbers above, restrictions stay active while identity is
508
- // ambiguous; only a confirmed account change may release an inherited guard.
509
- return quotaBackoffActive(this.mainLastApiError, this.now());
510
- }
511
- /**
512
- * Whether a specific fallback account's quota API is in backoff.
513
- */
514
- isFallbackBackedOff(accountId) {
515
- return quotaBackoffActive(this.fallbackApiErrors.get(accountId), this.now());
516
- }
517
- getLastApiError() {
518
- return this.mainLastApiError;
519
- }
520
- getMainQuotaErrorGeneration() {
521
- return this.mainQuotaErrorGeneration;
522
- }
523
- getMainQuotaIdentityGeneration() {
524
- return this.mainGeneration;
525
- }
526
- getMainQuotaErrorClearedAt() {
527
- return this.mainQuotaErrorClearedAt;
528
- }
529
- clearMainBackoff() {
530
- if (!this.mainLastApiError)
531
- return undefined;
532
- this.mainLastApiError = undefined;
533
- this.mainQuotaErrorGeneration += 1;
534
- this.mainQuotaErrorClearedAt = mergeMainQuotaErrorClearedAt(this.mainQuotaErrorClearedAt, this.now());
535
- return this.mainQuotaErrorGeneration;
536
- }
537
- // =========================================================================
538
- // Private
539
- // =========================================================================
540
- /** Minimum gap between consecutive quota API calls (ms). */
541
- static API_CALL_GAP_MS = 1_000;
542
- static fallbackInflightKey(accountId) {
543
- return accountId;
544
- }
545
- static quotaLockName(accountId) {
546
- const safeId = accountId.replace(/[^a-zA-Z0-9._-]+/g, '-');
547
- return `opencode-fallback-quota-refresh-${safeId || 'account'}`;
548
- }
549
- bindFallbackLineage(accountId, account) {
550
- if (!account)
551
- return false;
552
- const hadLineage = this.fallbackAuthLineages.has(accountId);
553
- const previousLineage = this.fallbackAuthLineages.get(accountId);
554
- const lineageChanged = hadLineage &&
555
- account.authLineageId !== undefined &&
556
- previousLineage !== account.authLineageId;
557
- if (lineageChanged)
558
- this.clearFallback(accountId);
559
- this.fallbackAuthLineages.set(accountId, account.authLineageId);
560
- return lineageChanged;
561
- }
562
- acceptFallbackQuotaObservation(accountId, account) {
563
- const hasLineage = this.fallbackAuthLineages.has(accountId);
564
- const currentLineage = this.fallbackAuthLineages.get(accountId);
565
- const observedLineage = account?.authLineageId;
566
- if (hasLineage && currentLineage !== observedLineage) {
567
- logger.trace('quota', 'skipped stale fallback quota observation after lineage change', {
568
- accountId,
569
- storedLineage: currentLineage,
570
- servedLineage: observedLineage,
571
- });
572
- return false;
573
- }
574
- if (!hasLineage)
575
- this.fallbackAuthLineages.set(accountId, observedLineage);
576
- return true;
577
- }
578
- /**
579
- * Serialize API calls through a shared gate so only one
580
- * quota API request runs at a time, with a minimum gap
581
- * between calls. Prevents concurrent and rapid-fire calls
582
- * from triggering Anthropic's rate limits.
583
- */
584
- _enqueueApiFetch(fn) {
585
- const gatedFn = async () => {
586
- // Wait until minimum gap since last API call
587
- const elapsed = this.now() - this.lastApiCallAt;
588
- if (elapsed < QuotaManager.API_CALL_GAP_MS) {
589
- await new Promise((r) => {
590
- const id = nativeSetTimeout(r, QuotaManager.API_CALL_GAP_MS - elapsed);
591
- if (typeof id === 'object' && 'unref' in id)
592
- id.unref();
593
- });
594
- }
595
- this.lastApiCallAt = this.now();
596
- return fn();
597
- };
598
- const queued = this.apiGate.then(gatedFn, gatedFn);
599
- this.apiGate = queued.catch(() => { });
600
- return queued;
601
- }
602
- async _fetchMain(mainAccountId, accessToken, generation) {
603
- return this._enqueueApiFetch(async () => {
604
- try {
605
- // Re-check backoff inside gate — may have been set by
606
- // a preceding queued call while we waited
607
- if (this.isBackedOff()) {
608
- if (this.main && this.mainAccountId === mainAccountId) {
609
- return { quota: this.main.quota, fetched: false };
610
- }
611
- throw new Error('Quota API rate-limited — try again later');
612
- }
613
- if (this.mainQuotaIdentityKnown &&
614
- this.mainQuotaIdentityStrict &&
615
- this.mainGeneration !== generation) {
616
- return {
617
- quota: { accountIdentity: mainAccountId },
618
- fetched: false,
619
- };
620
- }
621
- const fileLock = await acquireRefreshFileLock({
622
- name: 'opencode-main-quota-refresh',
623
- ttlMs: 30_000,
624
- });
625
- if (!fileLock) {
626
- const cached = this.main;
627
- if (cached &&
628
- this.mainAccountId === mainAccountId &&
629
- this.now() < cached.refreshAfter) {
630
- return { quota: cached.quota, fetched: false };
631
- }
632
- throw new Error('Quota refresh is already in progress');
633
- }
634
- try {
635
- const fetchStartedAt = this.now();
636
- const quota = await fetchOAuthQuotaSnapshot({
637
- accessToken,
638
- fetchImpl: this.fetchImpl,
639
- now: this.now,
640
- });
641
- const now = this.now();
642
- if (this.mainQuotaIdentityKnown &&
643
- this.mainQuotaIdentityStrict &&
644
- this.mainGeneration !== generation) {
645
- return {
646
- quota: {
647
- ...quota,
648
- ...(mainAccountId !== undefined && {
649
- accountIdentity: mainAccountId,
650
- }),
651
- },
652
- fetched: true,
653
- };
654
- }
655
- const completedQuota = mergePollCompletionWithNewerHeaders(this.mainAccountId === mainAccountId &&
656
- (!this.mainQuotaIdentityKnown ||
657
- !this.mainQuotaIdentityStrict ||
658
- this.main?.quota.accountIdentity === mainAccountId)
659
- ? this.main?.quota
660
- : undefined, {
661
- ...quota,
662
- ...(mainAccountId !== undefined && {
663
- accountIdentity: mainAccountId,
664
- }),
665
- });
666
- this.mainAccountId = mainAccountId;
667
- this.main = {
668
- quota: completedQuota,
669
- refreshAfter: getQuotaNextRefreshAt(completedQuota, this.storage, now),
670
- checkedAt: completedQuota.checkedAt ?? now,
671
- };
672
- if (this.mainLastApiError) {
673
- this.mainLastApiError = undefined;
674
- this.mainQuotaErrorGeneration += 1;
675
- this.mainQuotaErrorClearedAt = mergeMainQuotaErrorClearedAt(this.mainQuotaErrorClearedAt, now);
676
- }
677
- this.onMainQuotaFetched?.(completedQuota, now, tokenFingerprint(accessToken), fetchStartedAt, this.mainQuotaErrorGeneration, this.mainQuotaErrorClearedAt);
678
- return { quota: completedQuota, fetched: true };
679
- }
680
- catch (error) {
681
- this._handleMainFetchError(error, generation);
682
- throw error;
683
- }
684
- finally {
685
- await fileLock.release();
686
- }
687
- }
688
- finally {
689
- if (this.inflightMainAccountId === mainAccountId) {
690
- this.inflightMain = null;
691
- this.inflightMainAccountId = undefined;
692
- }
693
- }
694
- });
695
- }
696
- async _fetchFallback(accountId, accessToken, generation, account) {
697
- return this._enqueueApiFetch(async () => {
698
- try {
699
- if ((this.fallbackGenerations.get(accountId) ?? 0) !== generation) {
700
- return { quota: { accountIdentity: accountId }, fetched: false };
701
- }
702
- // Re-check backoff inside gate — scoped to this fallback account
703
- if (this.isFallbackBackedOff(accountId)) {
704
- const cached = this.getFallback(accountId, account);
705
- if (cached)
706
- return { quota: cached.quota, fetched: false };
707
- throw new Error('Quota API rate-limited — try again later');
708
- }
709
- const fileLock = await acquireRefreshFileLock({
710
- name: QuotaManager.quotaLockName(accountId),
711
- ttlMs: 30_000,
712
- });
713
- if (!fileLock) {
714
- const cached = this.getFallback(accountId, account);
715
- if (cached && this.now() < cached.refreshAfter) {
716
- return { quota: cached.quota, fetched: false };
717
- }
718
- throw new Error('Quota refresh is already in progress');
719
- }
720
- try {
721
- const fetchStartedAt = this.now();
722
- const quota = await fetchOAuthQuotaSnapshot({
723
- accessToken,
724
- fetchImpl: this.fetchImpl,
725
- now: this.now,
726
- });
727
- const now = this.now();
728
- if ((this.fallbackGenerations.get(accountId) ?? 0) !== generation) {
729
- return {
730
- quota: { ...quota, accountIdentity: accountId },
731
- fetched: true,
732
- };
733
- }
734
- const completedQuota = mergePollCompletionWithNewerHeaders(this.getFallback(accountId, account)?.quota, { ...quota, accountIdentity: accountId });
735
- this.setFallback(accountId, {
736
- quota: completedQuota,
737
- refreshAfter: getQuotaNextRefreshAt(completedQuota, this.storage, now),
738
- checkedAt: completedQuota.checkedAt ?? now,
739
- }, account);
740
- this.fallbackApiErrors.delete(accountId);
741
- this.onFallbackQuotaFetched?.(accountId, completedQuota, now, fetchStartedAt);
742
- return { quota: completedQuota, fetched: true };
743
- }
744
- finally {
745
- await fileLock.release();
746
- }
747
- }
748
- catch (error) {
749
- this._handleFallbackFetchError(accountId, error, generation);
750
- throw error;
751
- }
752
- });
753
- }
754
- // A 401 is an auth/token problem and a 403 is an account/org policy problem,
755
- // not quota endpoint saturation. Surface both without recording quota backoff
756
- // so callers can refresh, re-auth, or try another account immediately.
757
- static isAuthError(error) {
758
- const status = error.status;
759
- if (status === 401 || isQuotaPolicyAuthError(error))
760
- return true;
761
- const message = error instanceof Error ? error.message : String(error);
762
- return /quota check failed: 401\b/.test(message);
763
- }
764
- /** Main quota failure: arms main-only backoff and persists via onApiError. */
765
- _handleMainFetchError(error, generation) {
766
- if (this.mainGeneration !== generation)
767
- return;
768
- if (this.mainQuotaIdentityKnown &&
769
- this.mainQuotaIdentityStrict &&
770
- this.mainAccountId === undefined) {
771
- return;
772
- }
773
- if (QuotaManager.isAuthError(error))
774
- return;
775
- this.mainLastApiError = buildQuotaOperationError({
776
- error,
777
- now: this.now(),
778
- accountIdentity: this.mainAccountId,
779
- previous: this.mainLastApiError,
780
- });
781
- this.mainQuotaErrorGeneration += 1;
782
- this.onApiError?.(this.mainLastApiError, this.mainQuotaErrorGeneration);
783
- }
784
- /**
785
- * Fallback quota failure: arms backoff for THIS account only. Never touches
786
- * main backoff state and never calls onApiError (which persists the main
787
- * quota error) — the per-account error is recorded by the caller via the
788
- * account's lastQuotaRefreshError.
789
- */
790
- _handleFallbackFetchError(accountId, error, generation) {
791
- if ((this.fallbackGenerations.get(accountId) ?? 0) !== generation)
792
- return;
793
- if (QuotaManager.isAuthError(error))
794
- return;
795
- const previous = this.fallbackApiErrors.get(accountId);
796
- const quotaError = buildQuotaOperationError({
797
- error,
798
- now: this.now(),
799
- previous,
800
- accountIdentity: accountId,
801
- });
802
- this.fallbackApiErrors.set(accountId, quotaError);
803
- this.onFallbackApiError?.(accountId, quotaError);
804
- }
805
- }