@cortexkit/anthropic-auth-core 1.15.1 → 1.17.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/dist/accounts.d.ts +48 -3
- package/dist/accounts.js +506 -54
- package/dist/cachekeep-registry.d.ts +21 -0
- package/dist/cachekeep-registry.js +124 -0
- package/dist/cachekeep.d.ts +16 -1
- package/dist/cachekeep.js +93 -14
- package/dist/commands/account.d.ts +1 -0
- package/dist/commands/account.js +7 -1
- package/dist/dump.d.ts +19 -0
- package/dist/dump.js +162 -10
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/oauth-profile.d.ts +11 -0
- package/dist/oauth-profile.js +48 -0
- package/dist/quota-headers.d.ts +4 -0
- package/dist/quota-headers.js +66 -0
- package/dist/quota-manager.d.ts +7 -9
- package/dist/quota-manager.js +72 -24
- package/dist/quotas.d.ts +5 -2
- package/dist/quotas.js +35 -5
- package/dist/relay.d.ts +7 -0
- package/dist/relay.js +37 -8
- package/dist/routing.d.ts +3 -1
- package/dist/routing.js +20 -2
- package/dist/sticky-routing.d.ts +86 -0
- package/dist/sticky-routing.js +450 -0
- package/dist/token-fingerprint.d.ts +5 -0
- package/dist/token-fingerprint.js +8 -0
- package/package.json +1 -1
package/dist/accounts.js
CHANGED
|
@@ -5,6 +5,7 @@ import { dirname, join } from 'node:path';
|
|
|
5
5
|
import { parseRetryAfterHeader, refreshClaudeOAuthToken } from "./auth.js";
|
|
6
6
|
import { CACHE_1H_MODES, CLAUDE_CODE_VERSION, DEFAULT_CACHE_1H_MODE, } from "./constants.js";
|
|
7
7
|
import { log, logger } from "./logger.js";
|
|
8
|
+
import { tokenFingerprint } from "./token-fingerprint.js";
|
|
8
9
|
const setRefreshLockRenewalTimeout = globalThis.setTimeout.bind(globalThis);
|
|
9
10
|
const clearRefreshLockRenewalTimeout = globalThis.clearTimeout.bind(globalThis);
|
|
10
11
|
export const ACCOUNT_FILE_NAME = 'anthropic-auth.json';
|
|
@@ -124,6 +125,28 @@ function normalizeAccount(value) {
|
|
|
124
125
|
lastRefreshError: normalizeOperationError(value.lastRefreshError),
|
|
125
126
|
lastQuotaRefreshError: normalizeOperationError(value.lastQuotaRefreshError),
|
|
126
127
|
quota: normalizeQuota(value.quota),
|
|
128
|
+
profile: normalizeOAuthAccountProfile(value.profile),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function normalizeOAuthAccountProfile(value) {
|
|
132
|
+
if (!isRecord(value))
|
|
133
|
+
return undefined;
|
|
134
|
+
if (typeof value.tier !== 'string' ||
|
|
135
|
+
!value.tier.trim() ||
|
|
136
|
+
typeof value.orgType !== 'string' ||
|
|
137
|
+
!value.orgType.trim() ||
|
|
138
|
+
typeof value.checkedAt !== 'number' ||
|
|
139
|
+
!Number.isFinite(value.checkedAt)) {
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
tier: value.tier.trim(),
|
|
144
|
+
orgType: value.orgType.trim(),
|
|
145
|
+
checkedAt: value.checkedAt,
|
|
146
|
+
...(typeof value.tokenFingerprint === 'string' &&
|
|
147
|
+
value.tokenFingerprint.trim() && {
|
|
148
|
+
tokenFingerprint: value.tokenFingerprint.trim(),
|
|
149
|
+
}),
|
|
127
150
|
};
|
|
128
151
|
}
|
|
129
152
|
function normalizeOperationError(value) {
|
|
@@ -220,8 +243,56 @@ function normalizeQuota(value) {
|
|
|
220
243
|
// — only inputs that already carried an array reach this line.
|
|
221
244
|
quota.scoped = scoped;
|
|
222
245
|
}
|
|
246
|
+
if (isRecord(value.extraUsage)) {
|
|
247
|
+
const used = normalizeQuotaMoney(value.extraUsage.used);
|
|
248
|
+
const limit = normalizeQuotaMoney(value.extraUsage.limit);
|
|
249
|
+
if (used && limit && typeof value.extraUsage.exhausted === 'boolean') {
|
|
250
|
+
quota.extraUsage = {
|
|
251
|
+
used,
|
|
252
|
+
limit,
|
|
253
|
+
...(typeof value.extraUsage.utilizationPercent === 'number' &&
|
|
254
|
+
Number.isFinite(value.extraUsage.utilizationPercent) && {
|
|
255
|
+
utilizationPercent: value.extraUsage.utilizationPercent,
|
|
256
|
+
}),
|
|
257
|
+
...(typeof value.extraUsage.severity === 'string' && {
|
|
258
|
+
severity: value.extraUsage.severity,
|
|
259
|
+
}),
|
|
260
|
+
exhausted: value.extraUsage.exhausted,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (typeof value.bindingWindow === 'string' && value.bindingWindow.trim()) {
|
|
265
|
+
quota.bindingWindow = value.bindingWindow.trim();
|
|
266
|
+
}
|
|
267
|
+
if (value.bindingWindowSource === 'poll' ||
|
|
268
|
+
value.bindingWindowSource === 'headers') {
|
|
269
|
+
quota.bindingWindowSource = value.bindingWindowSource;
|
|
270
|
+
}
|
|
271
|
+
if (typeof value.fallbackAdvised === 'boolean') {
|
|
272
|
+
quota.fallbackAdvised = value.fallbackAdvised;
|
|
273
|
+
}
|
|
274
|
+
if (value.source === 'poll' || value.source === 'headers') {
|
|
275
|
+
quota.source = value.source;
|
|
276
|
+
}
|
|
223
277
|
return Object.keys(quota).length ? quota : undefined;
|
|
224
278
|
}
|
|
279
|
+
function normalizeQuotaMoney(value) {
|
|
280
|
+
if (!isRecord(value))
|
|
281
|
+
return undefined;
|
|
282
|
+
if (typeof value.amountMinor !== 'number' ||
|
|
283
|
+
!Number.isFinite(value.amountMinor) ||
|
|
284
|
+
typeof value.currency !== 'string' ||
|
|
285
|
+
!value.currency.trim() ||
|
|
286
|
+
typeof value.exponent !== 'number' ||
|
|
287
|
+
!Number.isFinite(value.exponent)) {
|
|
288
|
+
return undefined;
|
|
289
|
+
}
|
|
290
|
+
return {
|
|
291
|
+
amountMinor: value.amountMinor,
|
|
292
|
+
currency: value.currency.trim(),
|
|
293
|
+
exponent: value.exponent,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
225
296
|
// Fresh empty storage shell — main OpenCode OAuth account, no fallback
|
|
226
297
|
// accounts. Returns a new object each call so mutating callers don't alias.
|
|
227
298
|
export function createEmptyStorage() {
|
|
@@ -236,7 +307,11 @@ function normalizeStorage(value) {
|
|
|
236
307
|
return null;
|
|
237
308
|
return {
|
|
238
309
|
version: 1,
|
|
239
|
-
main: {
|
|
310
|
+
main: {
|
|
311
|
+
type: 'opencode',
|
|
312
|
+
provider: 'anthropic',
|
|
313
|
+
profile: normalizeOAuthAccountProfile(isRecord(value.main) ? value.main.profile : undefined),
|
|
314
|
+
},
|
|
240
315
|
routing: isRecord(value.routing) ? value.routing : undefined,
|
|
241
316
|
fallbackOn: Array.isArray(value.fallbackOn)
|
|
242
317
|
? value.fallbackOn.filter((status) => Number.isInteger(status))
|
|
@@ -330,6 +405,11 @@ function mergeConfigAndState(configValue, stateValue) {
|
|
|
330
405
|
: [];
|
|
331
406
|
return {
|
|
332
407
|
...configValue,
|
|
408
|
+
main: {
|
|
409
|
+
type: 'opencode',
|
|
410
|
+
provider: 'anthropic',
|
|
411
|
+
profile: normalizeOAuthAccountProfile(mainState?.profile),
|
|
412
|
+
},
|
|
333
413
|
refresh: objectWithDefinedEntries({
|
|
334
414
|
...refreshConfig,
|
|
335
415
|
mainLastRefreshError: mainRefreshSource.lastRefreshError,
|
|
@@ -392,52 +472,127 @@ function accountRuntimeState(account) {
|
|
|
392
472
|
lastRefreshError: account.lastRefreshError,
|
|
393
473
|
lastQuotaRefreshError: account.lastQuotaRefreshError,
|
|
394
474
|
quota: account.quota,
|
|
475
|
+
profile: account.profile,
|
|
395
476
|
});
|
|
396
477
|
}
|
|
397
478
|
function quotaSnapshotCheckedAt(quota) {
|
|
398
479
|
return Math.max(quota?.five_hour?.checkedAt ?? 0, quota?.seven_day?.checkedAt ?? 0, ...(quota?.scoped?.map((window) => window.checkedAt) ?? []), quota?.checkedAt ?? 0);
|
|
399
480
|
}
|
|
481
|
+
function quotaSourcePrecedence(quota) {
|
|
482
|
+
if (quota?.source === 'poll')
|
|
483
|
+
return 2;
|
|
484
|
+
if (quota?.source === 'headers')
|
|
485
|
+
return 1;
|
|
486
|
+
return 0;
|
|
487
|
+
}
|
|
488
|
+
function mergeHeaderScopedQuota(existing, incoming) {
|
|
489
|
+
if (!('scoped' in existing))
|
|
490
|
+
return incoming.scoped;
|
|
491
|
+
if (!Array.isArray(existing.scoped) || existing.scoped.length === 0) {
|
|
492
|
+
return existing.scoped;
|
|
493
|
+
}
|
|
494
|
+
if (!Array.isArray(incoming.scoped) || incoming.scoped.length === 0) {
|
|
495
|
+
return existing.scoped;
|
|
496
|
+
}
|
|
497
|
+
const merged = new Map(incoming.scoped.map((window) => [window.id, window]));
|
|
498
|
+
for (const window of existing.scoped) {
|
|
499
|
+
const candidate = merged.get(window.id);
|
|
500
|
+
if (!candidate || window.checkedAt >= candidate.checkedAt) {
|
|
501
|
+
merged.set(window.id, window);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
return [...merged.values()];
|
|
505
|
+
}
|
|
506
|
+
function mergeHeaderOwnedWindow(existingSnapshot, incomingSnapshot, key) {
|
|
507
|
+
const existing = existingSnapshot[key];
|
|
508
|
+
const incoming = incomingSnapshot[key];
|
|
509
|
+
if (!incoming)
|
|
510
|
+
return existing;
|
|
511
|
+
if (!existing)
|
|
512
|
+
return incoming;
|
|
513
|
+
if (incoming.checkedAt > existing.checkedAt)
|
|
514
|
+
return incoming;
|
|
515
|
+
if (incoming.checkedAt < existing.checkedAt)
|
|
516
|
+
return existing;
|
|
517
|
+
return quotaSourcePrecedence(existingSnapshot) >
|
|
518
|
+
quotaSourcePrecedence(incomingSnapshot)
|
|
519
|
+
? existing
|
|
520
|
+
: incoming;
|
|
521
|
+
}
|
|
522
|
+
function mergeHeaderQuotaForPersistence(existing, incoming) {
|
|
523
|
+
if (!existing || incoming.source !== 'headers')
|
|
524
|
+
return incoming;
|
|
525
|
+
const preservePollBinding = existing.bindingWindowSource === 'poll';
|
|
526
|
+
return {
|
|
527
|
+
...existing,
|
|
528
|
+
...incoming,
|
|
529
|
+
five_hour: mergeHeaderOwnedWindow(existing, incoming, 'five_hour'),
|
|
530
|
+
seven_day: mergeHeaderOwnedWindow(existing, incoming, 'seven_day'),
|
|
531
|
+
scoped: mergeHeaderScopedQuota(existing, incoming),
|
|
532
|
+
extraUsage: existing.extraUsage ?? incoming.extraUsage,
|
|
533
|
+
bindingWindow: preservePollBinding
|
|
534
|
+
? existing.bindingWindow
|
|
535
|
+
: (incoming.bindingWindow ?? existing.bindingWindow),
|
|
536
|
+
bindingWindowSource: preservePollBinding
|
|
537
|
+
? 'poll'
|
|
538
|
+
: (incoming.bindingWindowSource ?? existing.bindingWindowSource),
|
|
539
|
+
};
|
|
540
|
+
}
|
|
400
541
|
function mergeAccountRuntimeState(existing, incoming) {
|
|
401
542
|
if (!isRecord(existing))
|
|
402
543
|
return incoming;
|
|
403
544
|
const existingEntry = existing;
|
|
404
|
-
const existingQuotaCheckedAt = quotaSnapshotCheckedAt(existingEntry.quota);
|
|
405
|
-
const incomingQuotaCheckedAt = quotaSnapshotCheckedAt(incoming.quota);
|
|
406
545
|
const tokenChanged = Boolean((existingEntry.access &&
|
|
407
546
|
incoming.access &&
|
|
408
547
|
existingEntry.access !== incoming.access) ||
|
|
409
548
|
(existingEntry.refresh &&
|
|
410
549
|
incoming.refresh &&
|
|
411
550
|
existingEntry.refresh !== incoming.refresh));
|
|
412
|
-
|
|
551
|
+
const mergesHeaderQuota = Boolean(!tokenChanged && incoming.quota?.source === 'headers');
|
|
552
|
+
const effectiveIncoming = mergesHeaderQuota && incoming.quota
|
|
553
|
+
? {
|
|
554
|
+
...incoming,
|
|
555
|
+
quota: mergeHeaderQuotaForPersistence(existingEntry.quota, incoming.quota),
|
|
556
|
+
}
|
|
557
|
+
: incoming;
|
|
558
|
+
const existingQuotaCheckedAt = quotaSnapshotCheckedAt(existingEntry.quota);
|
|
559
|
+
const incomingQuotaCheckedAt = quotaSnapshotCheckedAt(effectiveIncoming.quota);
|
|
560
|
+
const existingQuotaWinsEqualTimestamp = Boolean(existingQuotaCheckedAt === incomingQuotaCheckedAt &&
|
|
561
|
+
quotaSourcePrecedence(existingEntry.quota) >
|
|
562
|
+
quotaSourcePrecedence(effectiveIncoming.quota));
|
|
563
|
+
if (!mergesHeaderQuota &&
|
|
564
|
+
(existingQuotaCheckedAt > incomingQuotaCheckedAt ||
|
|
565
|
+
existingQuotaWinsEqualTimestamp)) {
|
|
413
566
|
const existingRefreshAt = existingEntry.lastRefreshedAt ?? 0;
|
|
414
|
-
const incomingRefreshAt =
|
|
567
|
+
const incomingRefreshAt = effectiveIncoming.lastRefreshedAt ?? 0;
|
|
415
568
|
if (tokenChanged && incomingRefreshAt <= existingRefreshAt) {
|
|
416
569
|
const merged = { ...existingEntry };
|
|
417
|
-
if (typeof
|
|
570
|
+
if (typeof effectiveIncoming.lastUsed === 'number' &&
|
|
418
571
|
(!(typeof existingEntry.lastUsed === 'number') ||
|
|
419
|
-
|
|
420
|
-
merged.lastUsed =
|
|
572
|
+
effectiveIncoming.lastUsed > existingEntry.lastUsed)) {
|
|
573
|
+
merged.lastUsed = effectiveIncoming.lastUsed;
|
|
421
574
|
}
|
|
422
575
|
return merged;
|
|
423
576
|
}
|
|
424
|
-
const merged = {
|
|
577
|
+
const merged = {
|
|
578
|
+
...existingEntry,
|
|
579
|
+
...effectiveIncoming,
|
|
580
|
+
};
|
|
425
581
|
if (tokenChanged) {
|
|
426
|
-
if ('
|
|
427
|
-
merged.
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
}
|
|
431
|
-
else {
|
|
432
|
-
delete merged.lastQuotaRefreshError;
|
|
433
|
-
}
|
|
582
|
+
if (!('profile' in effectiveIncoming))
|
|
583
|
+
delete merged.profile;
|
|
584
|
+
if (effectiveIncoming.quota?.source) {
|
|
585
|
+
merged.quota = effectiveIncoming.quota;
|
|
434
586
|
}
|
|
435
587
|
else {
|
|
436
588
|
delete merged.quota;
|
|
589
|
+
}
|
|
590
|
+
if (!('lastQuotaRefreshError' in effectiveIncoming)) {
|
|
437
591
|
delete merged.lastQuotaRefreshError;
|
|
438
592
|
}
|
|
439
|
-
if (!('lastRefreshError' in
|
|
593
|
+
if (!('lastRefreshError' in effectiveIncoming)) {
|
|
440
594
|
delete merged.lastRefreshError;
|
|
595
|
+
}
|
|
441
596
|
return merged;
|
|
442
597
|
}
|
|
443
598
|
return {
|
|
@@ -446,11 +601,20 @@ function mergeAccountRuntimeState(existing, incoming) {
|
|
|
446
601
|
lastQuotaRefreshError: existingEntry.lastQuotaRefreshError,
|
|
447
602
|
};
|
|
448
603
|
}
|
|
449
|
-
const merged = {
|
|
450
|
-
|
|
604
|
+
const merged = {
|
|
605
|
+
...existingEntry,
|
|
606
|
+
...effectiveIncoming,
|
|
607
|
+
};
|
|
608
|
+
if (tokenChanged) {
|
|
609
|
+
if (!('profile' in effectiveIncoming))
|
|
610
|
+
delete merged.profile;
|
|
611
|
+
if (!effectiveIncoming.quota?.source)
|
|
612
|
+
delete merged.quota;
|
|
613
|
+
}
|
|
614
|
+
if (!('lastQuotaRefreshError' in effectiveIncoming)) {
|
|
451
615
|
delete merged.lastQuotaRefreshError;
|
|
452
616
|
}
|
|
453
|
-
if (!('lastRefreshError' in
|
|
617
|
+
if (!('lastRefreshError' in effectiveIncoming)) {
|
|
454
618
|
delete merged.lastRefreshError;
|
|
455
619
|
}
|
|
456
620
|
return merged;
|
|
@@ -475,7 +639,7 @@ function configFromStorage(storage) {
|
|
|
475
639
|
: undefined;
|
|
476
640
|
return omitUndefinedTopLevel({
|
|
477
641
|
version: 1,
|
|
478
|
-
main:
|
|
642
|
+
main: { type: 'opencode', provider: 'anthropic' },
|
|
479
643
|
routing: storage.routing,
|
|
480
644
|
fallbackOn: storage.fallbackOn,
|
|
481
645
|
refresh,
|
|
@@ -517,32 +681,142 @@ async function writeJsonAtomic(path, value) {
|
|
|
517
681
|
throw error;
|
|
518
682
|
}
|
|
519
683
|
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
const
|
|
526
|
-
const
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
684
|
+
function sameAccountIdentity(left, right) {
|
|
685
|
+
return (left.id === right.id ||
|
|
686
|
+
Boolean(left.label && right.label && left.label === right.label));
|
|
687
|
+
}
|
|
688
|
+
function mergeAccountsForSave(existing, incoming, options) {
|
|
689
|
+
const removedIds = new Set(options.removedAccountIds ?? []);
|
|
690
|
+
const current = existing.filter((account) => !removedIds.has(account.id));
|
|
691
|
+
const next = incoming.filter((account) => !removedIds.has(account.id));
|
|
692
|
+
const missing = current.filter((account) => !next.some((candidate) => sameAccountIdentity(candidate, account)));
|
|
693
|
+
if (!missing.length)
|
|
694
|
+
return [...next];
|
|
695
|
+
if (options.preserveExistingAccountOrder === false) {
|
|
696
|
+
return [...next, ...missing];
|
|
697
|
+
}
|
|
698
|
+
const usedIncoming = new Set();
|
|
699
|
+
const merged = current.map((account) => {
|
|
700
|
+
const index = next.findIndex((candidate, candidateIndex) => !usedIncoming.has(candidateIndex) &&
|
|
701
|
+
sameAccountIdentity(candidate, account));
|
|
702
|
+
const candidate = next[index];
|
|
703
|
+
if (!candidate)
|
|
704
|
+
return account;
|
|
705
|
+
usedIncoming.add(index);
|
|
706
|
+
return candidate;
|
|
707
|
+
});
|
|
708
|
+
for (let index = 0; index < next.length; index++) {
|
|
709
|
+
const candidate = next[index];
|
|
710
|
+
if (!usedIncoming.has(index) && candidate)
|
|
711
|
+
merged.push(candidate);
|
|
712
|
+
}
|
|
713
|
+
return merged;
|
|
714
|
+
}
|
|
715
|
+
const ACCOUNT_CONFIG_LOCK_TTL_MS = 10_000;
|
|
716
|
+
const ACCOUNT_CONFIG_LOCK_WAIT_MS = 12_000;
|
|
717
|
+
const ACCOUNT_STATE_LOCK_TTL_MS = 10_000;
|
|
718
|
+
const ACCOUNT_STATE_LOCK_WAIT_MS = 12_000;
|
|
719
|
+
async function acquireAccountWriteLock(input) {
|
|
720
|
+
const { path, name, ttlMs, waitMs, description } = input;
|
|
721
|
+
await mkdir(dirname(path), { recursive: true });
|
|
722
|
+
const deadline = Date.now() + waitMs;
|
|
723
|
+
while (true) {
|
|
724
|
+
const lock = await acquireRefreshFileLock({
|
|
725
|
+
name,
|
|
726
|
+
ttlMs,
|
|
727
|
+
path,
|
|
728
|
+
renew: true,
|
|
729
|
+
});
|
|
730
|
+
if (lock)
|
|
731
|
+
return lock;
|
|
732
|
+
if (Date.now() >= deadline) {
|
|
733
|
+
throw new Error(`Timed out waiting for the account ${description} lock`);
|
|
734
|
+
}
|
|
735
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
function acquireAccountConfigWriteLock(path) {
|
|
739
|
+
return acquireAccountWriteLock({
|
|
740
|
+
path,
|
|
741
|
+
name: 'config-write',
|
|
742
|
+
ttlMs: ACCOUNT_CONFIG_LOCK_TTL_MS,
|
|
743
|
+
waitMs: ACCOUNT_CONFIG_LOCK_WAIT_MS,
|
|
744
|
+
description: 'configuration write',
|
|
532
745
|
});
|
|
533
746
|
}
|
|
747
|
+
function acquireAccountStateWriteLock(path) {
|
|
748
|
+
return acquireAccountWriteLock({
|
|
749
|
+
path,
|
|
750
|
+
name: 'state-write',
|
|
751
|
+
ttlMs: ACCOUNT_STATE_LOCK_TTL_MS,
|
|
752
|
+
waitMs: ACCOUNT_STATE_LOCK_WAIT_MS,
|
|
753
|
+
description: 'state write',
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
export function saveAccounts(storage, path = getAccountStoragePath(), options = {}) {
|
|
757
|
+
const resolvedPath = path;
|
|
758
|
+
return enqueueSave(() => saveAccountsLocked(storage, resolvedPath, options));
|
|
759
|
+
}
|
|
760
|
+
async function saveAccountsLocked(storage, path, options) {
|
|
761
|
+
const lock = await acquireAccountConfigWriteLock(path);
|
|
762
|
+
try {
|
|
763
|
+
const current = await loadAccounts(path);
|
|
764
|
+
const nextStorage = {
|
|
765
|
+
...storage,
|
|
766
|
+
accounts: mergeAccountsForSave(current?.accounts ?? [], storage.accounts, options),
|
|
767
|
+
};
|
|
768
|
+
const existing = await loadExistingTopLevelFields(path);
|
|
769
|
+
const nextConfig = { ...existing, ...configFromStorage(nextStorage) };
|
|
770
|
+
await writeJsonAtomic(path, nextConfig);
|
|
771
|
+
// Config precedes state everywhere both locks are needed; reversing this
|
|
772
|
+
// order can deadlock profile mutations against full account saves.
|
|
773
|
+
const stateLock = await acquireAccountStateWriteLock(path);
|
|
774
|
+
try {
|
|
775
|
+
await saveAccountStateUnlocked(nextStorage, path, {
|
|
776
|
+
mainQuota: true,
|
|
777
|
+
mainRefresh: true,
|
|
778
|
+
accounts: true,
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
finally {
|
|
782
|
+
await stateLock.release();
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
finally {
|
|
786
|
+
await lock.release();
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
function applyMainProfileStatePatch(state, storage) {
|
|
790
|
+
state.main = state.main ?? {};
|
|
791
|
+
state.main.profile = storage.main?.profile;
|
|
792
|
+
}
|
|
534
793
|
function applyMainQuotaStatePatch(state, storage) {
|
|
535
794
|
state.main = state.main ?? {};
|
|
795
|
+
const incomingQuota = storage.quota?.mainQuota;
|
|
796
|
+
const sameToken = Boolean(state.main.quotaToken &&
|
|
797
|
+
storage.quota?.mainQuotaToken &&
|
|
798
|
+
state.main.quotaToken === storage.quota.mainQuotaToken);
|
|
799
|
+
const effectiveIncomingQuota = sameToken && incomingQuota?.source === 'headers'
|
|
800
|
+
? mergeHeaderQuotaForPersistence(state.main.quota, incomingQuota)
|
|
801
|
+
: incomingQuota;
|
|
802
|
+
const mergesHeaderQuota = Boolean(sameToken && incomingQuota?.source === 'headers');
|
|
536
803
|
const existingCheckedAt = typeof state.main.quotaCheckedAt === 'number'
|
|
537
804
|
? state.main.quotaCheckedAt
|
|
538
805
|
: quotaSnapshotCheckedAt(state.main.quota);
|
|
539
806
|
const incomingCheckedAt = typeof storage.quota?.mainQuotaCheckedAt === 'number'
|
|
540
807
|
? storage.quota.mainQuotaCheckedAt
|
|
541
|
-
: quotaSnapshotCheckedAt(
|
|
542
|
-
if (
|
|
808
|
+
: quotaSnapshotCheckedAt(effectiveIncomingQuota);
|
|
809
|
+
if (!mergesHeaderQuota &&
|
|
810
|
+
(existingCheckedAt > incomingCheckedAt ||
|
|
811
|
+
(existingCheckedAt === incomingCheckedAt &&
|
|
812
|
+
quotaSourcePrecedence(state.main.quota) >
|
|
813
|
+
quotaSourcePrecedence(effectiveIncomingQuota)))) {
|
|
543
814
|
return;
|
|
544
|
-
|
|
545
|
-
state.main.
|
|
815
|
+
}
|
|
816
|
+
state.main.quota = effectiveIncomingQuota;
|
|
817
|
+
state.main.quotaCheckedAt = mergesHeaderQuota
|
|
818
|
+
? Math.max(existingCheckedAt, incomingCheckedAt)
|
|
819
|
+
: storage.quota?.mainQuotaCheckedAt;
|
|
546
820
|
state.main.quotaToken = storage.quota?.mainQuotaToken;
|
|
547
821
|
state.main.lastQuotaApiError = storage.quota?.mainLastQuotaApiError;
|
|
548
822
|
}
|
|
@@ -563,12 +837,97 @@ function pruneUndefined(value) {
|
|
|
563
837
|
.map(([key, entry]) => [key, pruneUndefined(entry)]));
|
|
564
838
|
}
|
|
565
839
|
export function saveAccountState(storage, path = getAccountStoragePath(), scope = {
|
|
840
|
+
mainProfile: true,
|
|
566
841
|
mainQuota: true,
|
|
567
842
|
mainRefresh: true,
|
|
568
843
|
accounts: true,
|
|
569
844
|
}) {
|
|
570
845
|
const resolvedPath = path;
|
|
571
|
-
return enqueueSave(() =>
|
|
846
|
+
return enqueueSave(async () => {
|
|
847
|
+
const lock = await acquireAccountStateWriteLock(resolvedPath);
|
|
848
|
+
try {
|
|
849
|
+
await saveAccountStateUnlocked(storage, resolvedPath, scope);
|
|
850
|
+
}
|
|
851
|
+
finally {
|
|
852
|
+
await lock.release();
|
|
853
|
+
}
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
export function saveOAuthProfileState(input, path = getAccountStoragePath()) {
|
|
857
|
+
const resolvedPath = path;
|
|
858
|
+
return enqueueSave(async () => {
|
|
859
|
+
const configLock = await acquireAccountConfigWriteLock(resolvedPath);
|
|
860
|
+
try {
|
|
861
|
+
const stateLock = await acquireAccountStateWriteLock(resolvedPath);
|
|
862
|
+
try {
|
|
863
|
+
const current = await loadAccounts(resolvedPath);
|
|
864
|
+
const statePath = getAccountStatePath(resolvedPath);
|
|
865
|
+
const existing = (await readJsonIfPresent(statePath)).value;
|
|
866
|
+
const next = isRecord(existing)
|
|
867
|
+
? { ...existing, version: 1 }
|
|
868
|
+
: { version: 1 };
|
|
869
|
+
const { accountId, expectedTokenFingerprint, profile } = input;
|
|
870
|
+
if (profile?.tokenFingerprint &&
|
|
871
|
+
profile.tokenFingerprint !== expectedTokenFingerprint) {
|
|
872
|
+
return false;
|
|
873
|
+
}
|
|
874
|
+
if (accountId === 'main') {
|
|
875
|
+
next.main = { ...(next.main ?? {}) };
|
|
876
|
+
const existingProfile = normalizeOAuthAccountProfile(next.main.profile);
|
|
877
|
+
const persistedToken = next.main.profileToken ?? existingProfile?.tokenFingerprint;
|
|
878
|
+
if (!profile &&
|
|
879
|
+
existingProfile &&
|
|
880
|
+
persistedToken === expectedTokenFingerprint) {
|
|
881
|
+
return false;
|
|
882
|
+
}
|
|
883
|
+
if (profile) {
|
|
884
|
+
if (persistedToken && persistedToken !== expectedTokenFingerprint) {
|
|
885
|
+
return false;
|
|
886
|
+
}
|
|
887
|
+
if (existingProfile &&
|
|
888
|
+
existingProfile.checkedAt > profile.checkedAt) {
|
|
889
|
+
return false;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
next.main.profile = profile;
|
|
893
|
+
next.main.profileToken = expectedTokenFingerprint;
|
|
894
|
+
}
|
|
895
|
+
else {
|
|
896
|
+
const account = current?.accounts.find((candidate) => candidate.id === accountId && isOAuthAccount(candidate));
|
|
897
|
+
if (!account?.access ||
|
|
898
|
+
tokenFingerprint(account.access) !== expectedTokenFingerprint) {
|
|
899
|
+
return false;
|
|
900
|
+
}
|
|
901
|
+
next.accounts = {
|
|
902
|
+
...(isRecord(next.accounts) ? next.accounts : {}),
|
|
903
|
+
};
|
|
904
|
+
const existingEntry = isRecord(next.accounts[accountId])
|
|
905
|
+
? { ...next.accounts[accountId] }
|
|
906
|
+
: {};
|
|
907
|
+
const existingProfile = normalizeOAuthAccountProfile(existingEntry.profile);
|
|
908
|
+
if (!profile &&
|
|
909
|
+
existingProfile?.tokenFingerprint === expectedTokenFingerprint) {
|
|
910
|
+
return false;
|
|
911
|
+
}
|
|
912
|
+
if (profile &&
|
|
913
|
+
existingProfile &&
|
|
914
|
+
existingProfile.checkedAt > profile.checkedAt) {
|
|
915
|
+
return false;
|
|
916
|
+
}
|
|
917
|
+
existingEntry.profile = profile;
|
|
918
|
+
next.accounts[accountId] = existingEntry;
|
|
919
|
+
}
|
|
920
|
+
await writeJsonAtomic(statePath, pruneUndefined(next));
|
|
921
|
+
return true;
|
|
922
|
+
}
|
|
923
|
+
finally {
|
|
924
|
+
await stateLock.release();
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
finally {
|
|
928
|
+
await configLock.release();
|
|
929
|
+
}
|
|
930
|
+
});
|
|
572
931
|
}
|
|
573
932
|
async function saveAccountStateUnlocked(storage, path, scope) {
|
|
574
933
|
const statePath = getAccountStatePath(path);
|
|
@@ -576,6 +935,8 @@ async function saveAccountStateUnlocked(storage, path, scope) {
|
|
|
576
935
|
const next = isRecord(existing)
|
|
577
936
|
? { ...existing, version: 1 }
|
|
578
937
|
: { version: 1 };
|
|
938
|
+
if (scope.mainProfile)
|
|
939
|
+
applyMainProfileStatePatch(next, storage);
|
|
579
940
|
if (scope.mainQuota)
|
|
580
941
|
applyMainQuotaStatePatch(next, storage);
|
|
581
942
|
if (scope.mainRefresh)
|
|
@@ -877,13 +1238,27 @@ export async function setFastModePersistentEnabled(enabled, path = getAccountSto
|
|
|
877
1238
|
export async function setCacheKeepPersistentWindow(startHour, endHour, path = getAccountStoragePath()) {
|
|
878
1239
|
const storage = (await loadAccounts(path)) ?? createEmptyStorage();
|
|
879
1240
|
storage.cacheKeep = {
|
|
1241
|
+
...(storage.cacheKeep ?? {}),
|
|
880
1242
|
enabled: true,
|
|
1243
|
+
always: false,
|
|
881
1244
|
startHour,
|
|
882
1245
|
endHour,
|
|
883
1246
|
};
|
|
884
1247
|
await saveAccounts(storage, path);
|
|
885
1248
|
return storage;
|
|
886
1249
|
}
|
|
1250
|
+
export async function setCacheKeepPersistentAlways(path = getAccountStoragePath()) {
|
|
1251
|
+
const storage = (await loadAccounts(path)) ?? createEmptyStorage();
|
|
1252
|
+
storage.cacheKeep = {
|
|
1253
|
+
...(storage.cacheKeep ?? {}),
|
|
1254
|
+
enabled: true,
|
|
1255
|
+
always: true,
|
|
1256
|
+
};
|
|
1257
|
+
delete storage.cacheKeep.startHour;
|
|
1258
|
+
delete storage.cacheKeep.endHour;
|
|
1259
|
+
await saveAccounts(storage, path);
|
|
1260
|
+
return storage;
|
|
1261
|
+
}
|
|
887
1262
|
export async function setCacheKeepPersistentEnabled(enabled, path = getAccountStoragePath()) {
|
|
888
1263
|
const storage = (await loadAccounts(path)) ?? createEmptyStorage();
|
|
889
1264
|
storage.cacheKeep = {
|
|
@@ -911,7 +1286,7 @@ function getFallbackStatuses(storage) {
|
|
|
911
1286
|
export function shouldFallbackStatus(status, storage) {
|
|
912
1287
|
return getFallbackStatuses(storage).includes(status);
|
|
913
1288
|
}
|
|
914
|
-
function
|
|
1289
|
+
export function getQuotaMinimumRemainingThresholds(storage) {
|
|
915
1290
|
const configured = storage?.quota?.minimumRemaining || {};
|
|
916
1291
|
return {
|
|
917
1292
|
five_hour: configured.five_hour ??
|
|
@@ -1141,7 +1516,7 @@ export function quotaSnapshotPassesModelScope(quota, model) {
|
|
|
1141
1516
|
export function quotaSnapshotPassesPolicy(quota, storage) {
|
|
1142
1517
|
if (!quotaEnabled(storage))
|
|
1143
1518
|
return true;
|
|
1144
|
-
const thresholds =
|
|
1519
|
+
const thresholds = getQuotaMinimumRemainingThresholds(storage);
|
|
1145
1520
|
for (const key of ['five_hour', 'seven_day']) {
|
|
1146
1521
|
const window = quota?.[key];
|
|
1147
1522
|
if (!window)
|
|
@@ -1293,8 +1668,9 @@ export async function removeAccountPersistent(id, path = getAccountStoragePath()
|
|
|
1293
1668
|
if (!storage)
|
|
1294
1669
|
return false;
|
|
1295
1670
|
const existed = removeAccount(storage, id);
|
|
1296
|
-
if (existed)
|
|
1297
|
-
await saveAccounts(storage, path);
|
|
1671
|
+
if (existed) {
|
|
1672
|
+
await saveAccounts(storage, path, { removedAccountIds: [id] });
|
|
1673
|
+
}
|
|
1298
1674
|
return existed;
|
|
1299
1675
|
}
|
|
1300
1676
|
export async function reorderAccountsPersistent(orderedIds, path = getAccountStoragePath()) {
|
|
@@ -1302,7 +1678,7 @@ export async function reorderAccountsPersistent(orderedIds, path = getAccountSto
|
|
|
1302
1678
|
if (!storage)
|
|
1303
1679
|
return;
|
|
1304
1680
|
reorderAccounts(storage, orderedIds);
|
|
1305
|
-
await saveAccounts(storage, path);
|
|
1681
|
+
await saveAccounts(storage, path, { preserveExistingAccountOrder: false });
|
|
1306
1682
|
}
|
|
1307
1683
|
export async function setAccountEnabledPersistent(id, enabled, path = getAccountStoragePath()) {
|
|
1308
1684
|
const storage = await loadAccounts(path);
|
|
@@ -1319,25 +1695,35 @@ export async function addAccountPersistent(account, path = getAccountStoragePath
|
|
|
1319
1695
|
await saveAccounts(storage, path);
|
|
1320
1696
|
}
|
|
1321
1697
|
export function getQuotaNextRefreshAt(quota, storage, now) {
|
|
1698
|
+
const intervalMs = getQuotaCheckIntervalMs(storage);
|
|
1322
1699
|
if (!quotaEnabled(storage))
|
|
1323
|
-
return now +
|
|
1324
|
-
const
|
|
1700
|
+
return now + intervalMs;
|
|
1701
|
+
const windowFreshnessDeadline = Math.min(...[
|
|
1702
|
+
...['five_hour', 'seven_day'].map((key) => quota?.[key]?.checkedAt),
|
|
1703
|
+
...(quota?.scoped ?? []).map((window) => window.checkedAt),
|
|
1704
|
+
]
|
|
1705
|
+
.filter((checkedAt) => Number.isFinite(checkedAt))
|
|
1706
|
+
.map((checkedAt) => checkedAt + intervalMs));
|
|
1707
|
+
const capAtOldestWindow = (candidate) => Number.isFinite(windowFreshnessDeadline)
|
|
1708
|
+
? Math.min(candidate, windowFreshnessDeadline)
|
|
1709
|
+
: candidate;
|
|
1710
|
+
const thresholds = getQuotaMinimumRemainingThresholds(storage);
|
|
1325
1711
|
const blockedResetTimes = [];
|
|
1326
1712
|
for (const key of ['five_hour', 'seven_day']) {
|
|
1327
1713
|
const window = quota?.[key];
|
|
1328
1714
|
if (!window)
|
|
1329
|
-
return now +
|
|
1715
|
+
return capAtOldestWindow(now + intervalMs);
|
|
1330
1716
|
if (window.remainingPercent >= thresholds[key])
|
|
1331
1717
|
continue;
|
|
1332
1718
|
const resetTime = window.resetsAt ? Date.parse(window.resetsAt) : Number.NaN;
|
|
1333
1719
|
if (!Number.isFinite(resetTime) || resetTime <= now) {
|
|
1334
|
-
return now +
|
|
1720
|
+
return capAtOldestWindow(now + intervalMs);
|
|
1335
1721
|
}
|
|
1336
1722
|
blockedResetTimes.push(resetTime);
|
|
1337
1723
|
}
|
|
1338
1724
|
if (!blockedResetTimes.length)
|
|
1339
|
-
return now +
|
|
1340
|
-
return Math.min(...blockedResetTimes) + 60_000;
|
|
1725
|
+
return capAtOldestWindow(now + intervalMs);
|
|
1726
|
+
return capAtOldestWindow(Math.min(...blockedResetTimes) + 60_000);
|
|
1341
1727
|
}
|
|
1342
1728
|
function tokenNeedsRefresh(account, storage, now) {
|
|
1343
1729
|
return (!account.access ||
|
|
@@ -1353,8 +1739,11 @@ function quotaSnapshotIsFresh(quota, storage, now) {
|
|
|
1353
1739
|
return Boolean(window && now - window.checkedAt < maxAge);
|
|
1354
1740
|
});
|
|
1355
1741
|
}
|
|
1356
|
-
function quotaIsStale(account, storage, now) {
|
|
1357
|
-
|
|
1742
|
+
function quotaIsStale(account, storage, now, modelId) {
|
|
1743
|
+
if (!quotaSnapshotIsFresh(account.quota, storage, now))
|
|
1744
|
+
return true;
|
|
1745
|
+
const scoped = getScopedQuotaWindowForModel(account.quota, modelId);
|
|
1746
|
+
return Boolean(scoped && now - scoped.checkedAt >= getQuotaCheckIntervalMs(storage));
|
|
1358
1747
|
}
|
|
1359
1748
|
function cachedQuotaWindowStillRelevant(window, now) {
|
|
1360
1749
|
if (!window)
|
|
@@ -1450,6 +1839,61 @@ function mapScopedWeeklyLimits(limits, checkedAt) {
|
|
|
1450
1839
|
}
|
|
1451
1840
|
return scoped;
|
|
1452
1841
|
}
|
|
1842
|
+
function mapExtraUsage(usage) {
|
|
1843
|
+
if (usage.extra_usage?.is_enabled !== true)
|
|
1844
|
+
return undefined;
|
|
1845
|
+
const usedAmount = usage.extra_usage.used_credits;
|
|
1846
|
+
const limitAmount = usage.extra_usage.monthly_limit;
|
|
1847
|
+
if (typeof usedAmount !== 'number' ||
|
|
1848
|
+
!Number.isFinite(usedAmount) ||
|
|
1849
|
+
typeof limitAmount !== 'number' ||
|
|
1850
|
+
!Number.isFinite(limitAmount)) {
|
|
1851
|
+
return undefined;
|
|
1852
|
+
}
|
|
1853
|
+
const rawCurrency = usage.spend?.limit?.currency;
|
|
1854
|
+
const currency = rawCurrency == null ? 'USD' : nonEmptyString(rawCurrency);
|
|
1855
|
+
const rawExponent = usage.spend?.limit?.exponent;
|
|
1856
|
+
const moneyExponent = rawExponent == null ? 2 : rawExponent;
|
|
1857
|
+
if (!currency ||
|
|
1858
|
+
!/^[A-Za-z]{3}$/.test(currency) ||
|
|
1859
|
+
!Number.isInteger(moneyExponent) ||
|
|
1860
|
+
moneyExponent < 0 ||
|
|
1861
|
+
moneyExponent > 20) {
|
|
1862
|
+
return undefined;
|
|
1863
|
+
}
|
|
1864
|
+
return {
|
|
1865
|
+
used: { amountMinor: usedAmount, currency, exponent: moneyExponent },
|
|
1866
|
+
limit: { amountMinor: limitAmount, currency, exponent: moneyExponent },
|
|
1867
|
+
...(typeof usage.extra_usage.utilization === 'number' &&
|
|
1868
|
+
Number.isFinite(usage.extra_usage.utilization) && {
|
|
1869
|
+
utilizationPercent: usage.extra_usage.utilization,
|
|
1870
|
+
}),
|
|
1871
|
+
...(nonEmptyString(usage.spend?.severity) && {
|
|
1872
|
+
severity: nonEmptyString(usage.spend?.severity),
|
|
1873
|
+
}),
|
|
1874
|
+
exhausted: usedAmount >= limitAmount,
|
|
1875
|
+
};
|
|
1876
|
+
}
|
|
1877
|
+
function mapBindingWindow(limits) {
|
|
1878
|
+
if (!Array.isArray(limits))
|
|
1879
|
+
return undefined;
|
|
1880
|
+
const active = limits.find((limit) => limit?.is_active === true);
|
|
1881
|
+
if (!active)
|
|
1882
|
+
return undefined;
|
|
1883
|
+
if (active.kind === 'session')
|
|
1884
|
+
return 'five_hour';
|
|
1885
|
+
if (active.kind === 'weekly_all')
|
|
1886
|
+
return 'seven_day';
|
|
1887
|
+
if (active.kind !== 'weekly_scoped' || active.group !== 'weekly') {
|
|
1888
|
+
return undefined;
|
|
1889
|
+
}
|
|
1890
|
+
const modelName = nonEmptyString(active.scope?.model?.display_name);
|
|
1891
|
+
if (!modelName)
|
|
1892
|
+
return undefined;
|
|
1893
|
+
const identity = nonEmptyString(active.scope?.model?.id) ?? modelName;
|
|
1894
|
+
const slug = slugForQuotaIdentity(identity);
|
|
1895
|
+
return slug ? `claude-weekly-scoped-${slug}` : undefined;
|
|
1896
|
+
}
|
|
1453
1897
|
function mapUsageWindow(window, checkedAt) {
|
|
1454
1898
|
if (typeof window?.utilization !== 'number')
|
|
1455
1899
|
return undefined;
|
|
@@ -1485,10 +1929,17 @@ export async function fetchOAuthQuotaSnapshot(input) {
|
|
|
1485
1929
|
}
|
|
1486
1930
|
const checkedAt = input.now?.() ?? Date.now();
|
|
1487
1931
|
const usage = (await response.json());
|
|
1932
|
+
const bindingWindow = mapBindingWindow(usage.limits);
|
|
1488
1933
|
return {
|
|
1489
1934
|
five_hour: mapUsageWindow(usage.five_hour, checkedAt),
|
|
1490
1935
|
seven_day: mapUsageWindow(usage.seven_day, checkedAt),
|
|
1491
1936
|
scoped: mapScopedWeeklyLimits(usage.limits, checkedAt),
|
|
1937
|
+
extraUsage: mapExtraUsage(usage),
|
|
1938
|
+
...(bindingWindow && {
|
|
1939
|
+
bindingWindow,
|
|
1940
|
+
bindingWindowSource: 'poll',
|
|
1941
|
+
}),
|
|
1942
|
+
source: 'poll',
|
|
1492
1943
|
checkedAt,
|
|
1493
1944
|
};
|
|
1494
1945
|
}
|
|
@@ -1507,6 +1958,7 @@ export function upsertAccount(storage, account) {
|
|
|
1507
1958
|
addedAt: storage.accounts[index]?.addedAt ?? account.addedAt,
|
|
1508
1959
|
...(account.type === 'oauth' && {
|
|
1509
1960
|
quota: account.quota,
|
|
1961
|
+
profile: account.profile,
|
|
1510
1962
|
lastRefreshedAt: account.lastRefreshedAt,
|
|
1511
1963
|
lastRefreshError: account.lastRefreshError,
|
|
1512
1964
|
lastQuotaRefreshError: account.lastQuotaRefreshError,
|
|
@@ -1656,8 +2108,8 @@ export class FallbackAccountManager {
|
|
|
1656
2108
|
}
|
|
1657
2109
|
this.seedFallbackQuota(next, storage);
|
|
1658
2110
|
const stale = this.quotaManager
|
|
1659
|
-
? this.quotaManager.isFallbackStale(next.id, next.access)
|
|
1660
|
-
: quotaIsStale(next, storage, this.now());
|
|
2111
|
+
? this.quotaManager.isFallbackStale(next.id, next.access, options.modelId)
|
|
2112
|
+
: quotaIsStale(next, storage, this.now(), options.modelId);
|
|
1661
2113
|
// Skip the request-time refresh when this account's quota API is
|
|
1662
2114
|
// backed off (recent 429/5xx). Hitting it again would extend the
|
|
1663
2115
|
// backoff; evaluate policy on the cached/seeded quota instead. Mirrors
|