@cortexkit/anthropic-auth-core 1.19.1 → 1.20.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 +68 -2
- package/dist/accounts.js +382 -23
- package/dist/auth.d.ts +2 -0
- package/dist/auth.js +3 -2
- package/dist/cache1h.d.ts +1 -1
- package/dist/cache1h.js +1 -1
- package/dist/cachekeep.d.ts +20 -2
- package/dist/cachekeep.js +134 -28
- package/dist/cch.d.ts +1 -0
- package/dist/cch.js +6 -1
- package/dist/claude-code.d.ts +2 -1
- package/dist/claude-code.js +3 -3
- package/dist/commands/account.js +1 -1
- package/dist/constants.d.ts +3 -3
- package/dist/dump.d.ts +12 -1
- package/dist/dump.js +166 -22
- package/dist/index.d.ts +2 -0
- package/dist/index.js +26 -24
- package/dist/killswitch.js +1 -1
- package/dist/models.d.ts +22 -6
- package/dist/models.js +16 -0
- package/dist/oauth-profile.js +1 -1
- package/dist/prime.d.ts +219 -0
- package/dist/prime.js +823 -0
- package/dist/quota-manager.d.ts +7 -0
- package/dist/quota-manager.js +24 -15
- package/dist/quotas.d.ts +4 -4
- package/dist/quotas.js +2 -2
- package/dist/relay.d.ts +2 -0
- package/dist/relay.js +10 -4
- package/dist/routing.d.ts +1 -1
- package/dist/routing.js +1 -1
- package/dist/start.d.ts +13 -0
- package/dist/start.js +29 -0
- package/dist/sticky-routing.d.ts +1 -0
- package/dist/sticky-routing.js +14 -4
- package/package.json +1 -1
package/dist/accounts.js
CHANGED
|
@@ -2,10 +2,10 @@ import { createHash, randomUUID } from 'node:crypto';
|
|
|
2
2
|
import { mkdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
|
-
import { parseRetryAfterHeader, refreshClaudeOAuthToken } from
|
|
6
|
-
import { CACHE_1H_MODES, CLAUDE_CODE_VERSION, DEFAULT_CACHE_1H_MODE, } from
|
|
7
|
-
import { log, logger } from
|
|
8
|
-
import { tokenFingerprint } from
|
|
5
|
+
import { parseRetryAfterHeader, refreshClaudeOAuthToken } from './auth.js';
|
|
6
|
+
import { CACHE_1H_MODES, CLAUDE_CODE_VERSION, DEFAULT_CACHE_1H_MODE, } from './constants.js';
|
|
7
|
+
import { log, logger } from './logger.js';
|
|
8
|
+
import { tokenFingerprint } from './token-fingerprint.js';
|
|
9
9
|
const setRefreshLockRenewalTimeout = globalThis.setTimeout.bind(globalThis);
|
|
10
10
|
const clearRefreshLockRenewalTimeout = globalThis.clearTimeout.bind(globalThis);
|
|
11
11
|
export const ACCOUNT_FILE_NAME = 'anthropic-auth.json';
|
|
@@ -116,6 +116,9 @@ function normalizeAccount(value) {
|
|
|
116
116
|
return {
|
|
117
117
|
...normalizeAccountBase(value),
|
|
118
118
|
type: 'oauth',
|
|
119
|
+
authLineageId: typeof value.authLineageId === 'string' && value.authLineageId.trim()
|
|
120
|
+
? value.authLineageId
|
|
121
|
+
: undefined,
|
|
119
122
|
access: typeof value.access === 'string' ? value.access : undefined,
|
|
120
123
|
refresh: value.refresh,
|
|
121
124
|
expires: typeof value.expires === 'number' ? value.expires : undefined,
|
|
@@ -126,6 +129,7 @@ function normalizeAccount(value) {
|
|
|
126
129
|
lastQuotaRefreshError: normalizeOperationError(value.lastQuotaRefreshError),
|
|
127
130
|
quota: normalizeQuota(value.quota),
|
|
128
131
|
profile: normalizeOAuthAccountProfile(value.profile),
|
|
132
|
+
prime: normalizePrimeUsageCounters(value.prime),
|
|
129
133
|
};
|
|
130
134
|
}
|
|
131
135
|
function normalizeOAuthAccountProfile(value) {
|
|
@@ -192,6 +196,27 @@ function normalizeQuotaWindow(value) {
|
|
|
192
196
|
resetsAt: typeof value.resetsAt === 'string' ? value.resetsAt : undefined,
|
|
193
197
|
};
|
|
194
198
|
}
|
|
199
|
+
function normalizePrimeUsageCounters(value) {
|
|
200
|
+
if (!isRecord(value))
|
|
201
|
+
return undefined;
|
|
202
|
+
const count = Number(value.count);
|
|
203
|
+
const inputTokens = Number(value.inputTokens);
|
|
204
|
+
const outputTokens = Number(value.outputTokens);
|
|
205
|
+
const since = Number(value.since);
|
|
206
|
+
if (![count, inputTokens, outputTokens, since].every(Number.isFinite) ||
|
|
207
|
+
count < 0 ||
|
|
208
|
+
inputTokens < 0 ||
|
|
209
|
+
outputTokens < 0 ||
|
|
210
|
+
since < 0) {
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
count: Math.floor(count),
|
|
215
|
+
inputTokens: Math.floor(inputTokens),
|
|
216
|
+
outputTokens: Math.floor(outputTokens),
|
|
217
|
+
since: Math.floor(since),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
195
220
|
function normalizeQuota(value) {
|
|
196
221
|
if (!isRecord(value))
|
|
197
222
|
return undefined;
|
|
@@ -326,6 +351,35 @@ function normalizeStorage(value) {
|
|
|
326
351
|
relay: isRecord(value.relay) ? value.relay : undefined,
|
|
327
352
|
logging: isRecord(value.logging) ? value.logging : undefined,
|
|
328
353
|
killswitch: isRecord(value.killswitch) ? value.killswitch : undefined,
|
|
354
|
+
prime: (() => {
|
|
355
|
+
if (!isRecord(value.prime))
|
|
356
|
+
return undefined;
|
|
357
|
+
const enabled = typeof value.prime.enabled === 'boolean'
|
|
358
|
+
? value.prime.enabled
|
|
359
|
+
: undefined;
|
|
360
|
+
const main = normalizePrimeUsageCounters(value.prime.main);
|
|
361
|
+
const mainAuthLineageId = typeof value.prime.mainAuthLineageId === 'string' &&
|
|
362
|
+
value.prime.mainAuthLineageId.trim()
|
|
363
|
+
? value.prime.mainAuthLineageId
|
|
364
|
+
: undefined;
|
|
365
|
+
const mainAuthLineageRefreshTokenFingerprint = typeof value.prime.mainAuthLineageRefreshTokenFingerprint ===
|
|
366
|
+
'string' && value.prime.mainAuthLineageRefreshTokenFingerprint.trim()
|
|
367
|
+
? value.prime.mainAuthLineageRefreshTokenFingerprint
|
|
368
|
+
: undefined;
|
|
369
|
+
if (enabled === undefined &&
|
|
370
|
+
!main &&
|
|
371
|
+
!mainAuthLineageId &&
|
|
372
|
+
!mainAuthLineageRefreshTokenFingerprint)
|
|
373
|
+
return undefined;
|
|
374
|
+
return {
|
|
375
|
+
...(enabled !== undefined && { enabled }),
|
|
376
|
+
...(main && { main }),
|
|
377
|
+
...(mainAuthLineageId && { mainAuthLineageId }),
|
|
378
|
+
...(mainAuthLineageRefreshTokenFingerprint && {
|
|
379
|
+
mainAuthLineageRefreshTokenFingerprint,
|
|
380
|
+
}),
|
|
381
|
+
};
|
|
382
|
+
})(),
|
|
329
383
|
accounts: value.accounts
|
|
330
384
|
.map(normalizeAccount)
|
|
331
385
|
.filter((account) => account != null),
|
|
@@ -424,6 +478,40 @@ function mergeConfigAndState(configValue, stateValue) {
|
|
|
424
478
|
mainQuotaToken: mainQuotaSource.quotaToken,
|
|
425
479
|
mainLastQuotaApiError: mainQuotaSource.lastQuotaApiError,
|
|
426
480
|
}),
|
|
481
|
+
// Carry the main-side prime counters from the state file back into the
|
|
482
|
+
// merged storage so a subsequent read sees the cumulative counters. The
|
|
483
|
+
// `enabled` flag stays sourced from the config side; main-side counters
|
|
484
|
+
// live exclusively on the state file.
|
|
485
|
+
prime: (() => {
|
|
486
|
+
const configPrime = isRecord(configValue.prime)
|
|
487
|
+
? configValue.prime
|
|
488
|
+
: undefined;
|
|
489
|
+
const mainCounters = normalizePrimeUsageCounters(mainState?.prime);
|
|
490
|
+
const mainAuthLineageId = typeof mainState?.primeAuthLineageId === 'string' &&
|
|
491
|
+
mainState.primeAuthLineageId.trim()
|
|
492
|
+
? mainState.primeAuthLineageId
|
|
493
|
+
: undefined;
|
|
494
|
+
const mainAuthLineageRefreshTokenFingerprint = typeof mainState?.primeAuthLineageRefreshTokenFingerprint ===
|
|
495
|
+
'string' && mainState.primeAuthLineageRefreshTokenFingerprint.trim()
|
|
496
|
+
? mainState.primeAuthLineageRefreshTokenFingerprint
|
|
497
|
+
: undefined;
|
|
498
|
+
if (!configPrime &&
|
|
499
|
+
!mainCounters &&
|
|
500
|
+
!mainAuthLineageId &&
|
|
501
|
+
!mainAuthLineageRefreshTokenFingerprint)
|
|
502
|
+
return undefined;
|
|
503
|
+
return {
|
|
504
|
+
...(configPrime &&
|
|
505
|
+
typeof configPrime.enabled === 'boolean' && {
|
|
506
|
+
enabled: configPrime.enabled,
|
|
507
|
+
}),
|
|
508
|
+
...(mainCounters && { main: mainCounters }),
|
|
509
|
+
...(mainAuthLineageId && { mainAuthLineageId }),
|
|
510
|
+
...(mainAuthLineageRefreshTokenFingerprint && {
|
|
511
|
+
mainAuthLineageRefreshTokenFingerprint,
|
|
512
|
+
}),
|
|
513
|
+
};
|
|
514
|
+
})(),
|
|
427
515
|
accounts,
|
|
428
516
|
};
|
|
429
517
|
}
|
|
@@ -464,6 +552,7 @@ function accountRuntimeState(account) {
|
|
|
464
552
|
});
|
|
465
553
|
}
|
|
466
554
|
return objectWithDefinedEntries({
|
|
555
|
+
authLineageId: account.authLineageId,
|
|
467
556
|
access: account.access,
|
|
468
557
|
refresh: account.refresh,
|
|
469
558
|
expires: account.expires,
|
|
@@ -473,9 +562,11 @@ function accountRuntimeState(account) {
|
|
|
473
562
|
lastQuotaRefreshError: account.lastQuotaRefreshError,
|
|
474
563
|
quota: account.quota,
|
|
475
564
|
profile: account.profile,
|
|
565
|
+
prime: account.prime,
|
|
476
566
|
});
|
|
477
567
|
}
|
|
478
|
-
|
|
568
|
+
/** Returns the latest timestamp carried by any quota snapshot window. */
|
|
569
|
+
export function quotaSnapshotCheckedAt(quota) {
|
|
479
570
|
return Math.max(quota?.five_hour?.checkedAt ?? 0, quota?.seven_day?.checkedAt ?? 0, ...(quota?.scoped?.map((window) => window.checkedAt) ?? []), quota?.checkedAt ?? 0);
|
|
480
571
|
}
|
|
481
572
|
function quotaSourcePrecedence(quota) {
|
|
@@ -652,6 +743,15 @@ function configFromStorage(storage) {
|
|
|
652
743
|
cacheKeep: storage.cacheKeep,
|
|
653
744
|
relay: storage.relay,
|
|
654
745
|
killswitch: storage.killswitch,
|
|
746
|
+
prime: (() => {
|
|
747
|
+
// Config side carries ONLY the `enabled` flag — runtime counters and
|
|
748
|
+
// lineage bindings stay on the state file. Write `enabled` whenever it was explicitly set so a
|
|
749
|
+
// toggle off persists `{ enabled: false }` and is visible to a stale
|
|
750
|
+
// reader that only inspects the config.
|
|
751
|
+
if (typeof storage.prime?.enabled !== 'boolean')
|
|
752
|
+
return undefined;
|
|
753
|
+
return { enabled: storage.prime.enabled };
|
|
754
|
+
})(),
|
|
655
755
|
accounts: storage.accounts.map(accountConfig),
|
|
656
756
|
});
|
|
657
757
|
}
|
|
@@ -827,6 +927,29 @@ function applyMainRefreshStatePatch(state, storage) {
|
|
|
827
927
|
state.main.refreshLeaseUntil = storage.refresh?.mainRefreshLeaseUntil;
|
|
828
928
|
state.main.refreshLeaseTokenHash = storage.refresh?.mainRefreshLeaseTokenHash;
|
|
829
929
|
}
|
|
930
|
+
function applyMainPrimeStatePatch(state, storage) {
|
|
931
|
+
const incoming = storage.prime?.main;
|
|
932
|
+
const incomingAuthLineageId = storage.prime?.mainAuthLineageId;
|
|
933
|
+
const incomingAuthLineageRefreshTokenFingerprint = storage.prime?.mainAuthLineageRefreshTokenFingerprint;
|
|
934
|
+
if (!incoming &&
|
|
935
|
+
!incomingAuthLineageId &&
|
|
936
|
+
!incomingAuthLineageRefreshTokenFingerprint)
|
|
937
|
+
return;
|
|
938
|
+
state.main = state.main ?? {};
|
|
939
|
+
// Last-writer-wins on prime counters — the only writer is the prime manager
|
|
940
|
+
// itself, monotonically accumulating per success, so there is no race for an
|
|
941
|
+
// older write to overwrite a newer one within this process. Across processes
|
|
942
|
+
// the cross-process claim marker (#1247) keeps the fire exclusive.
|
|
943
|
+
if (incoming)
|
|
944
|
+
state.main.prime = incoming;
|
|
945
|
+
if (incomingAuthLineageId) {
|
|
946
|
+
state.main.primeAuthLineageId = incomingAuthLineageId;
|
|
947
|
+
}
|
|
948
|
+
if (incomingAuthLineageRefreshTokenFingerprint) {
|
|
949
|
+
state.main.primeAuthLineageRefreshTokenFingerprint =
|
|
950
|
+
incomingAuthLineageRefreshTokenFingerprint;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
830
953
|
function pruneUndefined(value) {
|
|
831
954
|
if (Array.isArray(value))
|
|
832
955
|
return value.map(pruneUndefined);
|
|
@@ -941,6 +1064,8 @@ async function saveAccountStateUnlocked(storage, path, scope) {
|
|
|
941
1064
|
applyMainQuotaStatePatch(next, storage);
|
|
942
1065
|
if (scope.mainRefresh)
|
|
943
1066
|
applyMainRefreshStatePatch(next, storage);
|
|
1067
|
+
if (scope.mainPrime)
|
|
1068
|
+
applyMainPrimeStatePatch(next, storage);
|
|
944
1069
|
if (scope.accounts) {
|
|
945
1070
|
const ids = scope.accounts === true ? null : new Set(scope.accounts);
|
|
946
1071
|
next.accounts = { ...(isRecord(next.accounts) ? next.accounts : {}) };
|
|
@@ -1284,6 +1409,203 @@ export async function setCacheKeepSubagentsEnabled(enabled, path = getAccountSto
|
|
|
1284
1409
|
await saveAccounts(storage, path);
|
|
1285
1410
|
return storage;
|
|
1286
1411
|
}
|
|
1412
|
+
export function isPrimePersistentlyEnabled(storage) {
|
|
1413
|
+
return storage?.prime?.enabled === true;
|
|
1414
|
+
}
|
|
1415
|
+
export async function setPrimePersistentEnabled(enabled, path = getAccountStoragePath()) {
|
|
1416
|
+
const storage = (await loadAccounts(path)) ?? createEmptyStorage();
|
|
1417
|
+
storage.prime = {
|
|
1418
|
+
...(storage.prime ?? {}),
|
|
1419
|
+
enabled,
|
|
1420
|
+
};
|
|
1421
|
+
await saveAccounts(storage, path);
|
|
1422
|
+
return storage;
|
|
1423
|
+
}
|
|
1424
|
+
/**
|
|
1425
|
+
* Return the stable prime marker identity for an OAuth account, seeding it in
|
|
1426
|
+
* runtime state when older storage has no lineage yet. Main lookups return
|
|
1427
|
+
* undefined when the live refresh token is unavailable and no lineage exists.
|
|
1428
|
+
*/
|
|
1429
|
+
export async function getOrCreatePrimeAuthLineageId(accountId, path = getAccountStoragePath(), mainRefreshToken) {
|
|
1430
|
+
return enqueueSave(async () => {
|
|
1431
|
+
const configLock = await acquireAccountConfigWriteLock(path);
|
|
1432
|
+
try {
|
|
1433
|
+
const stateLock = await acquireAccountStateWriteLock(path);
|
|
1434
|
+
try {
|
|
1435
|
+
const storage = (await loadAccounts(path)) ?? createEmptyStorage();
|
|
1436
|
+
if (accountId === 'main') {
|
|
1437
|
+
const existing = storage.prime?.mainAuthLineageId;
|
|
1438
|
+
const observedFingerprint = mainRefreshToken
|
|
1439
|
+
? tokenFingerprint(mainRefreshToken)
|
|
1440
|
+
: undefined;
|
|
1441
|
+
const boundFingerprint = storage.prime?.mainAuthLineageRefreshTokenFingerprint;
|
|
1442
|
+
if (existing) {
|
|
1443
|
+
if (!observedFingerprint ||
|
|
1444
|
+
boundFingerprint === observedFingerprint)
|
|
1445
|
+
return existing;
|
|
1446
|
+
if (boundFingerprint &&
|
|
1447
|
+
storage.refresh?.mainRefreshLeaseUntil &&
|
|
1448
|
+
storage.refresh.mainRefreshLeaseUntil > Date.now()) {
|
|
1449
|
+
// The owner binds before publishing its rotated credential. An
|
|
1450
|
+
// active lease makes either side of that handoff ambiguous, so
|
|
1451
|
+
// suppress this tick instead of classifying it as a re-login.
|
|
1452
|
+
return existing;
|
|
1453
|
+
}
|
|
1454
|
+
if (!boundFingerprint) {
|
|
1455
|
+
// Legacy lineages predate refresh-token binding. The first
|
|
1456
|
+
// observation attaches the credential without changing markers.
|
|
1457
|
+
storage.prime = {
|
|
1458
|
+
...(storage.prime ?? {}),
|
|
1459
|
+
mainAuthLineageRefreshTokenFingerprint: observedFingerprint,
|
|
1460
|
+
};
|
|
1461
|
+
await saveAccountStateUnlocked(storage, path, {
|
|
1462
|
+
mainPrime: true,
|
|
1463
|
+
});
|
|
1464
|
+
return existing;
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
// Without the live host credential there is no safe way to bind a
|
|
1468
|
+
// new identity. Skip this tick rather than creating an unbound
|
|
1469
|
+
// namespace that could churn or suppress another account.
|
|
1470
|
+
if (!observedFingerprint)
|
|
1471
|
+
return undefined;
|
|
1472
|
+
// A bound mismatch that did not pass through the owned refresh path
|
|
1473
|
+
// is a host credential replacement and needs a fresh marker identity.
|
|
1474
|
+
const authLineageId = randomUUID();
|
|
1475
|
+
storage.prime = {
|
|
1476
|
+
...(storage.prime ?? {}),
|
|
1477
|
+
mainAuthLineageId: authLineageId,
|
|
1478
|
+
...(observedFingerprint && {
|
|
1479
|
+
mainAuthLineageRefreshTokenFingerprint: observedFingerprint,
|
|
1480
|
+
}),
|
|
1481
|
+
};
|
|
1482
|
+
await saveAccountStateUnlocked(storage, path, { mainPrime: true });
|
|
1483
|
+
return authLineageId;
|
|
1484
|
+
}
|
|
1485
|
+
const account = storage.accounts.find((candidate) => candidate.id === accountId && isOAuthAccount(candidate));
|
|
1486
|
+
if (!account) {
|
|
1487
|
+
throw new Error(`getOrCreatePrimeAuthLineageId: OAuth account "${accountId}" not found`);
|
|
1488
|
+
}
|
|
1489
|
+
if (account.authLineageId)
|
|
1490
|
+
return account.authLineageId;
|
|
1491
|
+
// Upgrading changes the marker namespace once, so an already-primed
|
|
1492
|
+
// window may fire again; persisting the seed prevents every later token
|
|
1493
|
+
// rotation from repeating that migration cost.
|
|
1494
|
+
const authLineageId = randomUUID();
|
|
1495
|
+
account.authLineageId = authLineageId;
|
|
1496
|
+
await saveAccountStateUnlocked(storage, path, {
|
|
1497
|
+
accounts: [accountId],
|
|
1498
|
+
});
|
|
1499
|
+
return authLineageId;
|
|
1500
|
+
}
|
|
1501
|
+
finally {
|
|
1502
|
+
await stateLock.release();
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
finally {
|
|
1506
|
+
await configLock.release();
|
|
1507
|
+
}
|
|
1508
|
+
});
|
|
1509
|
+
}
|
|
1510
|
+
/** Advance a main lineage across a refresh performed by this plugin. */
|
|
1511
|
+
export async function continueMainPrimeAuthLineageAfterRefresh(input, path = getAccountStoragePath()) {
|
|
1512
|
+
return enqueueSave(async () => {
|
|
1513
|
+
const configLock = await acquireAccountConfigWriteLock(path);
|
|
1514
|
+
try {
|
|
1515
|
+
const stateLock = await acquireAccountStateWriteLock(path);
|
|
1516
|
+
try {
|
|
1517
|
+
const storage = (await loadAccounts(path)) ?? createEmptyStorage();
|
|
1518
|
+
const authLineageId = storage.prime?.mainAuthLineageId;
|
|
1519
|
+
if (!authLineageId)
|
|
1520
|
+
return;
|
|
1521
|
+
const previousFingerprint = tokenFingerprint(input.previousRefreshToken);
|
|
1522
|
+
const currentFingerprint = tokenFingerprint(input.currentRefreshToken);
|
|
1523
|
+
const boundFingerprint = storage.prime?.mainAuthLineageRefreshTokenFingerprint;
|
|
1524
|
+
if (boundFingerprint &&
|
|
1525
|
+
boundFingerprint !== previousFingerprint &&
|
|
1526
|
+
boundFingerprint !== currentFingerprint) {
|
|
1527
|
+
// Another process observed a host replacement after this refresh
|
|
1528
|
+
// started; the stale rotation must not rebind that newer lineage.
|
|
1529
|
+
return;
|
|
1530
|
+
}
|
|
1531
|
+
storage.prime = {
|
|
1532
|
+
...(storage.prime ?? {}),
|
|
1533
|
+
mainAuthLineageRefreshTokenFingerprint: currentFingerprint,
|
|
1534
|
+
};
|
|
1535
|
+
await saveAccountStateUnlocked(storage, path, { mainPrime: true });
|
|
1536
|
+
}
|
|
1537
|
+
finally {
|
|
1538
|
+
await stateLock.release();
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
finally {
|
|
1542
|
+
await configLock.release();
|
|
1543
|
+
}
|
|
1544
|
+
});
|
|
1545
|
+
}
|
|
1546
|
+
/**
|
|
1547
|
+
* Atomically increment an account's cumulative prime counters and persist via
|
|
1548
|
+
* the scoped runtime-state path. The `main` account lives at state.main.prime;
|
|
1549
|
+
* every other account lives at state.accounts[id].prime. Config-side writes
|
|
1550
|
+
* are intentionally NOT triggered so prime counters cannot leak into
|
|
1551
|
+
* `anthropic-auth.json`. Callers should not depend on this function to mutate
|
|
1552
|
+
* the caller's storage object.
|
|
1553
|
+
*/
|
|
1554
|
+
export async function incrementPrimeUsagePersistent(accountId, usage, path = getAccountStoragePath(), now = Date.now()) {
|
|
1555
|
+
const inputTokens = Number.isFinite(usage?.inputTokens)
|
|
1556
|
+
? Math.max(0, Math.floor(usage.inputTokens))
|
|
1557
|
+
: 0;
|
|
1558
|
+
const outputTokens = Number.isFinite(usage?.outputTokens)
|
|
1559
|
+
? Math.max(0, Math.floor(usage.outputTokens))
|
|
1560
|
+
: 0;
|
|
1561
|
+
return enqueueSave(async () => {
|
|
1562
|
+
// The config-write lock is the repository-wide outer lock for config and
|
|
1563
|
+
// runtime-state RMW operations; taking it before the state write preserves
|
|
1564
|
+
// saveAccountsLocked's config → state ordering across processes.
|
|
1565
|
+
const lock = await acquireAccountConfigWriteLock(path);
|
|
1566
|
+
try {
|
|
1567
|
+
const stateLock = await acquireAccountStateWriteLock(path);
|
|
1568
|
+
try {
|
|
1569
|
+
const storage = (await loadAccounts(path)) ?? createEmptyStorage();
|
|
1570
|
+
if (accountId === 'main') {
|
|
1571
|
+
const existing = storage.prime?.main;
|
|
1572
|
+
const next = {
|
|
1573
|
+
count: (existing?.count ?? 0) + 1,
|
|
1574
|
+
inputTokens: (existing?.inputTokens ?? 0) + inputTokens,
|
|
1575
|
+
outputTokens: (existing?.outputTokens ?? 0) + outputTokens,
|
|
1576
|
+
since: existing?.since ?? Math.floor(now),
|
|
1577
|
+
};
|
|
1578
|
+
storage.prime = { ...(storage.prime ?? {}), main: next };
|
|
1579
|
+
await saveAccountStateUnlocked(storage, path, { mainPrime: true });
|
|
1580
|
+
return next;
|
|
1581
|
+
}
|
|
1582
|
+
const index = storage.accounts.findIndex((account) => account.id === accountId);
|
|
1583
|
+
if (index < 0) {
|
|
1584
|
+
throw new Error(`incrementPrimeUsagePersistent: account "${accountId}" not found`);
|
|
1585
|
+
}
|
|
1586
|
+
const account = storage.accounts[index];
|
|
1587
|
+
const existing = account.prime;
|
|
1588
|
+
const next = {
|
|
1589
|
+
count: (existing?.count ?? 0) + 1,
|
|
1590
|
+
inputTokens: (existing?.inputTokens ?? 0) + inputTokens,
|
|
1591
|
+
outputTokens: (existing?.outputTokens ?? 0) + outputTokens,
|
|
1592
|
+
since: existing?.since ?? Math.floor(now),
|
|
1593
|
+
};
|
|
1594
|
+
storage.accounts[index] = { ...account, prime: next };
|
|
1595
|
+
await saveAccountStateUnlocked(storage, path, {
|
|
1596
|
+
accounts: [accountId],
|
|
1597
|
+
});
|
|
1598
|
+
return next;
|
|
1599
|
+
}
|
|
1600
|
+
finally {
|
|
1601
|
+
await stateLock.release();
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
finally {
|
|
1605
|
+
await lock.release();
|
|
1606
|
+
}
|
|
1607
|
+
});
|
|
1608
|
+
}
|
|
1287
1609
|
function getFallbackStatuses(storage) {
|
|
1288
1610
|
return storage?.fallbackOn?.length ? storage.fallbackOn : DEFAULT_FALLBACK_ON;
|
|
1289
1611
|
}
|
|
@@ -1418,6 +1740,27 @@ export function formatRefreshBackoffMessage(error, now) {
|
|
|
1418
1740
|
const seconds = Math.max(1, Math.ceil(((error.nextRetryAt ?? now) - now) / 1000));
|
|
1419
1741
|
return `Claude OAuth refresh is backed off for ${seconds}s after: ${error.message}`;
|
|
1420
1742
|
}
|
|
1743
|
+
function createRefreshBackoffActiveError(error, now) {
|
|
1744
|
+
return Object.assign(new Error(formatRefreshBackoffMessage(error, now)), {
|
|
1745
|
+
refreshBackoffError: error,
|
|
1746
|
+
});
|
|
1747
|
+
}
|
|
1748
|
+
function existingRefreshBackoffError(error) {
|
|
1749
|
+
if (!error || typeof error !== 'object')
|
|
1750
|
+
return undefined;
|
|
1751
|
+
const existing = error
|
|
1752
|
+
.refreshBackoffError;
|
|
1753
|
+
return existing && typeof existing.message === 'string' ? existing : undefined;
|
|
1754
|
+
}
|
|
1755
|
+
export function getFallbackReauthLabels(storage) {
|
|
1756
|
+
if (!storage)
|
|
1757
|
+
return [];
|
|
1758
|
+
return storage.accounts
|
|
1759
|
+
.filter((account) => account.enabled !== false &&
|
|
1760
|
+
isOAuthAccount(account) &&
|
|
1761
|
+
isPermanentRefreshError(account.lastRefreshError))
|
|
1762
|
+
.map((account) => account.label?.trim() || account.id);
|
|
1763
|
+
}
|
|
1421
1764
|
export function isQuotaPolicyAuthError(error) {
|
|
1422
1765
|
const status = error.status;
|
|
1423
1766
|
if (status === 403)
|
|
@@ -1454,7 +1797,7 @@ export function getPersistedLogLevel(storage) {
|
|
|
1454
1797
|
return storage?.logging?.level;
|
|
1455
1798
|
}
|
|
1456
1799
|
export async function setLogLevelPersistent(level, path = getAccountStoragePath()) {
|
|
1457
|
-
const { setLogLevel } = await import(
|
|
1800
|
+
const { setLogLevel } = await import('./logger.js');
|
|
1458
1801
|
const storage = (await loadAccounts(path)) ?? createEmptyStorage();
|
|
1459
1802
|
storage.logging = {
|
|
1460
1803
|
...(storage.logging ?? {}),
|
|
@@ -1486,15 +1829,18 @@ function failClosedOnUnknownQuota(storage) {
|
|
|
1486
1829
|
return (storage?.quota?.failClosedOnUnknownQuota ??
|
|
1487
1830
|
DEFAULT_FAIL_CLOSED_ON_UNKNOWN_QUOTA);
|
|
1488
1831
|
}
|
|
1832
|
+
function normalizeScopedQuotaModel(value) {
|
|
1833
|
+
return value.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
1834
|
+
}
|
|
1489
1835
|
function scopedQuotaModelKey(model) {
|
|
1490
1836
|
if (typeof model !== 'string')
|
|
1491
1837
|
return null;
|
|
1492
|
-
const normalized = model
|
|
1838
|
+
const normalized = normalizeScopedQuotaModel(model);
|
|
1493
1839
|
if (normalized.includes('fable'))
|
|
1494
1840
|
return 'fable';
|
|
1495
1841
|
if (normalized.includes('mythos'))
|
|
1496
1842
|
return 'mythos';
|
|
1497
|
-
return
|
|
1843
|
+
return normalized;
|
|
1498
1844
|
}
|
|
1499
1845
|
export function getScopedQuotaWindowForModel(quota, model) {
|
|
1500
1846
|
const key = scopedQuotaModelKey(model);
|
|
@@ -1503,8 +1849,8 @@ export function getScopedQuotaWindowForModel(quota, model) {
|
|
|
1503
1849
|
return quota?.scoped?.find((window) => {
|
|
1504
1850
|
const haystack = [window.modelId, window.modelName, window.title]
|
|
1505
1851
|
.filter((value) => typeof value === 'string')
|
|
1506
|
-
.
|
|
1507
|
-
.
|
|
1852
|
+
.map(normalizeScopedQuotaModel)
|
|
1853
|
+
.join(' ');
|
|
1508
1854
|
return haystack.includes(key);
|
|
1509
1855
|
});
|
|
1510
1856
|
}
|
|
@@ -1997,6 +2343,11 @@ function formatErrorMessage(error) {
|
|
|
1997
2343
|
return error instanceof Error ? error.message : String(error);
|
|
1998
2344
|
}
|
|
1999
2345
|
function recordRefreshError(account, error, now) {
|
|
2346
|
+
const existing = existingRefreshBackoffError(error);
|
|
2347
|
+
if (existing) {
|
|
2348
|
+
account.lastRefreshError = existing;
|
|
2349
|
+
return;
|
|
2350
|
+
}
|
|
2000
2351
|
account.lastRefreshError = buildRefreshOperationError({
|
|
2001
2352
|
error,
|
|
2002
2353
|
now,
|
|
@@ -2005,7 +2356,7 @@ function recordRefreshError(account, error, now) {
|
|
|
2005
2356
|
});
|
|
2006
2357
|
}
|
|
2007
2358
|
function recordQuotaRefreshError(account, error, now) {
|
|
2008
|
-
if (isQuotaPolicyAuthError(error))
|
|
2359
|
+
if (isQuotaPolicyAuthError(error) || existingRefreshBackoffError(error))
|
|
2009
2360
|
return;
|
|
2010
2361
|
account.lastQuotaRefreshError = buildQuotaOperationError({
|
|
2011
2362
|
error,
|
|
@@ -2052,7 +2403,7 @@ export class FallbackAccountManager {
|
|
|
2052
2403
|
return;
|
|
2053
2404
|
if (!account.quota)
|
|
2054
2405
|
return;
|
|
2055
|
-
const checkedAt =
|
|
2406
|
+
const checkedAt = quotaSnapshotCheckedAt(account.quota);
|
|
2056
2407
|
if (checkedAt <= 0)
|
|
2057
2408
|
return;
|
|
2058
2409
|
const existing = this.quotaManager.getFallback(account.id, account.access);
|
|
@@ -2110,7 +2461,7 @@ export class FallbackAccountManager {
|
|
|
2110
2461
|
const refreshError = next.lastRefreshError;
|
|
2111
2462
|
if (refreshError &&
|
|
2112
2463
|
refreshBackoffActive(refreshError, next.refresh, this.now())) {
|
|
2113
|
-
throw
|
|
2464
|
+
throw createRefreshBackoffActiveError(refreshError, this.now());
|
|
2114
2465
|
}
|
|
2115
2466
|
next = await this.refreshAccount(next, storage);
|
|
2116
2467
|
changed = true;
|
|
@@ -2125,7 +2476,7 @@ export class FallbackAccountManager {
|
|
|
2125
2476
|
// the background refreshQuotaForDueAccounts() guard.
|
|
2126
2477
|
if (stale &&
|
|
2127
2478
|
!quotaBackoffActive(next.lastQuotaRefreshError, this.now())) {
|
|
2128
|
-
next = await this.refreshAccountQuota(next, storage);
|
|
2479
|
+
next = (await this.refreshAccountQuota(next, storage)).account;
|
|
2129
2480
|
changed = true;
|
|
2130
2481
|
}
|
|
2131
2482
|
// Single source of truth: evaluate quota policy from the unified
|
|
@@ -2285,7 +2636,7 @@ export class FallbackAccountManager {
|
|
|
2285
2636
|
const refreshError = next.lastRefreshError;
|
|
2286
2637
|
if (refreshError &&
|
|
2287
2638
|
refreshBackoffActive(refreshError, next.refresh, this.now())) {
|
|
2288
|
-
throw
|
|
2639
|
+
throw createRefreshBackoffActiveError(refreshError, this.now());
|
|
2289
2640
|
}
|
|
2290
2641
|
next = await this.refreshAccount(next, storage);
|
|
2291
2642
|
changed = true;
|
|
@@ -2360,7 +2711,7 @@ export class FallbackAccountManager {
|
|
|
2360
2711
|
if (refreshError &&
|
|
2361
2712
|
refreshBackoffActive(refreshError, latestAccount.refresh, this.now())) {
|
|
2362
2713
|
updateStoredAccount(storage, latestAccount);
|
|
2363
|
-
throw
|
|
2714
|
+
throw createRefreshBackoffActiveError(refreshError, this.now());
|
|
2364
2715
|
}
|
|
2365
2716
|
}
|
|
2366
2717
|
return null;
|
|
@@ -2411,11 +2762,14 @@ export class FallbackAccountManager {
|
|
|
2411
2762
|
});
|
|
2412
2763
|
const refreshed = await refreshClaudeOAuthToken({
|
|
2413
2764
|
refreshToken,
|
|
2765
|
+
authLineageId: sourceAccount.authLineageId,
|
|
2414
2766
|
fetchImpl: this.fetchImpl,
|
|
2415
2767
|
now: this.now,
|
|
2416
2768
|
});
|
|
2417
2769
|
sourceAccount.access = refreshed.access;
|
|
2418
2770
|
sourceAccount.refresh = refreshed.refresh;
|
|
2771
|
+
sourceAccount.authLineageId =
|
|
2772
|
+
refreshed.authLineageId ?? sourceAccount.authLineageId;
|
|
2419
2773
|
sourceAccount.expires = refreshed.expires;
|
|
2420
2774
|
sourceAccount.lastRefreshedAt =
|
|
2421
2775
|
refreshed.expires - refreshed.expiresIn * 1000;
|
|
@@ -2443,15 +2797,18 @@ export class FallbackAccountManager {
|
|
|
2443
2797
|
// deduplication and 429 backoff gating around the same quota API. Fall back
|
|
2444
2798
|
// to a direct fetch only when no QuotaManager is wired (e.g. in isolation).
|
|
2445
2799
|
const fetchSnapshot = (accessToken) => this.quotaManager
|
|
2446
|
-
? this.quotaManager.
|
|
2800
|
+
? this.quotaManager.refreshFallbackWithMetadata(target.id, accessToken)
|
|
2447
2801
|
: fetchOAuthQuotaSnapshot({
|
|
2448
2802
|
accessToken,
|
|
2449
2803
|
fetchImpl: this.fetchImpl,
|
|
2450
2804
|
now: this.now,
|
|
2451
|
-
});
|
|
2805
|
+
}).then((quota) => ({ quota, fetched: true }));
|
|
2452
2806
|
const fetchStartedAt = this.now();
|
|
2807
|
+
let fetched = false;
|
|
2453
2808
|
try {
|
|
2454
|
-
|
|
2809
|
+
const result = await fetchSnapshot(target.access);
|
|
2810
|
+
target.quota = result.quota;
|
|
2811
|
+
fetched = result.fetched;
|
|
2455
2812
|
}
|
|
2456
2813
|
catch (error) {
|
|
2457
2814
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -2463,7 +2820,9 @@ export class FallbackAccountManager {
|
|
|
2463
2820
|
if (!target.access)
|
|
2464
2821
|
throw error;
|
|
2465
2822
|
// 401 does not arm QuotaManager backoff, so this retry proceeds.
|
|
2466
|
-
|
|
2823
|
+
const result = await fetchSnapshot(target.access);
|
|
2824
|
+
target.quota = result.quota;
|
|
2825
|
+
fetched = result.fetched;
|
|
2467
2826
|
}
|
|
2468
2827
|
const latestStorage = await this.load();
|
|
2469
2828
|
const latestAccount = latestStorage?.accounts.find((candidate) => candidate.id === target.id && isOAuthAccount(candidate));
|
|
@@ -2472,7 +2831,7 @@ export class FallbackAccountManager {
|
|
|
2472
2831
|
latestAccount.access !== target.access) {
|
|
2473
2832
|
this.seedFallbackQuota(latestAccount, latestStorage);
|
|
2474
2833
|
updateStoredAccount(storage, latestAccount);
|
|
2475
|
-
return latestAccount;
|
|
2834
|
+
return { account: latestAccount, fetched: false };
|
|
2476
2835
|
}
|
|
2477
2836
|
if (latestStorage &&
|
|
2478
2837
|
latestAccount?.access === target.access &&
|
|
@@ -2481,7 +2840,7 @@ export class FallbackAccountManager {
|
|
|
2481
2840
|
quotaSnapshotIsFresh(latestAccount.quota, latestStorage, this.now())) {
|
|
2482
2841
|
this.seedFallbackQuota(latestAccount, latestStorage);
|
|
2483
2842
|
updateStoredAccount(storage, latestAccount);
|
|
2484
|
-
return latestAccount;
|
|
2843
|
+
return { account: latestAccount, fetched };
|
|
2485
2844
|
}
|
|
2486
2845
|
target.lastQuotaRefreshError = undefined;
|
|
2487
2846
|
updateStoredAccount(storage, target);
|
|
@@ -2496,6 +2855,6 @@ export class FallbackAccountManager {
|
|
|
2496
2855
|
checkedAt: now,
|
|
2497
2856
|
}, target.access);
|
|
2498
2857
|
}
|
|
2499
|
-
return target;
|
|
2858
|
+
return { account: target, fetched };
|
|
2500
2859
|
}
|
|
2501
2860
|
}
|
package/dist/auth.d.ts
CHANGED
|
@@ -18,9 +18,11 @@ export type ClaudeOAuthRefreshResult = {
|
|
|
18
18
|
refresh: string;
|
|
19
19
|
expires: number;
|
|
20
20
|
expiresIn: number;
|
|
21
|
+
authLineageId?: string;
|
|
21
22
|
};
|
|
22
23
|
export declare function refreshClaudeOAuthToken(input: {
|
|
23
24
|
refreshToken: string;
|
|
25
|
+
authLineageId?: string;
|
|
24
26
|
fetchImpl?: typeof fetch;
|
|
25
27
|
now?: () => number;
|
|
26
28
|
maxRetries?: number;
|
package/dist/auth.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { AUTHORIZE_URLS, AXIOS_USER_AGENT, CLIENT_ID, CODE_CALLBACK_URL, OAUTH_SCOPES, REFRESH_SCOPE, TOKEN_URL, } from
|
|
2
|
-
import { generatePKCE } from
|
|
1
|
+
import { AUTHORIZE_URLS, AXIOS_USER_AGENT, CLIENT_ID, CODE_CALLBACK_URL, OAUTH_SCOPES, REFRESH_SCOPE, TOKEN_URL, } from './constants.js';
|
|
2
|
+
import { generatePKCE } from './pkce.js';
|
|
3
3
|
export function parseRetryAfterHeader(value) {
|
|
4
4
|
if (!value)
|
|
5
5
|
return undefined;
|
|
@@ -83,6 +83,7 @@ export async function refreshClaudeOAuthToken(input) {
|
|
|
83
83
|
refresh: json.refresh_token ?? input.refreshToken,
|
|
84
84
|
expires: refreshedAt + json.expires_in * 1000,
|
|
85
85
|
expiresIn: json.expires_in,
|
|
86
|
+
authLineageId: input.authLineageId,
|
|
86
87
|
};
|
|
87
88
|
}
|
|
88
89
|
catch (error) {
|
package/dist/cache1h.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export type Cache1hCommandAction = {
|
|
|
13
13
|
type: 'usage';
|
|
14
14
|
};
|
|
15
15
|
export declare function isCache1hEnabled(): boolean;
|
|
16
|
-
export declare function getCache1hMode(): "
|
|
16
|
+
export declare function getCache1hMode(): "automatic" | "explicit" | "hybrid";
|
|
17
17
|
export declare function setCache1hEnabled(enabled: boolean): void;
|
|
18
18
|
export declare function setCache1hMode(mode: Cache1hMode): void;
|
|
19
19
|
export declare function setCache1hState(input: {
|
package/dist/cache1h.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DEFAULT_CACHE_1H_MODE } from
|
|
1
|
+
import { DEFAULT_CACHE_1H_MODE } from './constants.js';
|
|
2
2
|
export const CACHE_1H_COMMAND_NAME = 'claude-cache';
|
|
3
3
|
const CACHE_1H_STATUS_TITLE = '## Claude Cache Status';
|
|
4
4
|
const CACHE_1H_ENABLED_TITLE = '## Claude Cache Enabled';
|