@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 +1 -0
- package/dist/index.js +97 -24
- package/dist/oauth-origin.d.ts +51 -0
- package/dist/relay.d.ts +6 -0
- package/dist/sticky-routing.d.ts +1 -1
- 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}`,
|
|
@@ -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
|
-
|
|
24197
|
-
|
|
24198
|
-
|
|
24199
|
-
|
|
24200
|
-
|
|
24201
|
-
|
|
24202
|
-
|
|
24203
|
-
|
|
24204
|
-
|
|
24205
|
-
|
|
24206
|
-
|
|
24207
|
-
if (
|
|
24208
|
-
|
|
24209
|
-
|
|
24210
|
-
|
|
24211
|
-
|
|
24212
|
-
|
|
24213
|
-
|
|
24214
|
-
|
|
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: "
|
|
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",
|
|
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";
|
package/dist/sticky-routing.d.ts
CHANGED
|
@@ -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';
|