@cortexkit/anthropic-auth-core 1.19.0 → 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 +72 -2
- package/dist/accounts.js +390 -26
- package/dist/auth.d.ts +3 -0
- package/dist/auth.js +5 -3
- package/dist/cache1h.d.ts +1 -1
- package/dist/cache1h.js +1 -1
- package/dist/cachekeep.d.ts +22 -2
- package/dist/cachekeep.js +137 -30
- 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 +4 -0
- package/dist/relay.js +29 -18
- 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 +13 -1
- package/dist/sticky-routing.js +58 -2
- 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,
|
|
@@ -2031,12 +2382,17 @@ export class FallbackAccountManager {
|
|
|
2031
2382
|
quotaTimer = null;
|
|
2032
2383
|
quotaManager;
|
|
2033
2384
|
onFallbackStorageChanged;
|
|
2385
|
+
setIntervalImpl;
|
|
2386
|
+
clearIntervalImpl;
|
|
2034
2387
|
constructor(options = {}) {
|
|
2035
2388
|
this.now = options.now ?? Date.now;
|
|
2036
2389
|
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
2037
2390
|
this.configPath = options.configPath ?? getAccountStoragePath();
|
|
2038
2391
|
this.quotaManager = options.quotaManager ?? null;
|
|
2039
2392
|
this.onFallbackStorageChanged = options.onFallbackStorageChanged;
|
|
2393
|
+
this.setIntervalImpl = options.setIntervalImpl ?? globalThis.setInterval;
|
|
2394
|
+
this.clearIntervalImpl =
|
|
2395
|
+
options.clearIntervalImpl ?? globalThis.clearInterval;
|
|
2040
2396
|
}
|
|
2041
2397
|
/**
|
|
2042
2398
|
* Seed QuotaManager from persisted account.quota if no cache entry exists
|
|
@@ -2047,7 +2403,7 @@ export class FallbackAccountManager {
|
|
|
2047
2403
|
return;
|
|
2048
2404
|
if (!account.quota)
|
|
2049
2405
|
return;
|
|
2050
|
-
const checkedAt =
|
|
2406
|
+
const checkedAt = quotaSnapshotCheckedAt(account.quota);
|
|
2051
2407
|
if (checkedAt <= 0)
|
|
2052
2408
|
return;
|
|
2053
2409
|
const existing = this.quotaManager.getFallback(account.id, account.access);
|
|
@@ -2075,7 +2431,7 @@ export class FallbackAccountManager {
|
|
|
2075
2431
|
};
|
|
2076
2432
|
void run().catch(() => { });
|
|
2077
2433
|
if (!this.refreshTimer) {
|
|
2078
|
-
this.refreshTimer =
|
|
2434
|
+
this.refreshTimer = this.setIntervalImpl(() => {
|
|
2079
2435
|
void run().catch(() => { });
|
|
2080
2436
|
}, BACKGROUND_TICK_MS + jitterMs(BACKGROUND_TICK_JITTER_MS));
|
|
2081
2437
|
if ('unref' in this.refreshTimer)
|
|
@@ -2084,9 +2440,9 @@ export class FallbackAccountManager {
|
|
|
2084
2440
|
}
|
|
2085
2441
|
stopBackgroundRefresh() {
|
|
2086
2442
|
if (this.refreshTimer)
|
|
2087
|
-
|
|
2443
|
+
this.clearIntervalImpl(this.refreshTimer);
|
|
2088
2444
|
if (this.quotaTimer)
|
|
2089
|
-
|
|
2445
|
+
this.clearIntervalImpl(this.quotaTimer);
|
|
2090
2446
|
this.refreshTimer = null;
|
|
2091
2447
|
this.quotaTimer = null;
|
|
2092
2448
|
}
|
|
@@ -2105,7 +2461,7 @@ export class FallbackAccountManager {
|
|
|
2105
2461
|
const refreshError = next.lastRefreshError;
|
|
2106
2462
|
if (refreshError &&
|
|
2107
2463
|
refreshBackoffActive(refreshError, next.refresh, this.now())) {
|
|
2108
|
-
throw
|
|
2464
|
+
throw createRefreshBackoffActiveError(refreshError, this.now());
|
|
2109
2465
|
}
|
|
2110
2466
|
next = await this.refreshAccount(next, storage);
|
|
2111
2467
|
changed = true;
|
|
@@ -2120,7 +2476,7 @@ export class FallbackAccountManager {
|
|
|
2120
2476
|
// the background refreshQuotaForDueAccounts() guard.
|
|
2121
2477
|
if (stale &&
|
|
2122
2478
|
!quotaBackoffActive(next.lastQuotaRefreshError, this.now())) {
|
|
2123
|
-
next = await this.refreshAccountQuota(next, storage);
|
|
2479
|
+
next = (await this.refreshAccountQuota(next, storage)).account;
|
|
2124
2480
|
changed = true;
|
|
2125
2481
|
}
|
|
2126
2482
|
// Single source of truth: evaluate quota policy from the unified
|
|
@@ -2280,7 +2636,7 @@ export class FallbackAccountManager {
|
|
|
2280
2636
|
const refreshError = next.lastRefreshError;
|
|
2281
2637
|
if (refreshError &&
|
|
2282
2638
|
refreshBackoffActive(refreshError, next.refresh, this.now())) {
|
|
2283
|
-
throw
|
|
2639
|
+
throw createRefreshBackoffActiveError(refreshError, this.now());
|
|
2284
2640
|
}
|
|
2285
2641
|
next = await this.refreshAccount(next, storage);
|
|
2286
2642
|
changed = true;
|
|
@@ -2355,7 +2711,7 @@ export class FallbackAccountManager {
|
|
|
2355
2711
|
if (refreshError &&
|
|
2356
2712
|
refreshBackoffActive(refreshError, latestAccount.refresh, this.now())) {
|
|
2357
2713
|
updateStoredAccount(storage, latestAccount);
|
|
2358
|
-
throw
|
|
2714
|
+
throw createRefreshBackoffActiveError(refreshError, this.now());
|
|
2359
2715
|
}
|
|
2360
2716
|
}
|
|
2361
2717
|
return null;
|
|
@@ -2406,11 +2762,14 @@ export class FallbackAccountManager {
|
|
|
2406
2762
|
});
|
|
2407
2763
|
const refreshed = await refreshClaudeOAuthToken({
|
|
2408
2764
|
refreshToken,
|
|
2765
|
+
authLineageId: sourceAccount.authLineageId,
|
|
2409
2766
|
fetchImpl: this.fetchImpl,
|
|
2410
2767
|
now: this.now,
|
|
2411
2768
|
});
|
|
2412
2769
|
sourceAccount.access = refreshed.access;
|
|
2413
2770
|
sourceAccount.refresh = refreshed.refresh;
|
|
2771
|
+
sourceAccount.authLineageId =
|
|
2772
|
+
refreshed.authLineageId ?? sourceAccount.authLineageId;
|
|
2414
2773
|
sourceAccount.expires = refreshed.expires;
|
|
2415
2774
|
sourceAccount.lastRefreshedAt =
|
|
2416
2775
|
refreshed.expires - refreshed.expiresIn * 1000;
|
|
@@ -2438,15 +2797,18 @@ export class FallbackAccountManager {
|
|
|
2438
2797
|
// deduplication and 429 backoff gating around the same quota API. Fall back
|
|
2439
2798
|
// to a direct fetch only when no QuotaManager is wired (e.g. in isolation).
|
|
2440
2799
|
const fetchSnapshot = (accessToken) => this.quotaManager
|
|
2441
|
-
? this.quotaManager.
|
|
2800
|
+
? this.quotaManager.refreshFallbackWithMetadata(target.id, accessToken)
|
|
2442
2801
|
: fetchOAuthQuotaSnapshot({
|
|
2443
2802
|
accessToken,
|
|
2444
2803
|
fetchImpl: this.fetchImpl,
|
|
2445
2804
|
now: this.now,
|
|
2446
|
-
});
|
|
2805
|
+
}).then((quota) => ({ quota, fetched: true }));
|
|
2447
2806
|
const fetchStartedAt = this.now();
|
|
2807
|
+
let fetched = false;
|
|
2448
2808
|
try {
|
|
2449
|
-
|
|
2809
|
+
const result = await fetchSnapshot(target.access);
|
|
2810
|
+
target.quota = result.quota;
|
|
2811
|
+
fetched = result.fetched;
|
|
2450
2812
|
}
|
|
2451
2813
|
catch (error) {
|
|
2452
2814
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -2458,7 +2820,9 @@ export class FallbackAccountManager {
|
|
|
2458
2820
|
if (!target.access)
|
|
2459
2821
|
throw error;
|
|
2460
2822
|
// 401 does not arm QuotaManager backoff, so this retry proceeds.
|
|
2461
|
-
|
|
2823
|
+
const result = await fetchSnapshot(target.access);
|
|
2824
|
+
target.quota = result.quota;
|
|
2825
|
+
fetched = result.fetched;
|
|
2462
2826
|
}
|
|
2463
2827
|
const latestStorage = await this.load();
|
|
2464
2828
|
const latestAccount = latestStorage?.accounts.find((candidate) => candidate.id === target.id && isOAuthAccount(candidate));
|
|
@@ -2467,7 +2831,7 @@ export class FallbackAccountManager {
|
|
|
2467
2831
|
latestAccount.access !== target.access) {
|
|
2468
2832
|
this.seedFallbackQuota(latestAccount, latestStorage);
|
|
2469
2833
|
updateStoredAccount(storage, latestAccount);
|
|
2470
|
-
return latestAccount;
|
|
2834
|
+
return { account: latestAccount, fetched: false };
|
|
2471
2835
|
}
|
|
2472
2836
|
if (latestStorage &&
|
|
2473
2837
|
latestAccount?.access === target.access &&
|
|
@@ -2476,7 +2840,7 @@ export class FallbackAccountManager {
|
|
|
2476
2840
|
quotaSnapshotIsFresh(latestAccount.quota, latestStorage, this.now())) {
|
|
2477
2841
|
this.seedFallbackQuota(latestAccount, latestStorage);
|
|
2478
2842
|
updateStoredAccount(storage, latestAccount);
|
|
2479
|
-
return latestAccount;
|
|
2843
|
+
return { account: latestAccount, fetched };
|
|
2480
2844
|
}
|
|
2481
2845
|
target.lastQuotaRefreshError = undefined;
|
|
2482
2846
|
updateStoredAccount(storage, target);
|
|
@@ -2491,6 +2855,6 @@ export class FallbackAccountManager {
|
|
|
2491
2855
|
checkedAt: now,
|
|
2492
2856
|
}, target.access);
|
|
2493
2857
|
}
|
|
2494
|
-
return target;
|
|
2858
|
+
return { account: target, fetched };
|
|
2495
2859
|
}
|
|
2496
2860
|
}
|
package/dist/auth.d.ts
CHANGED
|
@@ -18,13 +18,16 @@ 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;
|
|
27
29
|
baseDelayMs?: number;
|
|
30
|
+
setTimeoutImpl?: typeof globalThis.setTimeout;
|
|
28
31
|
}): Promise<ClaudeOAuthRefreshResult>;
|
|
29
32
|
export type AuthorizationResult = {
|
|
30
33
|
url: string;
|