@cortexkit/anthropic-auth-core 1.16.0 → 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 +34 -1
- package/dist/accounts.js +414 -42
- package/dist/commands/account.d.ts +1 -0
- package/dist/commands/account.js +7 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -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)
|
|
@@ -1396,25 +1695,35 @@ export async function addAccountPersistent(account, path = getAccountStoragePath
|
|
|
1396
1695
|
await saveAccounts(storage, path);
|
|
1397
1696
|
}
|
|
1398
1697
|
export function getQuotaNextRefreshAt(quota, storage, now) {
|
|
1698
|
+
const intervalMs = getQuotaCheckIntervalMs(storage);
|
|
1399
1699
|
if (!quotaEnabled(storage))
|
|
1400
|
-
return now +
|
|
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;
|
|
1401
1710
|
const thresholds = getQuotaMinimumRemainingThresholds(storage);
|
|
1402
1711
|
const blockedResetTimes = [];
|
|
1403
1712
|
for (const key of ['five_hour', 'seven_day']) {
|
|
1404
1713
|
const window = quota?.[key];
|
|
1405
1714
|
if (!window)
|
|
1406
|
-
return now +
|
|
1715
|
+
return capAtOldestWindow(now + intervalMs);
|
|
1407
1716
|
if (window.remainingPercent >= thresholds[key])
|
|
1408
1717
|
continue;
|
|
1409
1718
|
const resetTime = window.resetsAt ? Date.parse(window.resetsAt) : Number.NaN;
|
|
1410
1719
|
if (!Number.isFinite(resetTime) || resetTime <= now) {
|
|
1411
|
-
return now +
|
|
1720
|
+
return capAtOldestWindow(now + intervalMs);
|
|
1412
1721
|
}
|
|
1413
1722
|
blockedResetTimes.push(resetTime);
|
|
1414
1723
|
}
|
|
1415
1724
|
if (!blockedResetTimes.length)
|
|
1416
|
-
return now +
|
|
1417
|
-
return Math.min(...blockedResetTimes) + 60_000;
|
|
1725
|
+
return capAtOldestWindow(now + intervalMs);
|
|
1726
|
+
return capAtOldestWindow(Math.min(...blockedResetTimes) + 60_000);
|
|
1418
1727
|
}
|
|
1419
1728
|
function tokenNeedsRefresh(account, storage, now) {
|
|
1420
1729
|
return (!account.access ||
|
|
@@ -1530,6 +1839,61 @@ function mapScopedWeeklyLimits(limits, checkedAt) {
|
|
|
1530
1839
|
}
|
|
1531
1840
|
return scoped;
|
|
1532
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
|
+
}
|
|
1533
1897
|
function mapUsageWindow(window, checkedAt) {
|
|
1534
1898
|
if (typeof window?.utilization !== 'number')
|
|
1535
1899
|
return undefined;
|
|
@@ -1565,10 +1929,17 @@ export async function fetchOAuthQuotaSnapshot(input) {
|
|
|
1565
1929
|
}
|
|
1566
1930
|
const checkedAt = input.now?.() ?? Date.now();
|
|
1567
1931
|
const usage = (await response.json());
|
|
1932
|
+
const bindingWindow = mapBindingWindow(usage.limits);
|
|
1568
1933
|
return {
|
|
1569
1934
|
five_hour: mapUsageWindow(usage.five_hour, checkedAt),
|
|
1570
1935
|
seven_day: mapUsageWindow(usage.seven_day, checkedAt),
|
|
1571
1936
|
scoped: mapScopedWeeklyLimits(usage.limits, checkedAt),
|
|
1937
|
+
extraUsage: mapExtraUsage(usage),
|
|
1938
|
+
...(bindingWindow && {
|
|
1939
|
+
bindingWindow,
|
|
1940
|
+
bindingWindowSource: 'poll',
|
|
1941
|
+
}),
|
|
1942
|
+
source: 'poll',
|
|
1572
1943
|
checkedAt,
|
|
1573
1944
|
};
|
|
1574
1945
|
}
|
|
@@ -1587,6 +1958,7 @@ export function upsertAccount(storage, account) {
|
|
|
1587
1958
|
addedAt: storage.accounts[index]?.addedAt ?? account.addedAt,
|
|
1588
1959
|
...(account.type === 'oauth' && {
|
|
1589
1960
|
quota: account.quota,
|
|
1961
|
+
profile: account.profile,
|
|
1590
1962
|
lastRefreshedAt: account.lastRefreshedAt,
|
|
1591
1963
|
lastRefreshError: account.lastRefreshError,
|
|
1592
1964
|
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: {
|
package/dist/commands/account.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { formatOAuthAccountTier } from "../oauth-profile.js";
|
|
1
2
|
export const CLAUDE_ACCOUNT_COMMAND_NAME = 'claude-account';
|
|
2
3
|
export function parseAccountCommandAction(argumentsText) {
|
|
3
4
|
const parts = argumentsText.trim().split(/\s+/).filter(Boolean);
|
|
@@ -89,6 +90,7 @@ export function buildAccountList(storage) {
|
|
|
89
90
|
role: 'main',
|
|
90
91
|
enabled: true,
|
|
91
92
|
quotaPercent: fiveHour?.usedPercent ?? null,
|
|
93
|
+
tierLabel: formatOAuthAccountTier(storage.main?.profile),
|
|
92
94
|
});
|
|
93
95
|
for (const account of storage.accounts) {
|
|
94
96
|
const fiveHourQuota = account.quota?.five_hour;
|
|
@@ -98,6 +100,9 @@ export function buildAccountList(storage) {
|
|
|
98
100
|
role: 'fallback',
|
|
99
101
|
enabled: account.enabled !== false,
|
|
100
102
|
quotaPercent: fiveHourQuota?.usedPercent ?? null,
|
|
103
|
+
tierLabel: account.type === 'oauth'
|
|
104
|
+
? formatOAuthAccountTier(account.profile)
|
|
105
|
+
: undefined,
|
|
101
106
|
});
|
|
102
107
|
}
|
|
103
108
|
return list;
|
|
@@ -124,7 +129,8 @@ export function executeAccountCommand(input) {
|
|
|
124
129
|
for (const a of list) {
|
|
125
130
|
const pct = a.quotaPercent != null ? ` ${Math.round(a.quotaPercent)}%` : '';
|
|
126
131
|
const status = !a.enabled ? ' (disabled)' : '';
|
|
127
|
-
|
|
132
|
+
const tier = a.tierLabel ? ` · ${a.tierLabel}` : '';
|
|
133
|
+
lines.push(`- **${a.label}** [${a.role}]${tier}${status}${pct}`);
|
|
128
134
|
}
|
|
129
135
|
lines.push('', USAGE_TEXT);
|
|
130
136
|
return { text: lines.join('\n') };
|