@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.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 "./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";
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
- function quotaSnapshotCheckedAt(quota) {
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("./logger.js");
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.toLowerCase();
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 null;
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
- .join(' ')
1507
- .toLowerCase();
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 = Math.max(account.quota.five_hour?.checkedAt ?? 0, account.quota.seven_day?.checkedAt ?? 0);
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 new Error(formatRefreshBackoffMessage(refreshError, this.now()));
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 new Error(formatRefreshBackoffMessage(refreshError, this.now()));
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 new Error(formatRefreshBackoffMessage(refreshError, this.now()));
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.refreshFallback(target.id, accessToken)
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
- target.quota = await fetchSnapshot(target.access);
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
- target.quota = await fetchSnapshot(target.access);
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 "./constants.js";
2
- import { generatePKCE } from "./pkce.js";
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(): "explicit" | "automatic" | "hybrid";
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 "./constants.js";
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';