@cortexkit/anthropic-auth-core 1.18.0 → 1.19.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.
@@ -209,6 +209,8 @@ export type AccountManagerOptions = {
209
209
  configPath?: string;
210
210
  quotaManager?: import('./quota-manager.ts').QuotaManager;
211
211
  onFallbackStorageChanged?: () => void;
212
+ setIntervalImpl?: typeof globalThis.setInterval;
213
+ clearIntervalImpl?: typeof globalThis.clearInterval;
212
214
  };
213
215
  export type AccountRefreshError = {
214
216
  accountId: string;
@@ -362,6 +364,8 @@ export declare class FallbackAccountManager {
362
364
  private quotaTimer;
363
365
  readonly quotaManager: import('./quota-manager.ts').QuotaManager | null;
364
366
  private readonly onFallbackStorageChanged;
367
+ private readonly setIntervalImpl;
368
+ private readonly clearIntervalImpl;
365
369
  constructor(options?: AccountManagerOptions);
366
370
  /**
367
371
  * Seed QuotaManager from persisted account.quota if no cache entry exists
package/dist/accounts.js CHANGED
@@ -2031,12 +2031,17 @@ export class FallbackAccountManager {
2031
2031
  quotaTimer = null;
2032
2032
  quotaManager;
2033
2033
  onFallbackStorageChanged;
2034
+ setIntervalImpl;
2035
+ clearIntervalImpl;
2034
2036
  constructor(options = {}) {
2035
2037
  this.now = options.now ?? Date.now;
2036
2038
  this.fetchImpl = options.fetchImpl ?? fetch;
2037
2039
  this.configPath = options.configPath ?? getAccountStoragePath();
2038
2040
  this.quotaManager = options.quotaManager ?? null;
2039
2041
  this.onFallbackStorageChanged = options.onFallbackStorageChanged;
2042
+ this.setIntervalImpl = options.setIntervalImpl ?? globalThis.setInterval;
2043
+ this.clearIntervalImpl =
2044
+ options.clearIntervalImpl ?? globalThis.clearInterval;
2040
2045
  }
2041
2046
  /**
2042
2047
  * Seed QuotaManager from persisted account.quota if no cache entry exists
@@ -2075,7 +2080,7 @@ export class FallbackAccountManager {
2075
2080
  };
2076
2081
  void run().catch(() => { });
2077
2082
  if (!this.refreshTimer) {
2078
- this.refreshTimer = setInterval(() => {
2083
+ this.refreshTimer = this.setIntervalImpl(() => {
2079
2084
  void run().catch(() => { });
2080
2085
  }, BACKGROUND_TICK_MS + jitterMs(BACKGROUND_TICK_JITTER_MS));
2081
2086
  if ('unref' in this.refreshTimer)
@@ -2084,9 +2089,9 @@ export class FallbackAccountManager {
2084
2089
  }
2085
2090
  stopBackgroundRefresh() {
2086
2091
  if (this.refreshTimer)
2087
- clearInterval(this.refreshTimer);
2092
+ this.clearIntervalImpl(this.refreshTimer);
2088
2093
  if (this.quotaTimer)
2089
- clearInterval(this.quotaTimer);
2094
+ this.clearIntervalImpl(this.quotaTimer);
2090
2095
  this.refreshTimer = null;
2091
2096
  this.quotaTimer = null;
2092
2097
  }
package/dist/auth.d.ts CHANGED
@@ -25,6 +25,7 @@ export declare function refreshClaudeOAuthToken(input: {
25
25
  now?: () => number;
26
26
  maxRetries?: number;
27
27
  baseDelayMs?: number;
28
+ setTimeoutImpl?: typeof globalThis.setTimeout;
28
29
  }): Promise<ClaudeOAuthRefreshResult>;
29
30
  export type AuthorizationResult = {
30
31
  url: string;
package/dist/auth.js CHANGED
@@ -47,11 +47,12 @@ export async function refreshClaudeOAuthToken(input) {
47
47
  const fetchImpl = input.fetchImpl ?? fetch;
48
48
  const maxRetries = input.maxRetries ?? 2;
49
49
  const baseDelayMs = input.baseDelayMs ?? 500;
50
+ const setTimeoutImpl = input.setTimeoutImpl ?? globalThis.setTimeout;
50
51
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
51
52
  try {
52
53
  if (attempt > 0) {
53
54
  const delay = baseDelayMs * 2 ** (attempt - 1);
54
- await new Promise((resolve) => setTimeout(resolve, delay));
55
+ await new Promise((resolve) => setTimeoutImpl(resolve, delay));
55
56
  }
56
57
  const response = await fetchImpl(TOKEN_URL, {
57
58
  method: 'POST',
@@ -94,6 +94,8 @@ export declare class CacheKeepManager {
94
94
  fetchImpl?: typeof fetch;
95
95
  now?: () => number;
96
96
  prewarmTimeoutMs?: number;
97
+ setIntervalImpl?: typeof globalThis.setInterval;
98
+ clearIntervalImpl?: typeof globalThis.clearInterval;
97
99
  prepareHeaders?: (headers: Headers, target: CacheKeepTarget) => Promise<Headers> | Headers;
98
100
  onTrackedSessionsChanged?: (sessions: readonly CacheKeepTrackedSession[]) => Promise<void> | void;
99
101
  });
package/dist/cachekeep.js CHANGED
@@ -239,7 +239,7 @@ export class CacheKeepManager {
239
239
  if (this.timer)
240
240
  return;
241
241
  logger.debug('cachekeep', 'started');
242
- this.timer = setInterval(() => {
242
+ this.timer = (this.options.setIntervalImpl ?? globalThis.setInterval)(() => {
243
243
  void this.tick().catch((error) => {
244
244
  logger.warn('cachekeep', 'tick failed', {
245
245
  error: error instanceof Error ? error.message : String(error),
@@ -253,7 +253,8 @@ export class CacheKeepManager {
253
253
  if (!this.timer)
254
254
  return;
255
255
  logger.debug('cachekeep', 'stopped');
256
- clearInterval(this.timer);
256
+ const clearIntervalImpl = this.options.clearIntervalImpl ?? globalThis.clearInterval;
257
+ clearIntervalImpl(this.timer);
257
258
  this.timer = null;
258
259
  }
259
260
  stats(window, now = this.options.now?.() ?? Date.now(), always = false) {
package/dist/relay.d.ts CHANGED
@@ -31,5 +31,7 @@ export declare function sendViaRelay(options: {
31
31
  * within an attempt are ignored.
32
32
  */
33
33
  onResponseHeaders?: (headers: Headers) => void;
34
+ setTimeoutImpl?: typeof globalThis.setTimeout;
35
+ clearTimeoutImpl?: typeof globalThis.clearTimeout;
34
36
  }): Promise<Response>;
35
37
  export declare const WORKER_SCRIPT = "\nasync function hashBody(body) {\n const bytes = new TextEncoder().encode(body)\n const digest = await crypto.subtle.digest('SHA-256', bytes)\n const hex = [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, '0')).join('')\n return 'sha256:' + hex\n}\n\nfunction applyPatch(base, patch) {\n return base.slice(0, patch.start) + patch.insert + base.slice(patch.start + patch.deleteCount)\n}\n\nfunction applyPatchSet(base, patch) {\n if (!Array.isArray(patch)) return applyPatch(base, patch)\n if (patch.length === 0) return base\n\n let result = ''\n let cursor = 0\n for (const item of patch) {\n if (item.start < cursor) return { error: 'overlapping patch', status: 400 }\n result += base.slice(cursor, item.start)\n result += item.insert\n cursor = item.start + item.deleteCount\n }\n return result + base.slice(cursor)\n}\n\nasync function readState(env, affinity) {\n const raw = await env.RELAY_STATE.get('session:' + affinity)\n return raw ? JSON.parse(raw) : null\n}\n\nasync function writeState(env, affinity, state) {\n await env.RELAY_STATE.put('session:' + affinity, JSON.stringify(state), { expirationTtl: 86400 })\n}\n\nasync function resolveBody(env, payload) {\n if (payload.mode === 'full_sync') return payload.body\n if (payload.mode === 'patch') {\n const state = await readState(env, payload.affinity)\n if (!state || state.hash !== payload.base_hash) {\n return { error: 'state mismatch', status: 409 }\n }\n return applyPatchSet(state.body, payload.patch)\n }\n return { error: 'unknown mode', status: 400 }\n}\n\nasync function prepareUpstream(env, payload) {\n if ((payload.protocol !== 1 && payload.protocol !== 2) || payload.type !== 'request' || !payload.affinity || !payload.upstream?.url || !payload.next_hash) {\n return { error: 'invalid payload', status: 400 }\n }\n\n const body = await resolveBody(env, payload)\n if (body && typeof body === 'object' && 'error' in body) return body\n\n if (typeof body !== 'string' || (await hashBody(body)) !== payload.next_hash) {\n return { error: 'hash mismatch', status: 409 }\n }\n\n const stateWrite = writeState(env, payload.affinity, { body, hash: payload.next_hash, revision: payload.revision }).catch(() => {})\n console.log(JSON.stringify({\n relay: 'opencode-anthropic-auth',\n transport: 'relay',\n mode: payload.mode,\n revision: payload.revision,\n affinity: String(payload.affinity).slice(0, 12),\n bodyBytes: body.length,\n }))\n\n return { body, stateWrite }\n}\n\nfunction resolveBodyFromState(state, payload) {\n if (payload.mode === 'full_sync') return payload.body\n if (payload.mode === 'patch') {\n if (!state || state.hash !== payload.base_hash) {\n return { error: 'state mismatch', status: 409 }\n }\n return applyPatchSet(state.body, payload.patch)\n }\n return { error: 'unknown mode', status: 400 }\n}\n\nfunction deferWorkerTask(task) {\n return new Promise((resolve) => setTimeout(resolve, 0)).then(task)\n}\n\nconst WEBSOCKET_STREAM_FLUSH_BYTES = 1024\nconst WEBSOCKET_STREAM_FLUSH_MS = 20\n\nfunction createWebSocketStreamCoalescer(socket, options = {}) {\n const flushBytes = options.flushBytes || WEBSOCKET_STREAM_FLUSH_BYTES\n const flushMs = options.flushMs ?? WEBSOCKET_STREAM_FLUSH_MS\n let chunks = []\n let byteLength = 0\n let flushTimer = null\n const stats = {\n upstreamChunks: 0,\n upstreamBytes: 0,\n frames: 0,\n frameBytes: 0,\n }\n\n const clearFlushTimer = () => {\n if (flushTimer === null) return\n clearTimeout(flushTimer)\n flushTimer = null\n }\n\n const mergeChunks = () => {\n if (chunks.length === 1) return chunks[0]\n const frame = new Uint8Array(byteLength)\n let offset = 0\n for (const chunk of chunks) {\n frame.set(chunk, offset)\n offset += chunk.byteLength\n }\n return frame\n }\n\n const flush = () => {\n clearFlushTimer()\n if (byteLength === 0) return\n const frame = mergeChunks()\n chunks = []\n byteLength = 0\n socket.send(frame)\n stats.frames += 1\n stats.frameBytes += frame.byteLength\n }\n\n const scheduleFlush = () => {\n if (flushTimer !== null) return\n flushTimer = setTimeout(flush, flushMs)\n }\n\n return {\n push(value) {\n const chunk = value instanceof Uint8Array ? value : new Uint8Array(value)\n if (chunk.byteLength === 0) return\n stats.upstreamChunks += 1\n stats.upstreamBytes += chunk.byteLength\n chunks.push(chunk)\n byteLength += chunk.byteLength\n if (byteLength >= flushBytes) flush()\n else scheduleFlush()\n },\n flush,\n stats() {\n return { ...stats, pendingBytes: byteLength }\n },\n }\n}\n\nfunction checkpointWebSocketState(env, payload, body, nextState) {\n return deferWorkerTask(async () => {\n try {\n if ((await hashBody(body)) !== payload.next_hash) {\n throw new Error('hash mismatch')\n }\n await writeState(env, payload.affinity, nextState)\n } catch (error) {\n console.log(JSON.stringify({\n relay: 'opencode-anthropic-auth',\n transport: 'websocket',\n event: 'checkpoint_write_failed',\n affinity: String(payload.affinity).slice(0, 12),\n message: error instanceof Error ? error.message : String(error),\n }))\n }\n })\n}\n\nasync function prepareWebSocketUpstream(env, state, payload) {\n if (payload.protocol !== 2 || payload.type !== 'request' || !payload.id || !payload.affinity || !payload.upstream?.url || !payload.next_hash) {\n return { error: 'invalid payload', status: 400 }\n }\n\n const body = resolveBodyFromState(state, payload)\n if (body && typeof body === 'object' && 'error' in body) return body\n if (typeof body !== 'string') return { error: 'invalid body', status: 400 }\n if ((await hashBody(body)) !== payload.next_hash) {\n return { error: 'hash mismatch', status: 409 }\n }\n\n const nextState = { body, hash: payload.next_hash, revision: payload.revision }\n const checkpoint = checkpointWebSocketState(env, payload, body, nextState)\n const logAccepted = () => console.log(JSON.stringify({\n relay: 'opencode-anthropic-auth',\n transport: 'websocket',\n mode: payload.mode,\n revision: payload.revision,\n affinity: String(payload.affinity).slice(0, 12),\n bodyBytes: body.length,\n }))\n\n return { body, state: nextState, checkpoint, logAccepted }\n}\n\nfunction headersToObject(headers) {\n const result = {}\n for (const [key, value] of headers.entries()) result[key] = value\n return result\n}\n\nasync function handleRelayPayload(env, payload) {\n const prepared = await prepareUpstream(env, payload)\n if (prepared.error) return prepared\n const upstream = await fetch(payload.upstream.url, {\n method: payload.upstream.method || 'POST',\n headers: payload.upstream.headers,\n body: prepared.body,\n })\n return { upstream, stateWrite: prepared.stateWrite }\n}\n\nasync function handleWebSocket(socket, env, ctx, payload, getState, setState) {\n const heartbeat = setInterval(() => {\n try {\n socket.send(JSON.stringify({ protocol: 2, type: 'keepalive' }))\n } catch {\n clearInterval(heartbeat)\n }\n }, 15000)\n let streamCoalescer = null\n try {\n const result = await prepareWebSocketUpstream(env, getState(), payload)\n if (result.error) {\n socket.send(JSON.stringify({ protocol: 2, type: 'error', id: payload.id, status: result.status, message: result.error }))\n return\n }\n\n setState(result.state)\n socket.send(JSON.stringify({ protocol: 2, type: 'accepted', id: payload.id, hash: result.state.hash, revision: result.state.revision }))\n ctx?.waitUntil?.(deferWorkerTask(result.logAccepted))\n\n const upstreamPromise = fetch(payload.upstream.url, {\n method: payload.upstream.method || 'POST',\n headers: payload.upstream.headers,\n body: result.body,\n })\n ctx?.waitUntil?.(result.checkpoint)\n const upstream = await upstreamPromise\n socket.send(JSON.stringify({\n protocol: 2,\n type: 'response_start',\n id: payload.id,\n status: upstream.status,\n statusText: upstream.statusText,\n headers: headersToObject(upstream.headers),\n }))\n\n const reader = upstream.body?.getReader()\n if (reader) {\n streamCoalescer = createWebSocketStreamCoalescer(socket)\n while (true) {\n const { done, value } = await reader.read()\n if (done) break\n streamCoalescer.push(value)\n }\n streamCoalescer.flush()\n }\n const stats = streamCoalescer?.stats?.() || {}\n socket.send(JSON.stringify({ protocol: 2, type: 'done', id: payload.id, ...stats }))\n } catch (error) {\n try {\n streamCoalescer?.flush?.()\n } catch {}\n socket.send(JSON.stringify({ protocol: 2, type: 'error', id: payload.id, status: 500, message: error instanceof Error ? error.message : String(error) }))\n } finally {\n clearInterval(heartbeat)\n }\n}\n\nexport default {\n async fetch(request, env, ctx) {\n if (request.headers.get('Upgrade') === 'websocket') {\n const url = new URL(request.url)\n const token = url.searchParams.get('token')\n const affinity = url.searchParams.get('affinity')\n if (token !== env.RELAY_TOKEN) return new Response('unauthorized', { status: 401 })\n if (!affinity) return new Response('missing affinity', { status: 400 })\n const pair = new WebSocketPair()\n const client = pair[0]\n const server = pair[1]\n server.binaryType = 'arraybuffer'\n let state = null\n let ready = false\n server.accept()\n const loadState = readState(env, affinity).then((loadedState) => {\n state = loadedState\n ready = true\n server.send(JSON.stringify({\n protocol: 2,\n type: 'ready',\n state: state ? { hash: state.hash, revision: state.revision } : null,\n }))\n }).catch((error) => {\n server.send(JSON.stringify({ protocol: 2, type: 'error', status: 500, message: error instanceof Error ? error.message : String(error) }))\n server.close(1011, 'state load failed')\n })\n ctx?.waitUntil?.(loadState)\n if (!ctx?.waitUntil) void loadState\n let busy = false\n server.addEventListener('message', (event) => {\n if (!ready) {\n server.send(JSON.stringify({ protocol: 2, type: 'error', status: 425, message: 'relay state is not ready' }))\n return\n }\n if (busy) {\n server.send(JSON.stringify({ protocol: 2, type: 'error', status: 429, message: 'request already in flight' }))\n return\n }\n let payload\n try {\n payload = JSON.parse(event.data)\n } catch {\n server.send(JSON.stringify({ protocol: 2, type: 'error', status: 400, message: 'invalid JSON payload' }))\n return\n }\n payload.affinity = affinity\n busy = true\n const run = handleWebSocket(server, env, ctx, payload, () => state, (nextState) => { state = nextState }).finally(() => { busy = false })\n ctx?.waitUntil?.(run)\n if (!ctx?.waitUntil) void run\n })\n return new Response(null, { status: 101, webSocket: client })\n }\n\n if (request.method === 'GET') {\n return Response.json({ status: 'ok', transports: ['http', 'websocket'] })\n }\n if (request.method !== 'POST') return new Response('method not allowed', { status: 405 })\n if (request.headers.get('x-relay-token') !== env.RELAY_TOKEN) {\n return new Response('unauthorized', { status: 401 })\n }\n\n try {\n const payload = await request.json()\n const result = await handleRelayPayload(env, payload)\n if (result.error) return Response.json({ error: result.error }, { status: result.status })\n\n if (result.stateWrite) ctx.waitUntil(result.stateWrite)\n\n const upstream = result.upstream\n return new Response(upstream.body, {\n status: upstream.status,\n statusText: upstream.statusText,\n headers: upstream.headers,\n })\n } catch (error) {\n return Response.json(\n { error: error instanceof Error ? error.message : 'internal relay error' },\n { status: 502 },\n )\n }\n },\n}\n";
package/dist/relay.js CHANGED
@@ -299,15 +299,17 @@ function relayControlErrorMessage(message) {
299
299
  class PersistentRelaySession {
300
300
  config;
301
301
  affinity;
302
+ timers;
302
303
  socket;
303
304
  connecting;
304
305
  queue = Promise.resolve();
305
306
  pending;
306
307
  serverState;
307
308
  lastUsedAt = Date.now();
308
- constructor(config, affinity) {
309
+ constructor(config, affinity, timers) {
309
310
  this.config = config;
310
311
  this.affinity = affinity;
312
+ this.timers = timers;
311
313
  }
312
314
  touch(now = Date.now()) {
313
315
  this.lastUsedAt = now;
@@ -397,7 +399,7 @@ class PersistentRelaySession {
397
399
  const socket = new WebSocket(parsed.toString());
398
400
  this.socket = socket;
399
401
  this.serverState = undefined;
400
- const timeout = setTimeout(() => {
402
+ const timeout = this.timers.setTimeout(() => {
401
403
  socket.close();
402
404
  reject(new Error('relay websocket ready timed out'));
403
405
  }, 15_000);
@@ -409,7 +411,7 @@ class PersistentRelaySession {
409
411
  }
410
412
  const message = parseRelayControlMessage(event.data);
411
413
  if (message.type === 'ready') {
412
- clearTimeout(timeout);
414
+ this.timers.clearTimeout(timeout);
413
415
  this.serverState = message.state;
414
416
  resolve();
415
417
  return;
@@ -417,20 +419,20 @@ class PersistentRelaySession {
417
419
  this.handleMessage(message);
418
420
  }
419
421
  catch (error) {
420
- clearTimeout(timeout);
422
+ this.timers.clearTimeout(timeout);
421
423
  reject(error);
422
424
  this.failPending(error);
423
425
  socket.close();
424
426
  }
425
427
  });
426
428
  socket.addEventListener('error', () => {
427
- clearTimeout(timeout);
429
+ this.timers.clearTimeout(timeout);
428
430
  const error = new RelayWebSocketConnectionResetError('relay websocket error');
429
431
  reject(error);
430
432
  this.failPending(error);
431
433
  });
432
434
  socket.addEventListener('close', (event) => {
433
- clearTimeout(timeout);
435
+ this.timers.clearTimeout(timeout);
434
436
  if (this.socket === socket) {
435
437
  this.socket = undefined;
436
438
  this.serverState = undefined;
@@ -503,8 +505,8 @@ class PersistentRelaySession {
503
505
  return { response, done };
504
506
  }
505
507
  resetPendingTimeout(pending) {
506
- clearTimeout(pending.timeout);
507
- pending.timeout = setTimeout(() => {
508
+ this.timers.clearTimeout(pending.timeout);
509
+ pending.timeout = this.timers.setTimeout(() => {
508
510
  if (this.pending !== pending)
509
511
  return;
510
512
  if (this.retryPendingBeforeResponse(pending, 'response timed out'))
@@ -522,7 +524,7 @@ class PersistentRelaySession {
522
524
  return false;
523
525
  pending.retryAttempts += 1;
524
526
  pending.retryingBeforeResponse = true;
525
- clearTimeout(pending.timeout);
527
+ this.timers.clearTimeout(pending.timeout);
526
528
  const attempt = pending.retryAttempts;
527
529
  const originalId = pending.payload.id;
528
530
  relayLog(`websocket ${reason}; reconnecting and retrying full_sync session=${shortAffinity(this.affinity)} request=${originalId} retry=${attempt} chunks=${pending.streamChunkCount} bytes=${pending.streamByteCount}${closeEvent ? ` ${formatCloseEvent(closeEvent)}` : ''}`);
@@ -681,7 +683,7 @@ class PersistentRelaySession {
681
683
  ? ` upstreamChunks=${done.upstreamChunks ?? 'unknown'} upstreamBytes=${done.upstreamBytes ?? 'unknown'} frames=${done.frames} frameBytes=${done.frameBytes ?? 'unknown'}`
682
684
  : '';
683
685
  relayLog(`perf websocket done session=${shortAffinity(this.affinity)} request=${pending.payload.id} sentMs=${formatMs(finishedAt - pending.sentAt)} streamMs=${pending.responseStartedAt == null ? 'unknown' : formatMs(finishedAt - pending.responseStartedAt)} chunks=${pending.streamChunkCount} bytes=${pending.streamByteCount}${workerStats}`);
684
- clearTimeout(pending.timeout);
686
+ this.timers.clearTimeout(pending.timeout);
685
687
  if (!pending.streamDone) {
686
688
  this.deliverResponseHeaders(pending);
687
689
  pending.streamDone = true;
@@ -694,7 +696,7 @@ class PersistentRelaySession {
694
696
  const pending = this.pending;
695
697
  if (!pending)
696
698
  return;
697
- clearTimeout(pending.timeout);
699
+ this.timers.clearTimeout(pending.timeout);
698
700
  this.pending = undefined;
699
701
  if (pending.responseStarted) {
700
702
  pending.streamController?.error(error);
@@ -737,20 +739,20 @@ function cleanupRelaySessionCaches(now = Date.now()) {
737
739
  break;
738
740
  }
739
741
  }
740
- function getPersistentRelaySession(config, affinity) {
742
+ function getPersistentRelaySession(config, affinity, timers) {
741
743
  cleanupRelaySessionCaches();
742
744
  const existing = websocketSessions.get(affinity);
743
745
  if (existing) {
744
746
  existing.touch();
745
747
  return existing;
746
748
  }
747
- const session = new PersistentRelaySession(config, affinity);
749
+ const session = new PersistentRelaySession(config, affinity, timers);
748
750
  websocketSessions.set(affinity, session);
749
751
  cleanupRelaySessionCaches();
750
752
  return session;
751
753
  }
752
754
  export async function sendViaRelay(options) {
753
- const { config, input, init, headers, body, fallback, affinity: explicitAffinity, optimisticResponse, onResponseHeaders, } = options;
755
+ const { config, input, init, headers, body, fallback, affinity: explicitAffinity, optimisticResponse, onResponseHeaders, setTimeoutImpl = globalThis.setTimeout, clearTimeoutImpl = globalThis.clearTimeout, } = options;
754
756
  if (!config || !isRelayableAnthropicRequest(input, body))
755
757
  return fallback();
756
758
  const affinity = explicitAffinity ||
@@ -791,7 +793,10 @@ export async function sendViaRelay(options) {
791
793
  const sendStart = perfNowMs();
792
794
  if (config.transport === 'websocket') {
793
795
  try {
794
- const session = getPersistentRelaySession(config, affinity);
796
+ const session = getPersistentRelaySession(config, affinity, {
797
+ setTimeout: setTimeoutImpl,
798
+ clearTimeout: clearTimeoutImpl,
799
+ });
795
800
  result = await session.send(payload, bodyText, optimisticResponse === true, onResponseHeaders);
796
801
  }
797
802
  catch (error) {
@@ -1,4 +1,4 @@
1
- import { type AccountStorage, type OAuthQuotaSnapshot } from './accounts.ts';
1
+ import { type AccountOperationError, type AccountStorage, type OAuthQuotaSnapshot } from './accounts.ts';
2
2
  export declare const STICKY_ROUTING_MAIN_ACCOUNT_ID = "main";
3
3
  export declare const STICKY_ROUTING_SHORT_RESET_GRACE_MS: number;
4
4
  export declare const STICKY_ROUTING_ASSIGNMENT_TTL_MS: number;
@@ -33,6 +33,17 @@ export type StickyQuotaFailureDecision = {
33
33
  action: 'migrate';
34
34
  reason: 'five-hour' | 'seven-day' | 'model-scoped';
35
35
  };
36
+ /**
37
+ * Build the terminal response for a complete sticky quota pool that has no
38
+ * eligible route. This prevents callers from silently falling through to the
39
+ * main account after the router deliberately excluded it.
40
+ */
41
+ export declare function createStickyNoRouteResponse(input: {
42
+ mainRefreshError?: AccountOperationError;
43
+ routeQuotas: readonly OAuthQuotaSnapshot[];
44
+ modelId?: string;
45
+ now?: number;
46
+ }): Response;
36
47
  export declare function stickyQuotaSnapshotIsFresh(quota: OAuthQuotaSnapshot | undefined, storage: AccountStorage | null, now?: number, modelId?: string): boolean;
37
48
  export declare function stickyRouteFamilyForModel(model: unknown): StickyRouteFamily;
38
49
  export declare function stickyRouteCandidateWeight(input: {
@@ -1,7 +1,7 @@
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, } from "./accounts.js";
4
+ import { acquireRefreshFileLock, getKillswitchThresholdsForAccount, getQuotaCheckIntervalMs, getQuotaMinimumRemainingThresholds, getScopedQuotaWindowForModel, isKillswitchEnabled, isPermanentRefreshError, killswitchRetryAfterSeconds, quotaSnapshotPassesModelScope, } from "./accounts.js";
5
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;
@@ -12,6 +12,52 @@ const STICKY_ROUTING_LOCK_WAIT_MS = 5_000;
12
12
  const STICKY_ROUTING_LOCK_POLL_MS = 10;
13
13
  const STICKY_ROUTING_MAX_ASSIGNMENTS = 4_096;
14
14
  const MIN_WEIGHT = 0.000_001;
15
+ /**
16
+ * Build the terminal response for a complete sticky quota pool that has no
17
+ * eligible route. This prevents callers from silently falling through to the
18
+ * main account after the router deliberately excluded it.
19
+ */
20
+ export function createStickyNoRouteResponse(input) {
21
+ const modelId = input.modelId;
22
+ const modelName = modelId
23
+ ? input.routeQuotas
24
+ .map((quota) => getScopedQuotaWindowForModel(quota, modelId))
25
+ .find(Boolean)?.modelName
26
+ : undefined;
27
+ if (isPermanentRefreshError(input.mainRefreshError)) {
28
+ const message = modelName
29
+ ? `Main Claude OAuth account requires re-login, and no fallback OAuth account is currently routable for ${modelName}.`
30
+ : 'Main Claude OAuth account requires re-login, and no fallback OAuth account is currently routable.';
31
+ return new Response(JSON.stringify({
32
+ type: 'error',
33
+ error: { type: 'authentication_error', message },
34
+ }), {
35
+ status: 401,
36
+ headers: { 'content-type': 'application/json' },
37
+ });
38
+ }
39
+ const scopedBlocked = Boolean(modelId &&
40
+ input.routeQuotas.length > 0 &&
41
+ input.routeQuotas.every((quota) => !quotaSnapshotPassesModelScope(quota, modelId)));
42
+ const [firstQuota, ...remainingQuotas] = input.routeQuotas;
43
+ const retryAfter = killswitchRetryAfterSeconds(firstQuota, remainingQuotas.map((quota) => ({ quota })), input.now ?? Date.now(), scopedBlocked ? modelId : undefined);
44
+ const minutes = Math.floor(retryAfter / 60);
45
+ const seconds = retryAfter % 60;
46
+ const reason = scopedBlocked && modelName
47
+ ? `${modelName} weekly limit reached, no routable OAuth accounts.`
48
+ : 'No OAuth account currently satisfies sticky-balanced quota policy.';
49
+ const message = `${reason} Retry in ${minutes}m ${seconds}s.`;
50
+ return new Response(JSON.stringify({
51
+ type: 'error',
52
+ error: { type: 'rate_limit_error', message },
53
+ }), {
54
+ status: 429,
55
+ headers: {
56
+ 'content-type': 'application/json',
57
+ 'retry-after': String(retryAfter),
58
+ },
59
+ });
60
+ }
15
61
  function isRecord(value) {
16
62
  return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
17
63
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cortexkit/anthropic-auth-core",
3
- "version": "1.18.0",
3
+ "version": "1.19.1",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",