@cortexkit/anthropic-auth-core 1.1.0 → 1.1.1

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.
@@ -20,6 +20,9 @@ export type OAuthAccount = {
20
20
  export type AccountOperationError = {
21
21
  message: string;
22
22
  checkedAt: number;
23
+ nextRetryAt?: number;
24
+ retryCount?: number;
25
+ tokenHash?: string;
23
26
  };
24
27
  export type AccountQuotaWindow = {
25
28
  usedPercent: number;
@@ -38,6 +41,10 @@ export type AccountStorage = {
38
41
  enabled?: boolean;
39
42
  intervalMinutes?: number;
40
43
  refreshBeforeExpiryMinutes?: number;
44
+ mainLastRefreshError?: AccountOperationError;
45
+ mainRefreshLeaseId?: string;
46
+ mainRefreshLeaseUntil?: number;
47
+ mainRefreshLeaseTokenHash?: string;
41
48
  };
42
49
  quota?: {
43
50
  enabled?: boolean;
@@ -86,6 +93,16 @@ export declare function setDumpPersistentEnabled(enabled: boolean, path?: string
86
93
  export declare function isFastModePersistentlyEnabled(storage: AccountStorage | null): boolean;
87
94
  export declare function setFastModePersistentEnabled(enabled: boolean, path?: string): Promise<AccountStorage>;
88
95
  export declare function shouldFallbackStatus(status: number, storage: AccountStorage | null): boolean;
96
+ export declare function getRefreshIntervalMs(storage: AccountStorage | null): number;
97
+ export declare function hashRefreshToken(refreshToken: string): string;
98
+ export declare function buildRefreshOperationError(input: {
99
+ error: unknown;
100
+ now: number;
101
+ refreshToken: string;
102
+ previous?: AccountOperationError;
103
+ }): AccountOperationError;
104
+ export declare function refreshBackoffActive(error: AccountOperationError | undefined, refreshToken: string | undefined, now: number): boolean;
105
+ export declare function formatRefreshBackoffMessage(error: AccountOperationError, now: number): string;
89
106
  export declare function getQuotaCheckIntervalMs(storage: AccountStorage | null): number;
90
107
  export declare function quotaSnapshotPassesPolicy(quota: OAuthQuotaSnapshot | undefined, storage: AccountStorage | null): boolean;
91
108
  export declare function getQuotaNextRefreshAt(quota: OAuthQuotaSnapshot | undefined, storage: AccountStorage | null, now: number): number;
package/dist/accounts.js CHANGED
@@ -1,13 +1,17 @@
1
- import { randomUUID } from 'node:crypto';
1
+ import { createHash, randomUUID } from 'node:crypto';
2
2
  import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
3
3
  import { homedir } from 'node:os';
4
4
  import { dirname, join } from 'node:path';
5
- import { refreshClaudeOAuthToken } from "./auth.js";
5
+ import { ClaudeOAuthRefreshError, refreshClaudeOAuthToken } from "./auth.js";
6
6
  import { CACHE_1H_MODES, CLAUDE_CODE_VERSION, DEFAULT_CACHE_1H_MODE, } from "./constants.js";
7
7
  export const ACCOUNT_FILE_NAME = 'anthropic-auth.json';
8
8
  export const QUOTA_URL = 'https://api.anthropic.com/api/oauth/usage';
9
9
  const DEFAULT_FALLBACK_ON = [401, 403, 429];
10
10
  const DEFAULT_REFRESH_BEFORE_EXPIRY_MINUTES = 30;
11
+ const DEFAULT_REFRESH_INTERVAL_MINUTES = 10;
12
+ const MIN_REFRESH_RETRY_DELAY_MS = 5 * 60_000;
13
+ const MAX_REFRESH_RETRY_DELAY_MS = 60 * 60_000;
14
+ const NON_TRANSIENT_REFRESH_RETRY_DELAY_MS = 24 * 60 * 60_000;
11
15
  const DEFAULT_QUOTA_CHECK_INTERVAL_MINUTES = 5;
12
16
  const DEFAULT_MINIMUM_REMAINING = {
13
17
  five_hour: 0,
@@ -63,7 +67,15 @@ function normalizeOperationError(value) {
63
67
  const checkedAt = Number(value.checkedAt);
64
68
  if (!Number.isFinite(checkedAt))
65
69
  return undefined;
66
- return { message: value.message, checkedAt };
70
+ const nextRetryAt = Number(value.nextRetryAt);
71
+ const retryCount = Number(value.retryCount);
72
+ return {
73
+ message: value.message,
74
+ checkedAt,
75
+ nextRetryAt: Number.isFinite(nextRetryAt) ? nextRetryAt : undefined,
76
+ retryCount: Number.isFinite(retryCount) ? retryCount : undefined,
77
+ tokenHash: typeof value.tokenHash === 'string' ? value.tokenHash : undefined,
78
+ };
67
79
  }
68
80
  function normalizeQuota(value) {
69
81
  if (!isRecord(value))
@@ -245,6 +257,50 @@ function refreshBeforeExpiryMs(storage) {
245
257
  DEFAULT_REFRESH_BEFORE_EXPIRY_MINUTES;
246
258
  return Math.max(0, minutes) * 60_000;
247
259
  }
260
+ export function getRefreshIntervalMs(storage) {
261
+ const minutes = storage?.refresh?.intervalMinutes ?? DEFAULT_REFRESH_INTERVAL_MINUTES;
262
+ return Math.max(1, minutes) * 60_000;
263
+ }
264
+ export function hashRefreshToken(refreshToken) {
265
+ return createHash('sha256').update(refreshToken).digest('hex');
266
+ }
267
+ function isTransientRefreshError(error) {
268
+ if (error instanceof ClaudeOAuthRefreshError) {
269
+ return error.status === 429 || error.status >= 500;
270
+ }
271
+ if (!(error instanceof Error))
272
+ return false;
273
+ return (error.message.includes('fetch failed') ||
274
+ ('code' in error &&
275
+ (error.code === 'ECONNRESET' ||
276
+ error.code === 'ECONNREFUSED' ||
277
+ error.code === 'ETIMEDOUT' ||
278
+ error.code === 'UND_ERR_CONNECT_TIMEOUT')));
279
+ }
280
+ export function buildRefreshOperationError(input) {
281
+ const retryCount = (input.previous?.retryCount ?? 0) + 1;
282
+ const delay = isTransientRefreshError(input.error)
283
+ ? Math.min(MAX_REFRESH_RETRY_DELAY_MS, MIN_REFRESH_RETRY_DELAY_MS * 2 ** Math.min(retryCount - 1, 6))
284
+ : NON_TRANSIENT_REFRESH_RETRY_DELAY_MS;
285
+ return {
286
+ message: formatErrorMessage(input.error),
287
+ checkedAt: input.now,
288
+ nextRetryAt: input.now + delay,
289
+ retryCount,
290
+ tokenHash: hashRefreshToken(input.refreshToken),
291
+ };
292
+ }
293
+ export function refreshBackoffActive(error, refreshToken, now) {
294
+ if (!error?.nextRetryAt || error.nextRetryAt <= now)
295
+ return false;
296
+ if (!refreshToken)
297
+ return true;
298
+ return error.tokenHash === hashRefreshToken(refreshToken);
299
+ }
300
+ export function formatRefreshBackoffMessage(error, now) {
301
+ const seconds = Math.max(1, Math.ceil(((error.nextRetryAt ?? now) - now) / 1000));
302
+ return `Claude OAuth refresh is backed off for ${seconds}s after: ${error.message}`;
303
+ }
248
304
  export function getQuotaCheckIntervalMs(storage) {
249
305
  const minutes = storage?.quota?.checkIntervalMinutes ?? DEFAULT_QUOTA_CHECK_INTERVAL_MINUTES;
250
306
  return Math.max(1, minutes) * 60_000;
@@ -353,16 +409,21 @@ function formatErrorMessage(error) {
353
409
  return error instanceof Error ? error.message : String(error);
354
410
  }
355
411
  function recordRefreshError(account, error, now) {
356
- account.lastRefreshError = {
357
- message: formatErrorMessage(error),
358
- checkedAt: now,
359
- };
412
+ account.lastRefreshError = buildRefreshOperationError({
413
+ error,
414
+ now,
415
+ refreshToken: account.refresh,
416
+ previous: account.lastRefreshError,
417
+ });
360
418
  }
361
419
  function recordQuotaRefreshError(account, error, now) {
362
420
  account.lastQuotaRefreshError = {
363
421
  message: formatErrorMessage(error),
364
422
  checkedAt: now,
365
423
  };
424
+ if (error instanceof ClaudeOAuthRefreshError) {
425
+ recordRefreshError(account, error, now);
426
+ }
366
427
  }
367
428
  export class FallbackAccountManager {
368
429
  now;
@@ -383,22 +444,18 @@ export class FallbackAccountManager {
383
444
  await saveAccounts(storage, this.configPath);
384
445
  }
385
446
  startBackgroundRefresh() {
386
- void this.refreshDueAccounts().catch(() => { });
387
- void this.refreshQuotaForDueAccounts().catch(() => { });
447
+ const run = async () => {
448
+ await this.refreshDueAccounts();
449
+ await this.refreshQuotaForDueAccounts();
450
+ };
451
+ void run().catch(() => { });
388
452
  if (!this.refreshTimer) {
389
453
  this.refreshTimer = setInterval(() => {
390
- void this.refreshDueAccounts().catch(() => { });
454
+ void run().catch(() => { });
391
455
  }, BACKGROUND_TICK_MS);
392
456
  if ('unref' in this.refreshTimer)
393
457
  this.refreshTimer.unref();
394
458
  }
395
- if (!this.quotaTimer) {
396
- this.quotaTimer = setInterval(() => {
397
- void this.refreshQuotaForDueAccounts().catch(() => { });
398
- }, BACKGROUND_TICK_MS);
399
- if ('unref' in this.quotaTimer)
400
- this.quotaTimer.unref();
401
- }
402
459
  }
403
460
  stopBackgroundRefresh() {
404
461
  if (this.refreshTimer)
@@ -420,6 +477,11 @@ export class FallbackAccountManager {
420
477
  try {
421
478
  let next = account;
422
479
  if (tokenNeedsRefresh(next, storage, this.now())) {
480
+ const refreshError = next.lastRefreshError;
481
+ if (refreshError &&
482
+ refreshBackoffActive(refreshError, next.refresh, this.now())) {
483
+ throw new Error(formatRefreshBackoffMessage(refreshError, this.now()));
484
+ }
423
485
  next = await this.refreshAccount(next, storage);
424
486
  changed = true;
425
487
  }
@@ -462,6 +524,9 @@ export class FallbackAccountManager {
462
524
  continue;
463
525
  if (!tokenNeedsRefresh(account, storage, this.now()))
464
526
  continue;
527
+ if (refreshBackoffActive(account.lastRefreshError, account.refresh, this.now())) {
528
+ continue;
529
+ }
465
530
  try {
466
531
  await this.refreshAccount(account, storage);
467
532
  changed = true;
@@ -487,6 +552,9 @@ export class FallbackAccountManager {
487
552
  let next = account;
488
553
  try {
489
554
  if (tokenNeedsRefresh(next, storage, this.now())) {
555
+ if (refreshBackoffActive(next.lastRefreshError, next.refresh, this.now())) {
556
+ continue;
557
+ }
490
558
  next = await this.refreshAccount(next, storage);
491
559
  changed = true;
492
560
  }
@@ -517,6 +585,11 @@ export class FallbackAccountManager {
517
585
  let next = account;
518
586
  try {
519
587
  if (tokenNeedsRefresh(next, storage, this.now())) {
588
+ const refreshError = next.lastRefreshError;
589
+ if (refreshError &&
590
+ refreshBackoffActive(refreshError, next.refresh, this.now())) {
591
+ throw new Error(formatRefreshBackoffMessage(refreshError, this.now()));
592
+ }
520
593
  next = await this.refreshAccount(next, storage);
521
594
  changed = true;
522
595
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cortexkit/anthropic-auth-core",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",