@cortexkit/anthropic-auth-core 1.9.2 → 1.9.3
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/dist/accounts.js +79 -8
- package/dist/dump.d.ts +12 -0
- package/dist/dump.js +98 -15
- package/dist/models.d.ts +1 -0
- package/dist/models.js +19 -0
- package/dist/quota-manager.d.ts +11 -3
- package/dist/quota-manager.js +53 -22
- package/package.json +1 -1
package/dist/accounts.js
CHANGED
|
@@ -289,6 +289,46 @@ function accountRuntimeState(account) {
|
|
|
289
289
|
quota: account.quota,
|
|
290
290
|
});
|
|
291
291
|
}
|
|
292
|
+
function quotaSnapshotCheckedAt(quota) {
|
|
293
|
+
return Math.max(quota?.five_hour?.checkedAt ?? 0, quota?.seven_day?.checkedAt ?? 0);
|
|
294
|
+
}
|
|
295
|
+
function mergeAccountRuntimeState(existing, incoming) {
|
|
296
|
+
if (!isRecord(existing))
|
|
297
|
+
return incoming;
|
|
298
|
+
const existingEntry = existing;
|
|
299
|
+
const existingQuotaCheckedAt = quotaSnapshotCheckedAt(existingEntry.quota);
|
|
300
|
+
const incomingQuotaCheckedAt = quotaSnapshotCheckedAt(incoming.quota);
|
|
301
|
+
if (existingQuotaCheckedAt > incomingQuotaCheckedAt) {
|
|
302
|
+
const tokenChanged = Boolean(existingEntry.access &&
|
|
303
|
+
incoming.access &&
|
|
304
|
+
existingEntry.access !== incoming.access);
|
|
305
|
+
const existingRefreshAt = existingEntry.lastRefreshedAt ?? 0;
|
|
306
|
+
const incomingRefreshAt = incoming.lastRefreshedAt ?? 0;
|
|
307
|
+
if (tokenChanged && incomingRefreshAt <= existingRefreshAt) {
|
|
308
|
+
const merged = { ...existingEntry };
|
|
309
|
+
if (typeof incoming.lastUsed === 'number' &&
|
|
310
|
+
(!(typeof existingEntry.lastUsed === 'number') ||
|
|
311
|
+
incoming.lastUsed > existingEntry.lastUsed)) {
|
|
312
|
+
merged.lastUsed = incoming.lastUsed;
|
|
313
|
+
}
|
|
314
|
+
return merged;
|
|
315
|
+
}
|
|
316
|
+
return {
|
|
317
|
+
...existingEntry,
|
|
318
|
+
...incoming,
|
|
319
|
+
quota: existingEntry.quota,
|
|
320
|
+
lastQuotaRefreshError: existingEntry.lastQuotaRefreshError,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
const merged = { ...existingEntry, ...incoming };
|
|
324
|
+
if (!('lastQuotaRefreshError' in incoming)) {
|
|
325
|
+
delete merged.lastQuotaRefreshError;
|
|
326
|
+
}
|
|
327
|
+
if (!('lastRefreshError' in incoming)) {
|
|
328
|
+
delete merged.lastRefreshError;
|
|
329
|
+
}
|
|
330
|
+
return merged;
|
|
331
|
+
}
|
|
292
332
|
function configFromStorage(storage) {
|
|
293
333
|
const refresh = storage.refresh
|
|
294
334
|
? objectWithDefinedEntries({
|
|
@@ -361,6 +401,14 @@ export async function saveAccounts(storage, path = getAccountStoragePath()) {
|
|
|
361
401
|
}
|
|
362
402
|
function applyMainQuotaStatePatch(state, storage) {
|
|
363
403
|
state.main = state.main ?? {};
|
|
404
|
+
const existingCheckedAt = typeof state.main.quotaCheckedAt === 'number'
|
|
405
|
+
? state.main.quotaCheckedAt
|
|
406
|
+
: quotaSnapshotCheckedAt(state.main.quota);
|
|
407
|
+
const incomingCheckedAt = typeof storage.quota?.mainQuotaCheckedAt === 'number'
|
|
408
|
+
? storage.quota.mainQuotaCheckedAt
|
|
409
|
+
: quotaSnapshotCheckedAt(storage.quota?.mainQuota);
|
|
410
|
+
if (existingCheckedAt > incomingCheckedAt)
|
|
411
|
+
return;
|
|
364
412
|
state.main.quota = storage.quota?.mainQuota;
|
|
365
413
|
state.main.quotaCheckedAt = storage.quota?.mainQuotaCheckedAt;
|
|
366
414
|
state.main.quotaToken = storage.quota?.mainQuotaToken;
|
|
@@ -402,7 +450,7 @@ export async function saveAccountState(storage, path = getAccountStoragePath(),
|
|
|
402
450
|
for (const account of storage.accounts) {
|
|
403
451
|
if (ids && !ids.has(account.id))
|
|
404
452
|
continue;
|
|
405
|
-
next.accounts[account.id] = accountRuntimeState(account);
|
|
453
|
+
next.accounts[account.id] = mergeAccountRuntimeState(next.accounts[account.id], accountRuntimeState(account));
|
|
406
454
|
}
|
|
407
455
|
if (ids) {
|
|
408
456
|
for (const id of ids) {
|
|
@@ -876,15 +924,18 @@ function tokenNeedsRefresh(account, storage, now) {
|
|
|
876
924
|
!account.expires ||
|
|
877
925
|
account.expires - now <= refreshBeforeExpiryMs(storage));
|
|
878
926
|
}
|
|
879
|
-
function
|
|
927
|
+
function quotaSnapshotIsFresh(quota, storage, now) {
|
|
880
928
|
if (!quotaEnabled(storage))
|
|
881
|
-
return
|
|
929
|
+
return true;
|
|
882
930
|
const maxAge = getQuotaCheckIntervalMs(storage);
|
|
883
|
-
return ['five_hour', 'seven_day'].
|
|
884
|
-
const window =
|
|
885
|
-
return
|
|
931
|
+
return ['five_hour', 'seven_day'].every((key) => {
|
|
932
|
+
const window = quota?.[key];
|
|
933
|
+
return Boolean(window && now - window.checkedAt < maxAge);
|
|
886
934
|
});
|
|
887
935
|
}
|
|
936
|
+
function quotaIsStale(account, storage, now) {
|
|
937
|
+
return !quotaSnapshotIsFresh(account.quota, storage, now);
|
|
938
|
+
}
|
|
888
939
|
function cachedQuotaWindowStillRelevant(window, now) {
|
|
889
940
|
if (!window)
|
|
890
941
|
return false;
|
|
@@ -1012,13 +1063,14 @@ export class FallbackAccountManager {
|
|
|
1012
1063
|
seedFallbackQuota(account, storage) {
|
|
1013
1064
|
if (!this.quotaManager)
|
|
1014
1065
|
return;
|
|
1015
|
-
if (this.quotaManager.getFallback(account.id, account.access))
|
|
1016
|
-
return;
|
|
1017
1066
|
if (!account.quota)
|
|
1018
1067
|
return;
|
|
1019
1068
|
const checkedAt = Math.max(account.quota.five_hour?.checkedAt ?? 0, account.quota.seven_day?.checkedAt ?? 0);
|
|
1020
1069
|
if (checkedAt <= 0)
|
|
1021
1070
|
return;
|
|
1071
|
+
const existing = this.quotaManager.getFallback(account.id, account.access);
|
|
1072
|
+
if (existing && existing.checkedAt >= checkedAt)
|
|
1073
|
+
return;
|
|
1022
1074
|
const checkInterval = getQuotaCheckIntervalMs(storage);
|
|
1023
1075
|
this.quotaManager.setFallback(account.id, {
|
|
1024
1076
|
quota: account.quota,
|
|
@@ -1402,6 +1454,7 @@ export class FallbackAccountManager {
|
|
|
1402
1454
|
fetchImpl: this.fetchImpl,
|
|
1403
1455
|
now: this.now,
|
|
1404
1456
|
});
|
|
1457
|
+
const fetchStartedAt = this.now();
|
|
1405
1458
|
try {
|
|
1406
1459
|
target.quota = await fetchSnapshot(target.access);
|
|
1407
1460
|
}
|
|
@@ -1417,6 +1470,24 @@ export class FallbackAccountManager {
|
|
|
1417
1470
|
// 401 does not arm QuotaManager backoff, so this retry proceeds.
|
|
1418
1471
|
target.quota = await fetchSnapshot(target.access);
|
|
1419
1472
|
}
|
|
1473
|
+
const latestStorage = await this.load();
|
|
1474
|
+
const latestAccount = latestStorage?.accounts.find((candidate) => candidate.id === target.id && isOAuthAccount(candidate));
|
|
1475
|
+
if (latestStorage &&
|
|
1476
|
+
latestAccount &&
|
|
1477
|
+
latestAccount.access !== target.access) {
|
|
1478
|
+
this.seedFallbackQuota(latestAccount, latestStorage);
|
|
1479
|
+
updateStoredAccount(storage, latestAccount);
|
|
1480
|
+
return latestAccount;
|
|
1481
|
+
}
|
|
1482
|
+
if (latestStorage &&
|
|
1483
|
+
latestAccount?.access === target.access &&
|
|
1484
|
+
latestAccount.quota &&
|
|
1485
|
+
quotaSnapshotCheckedAt(latestAccount.quota) >= fetchStartedAt &&
|
|
1486
|
+
quotaSnapshotIsFresh(latestAccount.quota, latestStorage, this.now())) {
|
|
1487
|
+
this.seedFallbackQuota(latestAccount, latestStorage);
|
|
1488
|
+
updateStoredAccount(storage, latestAccount);
|
|
1489
|
+
return latestAccount;
|
|
1490
|
+
}
|
|
1420
1491
|
target.lastQuotaRefreshError = undefined;
|
|
1421
1492
|
updateStoredAccount(storage, target);
|
|
1422
1493
|
// Sync to shared QuotaManager so all consumers see the same cache. The
|
package/dist/dump.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
type DumpHeaders = ConstructorParameters<typeof Headers>[0];
|
|
1
2
|
export declare const CLAUDE_DUMP_COMMAND_NAME = "claude-dump";
|
|
2
3
|
export type DumpCommandAction = {
|
|
3
4
|
type: 'status';
|
|
@@ -20,6 +21,16 @@ export declare function executeDumpCommand(input: {
|
|
|
20
21
|
argumentsText: string;
|
|
21
22
|
enabled?: boolean;
|
|
22
23
|
}): string;
|
|
24
|
+
export declare function dumpDirectRequest(input: {
|
|
25
|
+
affinity?: string | null;
|
|
26
|
+
route?: string;
|
|
27
|
+
status?: number;
|
|
28
|
+
error?: string;
|
|
29
|
+
bodyText: string;
|
|
30
|
+
url?: string;
|
|
31
|
+
method?: string;
|
|
32
|
+
headers?: DumpHeaders;
|
|
33
|
+
}): Promise<void>;
|
|
23
34
|
export declare function dumpRelayRequest(input: {
|
|
24
35
|
affinity: string;
|
|
25
36
|
transport: 'http' | 'websocket';
|
|
@@ -31,3 +42,4 @@ export declare function dumpRelayRequest(input: {
|
|
|
31
42
|
payload: unknown;
|
|
32
43
|
relayBytes: number;
|
|
33
44
|
}): Promise<void>;
|
|
45
|
+
export {};
|
package/dist/dump.js
CHANGED
|
@@ -14,6 +14,8 @@ const DUMP_DIR_ENV = 'OPENCODE_ANTHROPIC_AUTH_DUMP_DIR';
|
|
|
14
14
|
const DEFAULT_DUMP_DIR = join(tmpdir(), 'opencode-anthropic-auth-dumps');
|
|
15
15
|
let dumpEnabled = false;
|
|
16
16
|
let nextDumpId = 0;
|
|
17
|
+
const DIRECT_DUMP_PREVIOUS_BODY_LIMIT = 100;
|
|
18
|
+
const directDumpPreviousBodies = new Map();
|
|
17
19
|
export function isDumpEnabled() {
|
|
18
20
|
return dumpEnabled;
|
|
19
21
|
}
|
|
@@ -22,6 +24,8 @@ export function setDumpEnabled(enabled) {
|
|
|
22
24
|
}
|
|
23
25
|
export function resetDumpState() {
|
|
24
26
|
dumpEnabled = false;
|
|
27
|
+
nextDumpId = 0;
|
|
28
|
+
directDumpPreviousBodies.clear();
|
|
25
29
|
}
|
|
26
30
|
export function getDumpDirectory() {
|
|
27
31
|
return process.env[DUMP_DIR_ENV] || DEFAULT_DUMP_DIR;
|
|
@@ -44,7 +48,7 @@ export function buildDumpStatusSummary(input) {
|
|
|
44
48
|
`- Enabled: ${enabled ? 'enabled' : 'disabled'}`,
|
|
45
49
|
`- Directory: ${getDumpDirectory()}`,
|
|
46
50
|
'- Persisted: ~/.config/opencode/anthropic-auth.json',
|
|
47
|
-
'- Captures: final rewritten Anthropic body
|
|
51
|
+
'- Captures: final rewritten Anthropic body in direct and relay modes; request/relay metadata is redacted',
|
|
48
52
|
'- Warning: body dumps may contain prompt/session content; turn this off after debugging',
|
|
49
53
|
].join('\n');
|
|
50
54
|
}
|
|
@@ -87,6 +91,35 @@ function dumpFileSessionSegment(affinity) {
|
|
|
87
91
|
return 'session-unknown';
|
|
88
92
|
return normalized.length <= 80 ? normalized : normalized.slice(0, 80);
|
|
89
93
|
}
|
|
94
|
+
function dumpRequestSegment(input) {
|
|
95
|
+
if (input.transport !== 'direct') {
|
|
96
|
+
return `${input.transport}-p${input.protocol}-${input.mode}`;
|
|
97
|
+
}
|
|
98
|
+
const route = input.route ? `-${dumpFileSessionSegment(input.route)}` : '';
|
|
99
|
+
return `direct${route}`;
|
|
100
|
+
}
|
|
101
|
+
function directDumpPreviousKey(input) {
|
|
102
|
+
const affinity = input.affinity?.trim();
|
|
103
|
+
if (affinity)
|
|
104
|
+
return `session:${affinity}`;
|
|
105
|
+
return `request:${input.route ?? 'direct'}:${input.url ?? ''}`;
|
|
106
|
+
}
|
|
107
|
+
function rememberDirectDumpBody(key, bodyText) {
|
|
108
|
+
if (!directDumpPreviousBodies.has(key)) {
|
|
109
|
+
while (directDumpPreviousBodies.size >= DIRECT_DUMP_PREVIOUS_BODY_LIMIT) {
|
|
110
|
+
const oldest = directDumpPreviousBodies.keys().next().value;
|
|
111
|
+
if (oldest === undefined)
|
|
112
|
+
break;
|
|
113
|
+
directDumpPreviousBodies.delete(oldest);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
directDumpPreviousBodies.set(key, bodyText);
|
|
117
|
+
}
|
|
118
|
+
function headersToRecord(headers) {
|
|
119
|
+
if (headers == null)
|
|
120
|
+
return undefined;
|
|
121
|
+
return Object.fromEntries(new Headers(headers).entries());
|
|
122
|
+
}
|
|
90
123
|
function hashText(value) {
|
|
91
124
|
return createHash('sha256').update(value).digest('hex');
|
|
92
125
|
}
|
|
@@ -189,42 +222,92 @@ function redactForDump(value) {
|
|
|
189
222
|
}
|
|
190
223
|
return redacted;
|
|
191
224
|
}
|
|
192
|
-
|
|
225
|
+
async function dumpRequest(input) {
|
|
193
226
|
if (!dumpEnabled)
|
|
194
227
|
return;
|
|
195
228
|
nextDumpId += 1;
|
|
196
|
-
const
|
|
229
|
+
const affinity = input.affinity?.trim() || 'session-unknown';
|
|
230
|
+
const id = `${new Date().toISOString().replace(/[:.]/g, '-')}-${String(nextDumpId).padStart(5, '0')}-${dumpFileSessionSegment(affinity)}-${dumpRequestSegment(input)}`;
|
|
197
231
|
const dumpDir = getDumpDirectory();
|
|
198
232
|
const prefix = join(dumpDir, id);
|
|
233
|
+
const files = {
|
|
234
|
+
body: `${prefix}.body.json`,
|
|
235
|
+
metadata: `${prefix}.meta.json`,
|
|
236
|
+
};
|
|
237
|
+
if (input.payload !== undefined)
|
|
238
|
+
files.relay = `${prefix}.relay.json`;
|
|
239
|
+
if (input.request !== undefined)
|
|
240
|
+
files.request = `${prefix}.request.json`;
|
|
199
241
|
try {
|
|
200
242
|
await mkdir(dumpDir, { recursive: true });
|
|
201
243
|
const metadata = {
|
|
202
244
|
id,
|
|
203
245
|
createdAt: new Date().toISOString(),
|
|
204
|
-
session: shortAffinity(
|
|
246
|
+
session: shortAffinity(affinity),
|
|
205
247
|
transport: input.transport,
|
|
206
248
|
protocol: input.protocol,
|
|
207
249
|
mode: input.mode,
|
|
250
|
+
route: input.route,
|
|
208
251
|
status: input.status,
|
|
252
|
+
error: input.error,
|
|
209
253
|
bodyBytes: input.bodyText.length,
|
|
210
254
|
relayBytes: input.relayBytes,
|
|
211
255
|
bodyHash: hashText(input.bodyText),
|
|
212
256
|
diff: diffSummary(input.previousBodyText, input.bodyText),
|
|
213
257
|
body: bodyStructureSummary(input.bodyText),
|
|
214
|
-
files
|
|
215
|
-
body: `${prefix}.body.json`,
|
|
216
|
-
relay: `${prefix}.relay.json`,
|
|
217
|
-
metadata: `${prefix}.meta.json`,
|
|
218
|
-
},
|
|
258
|
+
files,
|
|
219
259
|
};
|
|
220
|
-
|
|
221
|
-
writeFile(
|
|
222
|
-
writeFile(
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
260
|
+
const writes = [
|
|
261
|
+
writeFile(files.body, input.bodyText, 'utf8'),
|
|
262
|
+
writeFile(files.metadata, `${JSON.stringify(metadata, null, 2)}\n`, 'utf8'),
|
|
263
|
+
];
|
|
264
|
+
if (input.payload !== undefined && files.relay) {
|
|
265
|
+
writes.push(writeFile(files.relay, `${JSON.stringify(redactForDump(input.payload), null, 2)}\n`, 'utf8'));
|
|
266
|
+
}
|
|
267
|
+
if (input.request !== undefined && files.request) {
|
|
268
|
+
writes.push(writeFile(files.request, `${JSON.stringify(redactForDump({
|
|
269
|
+
...input.request,
|
|
270
|
+
headers: headersToRecord(input.request.headers),
|
|
271
|
+
}), null, 2)}\n`, 'utf8'));
|
|
272
|
+
}
|
|
273
|
+
await Promise.all(writes);
|
|
274
|
+
relayLog(`dumped request id=${id} session=${shortAffinity(affinity)} body=${files.body} meta=${files.metadata}`);
|
|
226
275
|
}
|
|
227
276
|
catch (error) {
|
|
228
277
|
relayLog(`dump failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
229
278
|
}
|
|
230
279
|
}
|
|
280
|
+
export async function dumpDirectRequest(input) {
|
|
281
|
+
if (!dumpEnabled)
|
|
282
|
+
return;
|
|
283
|
+
const previousKey = directDumpPreviousKey(input);
|
|
284
|
+
const previousBodyText = directDumpPreviousBodies.get(previousKey);
|
|
285
|
+
await dumpRequest({
|
|
286
|
+
affinity: input.affinity,
|
|
287
|
+
transport: 'direct',
|
|
288
|
+
route: input.route,
|
|
289
|
+
status: input.status,
|
|
290
|
+
error: input.error,
|
|
291
|
+
bodyText: input.bodyText,
|
|
292
|
+
previousBodyText,
|
|
293
|
+
request: {
|
|
294
|
+
url: input.url,
|
|
295
|
+
method: input.method,
|
|
296
|
+
headers: input.headers,
|
|
297
|
+
},
|
|
298
|
+
});
|
|
299
|
+
rememberDirectDumpBody(previousKey, input.bodyText);
|
|
300
|
+
}
|
|
301
|
+
export async function dumpRelayRequest(input) {
|
|
302
|
+
await dumpRequest({
|
|
303
|
+
affinity: input.affinity,
|
|
304
|
+
transport: input.transport,
|
|
305
|
+
protocol: input.protocol,
|
|
306
|
+
mode: input.mode,
|
|
307
|
+
status: input.status,
|
|
308
|
+
bodyText: input.bodyText,
|
|
309
|
+
previousBodyText: input.previousBodyText,
|
|
310
|
+
payload: input.payload,
|
|
311
|
+
relayBytes: input.relayBytes,
|
|
312
|
+
});
|
|
313
|
+
}
|
package/dist/models.d.ts
CHANGED
package/dist/models.js
CHANGED
|
@@ -33,3 +33,22 @@ export function isClaudeFableOrMythos5Model(model) {
|
|
|
33
33
|
return (typeof model === 'string' &&
|
|
34
34
|
CLAUDE_FABLE_MYTHOS_5_MODEL_IDS.some((id) => model === id || model.startsWith(`${id}-`)));
|
|
35
35
|
}
|
|
36
|
+
export function isOpenAIReasoningSignature(value) {
|
|
37
|
+
if (typeof value !== 'string')
|
|
38
|
+
return false;
|
|
39
|
+
if (value.startsWith('gAAAA'))
|
|
40
|
+
return true;
|
|
41
|
+
if (!value.startsWith('{'))
|
|
42
|
+
return false;
|
|
43
|
+
try {
|
|
44
|
+
const parsed = JSON.parse(value);
|
|
45
|
+
return (parsed.type === 'reasoning' &&
|
|
46
|
+
typeof parsed.id === 'string' &&
|
|
47
|
+
parsed.id.startsWith('rs_') &&
|
|
48
|
+
typeof parsed.encrypted_content === 'string' &&
|
|
49
|
+
parsed.encrypted_content.startsWith('gAAAA'));
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
package/dist/quota-manager.d.ts
CHANGED
|
@@ -21,7 +21,7 @@ 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) => void;
|
|
24
|
+
onMainQuotaFetched?: (quota: OAuthQuotaSnapshot, checkedAt: number, tokenFingerprint: string, fetchStartedAt: number) => void;
|
|
25
25
|
onApiError?: (error: AccountOperationError) => void;
|
|
26
26
|
};
|
|
27
27
|
export declare class QuotaManager {
|
|
@@ -75,10 +75,18 @@ export declare class QuotaManager {
|
|
|
75
75
|
*/
|
|
76
76
|
needsRefresh(requestCount: number): boolean;
|
|
77
77
|
updateStorage(storage: AccountStorage | null): void;
|
|
78
|
+
/**
|
|
79
|
+
* Seed/update the main quota cache from persisted state. This is deliberately
|
|
80
|
+
* callable after every disk load so another plugin process's fresh quota write
|
|
81
|
+
* can stop this process from showing "checking…" or making a redundant quota
|
|
82
|
+
* API call.
|
|
83
|
+
*/
|
|
84
|
+
seedMainFromStorage(storage: AccountStorage | null, accessToken?: string): void;
|
|
85
|
+
private seedMainBackoffFromStorage;
|
|
78
86
|
/**
|
|
79
87
|
* Seed fallback cache entries from persisted account.quota data.
|
|
80
|
-
*
|
|
81
|
-
*
|
|
88
|
+
* Updates older in-memory entries so a fresh quota write from another plugin
|
|
89
|
+
* process prevents redundant checks and stale sidebar writes.
|
|
82
90
|
*/
|
|
83
91
|
seedFallbacksFromAccounts(accounts: OAuthAccount[]): void;
|
|
84
92
|
/**
|
package/dist/quota-manager.js
CHANGED
|
@@ -56,20 +56,8 @@ export class QuotaManager {
|
|
|
56
56
|
// that produced it. refreshMain() drops this seed if the live token's
|
|
57
57
|
// fingerprint differs (main-account switch), preventing stale wrong-account
|
|
58
58
|
// quota from being served during backoff.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
this.main = {
|
|
62
|
-
quota: persisted.quota,
|
|
63
|
-
refreshAfter: getQuotaNextRefreshAt(persisted.quota, opts.storage, persisted.checkedAt),
|
|
64
|
-
checkedAt: persisted.checkedAt,
|
|
65
|
-
};
|
|
66
|
-
this.mainTokenFp = persisted.tokenFingerprint ?? null;
|
|
67
|
-
}
|
|
68
|
-
// Seed backoff state from persisted storage
|
|
69
|
-
const persistedError = opts.storage?.quota?.mainLastQuotaApiError;
|
|
70
|
-
if (persistedError && quotaBackoffActive(persistedError, this.now())) {
|
|
71
|
-
this.mainLastApiError = persistedError;
|
|
72
|
-
}
|
|
59
|
+
this.seedMainFromStorage(opts.storage);
|
|
60
|
+
this.seedMainBackoffFromStorage(opts.storage);
|
|
73
61
|
}
|
|
74
62
|
// =========================================================================
|
|
75
63
|
// Get (synchronous, from cache)
|
|
@@ -230,24 +218,65 @@ export class QuotaManager {
|
|
|
230
218
|
// =========================================================================
|
|
231
219
|
updateStorage(storage) {
|
|
232
220
|
this.storage = storage;
|
|
221
|
+
this.seedMainFromStorage(storage);
|
|
222
|
+
this.seedMainBackoffFromStorage(storage);
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Seed/update the main quota cache from persisted state. This is deliberately
|
|
226
|
+
* callable after every disk load so another plugin process's fresh quota write
|
|
227
|
+
* can stop this process from showing "checking…" or making a redundant quota
|
|
228
|
+
* API call.
|
|
229
|
+
*/
|
|
230
|
+
seedMainFromStorage(storage, accessToken) {
|
|
231
|
+
const persisted = getPersistedMainQuota(storage);
|
|
232
|
+
if (!persisted)
|
|
233
|
+
return;
|
|
234
|
+
const accessTokenFp = accessToken ? tokenFingerprint(accessToken) : null;
|
|
235
|
+
if (accessTokenFp &&
|
|
236
|
+
persisted.tokenFingerprint &&
|
|
237
|
+
persisted.tokenFingerprint !== accessTokenFp) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const entry = {
|
|
241
|
+
quota: persisted.quota,
|
|
242
|
+
refreshAfter: getQuotaNextRefreshAt(persisted.quota, storage, persisted.checkedAt),
|
|
243
|
+
checkedAt: persisted.checkedAt,
|
|
244
|
+
};
|
|
245
|
+
if (this.main &&
|
|
246
|
+
this.main.checkedAt >= entry.checkedAt &&
|
|
247
|
+
(!accessTokenFp ||
|
|
248
|
+
!this.mainTokenFp ||
|
|
249
|
+
this.mainTokenFp === accessTokenFp)) {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
this.main = entry;
|
|
253
|
+
this.mainTokenFp = persisted.tokenFingerprint ?? null;
|
|
254
|
+
}
|
|
255
|
+
seedMainBackoffFromStorage(storage) {
|
|
256
|
+
const persistedError = storage?.quota?.mainLastQuotaApiError;
|
|
257
|
+
this.mainLastApiError =
|
|
258
|
+
persistedError && quotaBackoffActive(persistedError, this.now())
|
|
259
|
+
? persistedError
|
|
260
|
+
: undefined;
|
|
233
261
|
}
|
|
234
262
|
/**
|
|
235
263
|
* Seed fallback cache entries from persisted account.quota data.
|
|
236
|
-
*
|
|
237
|
-
*
|
|
264
|
+
* Updates older in-memory entries so a fresh quota write from another plugin
|
|
265
|
+
* process prevents redundant checks and stale sidebar writes.
|
|
238
266
|
*/
|
|
239
267
|
seedFallbacksFromAccounts(accounts) {
|
|
240
268
|
const checkInterval = getQuotaCheckIntervalMs(this.storage);
|
|
241
269
|
for (const account of accounts) {
|
|
242
270
|
if (account.enabled === false)
|
|
243
271
|
continue;
|
|
244
|
-
if (this.getFallback(account.id, account.access))
|
|
245
|
-
continue;
|
|
246
272
|
if (!account.quota)
|
|
247
273
|
continue;
|
|
248
274
|
const checkedAt = Math.max(account.quota.five_hour?.checkedAt ?? 0, account.quota.seven_day?.checkedAt ?? 0);
|
|
249
275
|
if (checkedAt <= 0)
|
|
250
276
|
continue;
|
|
277
|
+
const existing = this.getFallback(account.id, account.access);
|
|
278
|
+
if (existing && existing.checkedAt >= checkedAt)
|
|
279
|
+
continue;
|
|
251
280
|
this.setFallback(account.id, {
|
|
252
281
|
quota: account.quota,
|
|
253
282
|
refreshAfter: checkedAt + checkInterval,
|
|
@@ -327,11 +356,13 @@ export class QuotaManager {
|
|
|
327
356
|
ttlMs: 30_000,
|
|
328
357
|
});
|
|
329
358
|
if (!fileLock) {
|
|
330
|
-
|
|
331
|
-
|
|
359
|
+
const cached = this.main;
|
|
360
|
+
if (cached && this.now() < cached.refreshAfter)
|
|
361
|
+
return cached.quota;
|
|
332
362
|
throw new Error('Quota refresh is already in progress');
|
|
333
363
|
}
|
|
334
364
|
try {
|
|
365
|
+
const fetchStartedAt = this.now();
|
|
335
366
|
const quota = await fetchOAuthQuotaSnapshot({
|
|
336
367
|
accessToken,
|
|
337
368
|
fetchImpl: this.fetchImpl,
|
|
@@ -345,7 +376,7 @@ export class QuotaManager {
|
|
|
345
376
|
checkedAt: now,
|
|
346
377
|
};
|
|
347
378
|
this.mainLastApiError = undefined;
|
|
348
|
-
this.onMainQuotaFetched?.(quota, now, this.mainTokenFp);
|
|
379
|
+
this.onMainQuotaFetched?.(quota, now, this.mainTokenFp, fetchStartedAt);
|
|
349
380
|
return quota;
|
|
350
381
|
}
|
|
351
382
|
catch (error) {
|
|
@@ -377,7 +408,7 @@ export class QuotaManager {
|
|
|
377
408
|
});
|
|
378
409
|
if (!fileLock) {
|
|
379
410
|
const cached = this.getFallback(accountId, accessToken);
|
|
380
|
-
if (cached)
|
|
411
|
+
if (cached && this.now() < cached.refreshAfter)
|
|
381
412
|
return cached.quota;
|
|
382
413
|
throw new Error('Quota refresh is already in progress');
|
|
383
414
|
}
|