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