@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.
- package/dist/accounts.d.ts +17 -0
- package/dist/accounts.js +90 -17
- package/package.json +1 -1
package/dist/accounts.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
358
|
-
|
|
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
|
-
|
|
387
|
-
|
|
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
|
|
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
|
}
|