@cortexkit/anthropic-auth-core 1.1.3 → 1.2.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.
@@ -141,7 +141,7 @@ export declare class FallbackAccountManager {
141
141
  save(storage: AccountStorage): Promise<void>;
142
142
  startBackgroundRefresh(): void;
143
143
  stopBackgroundRefresh(): void;
144
- getUsableFallbackAccounts(): Promise<OAuthAccount[]>;
144
+ getUsableFallbackAccounts(existingStorage?: AccountStorage | null): Promise<OAuthAccount[]>;
145
145
  markUsed(account: OAuthAccount): Promise<void>;
146
146
  accountPassesQuotaPolicy(account: OAuthAccount, storage: AccountStorage | null): boolean;
147
147
  refreshDueAccounts(): Promise<void>;
package/dist/accounts.js CHANGED
@@ -544,8 +544,8 @@ export class FallbackAccountManager {
544
544
  this.refreshTimer = null;
545
545
  this.quotaTimer = null;
546
546
  }
547
- async getUsableFallbackAccounts() {
548
- const storage = await this.load();
547
+ async getUsableFallbackAccounts(existingStorage) {
548
+ const storage = existingStorage !== undefined ? existingStorage : await this.load();
549
549
  if (!storage)
550
550
  return [];
551
551
  const usable = [];
package/dist/auth.d.ts CHANGED
@@ -13,6 +13,8 @@ export declare function refreshClaudeOAuthToken(input: {
13
13
  refreshToken: string;
14
14
  fetchImpl?: typeof fetch;
15
15
  now?: () => number;
16
+ maxRetries?: number;
17
+ baseDelayMs?: number;
16
18
  }): Promise<ClaudeOAuthRefreshResult>;
17
19
  export type AuthorizationResult = {
18
20
  url: string;
package/dist/auth.js CHANGED
@@ -10,32 +10,64 @@ export class ClaudeOAuthRefreshError extends Error {
10
10
  this.name = 'ClaudeOAuthRefreshError';
11
11
  }
12
12
  }
13
+ function isTransientNetworkError(error) {
14
+ if (!(error instanceof Error))
15
+ return false;
16
+ const code = error.code;
17
+ return (error.message.includes('fetch failed') ||
18
+ code === 'ECONNRESET' ||
19
+ code === 'ECONNREFUSED' ||
20
+ code === 'ETIMEDOUT' ||
21
+ code === 'UND_ERR_CONNECT_TIMEOUT');
22
+ }
13
23
  export async function refreshClaudeOAuthToken(input) {
14
24
  const fetchImpl = input.fetchImpl ?? fetch;
15
- const response = await fetchImpl(TOKEN_URL, {
16
- method: 'POST',
17
- headers: {
18
- 'Content-Type': 'application/json',
19
- Accept: 'application/json',
20
- },
21
- body: JSON.stringify({
22
- grant_type: 'refresh_token',
23
- refresh_token: input.refreshToken,
24
- client_id: CLIENT_ID,
25
- }),
26
- });
27
- if (!response.ok) {
28
- const body = await response.text().catch(() => '');
29
- throw new ClaudeOAuthRefreshError(response.status, body);
25
+ const maxRetries = input.maxRetries ?? 2;
26
+ const baseDelayMs = input.baseDelayMs ?? 500;
27
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
28
+ try {
29
+ if (attempt > 0) {
30
+ const delay = baseDelayMs * 2 ** (attempt - 1);
31
+ await new Promise((resolve) => setTimeout(resolve, delay));
32
+ }
33
+ const response = await fetchImpl(TOKEN_URL, {
34
+ method: 'POST',
35
+ headers: {
36
+ 'Content-Type': 'application/json',
37
+ Accept: 'application/json',
38
+ },
39
+ body: JSON.stringify({
40
+ grant_type: 'refresh_token',
41
+ refresh_token: input.refreshToken,
42
+ client_id: CLIENT_ID,
43
+ }),
44
+ });
45
+ if (!response.ok) {
46
+ if (response.status >= 500 && attempt < maxRetries) {
47
+ await response.body?.cancel().catch(() => { });
48
+ continue;
49
+ }
50
+ const body = await response.text().catch(() => '');
51
+ throw new ClaudeOAuthRefreshError(response.status, body);
52
+ }
53
+ const json = (await response.json());
54
+ const refreshedAt = input.now?.() ?? Date.now();
55
+ return {
56
+ access: json.access_token,
57
+ refresh: json.refresh_token ?? input.refreshToken,
58
+ expires: refreshedAt + json.expires_in * 1000,
59
+ expiresIn: json.expires_in,
60
+ };
61
+ }
62
+ catch (error) {
63
+ if (error instanceof ClaudeOAuthRefreshError)
64
+ throw error;
65
+ if (attempt < maxRetries && isTransientNetworkError(error))
66
+ continue;
67
+ throw error;
68
+ }
30
69
  }
31
- const json = (await response.json());
32
- const refreshedAt = input.now?.() ?? Date.now();
33
- return {
34
- access: json.access_token,
35
- refresh: json.refresh_token ?? input.refreshToken,
36
- expires: refreshedAt + json.expires_in * 1000,
37
- expiresIn: json.expires_in,
38
- };
70
+ throw new Error('Token refresh exhausted all retries');
39
71
  }
40
72
  function generateState() {
41
73
  return crypto.randomUUID().replace(/-/g, '');
@@ -4,7 +4,7 @@ export declare const AUTHORIZE_URLS: {
4
4
  readonly max: "https://claude.ai/oauth/authorize";
5
5
  };
6
6
  export declare const CODE_CALLBACK_URL = "https://platform.claude.com/oauth/code/callback";
7
- export declare const TOKEN_URL = "https://platform.claude.com/v1/oauth/token";
7
+ export declare const TOKEN_URL = "https://api.anthropic.com/v1/oauth/token";
8
8
  export declare const OAUTH_SCOPES: string[];
9
9
  export declare const TOOL_PREFIX = "mcp_";
10
10
  export declare const REQUIRED_BETAS: string[];
package/dist/constants.js CHANGED
@@ -4,7 +4,7 @@ export const AUTHORIZE_URLS = {
4
4
  max: 'https://claude.ai/oauth/authorize',
5
5
  };
6
6
  export const CODE_CALLBACK_URL = 'https://platform.claude.com/oauth/code/callback';
7
- export const TOKEN_URL = 'https://platform.claude.com/v1/oauth/token';
7
+ export const TOKEN_URL = 'https://api.anthropic.com/v1/oauth/token';
8
8
  export const OAUTH_SCOPES = [
9
9
  'org:create_api_key',
10
10
  'user:profile',
package/dist/relay.d.ts CHANGED
@@ -25,4 +25,4 @@ export declare function sendViaRelay(options: {
25
25
  affinity?: string | null;
26
26
  optimisticResponse?: boolean;
27
27
  }): Promise<Response>;
28
- 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 await writeState(env, payload.affinity, { body, hash: payload.next_hash, revision: payload.revision })\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 }\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\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 }\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 }, 5000)\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 while (true) {\n const { done, value } = await reader.read()\n if (done) break\n socket.send(value)\n }\n }\n socket.send(JSON.stringify({ protocol: 2, type: 'done', id: payload.id }))\n } catch (error) {\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') return new Response('ok')\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 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 const upstream = result.upstream\n return new Response(upstream.body, {\n status: upstream.status,\n statusText: upstream.statusText,\n headers: upstream.headers,\n })\n },\n}\n";
28
+ 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\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 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 while (true) {\n const { done, value } = await reader.read()\n if (done) break\n socket.send(value)\n }\n }\n socket.send(JSON.stringify({ protocol: 2, type: 'done', id: payload.id }))\n } catch (error) {\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
@@ -544,6 +544,17 @@ class PersistentRelaySession {
544
544
  return;
545
545
  }
546
546
  if (message.type === 'error') {
547
+ // If an optimistic response is already streaming (responseStartedAt is
548
+ // set), the pending promise was already resolved — calling failPending
549
+ // would reject a settled promise and leave the stream hanging. Instead,
550
+ // inject an SSE error event and close cleanly.
551
+ if (pending.optimisticResponse && pending.responseStartedAt != null) {
552
+ relayLog(`websocket relay error during optimistic response session=${shortAffinity(this.affinity)} request=${pending.payload.id} status=${message.status} message=${message.message || 'unknown'}`);
553
+ pending.streamController?.enqueue(new TextEncoder().encode(`event: error\ndata: ${JSON.stringify({ type: 'error', error: { type: 'relay_error', message: message.message || 'relay error' } })}\n\n`));
554
+ this.finishPending();
555
+ this.socket?.close();
556
+ return;
557
+ }
547
558
  if (message.status === 409 &&
548
559
  this.retryPendingBeforeResponse(pending, message.message || 'state mismatch')) {
549
560
  return;
@@ -718,7 +729,7 @@ async function prepareUpstream(env, payload) {
718
729
  return { error: 'hash mismatch', status: 409 }
719
730
  }
720
731
 
721
- await writeState(env, payload.affinity, { body, hash: payload.next_hash, revision: payload.revision })
732
+ const stateWrite = writeState(env, payload.affinity, { body, hash: payload.next_hash, revision: payload.revision }).catch(() => {})
722
733
  console.log(JSON.stringify({
723
734
  relay: 'opencode-anthropic-auth',
724
735
  transport: 'relay',
@@ -728,7 +739,7 @@ async function prepareUpstream(env, payload) {
728
739
  bodyBytes: body.length,
729
740
  }))
730
741
 
731
- return { body }
742
+ return { body, stateWrite }
732
743
  }
733
744
 
734
745
  function resolveBodyFromState(state, payload) {
@@ -805,7 +816,7 @@ async function handleRelayPayload(env, payload) {
805
816
  headers: payload.upstream.headers,
806
817
  body: prepared.body,
807
818
  })
808
- return { upstream }
819
+ return { upstream, stateWrite: prepared.stateWrite }
809
820
  }
810
821
 
811
822
  async function handleWebSocket(socket, env, ctx, payload, getState, setState) {
@@ -815,7 +826,7 @@ async function handleWebSocket(socket, env, ctx, payload, getState, setState) {
815
826
  } catch {
816
827
  clearInterval(heartbeat)
817
828
  }
818
- }, 5000)
829
+ }, 15000)
819
830
  try {
820
831
  const result = await prepareWebSocketUpstream(env, getState(), payload)
821
832
  if (result.error) {
@@ -914,22 +925,33 @@ export default {
914
925
  return new Response(null, { status: 101, webSocket: client })
915
926
  }
916
927
 
917
- if (request.method === 'GET') return new Response('ok')
928
+ if (request.method === 'GET') {
929
+ return Response.json({ status: 'ok', transports: ['http', 'websocket'] })
930
+ }
918
931
  if (request.method !== 'POST') return new Response('method not allowed', { status: 405 })
919
932
  if (request.headers.get('x-relay-token') !== env.RELAY_TOKEN) {
920
933
  return new Response('unauthorized', { status: 401 })
921
934
  }
922
935
 
923
- const payload = await request.json()
924
- const result = await handleRelayPayload(env, payload)
925
- if (result.error) return Response.json({ error: result.error }, { status: result.status })
936
+ try {
937
+ const payload = await request.json()
938
+ const result = await handleRelayPayload(env, payload)
939
+ if (result.error) return Response.json({ error: result.error }, { status: result.status })
926
940
 
927
- const upstream = result.upstream
928
- return new Response(upstream.body, {
929
- status: upstream.status,
930
- statusText: upstream.statusText,
931
- headers: upstream.headers,
932
- })
941
+ if (result.stateWrite) ctx.waitUntil(result.stateWrite)
942
+
943
+ const upstream = result.upstream
944
+ return new Response(upstream.body, {
945
+ status: upstream.status,
946
+ statusText: upstream.statusText,
947
+ headers: upstream.headers,
948
+ })
949
+ } catch (error) {
950
+ return Response.json(
951
+ { error: error instanceof Error ? error.message : 'internal relay error' },
952
+ { status: 502 },
953
+ )
954
+ }
933
955
  },
934
956
  }
935
957
  `;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cortexkit/anthropic-auth-core",
3
- "version": "1.1.3",
3
+ "version": "1.2.1",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",