@cortexkit/anthropic-auth-core 1.16.0 → 1.18.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 +34 -1
- package/dist/accounts.js +419 -43
- package/dist/commands/account.d.ts +1 -0
- package/dist/commands/account.js +7 -1
- package/dist/constants.js +2 -1
- package/dist/fast.js +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/models.d.ts +14 -0
- package/dist/models.js +15 -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 +3 -6
- package/dist/quota-manager.js +57 -17
- 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/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,
|
|
@@ -550,24 +714,45 @@ function mergeAccountsForSave(existing, incoming, options) {
|
|
|
550
714
|
}
|
|
551
715
|
const ACCOUNT_CONFIG_LOCK_TTL_MS = 10_000;
|
|
552
716
|
const ACCOUNT_CONFIG_LOCK_WAIT_MS = 12_000;
|
|
553
|
-
|
|
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;
|
|
554
721
|
await mkdir(dirname(path), { recursive: true });
|
|
555
|
-
const deadline = Date.now() +
|
|
722
|
+
const deadline = Date.now() + waitMs;
|
|
556
723
|
while (true) {
|
|
557
724
|
const lock = await acquireRefreshFileLock({
|
|
558
|
-
name
|
|
559
|
-
ttlMs
|
|
725
|
+
name,
|
|
726
|
+
ttlMs,
|
|
560
727
|
path,
|
|
561
728
|
renew: true,
|
|
562
729
|
});
|
|
563
730
|
if (lock)
|
|
564
731
|
return lock;
|
|
565
732
|
if (Date.now() >= deadline) {
|
|
566
|
-
throw new Error(
|
|
733
|
+
throw new Error(`Timed out waiting for the account ${description} lock`);
|
|
567
734
|
}
|
|
568
735
|
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
569
736
|
}
|
|
570
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',
|
|
745
|
+
});
|
|
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
|
+
}
|
|
571
756
|
export function saveAccounts(storage, path = getAccountStoragePath(), options = {}) {
|
|
572
757
|
const resolvedPath = path;
|
|
573
758
|
return enqueueSave(() => saveAccountsLocked(storage, resolvedPath, options));
|
|
@@ -583,28 +768,55 @@ async function saveAccountsLocked(storage, path, options) {
|
|
|
583
768
|
const existing = await loadExistingTopLevelFields(path);
|
|
584
769
|
const nextConfig = { ...existing, ...configFromStorage(nextStorage) };
|
|
585
770
|
await writeJsonAtomic(path, nextConfig);
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
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
|
+
}
|
|
591
784
|
}
|
|
592
785
|
finally {
|
|
593
786
|
await lock.release();
|
|
594
787
|
}
|
|
595
788
|
}
|
|
789
|
+
function applyMainProfileStatePatch(state, storage) {
|
|
790
|
+
state.main = state.main ?? {};
|
|
791
|
+
state.main.profile = storage.main?.profile;
|
|
792
|
+
}
|
|
596
793
|
function applyMainQuotaStatePatch(state, storage) {
|
|
597
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');
|
|
598
803
|
const existingCheckedAt = typeof state.main.quotaCheckedAt === 'number'
|
|
599
804
|
? state.main.quotaCheckedAt
|
|
600
805
|
: quotaSnapshotCheckedAt(state.main.quota);
|
|
601
806
|
const incomingCheckedAt = typeof storage.quota?.mainQuotaCheckedAt === 'number'
|
|
602
807
|
? storage.quota.mainQuotaCheckedAt
|
|
603
|
-
: quotaSnapshotCheckedAt(
|
|
604
|
-
if (
|
|
808
|
+
: quotaSnapshotCheckedAt(effectiveIncomingQuota);
|
|
809
|
+
if (!mergesHeaderQuota &&
|
|
810
|
+
(existingCheckedAt > incomingCheckedAt ||
|
|
811
|
+
(existingCheckedAt === incomingCheckedAt &&
|
|
812
|
+
quotaSourcePrecedence(state.main.quota) >
|
|
813
|
+
quotaSourcePrecedence(effectiveIncomingQuota)))) {
|
|
605
814
|
return;
|
|
606
|
-
|
|
607
|
-
state.main.
|
|
815
|
+
}
|
|
816
|
+
state.main.quota = effectiveIncomingQuota;
|
|
817
|
+
state.main.quotaCheckedAt = mergesHeaderQuota
|
|
818
|
+
? Math.max(existingCheckedAt, incomingCheckedAt)
|
|
819
|
+
: storage.quota?.mainQuotaCheckedAt;
|
|
608
820
|
state.main.quotaToken = storage.quota?.mainQuotaToken;
|
|
609
821
|
state.main.lastQuotaApiError = storage.quota?.mainLastQuotaApiError;
|
|
610
822
|
}
|
|
@@ -625,12 +837,97 @@ function pruneUndefined(value) {
|
|
|
625
837
|
.map(([key, entry]) => [key, pruneUndefined(entry)]));
|
|
626
838
|
}
|
|
627
839
|
export function saveAccountState(storage, path = getAccountStoragePath(), scope = {
|
|
840
|
+
mainProfile: true,
|
|
628
841
|
mainQuota: true,
|
|
629
842
|
mainRefresh: true,
|
|
630
843
|
accounts: true,
|
|
631
844
|
}) {
|
|
632
845
|
const resolvedPath = path;
|
|
633
|
-
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
|
+
});
|
|
634
931
|
}
|
|
635
932
|
async function saveAccountStateUnlocked(storage, path, scope) {
|
|
636
933
|
const statePath = getAccountStatePath(path);
|
|
@@ -638,6 +935,8 @@ async function saveAccountStateUnlocked(storage, path, scope) {
|
|
|
638
935
|
const next = isRecord(existing)
|
|
639
936
|
? { ...existing, version: 1 }
|
|
640
937
|
: { version: 1 };
|
|
938
|
+
if (scope.mainProfile)
|
|
939
|
+
applyMainProfileStatePatch(next, storage);
|
|
641
940
|
if (scope.mainQuota)
|
|
642
941
|
applyMainQuotaStatePatch(next, storage);
|
|
643
942
|
if (scope.mainRefresh)
|
|
@@ -773,7 +1072,11 @@ export async function acquireRefreshFileLock(options) {
|
|
|
773
1072
|
await writeFile(evictOwnerPath, `${JSON.stringify({ ownerId: evictOwnerId, createdAt: now() })}\n`, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
|
774
1073
|
}
|
|
775
1074
|
catch (error) {
|
|
776
|
-
|
|
1075
|
+
const code = error.code;
|
|
1076
|
+
// Another contender can rename the marker directory between mkdir and
|
|
1077
|
+
// this exclusive create. Darwin/Bun reports that lost-parent race as
|
|
1078
|
+
// either ENOENT or EINVAL; both mean this contender lost the marker.
|
|
1079
|
+
if (code === 'ENOENT' || code === 'EINVAL')
|
|
777
1080
|
return false;
|
|
778
1081
|
await releaseEvictionMarker();
|
|
779
1082
|
throw error;
|
|
@@ -1396,25 +1699,35 @@ export async function addAccountPersistent(account, path = getAccountStoragePath
|
|
|
1396
1699
|
await saveAccounts(storage, path);
|
|
1397
1700
|
}
|
|
1398
1701
|
export function getQuotaNextRefreshAt(quota, storage, now) {
|
|
1702
|
+
const intervalMs = getQuotaCheckIntervalMs(storage);
|
|
1399
1703
|
if (!quotaEnabled(storage))
|
|
1400
|
-
return now +
|
|
1704
|
+
return now + intervalMs;
|
|
1705
|
+
const windowFreshnessDeadline = Math.min(...[
|
|
1706
|
+
...['five_hour', 'seven_day'].map((key) => quota?.[key]?.checkedAt),
|
|
1707
|
+
...(quota?.scoped ?? []).map((window) => window.checkedAt),
|
|
1708
|
+
]
|
|
1709
|
+
.filter((checkedAt) => Number.isFinite(checkedAt))
|
|
1710
|
+
.map((checkedAt) => checkedAt + intervalMs));
|
|
1711
|
+
const capAtOldestWindow = (candidate) => Number.isFinite(windowFreshnessDeadline)
|
|
1712
|
+
? Math.min(candidate, windowFreshnessDeadline)
|
|
1713
|
+
: candidate;
|
|
1401
1714
|
const thresholds = getQuotaMinimumRemainingThresholds(storage);
|
|
1402
1715
|
const blockedResetTimes = [];
|
|
1403
1716
|
for (const key of ['five_hour', 'seven_day']) {
|
|
1404
1717
|
const window = quota?.[key];
|
|
1405
1718
|
if (!window)
|
|
1406
|
-
return now +
|
|
1719
|
+
return capAtOldestWindow(now + intervalMs);
|
|
1407
1720
|
if (window.remainingPercent >= thresholds[key])
|
|
1408
1721
|
continue;
|
|
1409
1722
|
const resetTime = window.resetsAt ? Date.parse(window.resetsAt) : Number.NaN;
|
|
1410
1723
|
if (!Number.isFinite(resetTime) || resetTime <= now) {
|
|
1411
|
-
return now +
|
|
1724
|
+
return capAtOldestWindow(now + intervalMs);
|
|
1412
1725
|
}
|
|
1413
1726
|
blockedResetTimes.push(resetTime);
|
|
1414
1727
|
}
|
|
1415
1728
|
if (!blockedResetTimes.length)
|
|
1416
|
-
return now +
|
|
1417
|
-
return Math.min(...blockedResetTimes) + 60_000;
|
|
1729
|
+
return capAtOldestWindow(now + intervalMs);
|
|
1730
|
+
return capAtOldestWindow(Math.min(...blockedResetTimes) + 60_000);
|
|
1418
1731
|
}
|
|
1419
1732
|
function tokenNeedsRefresh(account, storage, now) {
|
|
1420
1733
|
return (!account.access ||
|
|
@@ -1530,6 +1843,61 @@ function mapScopedWeeklyLimits(limits, checkedAt) {
|
|
|
1530
1843
|
}
|
|
1531
1844
|
return scoped;
|
|
1532
1845
|
}
|
|
1846
|
+
function mapExtraUsage(usage) {
|
|
1847
|
+
if (usage.extra_usage?.is_enabled !== true)
|
|
1848
|
+
return undefined;
|
|
1849
|
+
const usedAmount = usage.extra_usage.used_credits;
|
|
1850
|
+
const limitAmount = usage.extra_usage.monthly_limit;
|
|
1851
|
+
if (typeof usedAmount !== 'number' ||
|
|
1852
|
+
!Number.isFinite(usedAmount) ||
|
|
1853
|
+
typeof limitAmount !== 'number' ||
|
|
1854
|
+
!Number.isFinite(limitAmount)) {
|
|
1855
|
+
return undefined;
|
|
1856
|
+
}
|
|
1857
|
+
const rawCurrency = usage.spend?.limit?.currency;
|
|
1858
|
+
const currency = rawCurrency == null ? 'USD' : nonEmptyString(rawCurrency);
|
|
1859
|
+
const rawExponent = usage.spend?.limit?.exponent;
|
|
1860
|
+
const moneyExponent = rawExponent == null ? 2 : rawExponent;
|
|
1861
|
+
if (!currency ||
|
|
1862
|
+
!/^[A-Za-z]{3}$/.test(currency) ||
|
|
1863
|
+
!Number.isInteger(moneyExponent) ||
|
|
1864
|
+
moneyExponent < 0 ||
|
|
1865
|
+
moneyExponent > 20) {
|
|
1866
|
+
return undefined;
|
|
1867
|
+
}
|
|
1868
|
+
return {
|
|
1869
|
+
used: { amountMinor: usedAmount, currency, exponent: moneyExponent },
|
|
1870
|
+
limit: { amountMinor: limitAmount, currency, exponent: moneyExponent },
|
|
1871
|
+
...(typeof usage.extra_usage.utilization === 'number' &&
|
|
1872
|
+
Number.isFinite(usage.extra_usage.utilization) && {
|
|
1873
|
+
utilizationPercent: usage.extra_usage.utilization,
|
|
1874
|
+
}),
|
|
1875
|
+
...(nonEmptyString(usage.spend?.severity) && {
|
|
1876
|
+
severity: nonEmptyString(usage.spend?.severity),
|
|
1877
|
+
}),
|
|
1878
|
+
exhausted: usedAmount >= limitAmount,
|
|
1879
|
+
};
|
|
1880
|
+
}
|
|
1881
|
+
function mapBindingWindow(limits) {
|
|
1882
|
+
if (!Array.isArray(limits))
|
|
1883
|
+
return undefined;
|
|
1884
|
+
const active = limits.find((limit) => limit?.is_active === true);
|
|
1885
|
+
if (!active)
|
|
1886
|
+
return undefined;
|
|
1887
|
+
if (active.kind === 'session')
|
|
1888
|
+
return 'five_hour';
|
|
1889
|
+
if (active.kind === 'weekly_all')
|
|
1890
|
+
return 'seven_day';
|
|
1891
|
+
if (active.kind !== 'weekly_scoped' || active.group !== 'weekly') {
|
|
1892
|
+
return undefined;
|
|
1893
|
+
}
|
|
1894
|
+
const modelName = nonEmptyString(active.scope?.model?.display_name);
|
|
1895
|
+
if (!modelName)
|
|
1896
|
+
return undefined;
|
|
1897
|
+
const identity = nonEmptyString(active.scope?.model?.id) ?? modelName;
|
|
1898
|
+
const slug = slugForQuotaIdentity(identity);
|
|
1899
|
+
return slug ? `claude-weekly-scoped-${slug}` : undefined;
|
|
1900
|
+
}
|
|
1533
1901
|
function mapUsageWindow(window, checkedAt) {
|
|
1534
1902
|
if (typeof window?.utilization !== 'number')
|
|
1535
1903
|
return undefined;
|
|
@@ -1565,10 +1933,17 @@ export async function fetchOAuthQuotaSnapshot(input) {
|
|
|
1565
1933
|
}
|
|
1566
1934
|
const checkedAt = input.now?.() ?? Date.now();
|
|
1567
1935
|
const usage = (await response.json());
|
|
1936
|
+
const bindingWindow = mapBindingWindow(usage.limits);
|
|
1568
1937
|
return {
|
|
1569
1938
|
five_hour: mapUsageWindow(usage.five_hour, checkedAt),
|
|
1570
1939
|
seven_day: mapUsageWindow(usage.seven_day, checkedAt),
|
|
1571
1940
|
scoped: mapScopedWeeklyLimits(usage.limits, checkedAt),
|
|
1941
|
+
extraUsage: mapExtraUsage(usage),
|
|
1942
|
+
...(bindingWindow && {
|
|
1943
|
+
bindingWindow,
|
|
1944
|
+
bindingWindowSource: 'poll',
|
|
1945
|
+
}),
|
|
1946
|
+
source: 'poll',
|
|
1572
1947
|
checkedAt,
|
|
1573
1948
|
};
|
|
1574
1949
|
}
|
|
@@ -1587,6 +1962,7 @@ export function upsertAccount(storage, account) {
|
|
|
1587
1962
|
addedAt: storage.accounts[index]?.addedAt ?? account.addedAt,
|
|
1588
1963
|
...(account.type === 'oauth' && {
|
|
1589
1964
|
quota: account.quota,
|
|
1965
|
+
profile: account.profile,
|
|
1590
1966
|
lastRefreshedAt: account.lastRefreshedAt,
|
|
1591
1967
|
lastRefreshError: account.lastRefreshError,
|
|
1592
1968
|
lastQuotaRefreshError: account.lastQuotaRefreshError,
|
|
@@ -39,6 +39,7 @@ export interface AccountListItem {
|
|
|
39
39
|
role: 'main' | 'fallback';
|
|
40
40
|
enabled: boolean;
|
|
41
41
|
quotaPercent: number | null;
|
|
42
|
+
tierLabel?: string;
|
|
42
43
|
}
|
|
43
44
|
export declare function buildAccountList(storage: AccountStorage): AccountListItem[];
|
|
44
45
|
export declare function executeAccountCommand(input: {
|