@cortexkit/anthropic-auth-core 2.2.7 → 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 +1 -0
- package/dist/index.js +61 -1
- package/dist/oauth-origin.d.ts +51 -0
- package/dist/relay.d.ts +6 -0
- package/package.json +1 -1
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}`,
|
|
@@ -25827,8 +25829,10 @@ async function sendViaRelay(options) {
|
|
|
25827
25829
|
dumpTag,
|
|
25828
25830
|
onDumpCreated,
|
|
25829
25831
|
setTimeoutImpl = globalThis.setTimeout,
|
|
25830
|
-
clearTimeoutImpl = globalThis.clearTimeout
|
|
25832
|
+
clearTimeoutImpl = globalThis.clearTimeout,
|
|
25833
|
+
requireUpstreamUrl
|
|
25831
25834
|
} = options;
|
|
25835
|
+
requireUpstreamUrl?.(input instanceof Request ? input.url : input.toString());
|
|
25832
25836
|
if (!config || !isRelayableAnthropicRequest(input, body))
|
|
25833
25837
|
return fallback();
|
|
25834
25838
|
const affinity = explicitAffinity || headers.get("x-session-affinity") || headers.get("x-opencode-session");
|
|
@@ -31363,6 +31367,57 @@ async function disconnectNativeVaultForHost(options) {
|
|
|
31363
31367
|
closeNativeVaultAuthority(paths.tokenPath);
|
|
31364
31368
|
return result;
|
|
31365
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
|
+
}
|
|
31366
31421
|
// src/prime.ts
|
|
31367
31422
|
import { createHash as createHash21 } from "node:crypto";
|
|
31368
31423
|
import { realpathSync as realpathSync2 } from "node:fs";
|
|
@@ -33093,6 +33148,7 @@ export {
|
|
|
33093
33148
|
resetCache1hState,
|
|
33094
33149
|
resetBillingHeaderCCH,
|
|
33095
33150
|
requireNoSupervisedAuthContentSnapshot,
|
|
33151
|
+
requireAnthropicOAuthOrigin,
|
|
33096
33152
|
requestGzipPadding,
|
|
33097
33153
|
remapRequestBodyModel,
|
|
33098
33154
|
remapModelId,
|
|
@@ -33328,6 +33384,7 @@ export {
|
|
|
33328
33384
|
createNativeCustody,
|
|
33329
33385
|
createNativeAccountRuntime,
|
|
33330
33386
|
createEmptyStorage,
|
|
33387
|
+
createAnthropicOAuthFetch,
|
|
33331
33388
|
countUserPrompts,
|
|
33332
33389
|
connectClaustrumEnrollmentClient2 as connectClaustrumEnrollmentClient,
|
|
33333
33390
|
configuredAnthropicOAuthAccountCount,
|
|
@@ -33524,8 +33581,11 @@ export {
|
|
|
33524
33581
|
CACHE_KEEP_EXTENDED_TTL_BETA,
|
|
33525
33582
|
CACHE_1H_MODES,
|
|
33526
33583
|
CACHE_1H_COMMAND_NAME,
|
|
33584
|
+
AnthropicOAuthOriginError,
|
|
33527
33585
|
AXIOS_USER_AGENT,
|
|
33528
33586
|
AUTHORIZE_URLS,
|
|
33587
|
+
ANTHROPIC_OAUTH_ORIGIN_REFUSAL,
|
|
33588
|
+
ANTHROPIC_OAUTH_ORIGIN,
|
|
33529
33589
|
ANTHROPIC_CUSTOM_HEADERS_ENV,
|
|
33530
33590
|
ACCOUNT_STATE_FILE_NAME,
|
|
33531
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";
|