@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.
@@ -12,6 +12,11 @@ export type QuotaEntry = {
12
12
  refreshAfter: number;
13
13
  checkedAt: number;
14
14
  };
15
+ export type QuotaRefreshResult = {
16
+ quota: OAuthQuotaSnapshot;
17
+ /** False when backoff or a cross-process lock served the existing cache. */
18
+ fetched: boolean;
19
+ };
15
20
  export type QuotaManagerOptions = {
16
21
  storage: AccountStorage | null;
17
22
  fetchImpl?: typeof fetch;
@@ -58,7 +63,9 @@ export declare class QuotaManager {
58
63
  pushMainFromHeaders(accessToken: string, incoming: OAuthQuotaSnapshot): QuotaEntry;
59
64
  pushFallbackFromHeaders(accountId: string, accessToken: string, incoming: OAuthQuotaSnapshot): QuotaEntry;
60
65
  refreshMain(accessToken: string): Promise<OAuthQuotaSnapshot>;
66
+ refreshMainWithMetadata(accessToken: string): Promise<QuotaRefreshResult>;
61
67
  refreshFallback(accountId: string, accessToken: string): Promise<OAuthQuotaSnapshot>;
68
+ refreshFallbackWithMetadata(accountId: string, accessToken: string): Promise<QuotaRefreshResult>;
62
69
  refreshAllFallbacks(accounts: OAuthAccount[]): Promise<void>;
63
70
  /**
64
71
  * Fire-and-forget refresh. Does not await, swallows errors.
@@ -5,10 +5,10 @@
5
5
  * share one QuotaManager instance so they see the same in-memory cache.
6
6
  * Handles deduplication, rate-limiting (429 backoff), and staleness.
7
7
  */
8
- import { acquireRefreshFileLock, buildQuotaOperationError, fetchOAuthQuotaSnapshot, getPersistedMainQuota, getQuotaCheckIntervalMs, getQuotaNextRefreshAt, getQuotaRefreshEveryNRequests, getScopedQuotaWindowForModel, isQuotaPolicyAuthError, quotaBackoffActive, } from "./accounts.js";
9
- import { mergeHeaderQuotaSnapshot } from "./quota-headers.js";
10
- export { tokenFingerprint } from "./token-fingerprint.js";
11
- import { tokenFingerprint } from "./token-fingerprint.js";
8
+ import { acquireRefreshFileLock, buildQuotaOperationError, fetchOAuthQuotaSnapshot, getPersistedMainQuota, getQuotaCheckIntervalMs, getQuotaNextRefreshAt, getQuotaRefreshEveryNRequests, getScopedQuotaWindowForModel, isQuotaPolicyAuthError, quotaBackoffActive, quotaSnapshotCheckedAt, } from './accounts.js';
9
+ import { mergeHeaderQuotaSnapshot } from './quota-headers.js';
10
+ export { tokenFingerprint } from './token-fingerprint.js';
11
+ import { tokenFingerprint } from './token-fingerprint.js';
12
12
  // Capture real setTimeout before tests can mock globalThis.setTimeout
13
13
  const nativeSetTimeout = globalThis.setTimeout;
14
14
  function mergePollCompletionWithNewerHeaders(current, polled) {
@@ -158,6 +158,9 @@ export class QuotaManager {
158
158
  // Refresh (async, deduplicated, rate-limited)
159
159
  // =========================================================================
160
160
  async refreshMain(accessToken) {
161
+ return (await this.refreshMainWithMetadata(accessToken)).quota;
162
+ }
163
+ async refreshMainWithMetadata(accessToken) {
161
164
  // If the main account/token changed, invalidate the cache (including a
162
165
  // persisted seed) BEFORE the backoff short-circuit so a different account's
163
166
  // stale quota is never returned while the quota API is backed off.
@@ -172,7 +175,7 @@ export class QuotaManager {
172
175
  // Rate-limit — if API recently 429'd, return stale or throw
173
176
  if (this.isBackedOff()) {
174
177
  if (this.main)
175
- return this.main.quota;
178
+ return { quota: this.main.quota, fetched: false };
176
179
  throw new Error('Quota API rate-limited — try again later');
177
180
  }
178
181
  this.inflightMainFp = fp;
@@ -180,6 +183,10 @@ export class QuotaManager {
180
183
  return this.inflightMain;
181
184
  }
182
185
  async refreshFallback(accountId, accessToken) {
186
+ return (await this.refreshFallbackWithMetadata(accountId, accessToken))
187
+ .quota;
188
+ }
189
+ async refreshFallbackWithMetadata(accountId, accessToken) {
183
190
  // Deduplicate per account+token so a same-label re-login never joins a
184
191
  // quota probe that was started with the previous credentials.
185
192
  const inflightKey = QuotaManager.fallbackInflightKey(accountId, accessToken);
@@ -190,7 +197,7 @@ export class QuotaManager {
190
197
  if (this.isFallbackBackedOff(accountId, accessToken)) {
191
198
  const cached = this.getFallback(accountId, accessToken);
192
199
  if (cached)
193
- return cached.quota;
200
+ return { quota: cached.quota, fetched: false };
194
201
  throw new Error('Quota API rate-limited — try again later');
195
202
  }
196
203
  const promise = this._fetchFallback(accountId, accessToken);
@@ -319,7 +326,7 @@ export class QuotaManager {
319
326
  continue;
320
327
  if (!account.quota)
321
328
  continue;
322
- const checkedAt = Math.max(account.quota.five_hour?.checkedAt ?? 0, account.quota.seven_day?.checkedAt ?? 0);
329
+ const checkedAt = quotaSnapshotCheckedAt(account.quota);
323
330
  if (checkedAt <= 0)
324
331
  continue;
325
332
  const existing = this.getFallback(account.id, account.access);
@@ -397,7 +404,7 @@ export class QuotaManager {
397
404
  // a preceding queued call while we waited
398
405
  if (this.isBackedOff()) {
399
406
  if (this.main)
400
- return this.main.quota;
407
+ return { quota: this.main.quota, fetched: false };
401
408
  throw new Error('Quota API rate-limited — try again later');
402
409
  }
403
410
  const fileLock = await acquireRefreshFileLock({
@@ -406,8 +413,9 @@ export class QuotaManager {
406
413
  });
407
414
  if (!fileLock) {
408
415
  const cached = this.main;
409
- if (cached && this.now() < cached.refreshAfter)
410
- return cached.quota;
416
+ if (cached && this.now() < cached.refreshAfter) {
417
+ return { quota: cached.quota, fetched: false };
418
+ }
411
419
  throw new Error('Quota refresh is already in progress');
412
420
  }
413
421
  try {
@@ -427,7 +435,7 @@ export class QuotaManager {
427
435
  };
428
436
  this.mainLastApiError = undefined;
429
437
  this.onMainQuotaFetched?.(completedQuota, now, this.mainTokenFp, fetchStartedAt);
430
- return completedQuota;
438
+ return { quota: completedQuota, fetched: true };
431
439
  }
432
440
  catch (error) {
433
441
  this._handleMainFetchError(error);
@@ -452,7 +460,7 @@ export class QuotaManager {
452
460
  if (this.isFallbackBackedOff(accountId, accessToken)) {
453
461
  const cached = this.getFallback(accountId, accessToken);
454
462
  if (cached)
455
- return cached.quota;
463
+ return { quota: cached.quota, fetched: false };
456
464
  throw new Error('Quota API rate-limited — try again later');
457
465
  }
458
466
  const fileLock = await acquireRefreshFileLock({
@@ -461,8 +469,9 @@ export class QuotaManager {
461
469
  });
462
470
  if (!fileLock) {
463
471
  const cached = this.getFallback(accountId, accessToken);
464
- if (cached && this.now() < cached.refreshAfter)
465
- return cached.quota;
472
+ if (cached && this.now() < cached.refreshAfter) {
473
+ return { quota: cached.quota, fetched: false };
474
+ }
466
475
  throw new Error('Quota refresh is already in progress');
467
476
  }
468
477
  try {
@@ -480,7 +489,7 @@ export class QuotaManager {
480
489
  }, accessToken);
481
490
  this.fallbackApiErrors.delete(accountId);
482
491
  this.fallbackErrorTokenFps.delete(accountId);
483
- return completedQuota;
492
+ return { quota: completedQuota, fetched: true };
484
493
  }
485
494
  finally {
486
495
  await fileLock.release();
package/dist/quotas.d.ts CHANGED
@@ -11,13 +11,13 @@ export type QuotaAccountSummary = {
11
11
  };
12
12
  export declare function formatQuotaMoney(money: QuotaMoney): string;
13
13
  export declare function buildFallbackQuotaSummaries(storage: AccountStorage | null, errors?: ReadonlyMap<string, string>): {
14
- error?: string | undefined;
15
- lastRefreshedAt: number | undefined;
16
- tierLabel?: string | undefined;
17
14
  name: string;
18
- role: "fallback";
15
+ role: 'fallback';
19
16
  enabled: boolean;
20
17
  quota: OAuthQuotaSnapshot | undefined;
18
+ tierLabel?: string | undefined;
19
+ lastRefreshedAt: number | undefined;
20
+ error?: string | undefined;
21
21
  }[];
22
22
  export declare function buildClaudeQuotaSummary(input: {
23
23
  accounts: QuotaAccountSummary[];
package/dist/quotas.js CHANGED
@@ -1,5 +1,5 @@
1
- import { isOAuthAccount } from "./accounts.js";
2
- import { formatOAuthAccountTier } from "./oauth-profile.js";
1
+ import { isOAuthAccount } from './accounts.js';
2
+ import { formatOAuthAccountTier } from './oauth-profile.js';
3
3
  export const CLAUDE_QUOTAS_COMMAND_NAME = 'claude-quota';
4
4
  const WINDOW_LABELS = {
5
5
  five_hour: '5h',
package/dist/relay.d.ts CHANGED
@@ -31,6 +31,8 @@ export declare function sendViaRelay(options: {
31
31
  * within an attempt are ignored.
32
32
  */
33
33
  onResponseHeaders?: (headers: Headers) => void;
34
+ dumpTag?: import('./dump.ts').DumpTag;
35
+ onDumpCreated?: (handle: import('./dump.ts').DumpHandle) => void;
34
36
  setTimeoutImpl?: typeof globalThis.setTimeout;
35
37
  clearTimeoutImpl?: typeof globalThis.clearTimeout;
36
38
  }): Promise<Response>;
package/dist/relay.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Buffer } from 'node:buffer';
2
2
  import { createHash, randomBytes } from 'node:crypto';
3
- import { dumpRelayRequest } from "./dump.js";
4
- import { relayLog } from "./logger.js";
3
+ import { dumpRelayRequest } from './dump.js';
4
+ import { relayLog } from './logger.js';
5
5
  class RelayStateMismatchError extends Error {
6
6
  }
7
7
  class RelayWebSocketConnectionResetError extends Error {
@@ -752,7 +752,7 @@ function getPersistentRelaySession(config, affinity, timers) {
752
752
  return session;
753
753
  }
754
754
  export async function sendViaRelay(options) {
755
- const { config, input, init, headers, body, fallback, affinity: explicitAffinity, optimisticResponse, onResponseHeaders, setTimeoutImpl = globalThis.setTimeout, clearTimeoutImpl = globalThis.clearTimeout, } = options;
755
+ const { config, input, init, headers, body, fallback, affinity: explicitAffinity, optimisticResponse, onResponseHeaders, dumpTag, onDumpCreated, setTimeoutImpl = globalThis.setTimeout, clearTimeoutImpl = globalThis.clearTimeout, } = options;
756
756
  if (!config || !isRelayableAnthropicRequest(input, body))
757
757
  return fallback();
758
758
  const affinity = explicitAffinity ||
@@ -848,7 +848,7 @@ export async function sendViaRelay(options) {
848
848
  });
849
849
  relayLog(`used relay transport=${result.transport} protocol=${result.protocol} mode=${result.payload.mode} status=${result.response.status} session=${shortAffinity(affinity)} bodyBytes=${bodyText.length} relayBytes=${actualPayloadBytes}`);
850
850
  const dumpStart = perfNowMs();
851
- await dumpRelayRequest({
851
+ const dumpHandle = await dumpRelayRequest({
852
852
  affinity,
853
853
  transport: result.transport,
854
854
  protocol: result.protocol,
@@ -858,7 +858,13 @@ export async function sendViaRelay(options) {
858
858
  previousBodyText: previous?.body,
859
859
  payload: result.payload,
860
860
  relayBytes: actualPayloadBytes,
861
+ tag: dumpTag,
861
862
  });
863
+ try {
864
+ if (dumpHandle)
865
+ onDumpCreated?.(dumpHandle);
866
+ }
867
+ catch { }
862
868
  relayPerfLog('dump', {
863
869
  session: shortAffinity(affinity),
864
870
  ms: formatMs(perfNowMs() - dumpStart),
package/dist/routing.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { type AccountStorage, type RoutingMode } from './accounts.ts';
2
2
  export declare const CLAUDE_ROUTING_COMMAND_NAME = "claude-routing";
3
- export declare const ROUTING_MODES: readonly ["main-first", "fallback-first", "sticky-balanced"];
3
+ export declare const ROUTING_MODES: readonly ['main-first', 'fallback-first', 'sticky-balanced'];
4
4
  export declare const DEFAULT_ROUTING_MODE: RoutingMode;
5
5
  type RoutingCommandAction = {
6
6
  type: 'status';
package/dist/routing.js CHANGED
@@ -1,4 +1,4 @@
1
- import { createEmptyStorage, getAccountStoragePath, loadAccounts, saveAccounts, } from "./accounts.js";
1
+ import { createEmptyStorage, getAccountStoragePath, loadAccounts, saveAccounts, } from './accounts.js';
2
2
  export const CLAUDE_ROUTING_COMMAND_NAME = 'claude-routing';
3
3
  export const ROUTING_MODES = [
4
4
  'main-first',
@@ -0,0 +1,13 @@
1
+ export declare const CLAUDE_START_COMMAND_NAME = "claude-start";
2
+ export type LaneStartCommandAction = {
3
+ type: 'fire';
4
+ } | {
5
+ type: 'usage';
6
+ };
7
+ export declare function parseLaneStartCommandAction(input: string): LaneStartCommandAction;
8
+ export declare function executeLaneStartCommand(input: {
9
+ argumentsText: string;
10
+ }): {
11
+ action: LaneStartCommandAction;
12
+ text: string;
13
+ };
package/dist/start.js ADDED
@@ -0,0 +1,29 @@
1
+ export const CLAUDE_START_COMMAND_NAME = 'claude-start';
2
+ const START_STATUS_TITLE = '## Claude Lane Start Status';
3
+ const START_QUEUED_TITLE = '## Claude Lane Start Queued';
4
+ const START_USAGE_TITLE = '## Claude Lane Start Usage';
5
+ const START_USAGE = 'Usage: `/claude-start`.';
6
+ export function parseLaneStartCommandAction(input) {
7
+ const normalized = input.trim().split(/\s+/).filter(Boolean);
8
+ if (normalized.length === 0)
9
+ return { type: 'fire' };
10
+ return { type: 'usage' };
11
+ }
12
+ export function executeLaneStartCommand(input) {
13
+ const action = parseLaneStartCommandAction(input.argumentsText);
14
+ if (action.type === 'fire') {
15
+ return {
16
+ action,
17
+ text: [
18
+ START_QUEUED_TITLE,
19
+ '',
20
+ '- Queued an explicit lane-start request.',
21
+ '- This is a billed request: the one-token limit does not remove prompt-cache read or write charges.',
22
+ ].join('\n'),
23
+ };
24
+ }
25
+ return {
26
+ action,
27
+ text: [START_USAGE_TITLE, '', START_USAGE, '', START_STATUS_TITLE].join('\n'),
28
+ };
29
+ }
@@ -40,6 +40,7 @@ export type StickyQuotaFailureDecision = {
40
40
  */
41
41
  export declare function createStickyNoRouteResponse(input: {
42
42
  mainRefreshError?: AccountOperationError;
43
+ fallbackReauthLabels?: readonly string[];
43
44
  routeQuotas: readonly OAuthQuotaSnapshot[];
44
45
  modelId?: string;
45
46
  now?: number;
@@ -1,8 +1,8 @@
1
1
  import { createHash, randomUUID } from 'node:crypto';
2
2
  import { mkdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
3
3
  import { basename, dirname, join } from 'node:path';
4
- import { acquireRefreshFileLock, getKillswitchThresholdsForAccount, getQuotaCheckIntervalMs, getQuotaMinimumRemainingThresholds, getScopedQuotaWindowForModel, isKillswitchEnabled, isPermanentRefreshError, killswitchRetryAfterSeconds, quotaSnapshotPassesModelScope, } from "./accounts.js";
5
- import { isClaudeFableOrMythos5Model } from "./models.js";
4
+ import { acquireRefreshFileLock, getKillswitchThresholdsForAccount, getQuotaCheckIntervalMs, getQuotaMinimumRemainingThresholds, getScopedQuotaWindowForModel, isKillswitchEnabled, isPermanentRefreshError, killswitchRetryAfterSeconds, quotaSnapshotPassesModelScope, } from './accounts.js';
5
+ import { isClaudeFableOrMythos5Model } from './models.js';
6
6
  export const STICKY_ROUTING_MAIN_ACCOUNT_ID = 'main';
7
7
  export const STICKY_ROUTING_SHORT_RESET_GRACE_MS = 15 * 60_000;
8
8
  export const STICKY_ROUTING_ASSIGNMENT_TTL_MS = 7 * 24 * 60 * 60_000;
@@ -24,13 +24,23 @@ export function createStickyNoRouteResponse(input) {
24
24
  .map((quota) => getScopedQuotaWindowForModel(quota, modelId))
25
25
  .find(Boolean)?.modelName
26
26
  : undefined;
27
+ let authenticationMessage;
27
28
  if (isPermanentRefreshError(input.mainRefreshError)) {
28
- const message = modelName
29
+ authenticationMessage = modelName
29
30
  ? `Main Claude OAuth account requires re-login, and no fallback OAuth account is currently routable for ${modelName}.`
30
31
  : 'Main Claude OAuth account requires re-login, and no fallback OAuth account is currently routable.';
32
+ }
33
+ else if (input.fallbackReauthLabels?.length) {
34
+ const scope = modelName ? ` for ${modelName}` : '';
35
+ authenticationMessage = `No OAuth account is currently routable${scope}. Fallback Claude OAuth accounts require re-login: ${input.fallbackReauthLabels.join(', ')}.`;
36
+ }
37
+ if (authenticationMessage) {
31
38
  return new Response(JSON.stringify({
32
39
  type: 'error',
33
- error: { type: 'authentication_error', message },
40
+ error: {
41
+ type: 'authentication_error',
42
+ message: authenticationMessage,
43
+ },
34
44
  }), {
35
45
  status: 401,
36
46
  headers: { 'content-type': 'application/json' },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cortexkit/anthropic-auth-core",
3
- "version": "1.19.1",
3
+ "version": "1.20.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",