@cortexkit/anthropic-auth-core 2.2.6 → 2.2.8

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/index.d.ts CHANGED
@@ -62,6 +62,7 @@ export type { NativePiAuthSlot, NativePiAuthSlotErrorCode, NativePiAuthSlotInspe
62
62
  export { disconnectNativeVaultForHost, NativePiAuthSlotError, nativePiAuthSlotPaths, openNativePiAuthSlot, } from './native-vault-connection.ts';
63
63
  export type { NativeVaultQuotaPurpose, NativeVaultRuntime, NativeVaultRuntimeOptions, NativeVaultWatch, NativeVaultWatchOptions, } from './native-vault-runtime.ts';
64
64
  export { acquireNativeVaultRuntime, createNativeVaultRuntime, discoverNativeVaultInventory, publishNativeVaultRosterSeed, publishNativeVaultRuntimeSeed, } from './native-vault-runtime.ts';
65
+ export * from './oauth-origin.ts';
65
66
  export * from './oauth-profile.ts';
66
67
  export { createWindowsOwnerOnly, ensureWindowsOwnerOnlyDirectory, isOwnerOnly, type OwnerOnlyOptions, } from './owner-only.ts';
67
68
  export * from './pkce.ts';
package/dist/index.js CHANGED
@@ -4918,6 +4918,7 @@ async function fetchOAuthQuotaSnapshot(input) {
4918
4918
  const fetchImpl = input.fetchImpl ?? fetch;
4919
4919
  const response = await fetchImpl(QUOTA_URL, {
4920
4920
  method: "GET",
4921
+ redirect: "error",
4921
4922
  headers: {
4922
4923
  Authorization: `Bearer ${input.accessToken}`,
4923
4924
  Accept: "application/json",
@@ -9397,6 +9398,7 @@ async function fetchOAuthAccountProfile(input) {
9397
9398
  assertNotCustodyTombstone(input.accessToken, "anthropic");
9398
9399
  const response = await (input.fetchImpl ?? fetch)(PROFILE_URL, {
9399
9400
  method: "GET",
9401
+ redirect: "error",
9400
9402
  signal: input.signal,
9401
9403
  headers: {
9402
9404
  Authorization: `Bearer ${input.accessToken}`,
@@ -24193,27 +24195,30 @@ function decideStickyQuotaFailure(input) {
24193
24195
  const quota2 = knownQuota(input.quota);
24194
24196
  if (!quota2)
24195
24197
  return { action: "retain", reason: "unknown" };
24196
- const scoped = getScopedQuotaWindowForModel(quota2, input.modelId);
24197
- if (scoped && Number.isFinite(scoped.remainingPercent) && scoped.remainingPercent <= 0) {
24198
- return { action: "migrate", reason: "model-scoped" };
24199
- }
24200
- const sevenDay = quota2.seven_day;
24201
- if (sevenDay && Number.isFinite(sevenDay.remainingPercent) && sevenDay.remainingPercent <= 0) {
24202
- return { action: "migrate", reason: "seven-day" };
24203
- }
24204
- const fiveHour = quota2.five_hour;
24205
- if (fiveHour && Number.isFinite(fiveHour.remainingPercent) && fiveHour.remainingPercent <= 0) {
24206
- const resetAt = fiveHour.resetsAt ? Date.parse(fiveHour.resetsAt) : NaN;
24207
- if (Number.isFinite(resetAt) && resetAt > now && resetAt - now <= STICKY_ROUTING_SHORT_RESET_GRACE_MS) {
24208
- return {
24209
- action: "hold",
24210
- reason: "five-hour-short-reset",
24211
- retryAfterSeconds: Math.max(1, Math.ceil((resetAt - now) / 1000))
24212
- };
24213
- }
24214
- return { action: "migrate", reason: "five-hour" };
24198
+ let blocking;
24199
+ for (const [reason, window2] of [
24200
+ ["model-scoped", getScopedQuotaWindowForModel(quota2, input.modelId)],
24201
+ ["seven-day", quota2.seven_day],
24202
+ ["five-hour", quota2.five_hour]
24203
+ ]) {
24204
+ if (!window2 || !Number.isFinite(window2.remainingPercent) || window2.remainingPercent > 0)
24205
+ continue;
24206
+ const resetAt = window2.resetsAt ? Date.parse(window2.resetsAt) : NaN;
24207
+ if (!Number.isFinite(resetAt) || resetAt <= now)
24208
+ return { action: "migrate", reason };
24209
+ if (!blocking || resetAt > blocking.resetAt)
24210
+ blocking = { resetAt, reason };
24211
+ }
24212
+ if (!blocking)
24213
+ return { action: "retain", reason: "not-exhausted" };
24214
+ if (blocking.resetAt - now <= STICKY_ROUTING_SHORT_RESET_GRACE_MS) {
24215
+ return {
24216
+ action: "hold",
24217
+ reason: `${blocking.reason}-short-reset`,
24218
+ retryAfterSeconds: Math.max(1, Math.ceil((blocking.resetAt - now) / 1000))
24219
+ };
24215
24220
  }
24216
- return { action: "retain", reason: "not-exhausted" };
24221
+ return { action: "migrate", reason: blocking.reason };
24217
24222
  }
24218
24223
  function getStickyRoutingStatePath(accountStoragePath) {
24219
24224
  const name = basename6(accountStoragePath);
@@ -25824,8 +25829,10 @@ async function sendViaRelay(options) {
25824
25829
  dumpTag,
25825
25830
  onDumpCreated,
25826
25831
  setTimeoutImpl = globalThis.setTimeout,
25827
- clearTimeoutImpl = globalThis.clearTimeout
25832
+ clearTimeoutImpl = globalThis.clearTimeout,
25833
+ requireUpstreamUrl
25828
25834
  } = options;
25835
+ requireUpstreamUrl?.(input instanceof Request ? input.url : input.toString());
25829
25836
  if (!config || !isRelayableAnthropicRequest(input, body))
25830
25837
  return fallback();
25831
25838
  const affinity = explicitAffinity || headers.get("x-session-affinity") || headers.get("x-opencode-session");
@@ -25864,6 +25871,11 @@ async function sendViaRelay(options) {
25864
25871
  responseMode ??= requestResponseMode(bodyText);
25865
25872
  return responseMode;
25866
25873
  };
25874
+ let directFallbackStarted = false;
25875
+ const directFallback = () => {
25876
+ directFallbackStarted = true;
25877
+ return fallback();
25878
+ };
25867
25879
  try {
25868
25880
  let result;
25869
25881
  const sendStart = perfNowMs();
@@ -25884,7 +25896,7 @@ async function sendViaRelay(options) {
25884
25896
  payload,
25885
25897
  bodyText,
25886
25898
  requestMode,
25887
- fallback,
25899
+ fallback: directFallback,
25888
25900
  signal: init?.signal,
25889
25901
  authorizeAttempt
25890
25902
  });
@@ -25895,7 +25907,7 @@ async function sendViaRelay(options) {
25895
25907
  payload,
25896
25908
  bodyText,
25897
25909
  requestMode,
25898
- fallback,
25910
+ fallback: directFallback,
25899
25911
  signal: init?.signal,
25900
25912
  authorizeAttempt
25901
25913
  });
@@ -25955,6 +25967,10 @@ async function sendViaRelay(options) {
25955
25967
  throw error.failure;
25956
25968
  if (error instanceof RelayResponseMetadataError)
25957
25969
  throw error;
25970
+ if (directFallbackStarted) {
25971
+ relayLog(`direct fallback failed session=${shortAffinity2(affinity)}; not sending it again code=relay_transport_error`);
25972
+ throw error;
25973
+ }
25958
25974
  if (!config.fallbackToDirect) {
25959
25975
  relayLog(`relay failed session=${shortAffinity2(affinity)} and fallbackToDirect=false code=relay_transport_error`);
25960
25976
  throw error;
@@ -26997,11 +27013,12 @@ function createNativeAccountRuntime(options) {
26997
27013
  } catch {
26998
27014
  throw new Error("Claude account identity could not be verified; retry sign-in. No account changes were saved.");
26999
27015
  }
27016
+ const { access, refresh, expires } = input.credential;
27000
27017
  return runtime2.putAccount({
27001
27018
  ...input,
27002
27019
  accountIdentity,
27003
27020
  replace: input.replace ?? true,
27004
- credential: { type: "oauth", ...input.credential }
27021
+ credential: { type: "oauth", access, refresh, expires }
27005
27022
  });
27006
27023
  },
27007
27024
  async putAccount(input) {
@@ -31350,6 +31367,57 @@ async function disconnectNativeVaultForHost(options) {
31350
31367
  closeNativeVaultAuthority(paths.tokenPath);
31351
31368
  return result;
31352
31369
  }
31370
+ // src/oauth-origin.ts
31371
+ var ANTHROPIC_OAUTH_ORIGIN = "https://api.anthropic.com";
31372
+ var ANTHROPIC_OAUTH_ORIGIN_REFUSAL = "Claude OAuth requests can only go to https://api.anthropic.com. A provider baseURL that points elsewhere is not used for Claude sign-in accounts.";
31373
+
31374
+ class AnthropicOAuthOriginError extends Error {
31375
+ check = "oauth_destination";
31376
+ constructor() {
31377
+ super(ANTHROPIC_OAUTH_ORIGIN_REFUSAL);
31378
+ this.name = "AnthropicOAuthOriginError";
31379
+ }
31380
+ }
31381
+ function sameOrigin(url, origin) {
31382
+ let allowed;
31383
+ try {
31384
+ allowed = new URL(origin);
31385
+ } catch {
31386
+ return false;
31387
+ }
31388
+ return url.protocol === allowed.protocol && url.origin === allowed.origin;
31389
+ }
31390
+ function requireAnthropicOAuthOrigin(value, options = {}) {
31391
+ const refuse5 = options.refusal ?? (() => new AnthropicOAuthOriginError);
31392
+ let url;
31393
+ try {
31394
+ url = new URL(value);
31395
+ } catch {
31396
+ throw refuse5("unparseable");
31397
+ }
31398
+ if (url.username || url.password)
31399
+ throw refuse5("destination");
31400
+ if (url.protocol === "https:" && url.origin === ANTHROPIC_OAUTH_ORIGIN)
31401
+ return url;
31402
+ if (options.extraOrigin && sameOrigin(url, options.extraOrigin))
31403
+ return url;
31404
+ throw refuse5("destination");
31405
+ }
31406
+ function fetchInputUrl(input) {
31407
+ return input instanceof Request ? input.url : String(input);
31408
+ }
31409
+ function createAnthropicOAuthFetch(options = {}) {
31410
+ const current = () => typeof options === "function" ? options() : options;
31411
+ return Object.assign(async (input, init) => {
31412
+ requireAnthropicOAuthOrigin(fetchInputUrl(input), current());
31413
+ return fetch(input, { ...init, redirect: "error" });
31414
+ }, {
31415
+ preconnect: (...args) => {
31416
+ requireAnthropicOAuthOrigin(String(args[0]), current());
31417
+ return fetch.preconnect(...args);
31418
+ }
31419
+ });
31420
+ }
31353
31421
  // src/prime.ts
31354
31422
  import { createHash as createHash21 } from "node:crypto";
31355
31423
  import { realpathSync as realpathSync2 } from "node:fs";
@@ -33080,6 +33148,7 @@ export {
33080
33148
  resetCache1hState,
33081
33149
  resetBillingHeaderCCH,
33082
33150
  requireNoSupervisedAuthContentSnapshot,
33151
+ requireAnthropicOAuthOrigin,
33083
33152
  requestGzipPadding,
33084
33153
  remapRequestBodyModel,
33085
33154
  remapModelId,
@@ -33315,6 +33384,7 @@ export {
33315
33384
  createNativeCustody,
33316
33385
  createNativeAccountRuntime,
33317
33386
  createEmptyStorage,
33387
+ createAnthropicOAuthFetch,
33318
33388
  countUserPrompts,
33319
33389
  connectClaustrumEnrollmentClient2 as connectClaustrumEnrollmentClient,
33320
33390
  configuredAnthropicOAuthAccountCount,
@@ -33511,8 +33581,11 @@ export {
33511
33581
  CACHE_KEEP_EXTENDED_TTL_BETA,
33512
33582
  CACHE_1H_MODES,
33513
33583
  CACHE_1H_COMMAND_NAME,
33584
+ AnthropicOAuthOriginError,
33514
33585
  AXIOS_USER_AGENT,
33515
33586
  AUTHORIZE_URLS,
33587
+ ANTHROPIC_OAUTH_ORIGIN_REFUSAL,
33588
+ ANTHROPIC_OAUTH_ORIGIN,
33516
33589
  ANTHROPIC_CUSTOM_HEADERS_ENV,
33517
33590
  ACCOUNT_STATE_FILE_NAME,
33518
33591
  ACCOUNT_FILE_NAME
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Where a Claude OAuth access token may be sent.
3
+ *
4
+ * A Claude sign-in (OAuth) access token is sent only to Anthropic's own HTTPS
5
+ * API server, unless a caller explicitly allows one test-only loopback origin
6
+ * (see `extraOrigin`). Hosts take the request URL from provider configuration,
7
+ * and that configuration can come from a project file in a repository the
8
+ * user opened, so the URL alone is not trusted: every OAuth send checks its
9
+ * final URL here before the token leaves the process. API-key routes are not
10
+ * checked here; they keep whatever base URL the user configured.
11
+ */
12
+ export declare const ANTHROPIC_OAUTH_ORIGIN = "https://api.anthropic.com";
13
+ /** Fixed refusal text. It never includes the refused URL or any credential. */
14
+ export declare const ANTHROPIC_OAUTH_ORIGIN_REFUSAL = "Claude OAuth requests can only go to https://api.anthropic.com. A provider baseURL that points elsewhere is not used for Claude sign-in accounts.";
15
+ /**
16
+ * A Claude OAuth request was refused locally because its destination is not
17
+ * Anthropic's API server. Nothing was sent, and another account cannot help,
18
+ * so callers treat it as a terminal request error, not an account failure.
19
+ */
20
+ export declare class AnthropicOAuthOriginError extends Error {
21
+ readonly check = "oauth_destination";
22
+ constructor();
23
+ }
24
+ export interface AnthropicOAuthOriginOptions {
25
+ /**
26
+ * One more exact origin (scheme, host and port) the caller accepts besides
27
+ * Anthropic's. Hosts use it only for a test-only loopback server; it is
28
+ * never derived from provider configuration.
29
+ */
30
+ readonly extraOrigin?: string;
31
+ /**
32
+ * Builds the error thrown for a refused URL. `unparseable` means the value
33
+ * is not a URL at all. Defaults to AnthropicOAuthOriginError for both.
34
+ */
35
+ readonly refusal?: (reason: 'unparseable' | 'destination') => Error;
36
+ }
37
+ /**
38
+ * Returns the parsed URL when an OAuth token may be sent to it: HTTPS on
39
+ * api.anthropic.com's default port (or the caller's extra origin), with no
40
+ * user name or password in the URL. Throws otherwise.
41
+ */
42
+ export declare function requireAnthropicOAuthOrigin(value: string, options?: AnthropicOAuthOriginOptions): URL;
43
+ /**
44
+ * A fetch for OAuth sends. It checks each URL with
45
+ * requireAnthropicOAuthOrigin before dispatch and always sets
46
+ * `redirect: 'error'`, so an approved destination cannot forward the token
47
+ * elsewhere with a redirect. It calls the global fetch at call time, so test
48
+ * stubs installed later still apply. Options are read on every call when
49
+ * given as a function.
50
+ */
51
+ export declare function createAnthropicOAuthFetch(options?: AnthropicOAuthOriginOptions | (() => AnthropicOAuthOriginOptions)): typeof fetch;
package/dist/relay.d.ts CHANGED
@@ -76,5 +76,11 @@ export declare function sendViaRelay(options: {
76
76
  onDumpCreated?: (handle: import('./dump.ts').DumpHandle) => void;
77
77
  setTimeoutImpl?: typeof globalThis.setTimeout;
78
78
  clearTimeoutImpl?: typeof globalThis.clearTimeout;
79
+ /**
80
+ * Checks the URL the relay will forward to (the envelope's upstream.url)
81
+ * and throws to refuse it. Runs before anything is sent, relay or direct;
82
+ * its error reaches the caller unchanged and never triggers a fallback.
83
+ */
84
+ requireUpstreamUrl?: (url: string) => unknown;
79
85
  }): Promise<Response>;
80
86
  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 {\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 code: 'checkpoint_write_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";
@@ -45,7 +45,7 @@ export type StickyQuotaFailureDecision = {
45
45
  reason: 'not-exhausted' | 'unknown';
46
46
  } | {
47
47
  action: 'hold';
48
- reason: 'five-hour-short-reset';
48
+ reason: 'five-hour-short-reset' | 'seven-day-short-reset' | 'model-scoped-short-reset';
49
49
  retryAfterSeconds: number;
50
50
  } | {
51
51
  action: 'migrate';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cortexkit/anthropic-auth-core",
3
- "version": "2.2.6",
3
+ "version": "2.2.8",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",