@cortexkit/anthropic-auth-core 1.20.0 → 1.22.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.
@@ -0,0 +1,279 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { chmod, mkdir, readdir, readFile, rename, rm, writeFile, } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { isOAuthAccount, QUOTA_FIELD_NAMES, } from './accounts.js';
6
+ export const QUOTA_HEADER_FEED_SCHEMA_VERSION = 3;
7
+ export const QUOTA_HEADER_FEED_LEASE_MS = 180_000;
8
+ export function getDefaultQuotaHeaderFeedDirectory() {
9
+ return (process.env.OPENCODE_ANTHROPIC_AUTH_QUOTA_FEED_DIR ??
10
+ join(tmpdir(), 'opencode-anthropic-auth', 'quota-header-feed'));
11
+ }
12
+ export function configuredAnthropicOAuthAccountCount(input) {
13
+ const fallbackCount = input.storage?.accounts.filter(isOAuthAccount).length ?? 0;
14
+ return fallbackCount + (input.mainOAuthConfigured ? 1 : 0);
15
+ }
16
+ function validIdentity(entry) {
17
+ if (entry.schema_version !== QUOTA_HEADER_FEED_SCHEMA_VERSION ||
18
+ entry.provider !== 'anthropic')
19
+ return false;
20
+ if (!Number.isFinite(entry.configured_account_count) ||
21
+ !Number.isFinite(entry.observed_at_ms))
22
+ return false;
23
+ if (!entry.quota || typeof entry.quota !== 'object')
24
+ return false;
25
+ if (entry.identity_source === 'none') {
26
+ return !('credential_id' in entry) && !('account_ref' in entry);
27
+ }
28
+ if (entry.identity_source === 'credential_id') {
29
+ return (typeof entry.credential_id === 'string' &&
30
+ entry.credential_id.length > 0 &&
31
+ entry.credential_id !== 'main' &&
32
+ !('account_ref' in entry));
33
+ }
34
+ if (entry.identity_source === 'account_ref') {
35
+ return (typeof entry.account_ref === 'string' &&
36
+ entry.account_ref.length > 0 &&
37
+ entry.account_ref !== 'main' &&
38
+ !('credential_id' in entry));
39
+ }
40
+ return false;
41
+ }
42
+ function validatePublishEntry(entry) {
43
+ if (entry.schema_version !== QUOTA_HEADER_FEED_SCHEMA_VERSION ||
44
+ entry.provider !== 'anthropic' ||
45
+ !validIdentity(entry)) {
46
+ throw new Error('Invalid quota header feed entry');
47
+ }
48
+ }
49
+ function finiteNumber(value) {
50
+ return typeof value === 'number' && Number.isFinite(value);
51
+ }
52
+ function projectQuotaWindow(value) {
53
+ if (!value || typeof value !== 'object')
54
+ return undefined;
55
+ const candidate = value;
56
+ if (!finiteNumber(candidate.usedPercent) ||
57
+ !finiteNumber(candidate.remainingPercent) ||
58
+ !finiteNumber(candidate.checkedAt)) {
59
+ return undefined;
60
+ }
61
+ return {
62
+ usedPercent: candidate.usedPercent,
63
+ remainingPercent: candidate.remainingPercent,
64
+ ...(typeof candidate.resetsAt === 'string' && {
65
+ resetsAt: candidate.resetsAt,
66
+ }),
67
+ checkedAt: candidate.checkedAt,
68
+ };
69
+ }
70
+ function projectScopedQuotaWindow(value) {
71
+ if (!value || typeof value !== 'object')
72
+ return undefined;
73
+ const candidate = value;
74
+ const window = projectQuotaWindow(candidate);
75
+ if (!window ||
76
+ typeof candidate.id !== 'string' ||
77
+ !candidate.id.trim() ||
78
+ typeof candidate.title !== 'string' ||
79
+ !candidate.title.trim() ||
80
+ typeof candidate.modelName !== 'string' ||
81
+ !candidate.modelName.trim()) {
82
+ return undefined;
83
+ }
84
+ return {
85
+ ...window,
86
+ id: candidate.id,
87
+ title: candidate.title,
88
+ ...(typeof candidate.modelId === 'string' &&
89
+ candidate.modelId.trim() && {
90
+ modelId: candidate.modelId,
91
+ }),
92
+ modelName: candidate.modelName,
93
+ };
94
+ }
95
+ function projectQuotaMoney(value) {
96
+ if (!value || typeof value !== 'object')
97
+ return undefined;
98
+ const candidate = value;
99
+ if (!finiteNumber(candidate.amountMinor) ||
100
+ typeof candidate.currency !== 'string' ||
101
+ !candidate.currency.trim() ||
102
+ !finiteNumber(candidate.exponent)) {
103
+ return undefined;
104
+ }
105
+ return {
106
+ amountMinor: candidate.amountMinor,
107
+ currency: candidate.currency,
108
+ exponent: candidate.exponent,
109
+ };
110
+ }
111
+ function projectExtraUsage(value) {
112
+ if (!value || typeof value !== 'object')
113
+ return undefined;
114
+ const candidate = value;
115
+ const used = projectQuotaMoney(candidate.used);
116
+ const limit = projectQuotaMoney(candidate.limit);
117
+ if (!used || !limit || typeof candidate.exhausted !== 'boolean') {
118
+ return undefined;
119
+ }
120
+ return {
121
+ used,
122
+ limit,
123
+ ...(finiteNumber(candidate.utilizationPercent) && {
124
+ utilizationPercent: candidate.utilizationPercent,
125
+ }),
126
+ ...(typeof candidate.severity === 'string' && {
127
+ severity: candidate.severity,
128
+ }),
129
+ exhausted: candidate.exhausted,
130
+ };
131
+ }
132
+ function projectQuotaProvenance(value, presentFields) {
133
+ if (!value || typeof value !== 'object')
134
+ return undefined;
135
+ const candidate = value;
136
+ const provenance = {};
137
+ for (const field of QUOTA_FIELD_NAMES) {
138
+ if (!presentFields.has(field))
139
+ continue;
140
+ const source = candidate[field];
141
+ if (source === 'poll' || source === 'headers')
142
+ provenance[field] = source;
143
+ }
144
+ return Object.keys(provenance).length > 0 ? provenance : undefined;
145
+ }
146
+ function projectQuota(quota) {
147
+ const fiveHour = projectQuotaWindow(quota.five_hour);
148
+ const sevenDay = projectQuotaWindow(quota.seven_day);
149
+ const scoped = Array.isArray(quota.scoped)
150
+ ? quota.scoped
151
+ .map(projectScopedQuotaWindow)
152
+ .filter((entry) => entry != null)
153
+ : undefined;
154
+ const extraUsage = projectExtraUsage(quota.extraUsage);
155
+ const presentFields = new Set();
156
+ if (fiveHour)
157
+ presentFields.add('five_hour');
158
+ if (sevenDay)
159
+ presentFields.add('seven_day');
160
+ if (typeof quota.bindingWindow === 'string') {
161
+ presentFields.add('bindingWindow');
162
+ }
163
+ if (typeof quota.fallbackAdvised === 'boolean') {
164
+ presentFields.add('fallbackAdvised');
165
+ }
166
+ if (scoped)
167
+ presentFields.add('scoped');
168
+ if (extraUsage)
169
+ presentFields.add('extraUsage');
170
+ const provenance = projectQuotaProvenance(quota.fieldSources, presentFields);
171
+ return {
172
+ ...(fiveHour && { five_hour: fiveHour }),
173
+ ...(sevenDay && { seven_day: sevenDay }),
174
+ ...(typeof quota.bindingWindow === 'string' && {
175
+ bindingWindow: quota.bindingWindow,
176
+ }),
177
+ ...(typeof quota.fallbackAdvised === 'boolean' && {
178
+ fallbackAdvised: quota.fallbackAdvised,
179
+ }),
180
+ ...(scoped && { scoped }),
181
+ ...(extraUsage && { extraUsage }),
182
+ ...(provenance && { provenance }),
183
+ };
184
+ }
185
+ export class QuotaHeaderFeedRegistry {
186
+ options;
187
+ filePath;
188
+ writeChain = Promise.resolve();
189
+ constructor(options = {}) {
190
+ this.options = options;
191
+ const instanceId = options.instanceId ?? `${process.pid}-${randomUUID()}`;
192
+ this.filePath = join(options.directory ?? getDefaultQuotaHeaderFeedDirectory(), `${instanceId}.json`);
193
+ }
194
+ publish(entry) {
195
+ const { accountKey, quota, ...entryWithoutQuota } = entry;
196
+ const cleanEntry = {
197
+ ...entryWithoutQuota,
198
+ quota: projectQuota(quota),
199
+ };
200
+ try {
201
+ validatePublishEntry(cleanEntry);
202
+ }
203
+ catch (error) {
204
+ return Promise.reject(error);
205
+ }
206
+ if (!accountKey)
207
+ return Promise.reject(new Error('Invalid quota header feed account key'));
208
+ this.writeChain = this.writeChain
209
+ .catch(() => { })
210
+ .then(async () => {
211
+ const directory = this.options.directory ?? getDefaultQuotaHeaderFeedDirectory();
212
+ await mkdir(directory, { recursive: true, mode: 0o700 });
213
+ await chmod(directory, 0o700);
214
+ let entries = {};
215
+ try {
216
+ const record = JSON.parse(await readFile(this.filePath, 'utf8'));
217
+ if (record.version === QUOTA_HEADER_FEED_SCHEMA_VERSION &&
218
+ record.entries &&
219
+ typeof record.entries === 'object')
220
+ entries = record.entries;
221
+ }
222
+ catch { }
223
+ entries[accountKey] = cleanEntry;
224
+ const tempPath = `${this.filePath}.${randomUUID()}.tmp`;
225
+ try {
226
+ await writeFile(tempPath, `${JSON.stringify({ version: QUOTA_HEADER_FEED_SCHEMA_VERSION, entries })}\n`, { mode: 0o600 });
227
+ await chmod(tempPath, 0o600);
228
+ await rename(tempPath, this.filePath);
229
+ }
230
+ finally {
231
+ await rm(tempPath, { force: true });
232
+ }
233
+ });
234
+ return this.writeChain;
235
+ }
236
+ async list() {
237
+ await this.writeChain.catch(() => { });
238
+ const directory = this.options.directory ?? getDefaultQuotaHeaderFeedDirectory();
239
+ const now = this.options.now?.() ?? Date.now();
240
+ const leaseMs = this.options.leaseMs ?? QUOTA_HEADER_FEED_LEASE_MS;
241
+ let names;
242
+ try {
243
+ names = (await readdir(directory)).filter((name) => name.endsWith('.json'));
244
+ }
245
+ catch {
246
+ return [];
247
+ }
248
+ const newest = new Map();
249
+ await Promise.all(names.map(async (name) => {
250
+ try {
251
+ const record = JSON.parse(await readFile(join(directory, name), 'utf8'));
252
+ if (record.version !== QUOTA_HEADER_FEED_SCHEMA_VERSION ||
253
+ !record.entries ||
254
+ typeof record.entries !== 'object')
255
+ return;
256
+ for (const [accountKey, candidate] of Object.entries(record.entries)) {
257
+ if (!candidate ||
258
+ typeof candidate !== 'object' ||
259
+ candidate.schema_version !== QUOTA_HEADER_FEED_SCHEMA_VERSION ||
260
+ candidate.provider !== 'anthropic' ||
261
+ !validIdentity(candidate))
262
+ continue;
263
+ const observed = candidate.observed_at_ms;
264
+ if (observed > now || now - observed >= leaseMs)
265
+ continue;
266
+ const existing = newest.get(accountKey);
267
+ if (!existing || observed > existing.observed_at_ms)
268
+ newest.set(accountKey, candidate);
269
+ }
270
+ }
271
+ catch { }
272
+ }));
273
+ return [...newest.values()].sort((a, b) => a.observed_at_ms - b.observed_at_ms);
274
+ }
275
+ async dispose() {
276
+ await this.writeChain.catch(() => { });
277
+ await rm(this.filePath, { force: true });
278
+ }
279
+ }
@@ -1,3 +1,4 @@
1
+ import { QUOTA_FIELD_NAMES, quotaFieldSource } from './accounts.js';
1
2
  const PREFIX = 'anthropic-ratelimit-unified-';
2
3
  const WINDOW_KEYS = {
3
4
  '5h': 'five_hour',
@@ -31,36 +32,90 @@ function normalizeWindow(headers, suffix, checkedAt) {
31
32
  };
32
33
  }
33
34
  export function normalizeQuotaHeaders(headers, now = Date.now()) {
35
+ const fieldSources = {};
36
+ const fallbackHeader = headers.get(`${PREFIX}fallback`);
34
37
  const snapshot = {
35
- fallbackAdvised: headers.get(`${PREFIX}fallback`) === 'available',
38
+ fallbackAdvised: fallbackHeader === 'available',
36
39
  source: 'headers',
37
40
  checkedAt: now,
38
41
  };
39
42
  for (const [suffix, key] of Object.entries(WINDOW_KEYS)) {
40
43
  const window = normalizeWindow(headers, suffix, now);
41
- if (window)
44
+ if (window) {
42
45
  snapshot[key] = window;
46
+ fieldSources[key] = 'headers';
47
+ }
43
48
  }
44
49
  const representativeClaim = headers.get(`${PREFIX}representative-claim`);
45
50
  if (representativeClaim) {
46
51
  snapshot.bindingWindow = representativeClaim;
47
52
  snapshot.bindingWindowSource = 'headers';
53
+ fieldSources.bindingWindow = 'headers';
48
54
  }
49
- return snapshot;
55
+ if (fallbackHeader !== null)
56
+ fieldSources.fallbackAdvised = 'headers';
57
+ return {
58
+ ...snapshot,
59
+ fieldSources,
60
+ };
50
61
  }
51
62
  export function mergeHeaderQuotaSnapshot(existing, incoming) {
63
+ const { scoped: existingScoped, extraUsage: existingExtraUsage, fieldSources: existingFieldSources, ...existingWithoutPollOwnedFields } = existing ?? {};
64
+ const { scoped: _incomingScoped, extraUsage: _incomingExtraUsage, fieldSources: incomingFieldSources, ...incomingWithoutPollOwnedFields } = incoming;
65
+ const fieldSources = {};
66
+ for (const field of QUOTA_FIELD_NAMES) {
67
+ const value = field === 'scoped'
68
+ ? existingScoped
69
+ : field === 'extraUsage'
70
+ ? existingExtraUsage
71
+ : field === 'fallbackAdvised'
72
+ ? incomingFieldSources?.fallbackAdvised !== undefined
73
+ ? incoming.fallbackAdvised
74
+ : (existing?.fallbackAdvised ?? incoming.fallbackAdvised)
75
+ : field === 'bindingWindow'
76
+ ? existing?.bindingWindowSource === 'poll'
77
+ ? existing?.bindingWindow
78
+ : (incoming.bindingWindow ?? existing?.bindingWindow)
79
+ : (incoming[field] ?? existing?.[field]);
80
+ if (value === undefined)
81
+ continue;
82
+ const source = field === 'fallbackAdvised'
83
+ ? (incomingFieldSources?.fallbackAdvised ??
84
+ (existing?.fallbackAdvised !== undefined
85
+ ? existingFieldSources
86
+ ? existingFieldSources.fallbackAdvised
87
+ : quotaFieldSource(existing, field)
88
+ : undefined))
89
+ : (field === 'scoped' || field === 'extraUsage') && value !== undefined
90
+ ? 'poll'
91
+ : field === 'bindingWindow' &&
92
+ existing?.bindingWindowSource === 'poll' &&
93
+ existing.bindingWindow !== undefined
94
+ ? 'poll'
95
+ : incoming[field] !== undefined
96
+ ? (incomingFieldSources?.[field] ??
97
+ quotaFieldSource(incoming, field))
98
+ : (existingFieldSources?.[field] ??
99
+ quotaFieldSource(existing, field));
100
+ if (source)
101
+ fieldSources[field] = source;
102
+ }
52
103
  return {
53
- ...existing,
54
- ...incoming,
55
- scoped: existing && 'scoped' in existing ? existing.scoped : undefined,
56
- extraUsage: existing?.extraUsage,
104
+ ...existingWithoutPollOwnedFields,
105
+ ...incomingWithoutPollOwnedFields,
106
+ ...(Array.isArray(existingScoped) && { scoped: existingScoped }),
107
+ ...(existingExtraUsage != null && { extraUsage: existingExtraUsage }),
108
+ fallbackAdvised: incomingFieldSources?.fallbackAdvised !== undefined
109
+ ? incoming.fallbackAdvised
110
+ : (existing?.fallbackAdvised ?? incoming.fallbackAdvised),
57
111
  bindingWindow: existing?.bindingWindowSource === 'poll'
58
112
  ? existing.bindingWindow
59
113
  : (incoming.bindingWindow ?? existing?.bindingWindow),
60
114
  bindingWindowSource: existing?.bindingWindowSource === 'poll'
61
115
  ? 'poll'
62
116
  : (incoming.bindingWindowSource ?? existing?.bindingWindowSource),
63
- // Source tracks 5h/7d freshness; preserved scoped and credit fields remain poll-owned.
117
+ ...(Object.keys(fieldSources).length > 0 && { fieldSources }),
118
+ // Top-level source remains headers for compatibility; fieldSources records ownership after partial harvests.
64
119
  source: 'headers',
65
120
  };
66
121
  }
@@ -21,20 +21,27 @@ export type QuotaManagerOptions = {
21
21
  storage: AccountStorage | null;
22
22
  fetchImpl?: typeof fetch;
23
23
  now?: () => number;
24
- onMainQuotaFetched?: (quota: OAuthQuotaSnapshot, checkedAt: number, tokenFingerprint: string, fetchStartedAt: number) => void;
25
- onApiError?: (error: AccountOperationError) => void;
24
+ onMainQuotaFetched?: (quota: OAuthQuotaSnapshot, checkedAt: number, tokenFingerprint: string, fetchStartedAt: number, quotaErrorGeneration: number, quotaErrorClearedAt: number | undefined) => void;
25
+ onApiError?: (error: AccountOperationError, quotaErrorGeneration: number) => void;
26
+ onFallbackQuotaFetched?: (accountId: string, quota: OAuthQuotaSnapshot, checkedAt: number, fetchStartedAt: number) => void;
27
+ onFallbackApiError?: (accountId: string, error: AccountOperationError) => void;
26
28
  };
27
29
  export declare class QuotaManager {
28
30
  private main;
29
- private mainTokenFp;
31
+ private mainAccountId;
32
+ private mainQuotaIdentityKnown;
33
+ private mainQuotaIdentityStrict;
34
+ private mainGeneration;
35
+ private mainQuotaErrorGeneration;
36
+ private mainQuotaErrorClearedAt;
30
37
  private fallbacks;
31
- private fallbackTokenFps;
38
+ private fallbackAuthLineages;
39
+ private fallbackGenerations;
32
40
  private inflightMain;
33
- private inflightMainFp;
41
+ private inflightMainAccountId;
34
42
  private inflightFallbacks;
35
43
  private mainLastApiError;
36
44
  private fallbackApiErrors;
37
- private fallbackErrorTokenFps;
38
45
  private apiGate;
39
46
  private lastApiCallAt;
40
47
  private storage;
@@ -42,38 +49,38 @@ export declare class QuotaManager {
42
49
  private readonly now;
43
50
  private readonly onMainQuotaFetched;
44
51
  private readonly onApiError;
52
+ private readonly onFallbackQuotaFetched;
53
+ private readonly onFallbackApiError;
45
54
  constructor(opts: QuotaManagerOptions);
46
55
  /**
47
- * Cached main quota entry. Pass the live access token to enforce token
48
- * binding: if the cached entry was produced by a different token (main
49
- * account switched), it is dropped and null is returned so the caller
50
- * refetches for the current account. Called without a token (e.g. for
51
- * display) it returns whatever is cached.
56
+ * Cached main quota entry, scoped to the account behind the main credential.
57
+ * An unresolved identity keeps a retained cache unverified rather than exposing it.
52
58
  */
53
- getMain(accessToken?: string): QuotaEntry | null;
59
+ getMain(mainAccountId?: string): QuotaEntry | null;
54
60
  /**
55
- * Cached fallback quota entry. Pass the live access token to enforce token
56
- * binding: if the entry was produced by a different token (account re-login),
57
- * it is dropped and null is returned so the caller refetches.
61
+ * Cached fallback quota entry, scoped to the configured account id.
58
62
  */
59
- getFallback(accountId: string, accessToken?: string): QuotaEntry | null;
63
+ getFallback(accountId: string, account?: Pick<OAuthAccount, 'authLineageId'>): QuotaEntry | null;
60
64
  getAllFallbacks(): Map<string, QuotaEntry>;
61
- setMain(accessToken: string, entry: QuotaEntry): void;
62
- setFallback(accountId: string, entry: QuotaEntry, accessToken?: string): void;
63
- pushMainFromHeaders(accessToken: string, incoming: OAuthQuotaSnapshot): QuotaEntry;
64
- pushFallbackFromHeaders(accountId: string, accessToken: string, incoming: OAuthQuotaSnapshot): QuotaEntry;
65
- refreshMain(accessToken: string): Promise<OAuthQuotaSnapshot>;
66
- refreshMainWithMetadata(accessToken: string): Promise<QuotaRefreshResult>;
67
- refreshFallback(accountId: string, accessToken: string): Promise<OAuthQuotaSnapshot>;
68
- refreshFallbackWithMetadata(accountId: string, accessToken: string): Promise<QuotaRefreshResult>;
69
- refreshAllFallbacks(accounts: OAuthAccount[]): Promise<void>;
65
+ setMain(mainAccountId: string | undefined, entry: QuotaEntry): void;
66
+ /** Bind quota state to the account behind the current main credential. */
67
+ setMainQuotaAccountIdentity(accountIdentity: string | undefined, strict?: boolean): number | undefined;
68
+ setFallback(accountId: string, entry: QuotaEntry, account: Pick<OAuthAccount, 'authLineageId'> | undefined): void;
69
+ clearFallback(accountId: string): void;
70
+ pushMainFromHeaders(mainAccountId: string | undefined, incoming: OAuthQuotaSnapshot): QuotaEntry;
71
+ pushFallbackFromHeaders(accountId: string, incoming: OAuthQuotaSnapshot, account: Pick<OAuthAccount, 'authLineageId'> | undefined): QuotaEntry | null;
72
+ refreshMain(mainAccountIdOrAccessToken: string | undefined, accessToken?: string, expectedIdentityGeneration?: number): Promise<OAuthQuotaSnapshot>;
73
+ refreshMainWithMetadata(mainAccountIdOrAccessToken: string | undefined, accessToken?: string, expectedIdentityGeneration?: number): Promise<QuotaRefreshResult>;
74
+ refreshFallback(accountId: string, accessToken: string, account: Pick<OAuthAccount, 'authLineageId'> | undefined): Promise<OAuthQuotaSnapshot>;
75
+ refreshFallbackWithMetadata(accountId: string, accessToken: string, account: Pick<OAuthAccount, 'authLineageId'> | undefined): Promise<QuotaRefreshResult>;
76
+ refreshAllFallbacks(accounts: OAuthAccount[], resolveAccessToken?: (account: OAuthAccount) => string | undefined): Promise<void>;
70
77
  /**
71
78
  * Fire-and-forget refresh. Does not await, swallows errors.
72
79
  */
73
80
  refreshMainInBackground(accessToken: string): void;
74
81
  private scopedWindowIsStale;
75
82
  isMainStale(modelId?: string): boolean;
76
- isFallbackStale(accountId: string, accessToken?: string, modelId?: string): boolean;
83
+ isFallbackStale(accountId: string, _accessToken?: string, modelId?: string, account?: Pick<OAuthAccount, 'authLineageId'>): boolean;
77
84
  shouldRefreshOnRequestCount(requestCount: number): boolean;
78
85
  /**
79
86
  * Combined check: should a refresh happen right now?
@@ -87,7 +94,7 @@ export declare class QuotaManager {
87
94
  * can stop this process from showing "checking…" or making a redundant quota
88
95
  * API call.
89
96
  */
90
- seedMainFromStorage(storage: AccountStorage | null, accessToken?: string): void;
97
+ seedMainFromStorage(storage: AccountStorage | null, mainAccountId?: string): void;
91
98
  private seedMainBackoffFromStorage;
92
99
  /**
93
100
  * Seed fallback cache entries from persisted account.quota data.
@@ -103,12 +110,18 @@ export declare class QuotaManager {
103
110
  /**
104
111
  * Whether a specific fallback account's quota API is in backoff.
105
112
  */
106
- isFallbackBackedOff(accountId: string, accessToken?: string): boolean;
113
+ isFallbackBackedOff(accountId: string): boolean;
107
114
  getLastApiError(): AccountOperationError | undefined;
115
+ getMainQuotaErrorGeneration(): number;
116
+ getMainQuotaIdentityGeneration(): number;
117
+ getMainQuotaErrorClearedAt(): number | undefined;
118
+ clearMainBackoff(): number | undefined;
108
119
  /** Minimum gap between consecutive quota API calls (ms). */
109
120
  private static readonly API_CALL_GAP_MS;
110
121
  private static fallbackInflightKey;
111
122
  private static quotaLockName;
123
+ private bindFallbackLineage;
124
+ private acceptFallbackQuotaObservation;
112
125
  /**
113
126
  * Serialize API calls through a shared gate so only one
114
127
  * quota API request runs at a time, with a minimum gap