@cortexkit/anthropic-auth-core 1.22.0 → 2.0.0

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.
Files changed (133) hide show
  1. package/README.md +2 -0
  2. package/dist/accounts.d.ts +52 -8
  3. package/dist/cachekeep.d.ts +27 -1
  4. package/dist/cch.d.ts +11 -1
  5. package/dist/claude-code.d.ts +23 -1
  6. package/dist/claustrum-enrollment.d.ts +72 -0
  7. package/dist/claustrum-scoped-roster.d.ts +27 -0
  8. package/dist/claustrum-scoped-runtime.d.ts +31 -0
  9. package/dist/claustrum-scoped.d.ts +53 -0
  10. package/dist/claustrum.d.ts +16 -82
  11. package/dist/commands/account.d.ts +38 -11
  12. package/dist/constants.d.ts +3 -2
  13. package/dist/custom-headers.d.ts +3 -0
  14. package/dist/index.d.ts +50 -0
  15. package/dist/index.js +29700 -31
  16. package/dist/internal-types/LICENSE +21 -0
  17. package/dist/internal-types/commands/builtins.d.ts +71 -0
  18. package/dist/internal-types/commands/index.d.ts +10 -0
  19. package/dist/internal-types/commands/menu.d.ts +61 -0
  20. package/dist/internal-types/commands/model.d.ts +238 -0
  21. package/dist/internal-types/commands/pi.d.ts +23 -0
  22. package/dist/internal-types/commands/seam.d.ts +72 -0
  23. package/dist/internal-types/fs/refresh-file-lock.d.ts +33 -0
  24. package/dist/internal-types/logger/capture-sink.d.ts +14 -0
  25. package/dist/internal-types/logger/engine.d.ts +100 -0
  26. package/dist/internal-types/logger/index.d.ts +6 -0
  27. package/dist/internal-types/logger/redact.d.ts +11 -0
  28. package/dist/internal-types/pi-slot/index.d.ts +3 -0
  29. package/dist/internal-types/pi-slot/slot.d.ts +49 -0
  30. package/dist/internal-types/quota/codec.d.ts +11 -0
  31. package/dist/internal-types/quota/format.d.ts +46 -0
  32. package/dist/internal-types/quota/index.d.ts +10 -0
  33. package/dist/internal-types/quota/map.d.ts +63 -0
  34. package/dist/internal-types/quota/merge.d.ts +48 -0
  35. package/dist/internal-types/quota/projection.d.ts +64 -0
  36. package/dist/internal-types/store/attribution.d.ts +28 -0
  37. package/dist/internal-types/store/errors.d.ts +55 -0
  38. package/dist/internal-types/store/hooks.d.ts +12 -0
  39. package/dist/internal-types/store/identity.d.ts +39 -0
  40. package/dist/internal-types/store/index.d.ts +21 -0
  41. package/dist/internal-types/store/mutate.d.ts +189 -0
  42. package/dist/internal-types/store/pool.d.ts +196 -0
  43. package/dist/internal-types/store/provider-state.d.ts +131 -0
  44. package/dist/internal-types/store/publication.d.ts +47 -0
  45. package/dist/internal-types/store/pull.d.ts +36 -0
  46. package/dist/internal-types/store/refresh-lock.d.ts +63 -0
  47. package/dist/internal-types/store/refresh.d.ts +72 -0
  48. package/dist/internal-types/store/rows.d.ts +313 -0
  49. package/dist/internal-types/store/runtime.d.ts +36 -0
  50. package/dist/internal-types/store/schema.d.ts +443 -0
  51. package/dist/internal-types/store/settings.d.ts +63 -0
  52. package/dist/internal-types/store/torn.d.ts +78 -0
  53. package/dist/migration-source.d.ts +33 -0
  54. package/dist/model-remap.d.ts +27 -0
  55. package/dist/models.d.ts +60 -0
  56. package/dist/native-account-runtime.d.ts +121 -0
  57. package/dist/native-account-view.d.ts +126 -0
  58. package/dist/native-command-effects.d.ts +59 -0
  59. package/dist/native-credential-validation.d.ts +42 -0
  60. package/dist/native-custody-activation.d.ts +45 -0
  61. package/dist/native-custody.d.ts +114 -0
  62. package/dist/native-enrollment-authority.d.ts +2 -0
  63. package/dist/native-host-auth-read.d.ts +3 -0
  64. package/dist/native-host-auth-write.d.ts +51 -0
  65. package/dist/native-host-auth.d.ts +12 -0
  66. package/dist/native-local-credential-service.d.ts +76 -0
  67. package/dist/native-local-runtime-readers.d.ts +22 -0
  68. package/dist/native-lock-observer.d.ts +12 -0
  69. package/dist/native-menu-executor.d.ts +243 -0
  70. package/dist/native-menu-model.d.ts +142 -0
  71. package/dist/native-migration-proof.d.ts +28 -0
  72. package/dist/native-migration-source.d.ts +47 -0
  73. package/dist/native-migration.d.ts +66 -0
  74. package/dist/native-quota-codec.d.ts +32 -0
  75. package/dist/native-refresh-coordinator.d.ts +240 -0
  76. package/dist/native-roster-seed.d.ts +115 -0
  77. package/dist/native-runtime-registry.d.ts +19 -0
  78. package/dist/native-runtime.d.ts +204 -0
  79. package/dist/native-transient-account.d.ts +38 -0
  80. package/dist/native-ui.d.ts +45 -0
  81. package/dist/native-usage-publication.d.ts +11 -0
  82. package/dist/native-vault-admission.d.ts +8 -0
  83. package/dist/native-vault-connection.d.ts +33 -0
  84. package/dist/native-vault-paths.d.ts +13 -0
  85. package/dist/native-vault-runtime.d.ts +102 -0
  86. package/dist/native-vault-seed.d.ts +16 -0
  87. package/dist/oauth-profile.d.ts +2 -1
  88. package/dist/pool-authority.d.ts +159 -0
  89. package/dist/pool-binding.d.ts +32 -0
  90. package/dist/pool-paths.d.ts +21 -0
  91. package/dist/pool-store.d.ts +32 -0
  92. package/dist/prime.d.ts +11 -3
  93. package/dist/provider-error.d.ts +139 -0
  94. package/dist/quota-header-feed.d.ts +13 -0
  95. package/dist/quota-manager.d.ts +30 -5
  96. package/dist/relay.d.ts +22 -1
  97. package/dist/request-history.d.ts +34 -0
  98. package/dist/sse.d.ts +81 -0
  99. package/dist/sticky-routing.d.ts +29 -0
  100. package/package.json +9 -3
  101. package/dist/accounts.js +0 -3330
  102. package/dist/auth.js +0 -182
  103. package/dist/cache1h.js +0 -97
  104. package/dist/cachekeep-registry.js +0 -124
  105. package/dist/cachekeep.js +0 -578
  106. package/dist/cch.js +0 -127
  107. package/dist/claude-code.js +0 -388
  108. package/dist/claustrum.js +0 -529
  109. package/dist/commands/account.js +0 -239
  110. package/dist/constants.js +0 -82
  111. package/dist/dump.js +0 -616
  112. package/dist/fast.js +0 -66
  113. package/dist/json.js +0 -10
  114. package/dist/killswitch.js +0 -132
  115. package/dist/logger.js +0 -286
  116. package/dist/logging.js +0 -59
  117. package/dist/mid-conversation-output-config.js +0 -89
  118. package/dist/models.js +0 -141
  119. package/dist/network-errors.js +0 -27
  120. package/dist/oauth-profile.js +0 -54
  121. package/dist/pkce.js +0 -17
  122. package/dist/prime.js +0 -823
  123. package/dist/provider.js +0 -0
  124. package/dist/quota-header-feed.js +0 -279
  125. package/dist/quota-headers.js +0 -121
  126. package/dist/quota-manager.js +0 -805
  127. package/dist/quotas.js +0 -141
  128. package/dist/relay.js +0 -1243
  129. package/dist/routing.js +0 -94
  130. package/dist/start.js +0 -29
  131. package/dist/sticky-routing.js +0 -523
  132. package/dist/thinking-binding.js +0 -54
  133. package/dist/token-fingerprint.js +0 -8
package/dist/relay.js DELETED
@@ -1,1243 +0,0 @@
1
- import { Buffer } from 'node:buffer';
2
- import { createHash, randomBytes } from 'node:crypto';
3
- import { dumpRelayRequest } from './dump.js';
4
- import { relayLog } from './logger.js';
5
- class RelayStateMismatchError extends Error {
6
- }
7
- class RelayWebSocketConnectionResetError extends Error {
8
- code = 'ECONNRESET';
9
- syscall = 'websocket';
10
- closeCode;
11
- closeReason;
12
- wasClean;
13
- constructor(message, event) {
14
- super(message);
15
- this.name = 'RelayWebSocketConnectionResetError';
16
- this.closeCode = event?.code;
17
- this.closeReason = event?.reason;
18
- this.wasClean = event?.wasClean;
19
- }
20
- }
21
- const sessionState = new Map();
22
- const websocketSessions = new Map();
23
- const loggedRelayConfigMessages = new Set();
24
- const RELAY_SESSION_STATE_TTL_MS = 2 * 60 * 60_000;
25
- const RELAY_WEBSOCKET_IDLE_TTL_MS = 30 * 60_000;
26
- const MAX_RELAY_SESSION_STATES = 128;
27
- const MAX_RELAY_WEBSOCKET_SESSIONS = 32;
28
- let nextRequestId = 0;
29
- function createRequestId() {
30
- nextRequestId += 1;
31
- return `req_${Date.now().toString(36)}_${nextRequestId.toString(36)}`;
32
- }
33
- function logRelayConfigOnce(message) {
34
- if (loggedRelayConfigMessages.has(message))
35
- return;
36
- loggedRelayConfigMessages.add(message);
37
- relayLog(message);
38
- }
39
- export function getRelayConfig(storage) {
40
- const relay = storage?.relay;
41
- if (!relay) {
42
- logRelayConfigOnce('disabled: no relay config found');
43
- return null;
44
- }
45
- if (relay.enabled !== true) {
46
- logRelayConfigOnce('disabled: relay.enabled is not true');
47
- return null;
48
- }
49
- if (!relay.url?.trim() || !relay.token?.trim()) {
50
- logRelayConfigOnce('disabled: missing relay url or token');
51
- return null;
52
- }
53
- const transport = relay.transport === 'http' ? 'http' : 'websocket';
54
- logRelayConfigOnce(`configured transport=${transport} protocol=${transport === 'websocket' ? 2 : 1} url=${relay.url.trim()} fallbackToDirect=${relay.fallbackToDirect !== false}`);
55
- return {
56
- enabled: true,
57
- url: relay.url.trim(),
58
- token: relay.token.trim(),
59
- fallbackToDirect: relay.fallbackToDirect !== false,
60
- transport,
61
- };
62
- }
63
- export function generateRelayToken() {
64
- return randomBytes(32).toString('base64url');
65
- }
66
- export function hashBody(body) {
67
- return `sha256:${createHash('sha256').update(body).digest('hex')}`;
68
- }
69
- export function createStringPatch(previous, next) {
70
- let start = 0;
71
- const maxPrefix = Math.min(previous.length, next.length);
72
- while (start < maxPrefix &&
73
- previous.charCodeAt(start) === next.charCodeAt(start)) {
74
- start++;
75
- }
76
- let previousEnd = previous.length;
77
- let nextEnd = next.length;
78
- while (previousEnd > start &&
79
- nextEnd > start &&
80
- previous.charCodeAt(previousEnd - 1) === next.charCodeAt(nextEnd - 1)) {
81
- previousEnd--;
82
- nextEnd--;
83
- }
84
- return {
85
- start,
86
- deleteCount: previousEnd - start,
87
- insert: next.slice(start, nextEnd),
88
- };
89
- }
90
- function isNoopPatch(patch) {
91
- return patch.deleteCount === 0 && patch.insert.length === 0;
92
- }
93
- function findCchTokenRange(body) {
94
- const match = body.match(/\bcch=([0-9a-f]{5});/);
95
- const token = match?.[1];
96
- if (!token || match.index == null)
97
- return null;
98
- const start = match.index + 'cch='.length;
99
- return { start, end: start + token.length, token };
100
- }
101
- function replaceRange(text, start, end, replacement) {
102
- return `${text.slice(0, start)}${replacement}${text.slice(end)}`;
103
- }
104
- function createRelayPatch(previous, next) {
105
- const previousCch = findCchTokenRange(previous);
106
- const nextCch = findCchTokenRange(next);
107
- if (previousCch &&
108
- nextCch &&
109
- previousCch.start === nextCch.start &&
110
- previousCch.end - previousCch.start === nextCch.end - nextCch.start &&
111
- previousCch.token !== nextCch.token) {
112
- const cchPatch = {
113
- start: previousCch.start,
114
- deleteCount: previousCch.end - previousCch.start,
115
- insert: nextCch.token,
116
- };
117
- const previousWithNextCch = replaceRange(previous, previousCch.start, previousCch.end, nextCch.token);
118
- const tailPatch = createStringPatch(previousWithNextCch, next);
119
- return isNoopPatch(tailPatch) ? cchPatch : [cchPatch, tailPatch];
120
- }
121
- return createStringPatch(previous, next);
122
- }
123
- function isRelayableAnthropicRequest(input, body) {
124
- if (typeof body !== 'string')
125
- return false;
126
- try {
127
- const url = input instanceof Request ? new URL(input.url) : new URL(input.toString());
128
- return url.pathname === '/v1/messages' || url.pathname === '/messages';
129
- }
130
- catch {
131
- return false;
132
- }
133
- }
134
- function jsonHeaders(headers) {
135
- const result = {};
136
- for (const [key, value] of headers.entries()) {
137
- if (key === 'x-session-affinity' || key === 'x-opencode-session')
138
- continue;
139
- result[key] = value;
140
- }
141
- return result;
142
- }
143
- async function postRelay(config, payload, signal) {
144
- const stringifyStart = perfNowMs();
145
- const body = JSON.stringify(payload);
146
- relayPerfLog('http_payload_stringify', {
147
- session: shortAffinity(payload.affinity),
148
- ms: formatMs(perfNowMs() - stringifyStart),
149
- relayBytes: body.length,
150
- mode: payload.mode,
151
- });
152
- return fetch(config.url, {
153
- method: 'POST',
154
- headers: {
155
- 'content-type': 'application/json',
156
- 'x-relay-token': config.token,
157
- },
158
- body,
159
- signal: signal ?? undefined,
160
- });
161
- }
162
- function shortAffinity(affinity) {
163
- return affinity.length > 12 ? `${affinity.slice(0, 12)}…` : affinity;
164
- }
165
- function perfNowMs() {
166
- return performance.now();
167
- }
168
- function formatMs(value) {
169
- return Math.round(value * 10) / 10;
170
- }
171
- function relayPerfLoggingEnabled() {
172
- return process.env.OPENCODE_ANTHROPIC_AUTH_PERF === '1';
173
- }
174
- function relayPerfLog(stage, data) {
175
- if (!relayPerfLoggingEnabled())
176
- return;
177
- relayLog(`[perf] relay ${stage} ${JSON.stringify(data)}`);
178
- }
179
- function createRelayPayload(options) {
180
- const { input, init, headers, bodyText, affinity, previous, nextHash } = options;
181
- const rewrittenUrl = input instanceof Request ? input.url : input.toString();
182
- const basePayload = {
183
- protocol: 1,
184
- type: 'request',
185
- affinity,
186
- upstream: {
187
- url: rewrittenUrl,
188
- method: init?.method || (input instanceof Request ? input.method : 'POST'),
189
- headers: jsonHeaders(headers),
190
- },
191
- next_hash: nextHash,
192
- };
193
- return previous
194
- ? {
195
- ...basePayload,
196
- mode: 'patch',
197
- base_hash: previous.hash,
198
- revision: previous.revision + 1,
199
- patch: createRelayPatch(previous.body, bodyText),
200
- }
201
- : {
202
- ...basePayload,
203
- mode: 'full_sync',
204
- revision: 1,
205
- body: bodyText,
206
- };
207
- }
208
- function createFullSyncPayload(payload, bodyText) {
209
- return {
210
- protocol: payload.protocol,
211
- type: 'request',
212
- affinity: payload.affinity,
213
- upstream: payload.upstream,
214
- next_hash: payload.next_hash,
215
- mode: 'full_sync',
216
- revision: 1,
217
- body: bodyText,
218
- };
219
- }
220
- function updateLocalRelayState(affinity, bodyText, nextHash, revision) {
221
- const now = Date.now();
222
- sessionState.delete(affinity);
223
- sessionState.set(affinity, {
224
- body: bodyText,
225
- hash: nextHash,
226
- revision,
227
- updatedAt: now,
228
- });
229
- cleanupRelaySessionCaches(now);
230
- }
231
- async function sendRelayHttp(options) {
232
- const { config, payload, bodyText, fallback, signal } = options;
233
- let actualPayload = payload;
234
- let response = await postRelay(config, actualPayload, signal);
235
- if (response.status === 409 && actualPayload.mode === 'patch') {
236
- relayLog(`state mismatch from relay session=${shortAffinity(actualPayload.affinity)}; retrying full_sync`);
237
- await response.body?.cancel().catch(() => { });
238
- actualPayload = createFullSyncPayload(actualPayload, bodyText);
239
- response = await postRelay(config, actualPayload, signal);
240
- }
241
- if (!response.ok && response.status >= 500 && config.fallbackToDirect) {
242
- relayLog(`relay returned ${response.status}; falling back direct session=${shortAffinity(actualPayload.affinity)}`);
243
- await response.body?.cancel().catch(() => { });
244
- return {
245
- response: await fallback(),
246
- payload: actualPayload,
247
- transport: 'http',
248
- protocol: actualPayload.protocol,
249
- usedRelay: false,
250
- };
251
- }
252
- return {
253
- response,
254
- payload: actualPayload,
255
- transport: 'http',
256
- protocol: actualPayload.protocol,
257
- usedRelay: true,
258
- };
259
- }
260
- function toWebSocketUrl(url, token) {
261
- const parsed = new URL(url);
262
- parsed.protocol = parsed.protocol === 'http:' ? 'ws:' : 'wss:';
263
- parsed.pathname = '/ws';
264
- parsed.searchParams.set('token', token);
265
- return parsed.toString();
266
- }
267
- function decodeRelayChunk(base64) {
268
- return new Uint8Array(Buffer.from(base64, 'base64'));
269
- }
270
- function toBinaryChunk(data) {
271
- if (data instanceof Uint8Array)
272
- return data;
273
- if (data instanceof ArrayBuffer)
274
- return new Uint8Array(data);
275
- if (ArrayBuffer.isView(data)) {
276
- return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
277
- }
278
- return new Uint8Array(Buffer.from(String(data)));
279
- }
280
- function parseRelayControlMessage(data) {
281
- return JSON.parse(data);
282
- }
283
- function formatCloseEvent(event) {
284
- const close = event;
285
- const parts = [
286
- `code=${close.code ?? 'unknown'}`,
287
- `wasClean=${close.wasClean ?? 'unknown'}`,
288
- ];
289
- if (close.reason)
290
- parts.push(`reason=${JSON.stringify(close.reason)}`);
291
- return parts.join(' ');
292
- }
293
- function relayControlErrorMessage(message) {
294
- const detail = message.message || 'relay websocket error';
295
- return message.status
296
- ? `relay websocket error ${message.status}: ${detail}`
297
- : detail;
298
- }
299
- class PersistentRelaySession {
300
- config;
301
- affinity;
302
- timers;
303
- socket;
304
- connecting;
305
- queue = Promise.resolve();
306
- pending;
307
- serverState;
308
- lastUsedAt = Date.now();
309
- constructor(config, affinity, timers) {
310
- this.config = config;
311
- this.affinity = affinity;
312
- this.timers = timers;
313
- }
314
- touch(now = Date.now()) {
315
- this.lastUsedAt = now;
316
- }
317
- canEvict(now = Date.now()) {
318
- return (!this.pending &&
319
- !this.connecting &&
320
- now - this.lastUsedAt > RELAY_WEBSOCKET_IDLE_TTL_MS);
321
- }
322
- dispose() {
323
- this.socket?.close();
324
- this.socket = undefined;
325
- this.serverState = undefined;
326
- this.connecting = undefined;
327
- }
328
- send(payload, bodyText, optimisticResponse = false, onResponseHeaders) {
329
- this.touch();
330
- const enqueuedAt = perfNowMs();
331
- const start = this.queue
332
- .catch(() => { })
333
- .then(() => this.startQueued(payload, bodyText, enqueuedAt, optimisticResponse, onResponseHeaders));
334
- const result = start.then(async ({ response, getPayload }) => ({
335
- response: await response,
336
- payload: getPayload(),
337
- transport: 'websocket',
338
- protocol: 2,
339
- usedRelay: true,
340
- }));
341
- this.queue = start.then(({ done }) => done.catch(() => { }), () => { });
342
- return result;
343
- }
344
- async startQueued(payload, bodyText, enqueuedAt, optimisticResponse, onResponseHeaders) {
345
- const connectStart = perfNowMs();
346
- await this.ensureConnected();
347
- const connectedAt = perfNowMs();
348
- const localState = sessionState.get(this.affinity);
349
- const serverHash = this.serverState?.hash;
350
- const requestPayload = {
351
- ...payload,
352
- protocol: 2,
353
- id: createRequestId(),
354
- };
355
- if (requestPayload.mode === 'patch' &&
356
- (!serverHash || serverHash !== requestPayload.base_hash)) {
357
- requestPayload.mode = 'full_sync';
358
- requestPayload.revision = (this.serverState?.revision ?? 0) + 1;
359
- delete requestPayload.base_hash;
360
- delete requestPayload.patch;
361
- requestPayload.body = bodyText;
362
- }
363
- if (requestPayload.mode === 'patch' && localState) {
364
- requestPayload.patch = createRelayPatch(localState.body, bodyText);
365
- }
366
- relayLog(`perf websocket send_start session=${shortAffinity(this.affinity)} request=${requestPayload.id} mode=${requestPayload.mode} queueMs=${formatMs(connectedAt - enqueuedAt)} connectMs=${formatMs(connectedAt - connectStart)} bodyBytes=${bodyText.length} relayBytes=${JSON.stringify(requestPayload).length}`);
367
- let activePayload = requestPayload;
368
- const first = this.sendPayload(requestPayload, bodyText, optimisticResponse, onResponseHeaders);
369
- void first.done.catch(() => { });
370
- let activeDone = first.done;
371
- const response = first.response.catch((error) => {
372
- if (!(error instanceof RelayStateMismatchError) ||
373
- requestPayload.mode !== 'patch') {
374
- throw error;
375
- }
376
- const fullSync = createFullSyncPayload(requestPayload, bodyText);
377
- fullSync.protocol = 2;
378
- fullSync.id = createRequestId();
379
- fullSync.revision = (this.serverState?.revision ?? 0) + 1;
380
- activePayload = fullSync;
381
- const retry = this.sendPayload(fullSync, bodyText, optimisticResponse, onResponseHeaders);
382
- void retry.done.catch(() => { });
383
- activeDone = retry.done;
384
- return retry.response;
385
- });
386
- const done = response.then(() => activeDone, () => activeDone.catch(() => { }));
387
- return { response, done, getPayload: () => activePayload };
388
- }
389
- async ensureConnected() {
390
- if (this.socket?.readyState === WebSocket.OPEN &&
391
- this.serverState !== undefined)
392
- return;
393
- if (this.connecting)
394
- return await this.connecting;
395
- this.connecting = new Promise((resolve, reject) => {
396
- const url = toWebSocketUrl(this.config.url, this.config.token);
397
- const parsed = new URL(url);
398
- parsed.searchParams.set('affinity', this.affinity);
399
- const socket = new WebSocket(parsed.toString());
400
- this.socket = socket;
401
- this.serverState = undefined;
402
- const timeout = this.timers.setTimeout(() => {
403
- socket.close();
404
- reject(new Error('relay websocket ready timed out'));
405
- }, 15_000);
406
- socket.addEventListener('message', (event) => {
407
- try {
408
- if (typeof event.data !== 'string') {
409
- this.handleBinaryChunk(event.data);
410
- return;
411
- }
412
- const message = parseRelayControlMessage(event.data);
413
- if (message.type === 'ready') {
414
- this.timers.clearTimeout(timeout);
415
- this.serverState = message.state;
416
- resolve();
417
- return;
418
- }
419
- this.handleMessage(message);
420
- }
421
- catch (error) {
422
- this.timers.clearTimeout(timeout);
423
- reject(error);
424
- this.failPending(error);
425
- socket.close();
426
- }
427
- });
428
- socket.addEventListener('error', () => {
429
- this.timers.clearTimeout(timeout);
430
- const error = new RelayWebSocketConnectionResetError('relay websocket error');
431
- reject(error);
432
- this.failPending(error);
433
- });
434
- socket.addEventListener('close', (event) => {
435
- this.timers.clearTimeout(timeout);
436
- if (this.socket === socket) {
437
- this.socket = undefined;
438
- this.serverState = undefined;
439
- this.connecting = undefined;
440
- }
441
- if (this.pending) {
442
- if (this.pending.retryingBeforeResponse)
443
- return;
444
- const closedAfterResponseStart = this.pending.responseStartedAt != null;
445
- const reason = closedAfterResponseStart
446
- ? 'closed after response_start before stream bytes'
447
- : 'closed before response';
448
- if (this.retryPendingBeforeResponse(this.pending, reason, event))
449
- return;
450
- const error = closedAfterResponseStart
451
- ? new RelayWebSocketConnectionResetError('relay websocket closed during response stream', event)
452
- : new Error('relay websocket closed before response');
453
- relayLog(`websocket ${closedAfterResponseStart ? 'closed during response stream' : 'closed before response without retry'} session=${shortAffinity(this.affinity)} request=${this.pending.payload.id} chunks=${this.pending.streamChunkCount} bytes=${this.pending.streamByteCount} ${formatCloseEvent(event)}`);
454
- this.failPending(error);
455
- }
456
- });
457
- }).finally(() => {
458
- this.connecting = undefined;
459
- });
460
- await this.connecting;
461
- }
462
- sendPayload(payload, bodyText, optimisticResponse, onResponseHeaders) {
463
- const socket = this.socket;
464
- if (!socket || socket.readyState !== WebSocket.OPEN) {
465
- throw new Error('relay websocket is not connected');
466
- }
467
- const sentAt = perfNowMs();
468
- let resolveDone;
469
- let rejectDone;
470
- const done = new Promise((resolve, reject) => {
471
- resolveDone = resolve;
472
- rejectDone = reject;
473
- });
474
- const response = new Promise((resolve, reject) => {
475
- const pending = {
476
- payload,
477
- bodyText,
478
- resolve,
479
- reject,
480
- streamDone: false,
481
- accepted: false,
482
- responseStarted: false,
483
- resolveDone,
484
- rejectDone,
485
- sentAt,
486
- optimisticResponse,
487
- streamChunkCount: 0,
488
- streamByteCount: 0,
489
- retryAttempts: 0,
490
- retryingBeforeResponse: false,
491
- onResponseHeaders,
492
- stagedResponseHeaders: undefined,
493
- responseHeadersDelivered: false,
494
- };
495
- this.pending = pending;
496
- this.resetPendingTimeout(pending);
497
- socket.send(JSON.stringify(payload));
498
- if (optimisticResponse) {
499
- this.resolvePendingResponse(pending, 200, 'OK', {
500
- 'content-type': 'text/event-stream',
501
- 'x-cortexkit-relay-optimistic': 'true',
502
- });
503
- }
504
- });
505
- return { response, done };
506
- }
507
- resetPendingTimeout(pending) {
508
- this.timers.clearTimeout(pending.timeout);
509
- pending.timeout = this.timers.setTimeout(() => {
510
- if (this.pending !== pending)
511
- return;
512
- if (this.retryPendingBeforeResponse(pending, 'response timed out'))
513
- return;
514
- this.failPending(new Error('relay websocket response timed out'));
515
- this.socket?.close();
516
- }, 45_000);
517
- }
518
- retryPendingBeforeResponse(pending, reason, closeEvent) {
519
- if (pending.responseStartedAt != null && pending.streamByteCount > 0)
520
- return false;
521
- if (pending.streamDone)
522
- return false;
523
- if (pending.retryAttempts >= 1)
524
- return false;
525
- pending.retryAttempts += 1;
526
- pending.retryingBeforeResponse = true;
527
- this.timers.clearTimeout(pending.timeout);
528
- const attempt = pending.retryAttempts;
529
- const originalId = pending.payload.id;
530
- relayLog(`websocket ${reason}; reconnecting and retrying full_sync session=${shortAffinity(this.affinity)} request=${originalId} retry=${attempt} chunks=${pending.streamChunkCount} bytes=${pending.streamByteCount}${closeEvent ? ` ${formatCloseEvent(closeEvent)}` : ''}`);
531
- this.socket?.close();
532
- this.socket = undefined;
533
- this.serverState = undefined;
534
- this.connecting = undefined;
535
- void (async () => {
536
- try {
537
- await this.ensureConnected();
538
- if (this.pending !== pending)
539
- return;
540
- const retryPayload = createFullSyncPayload(pending.payload, pending.bodyText);
541
- retryPayload.protocol = 2;
542
- retryPayload.id = createRequestId();
543
- retryPayload.revision = (this.serverState?.revision ?? 0) + 1;
544
- pending.payload = retryPayload;
545
- pending.accepted = false;
546
- pending.acceptedAt = undefined;
547
- pending.responseStartedAt = undefined;
548
- pending.stagedResponseHeaders = undefined;
549
- pending.responseHeadersDelivered = false;
550
- pending.sentAt = perfNowMs();
551
- pending.retryingBeforeResponse = false;
552
- const socket = this.socket;
553
- if (!socket || socket.readyState !== WebSocket.OPEN) {
554
- throw new Error('relay websocket is not connected');
555
- }
556
- this.resetPendingTimeout(pending);
557
- relayLog(`perf websocket retry_send session=${shortAffinity(this.affinity)} request=${retryPayload.id} previous=${originalId} mode=full_sync retry=${attempt} bodyBytes=${pending.bodyText.length} relayBytes=${JSON.stringify(retryPayload).length}`);
558
- socket.send(JSON.stringify(retryPayload));
559
- }
560
- catch (error) {
561
- pending.retryingBeforeResponse = false;
562
- if (this.pending === pending)
563
- this.failPending(error);
564
- }
565
- })();
566
- return true;
567
- }
568
- enqueueStreamChunk(pending, chunk) {
569
- this.deliverResponseHeaders(pending);
570
- pending.streamChunkCount += 1;
571
- pending.streamByteCount += chunk.byteLength;
572
- pending.streamController?.enqueue(chunk);
573
- }
574
- deliverResponseHeaders(pending) {
575
- if (pending.responseHeadersDelivered ||
576
- pending.stagedResponseHeaders == null)
577
- return;
578
- pending.responseHeadersDelivered = true;
579
- try {
580
- pending.onResponseHeaders?.(pending.stagedResponseHeaders);
581
- }
582
- catch { }
583
- }
584
- handleBinaryChunk(data) {
585
- const pending = this.pending;
586
- if (!pending?.responseStarted)
587
- return;
588
- this.enqueueStreamChunk(pending, toBinaryChunk(data));
589
- }
590
- resolvePendingResponse(pending, status, statusText, headers) {
591
- if (pending.responseStarted)
592
- return;
593
- pending.responseStarted = true;
594
- const stream = new ReadableStream({
595
- start: (controller) => {
596
- pending.streamController = controller;
597
- },
598
- cancel: () => {
599
- this.socket?.close();
600
- },
601
- });
602
- pending.resolve(new Response(stream, {
603
- status,
604
- statusText,
605
- headers,
606
- }));
607
- }
608
- handleMessage(message) {
609
- if (message.type === 'keepalive') {
610
- if (this.pending)
611
- this.resetPendingTimeout(this.pending);
612
- return;
613
- }
614
- const pending = this.pending;
615
- if (!pending)
616
- return;
617
- if ('id' in message && message.id && message.id !== pending.payload.id)
618
- return;
619
- if (message.type === 'accepted') {
620
- const acceptedAt = perfNowMs();
621
- pending.accepted = true;
622
- pending.acceptedAt = acceptedAt;
623
- relayLog(`perf websocket accepted session=${shortAffinity(this.affinity)} request=${pending.payload.id} sentMs=${formatMs(acceptedAt - pending.sentAt)} mode=${pending.payload.mode}`);
624
- this.serverState = { hash: message.hash, revision: message.revision };
625
- updateLocalRelayState(this.affinity, pending.bodyText, message.hash, message.revision);
626
- this.resetPendingTimeout(pending);
627
- return;
628
- }
629
- if (message.type === 'response_start') {
630
- if (pending.responseStartedAt != null)
631
- return;
632
- const responseStartedAt = perfNowMs();
633
- pending.responseStartedAt = responseStartedAt;
634
- relayLog(`perf websocket response_start session=${shortAffinity(this.affinity)} request=${pending.payload.id} sentMs=${formatMs(responseStartedAt - pending.sentAt)} upstreamMs=${pending.acceptedAt == null ? 'unknown' : formatMs(responseStartedAt - pending.acceptedAt)} status=${message.status}`);
635
- clearTimeout(pending.timeout);
636
- if (message.headers)
637
- pending.stagedResponseHeaders = new Headers(message.headers);
638
- this.resolvePendingResponse(pending, message.status, message.statusText, message.headers);
639
- if (pending.optimisticResponse && message.status >= 400) {
640
- this.deliverResponseHeaders(pending);
641
- pending.streamController?.enqueue(new TextEncoder().encode(`event: error\ndata: ${JSON.stringify({ type: 'error', error: { type: 'relay_upstream_error', message: `Relay upstream returned HTTP ${message.status}` } })}\n\n`));
642
- this.finishPending();
643
- }
644
- return;
645
- }
646
- if (message.type === 'chunk') {
647
- this.enqueueStreamChunk(pending, decodeRelayChunk(message.base64));
648
- return;
649
- }
650
- if (message.type === 'done') {
651
- this.finishPending(message);
652
- return;
653
- }
654
- if (message.type === 'error') {
655
- // If an optimistic response is already streaming (responseStartedAt is
656
- // set), the pending promise was already resolved — calling failPending
657
- // would reject a settled promise and leave the stream hanging. Instead,
658
- // inject an SSE error event and close cleanly.
659
- if (pending.optimisticResponse && pending.responseStartedAt != null) {
660
- relayLog(`websocket relay error during optimistic response session=${shortAffinity(this.affinity)} request=${pending.payload.id} status=${message.status} message=${message.message || 'unknown'} chunks=${pending.streamChunkCount} bytes=${pending.streamByteCount}`);
661
- this.deliverResponseHeaders(pending);
662
- pending.streamController?.enqueue(new TextEncoder().encode(`event: error\ndata: ${JSON.stringify({ type: 'error', error: { type: 'relay_error', message: message.message || 'relay error' } })}\n\n`));
663
- this.finishPending();
664
- this.socket?.close();
665
- return;
666
- }
667
- if (message.status === 409 &&
668
- this.retryPendingBeforeResponse(pending, message.message || 'state mismatch')) {
669
- return;
670
- }
671
- const error = message.status === 409
672
- ? new RelayStateMismatchError(message.message || 'state mismatch')
673
- : new Error(relayControlErrorMessage(message));
674
- this.failPending(error);
675
- }
676
- }
677
- finishPending(done) {
678
- const pending = this.pending;
679
- if (!pending)
680
- return;
681
- const finishedAt = perfNowMs();
682
- const workerStats = done && 'frames' in done
683
- ? ` upstreamChunks=${done.upstreamChunks ?? 'unknown'} upstreamBytes=${done.upstreamBytes ?? 'unknown'} frames=${done.frames} frameBytes=${done.frameBytes ?? 'unknown'}`
684
- : '';
685
- relayLog(`perf websocket done session=${shortAffinity(this.affinity)} request=${pending.payload.id} sentMs=${formatMs(finishedAt - pending.sentAt)} streamMs=${pending.responseStartedAt == null ? 'unknown' : formatMs(finishedAt - pending.responseStartedAt)} chunks=${pending.streamChunkCount} bytes=${pending.streamByteCount}${workerStats}`);
686
- this.timers.clearTimeout(pending.timeout);
687
- if (!pending.streamDone) {
688
- this.deliverResponseHeaders(pending);
689
- pending.streamDone = true;
690
- pending.streamController?.close();
691
- }
692
- pending.resolveDone();
693
- this.pending = undefined;
694
- }
695
- failPending(error) {
696
- const pending = this.pending;
697
- if (!pending)
698
- return;
699
- this.timers.clearTimeout(pending.timeout);
700
- this.pending = undefined;
701
- if (pending.responseStarted) {
702
- pending.streamController?.error(error);
703
- }
704
- else {
705
- pending.reject(error);
706
- }
707
- pending.rejectDone(error);
708
- }
709
- }
710
- function cleanupRelaySessionCaches(now = Date.now()) {
711
- for (const [affinity, state] of sessionState) {
712
- if (now - state.updatedAt > RELAY_SESSION_STATE_TTL_MS) {
713
- sessionState.delete(affinity);
714
- }
715
- }
716
- while (sessionState.size > MAX_RELAY_SESSION_STATES) {
717
- const oldest = sessionState.keys().next().value;
718
- if (oldest === undefined)
719
- break;
720
- sessionState.delete(oldest);
721
- }
722
- for (const [affinity, session] of websocketSessions) {
723
- if (session.canEvict(now)) {
724
- session.dispose();
725
- websocketSessions.delete(affinity);
726
- }
727
- }
728
- while (websocketSessions.size > MAX_RELAY_WEBSOCKET_SESSIONS) {
729
- let evicted = false;
730
- for (const [affinity, session] of websocketSessions) {
731
- if (!session.canEvict(now))
732
- continue;
733
- session.dispose();
734
- websocketSessions.delete(affinity);
735
- evicted = true;
736
- break;
737
- }
738
- if (!evicted)
739
- break;
740
- }
741
- }
742
- function getPersistentRelaySession(config, affinity, timers) {
743
- cleanupRelaySessionCaches();
744
- const existing = websocketSessions.get(affinity);
745
- if (existing) {
746
- existing.touch();
747
- return existing;
748
- }
749
- const session = new PersistentRelaySession(config, affinity, timers);
750
- websocketSessions.set(affinity, session);
751
- cleanupRelaySessionCaches();
752
- return session;
753
- }
754
- export async function sendViaRelay(options) {
755
- const { config, input, init, headers, body, fallback, affinity: explicitAffinity, optimisticResponse, onResponseHeaders, dumpTag, onDumpCreated, setTimeoutImpl = globalThis.setTimeout, clearTimeoutImpl = globalThis.clearTimeout, } = options;
756
- if (!config || !isRelayableAnthropicRequest(input, body))
757
- return fallback();
758
- const affinity = explicitAffinity ||
759
- headers.get('x-session-affinity') ||
760
- headers.get('x-opencode-session');
761
- if (!affinity) {
762
- relayLog('skipping relay: missing x-session-affinity header');
763
- return fallback();
764
- }
765
- const bodyText = body;
766
- const hashStart = perfNowMs();
767
- const nextHash = hashBody(bodyText);
768
- relayPerfLog('hash_body', {
769
- session: shortAffinity(affinity),
770
- ms: formatMs(perfNowMs() - hashStart),
771
- bodyBytes: bodyText.length,
772
- });
773
- const previous = sessionState.get(affinity);
774
- const payloadStart = perfNowMs();
775
- const payload = createRelayPayload({
776
- input,
777
- init,
778
- headers,
779
- bodyText,
780
- affinity,
781
- previous,
782
- nextHash,
783
- });
784
- relayPerfLog('create_payload', {
785
- session: shortAffinity(affinity),
786
- ms: formatMs(perfNowMs() - payloadStart),
787
- mode: payload.mode,
788
- bodyBytes: bodyText.length,
789
- previousBytes: previous?.body.length,
790
- });
791
- try {
792
- let result;
793
- const sendStart = perfNowMs();
794
- if (config.transport === 'websocket') {
795
- try {
796
- const session = getPersistentRelaySession(config, affinity, {
797
- setTimeout: setTimeoutImpl,
798
- clearTimeout: clearTimeoutImpl,
799
- });
800
- result = await session.send(payload, bodyText, optimisticResponse === true, onResponseHeaders);
801
- }
802
- catch (error) {
803
- relayLog(`websocket relay failed session=${shortAffinity(affinity)}; trying http relay: ${error instanceof Error ? error.message : String(error)}`);
804
- result = await sendRelayHttp({
805
- config,
806
- payload,
807
- bodyText,
808
- fallback,
809
- signal: init?.signal,
810
- });
811
- }
812
- }
813
- else {
814
- result = await sendRelayHttp({
815
- config,
816
- payload,
817
- bodyText,
818
- fallback,
819
- signal: init?.signal,
820
- });
821
- }
822
- relayPerfLog('send', {
823
- session: shortAffinity(affinity),
824
- ms: formatMs(perfNowMs() - sendStart),
825
- transport: result.transport,
826
- protocol: result.protocol,
827
- mode: result.payload.mode,
828
- status: result.response.status,
829
- usedRelay: result.usedRelay,
830
- });
831
- if (!result.usedRelay)
832
- return result.response;
833
- if (result.transport === 'http') {
834
- try {
835
- onResponseHeaders?.(result.response.headers);
836
- }
837
- catch { }
838
- }
839
- if (result.transport === 'http') {
840
- updateLocalRelayState(affinity, bodyText, result.payload.next_hash, result.payload.revision);
841
- }
842
- const payloadStringifyStart = perfNowMs();
843
- const actualPayloadBytes = JSON.stringify(result.payload).length;
844
- relayPerfLog('payload_stringify', {
845
- session: shortAffinity(affinity),
846
- ms: formatMs(perfNowMs() - payloadStringifyStart),
847
- relayBytes: actualPayloadBytes,
848
- });
849
- relayLog(`used relay transport=${result.transport} protocol=${result.protocol} mode=${result.payload.mode} status=${result.response.status} session=${shortAffinity(affinity)} bodyBytes=${bodyText.length} relayBytes=${actualPayloadBytes}`);
850
- const dumpStart = perfNowMs();
851
- const dumpHandle = await dumpRelayRequest({
852
- affinity,
853
- transport: result.transport,
854
- protocol: result.protocol,
855
- mode: result.payload.mode,
856
- status: result.response.status,
857
- bodyText,
858
- previousBodyText: previous?.body,
859
- payload: result.payload,
860
- relayBytes: actualPayloadBytes,
861
- tag: dumpTag,
862
- });
863
- try {
864
- if (dumpHandle)
865
- onDumpCreated?.(dumpHandle);
866
- }
867
- catch { }
868
- relayPerfLog('dump', {
869
- session: shortAffinity(affinity),
870
- ms: formatMs(perfNowMs() - dumpStart),
871
- enabled: true,
872
- });
873
- return result.response;
874
- }
875
- catch (error) {
876
- if (!config.fallbackToDirect) {
877
- relayLog(`relay failed session=${shortAffinity(affinity)} and fallbackToDirect=false: ${error instanceof Error ? error.message : String(error)}`);
878
- throw error;
879
- }
880
- relayLog(`relay failed; falling back direct session=${shortAffinity(affinity)}: ${error instanceof Error ? error.message : String(error)}`);
881
- return fallback();
882
- }
883
- }
884
- export const WORKER_SCRIPT = `
885
- async function hashBody(body) {
886
- const bytes = new TextEncoder().encode(body)
887
- const digest = await crypto.subtle.digest('SHA-256', bytes)
888
- const hex = [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, '0')).join('')
889
- return 'sha256:' + hex
890
- }
891
-
892
- function applyPatch(base, patch) {
893
- return base.slice(0, patch.start) + patch.insert + base.slice(patch.start + patch.deleteCount)
894
- }
895
-
896
- function applyPatchSet(base, patch) {
897
- if (!Array.isArray(patch)) return applyPatch(base, patch)
898
- if (patch.length === 0) return base
899
-
900
- let result = ''
901
- let cursor = 0
902
- for (const item of patch) {
903
- if (item.start < cursor) return { error: 'overlapping patch', status: 400 }
904
- result += base.slice(cursor, item.start)
905
- result += item.insert
906
- cursor = item.start + item.deleteCount
907
- }
908
- return result + base.slice(cursor)
909
- }
910
-
911
- async function readState(env, affinity) {
912
- const raw = await env.RELAY_STATE.get('session:' + affinity)
913
- return raw ? JSON.parse(raw) : null
914
- }
915
-
916
- async function writeState(env, affinity, state) {
917
- await env.RELAY_STATE.put('session:' + affinity, JSON.stringify(state), { expirationTtl: 86400 })
918
- }
919
-
920
- async function resolveBody(env, payload) {
921
- if (payload.mode === 'full_sync') return payload.body
922
- if (payload.mode === 'patch') {
923
- const state = await readState(env, payload.affinity)
924
- if (!state || state.hash !== payload.base_hash) {
925
- return { error: 'state mismatch', status: 409 }
926
- }
927
- return applyPatchSet(state.body, payload.patch)
928
- }
929
- return { error: 'unknown mode', status: 400 }
930
- }
931
-
932
- async function prepareUpstream(env, payload) {
933
- if ((payload.protocol !== 1 && payload.protocol !== 2) || payload.type !== 'request' || !payload.affinity || !payload.upstream?.url || !payload.next_hash) {
934
- return { error: 'invalid payload', status: 400 }
935
- }
936
-
937
- const body = await resolveBody(env, payload)
938
- if (body && typeof body === 'object' && 'error' in body) return body
939
-
940
- if (typeof body !== 'string' || (await hashBody(body)) !== payload.next_hash) {
941
- return { error: 'hash mismatch', status: 409 }
942
- }
943
-
944
- const stateWrite = writeState(env, payload.affinity, { body, hash: payload.next_hash, revision: payload.revision }).catch(() => {})
945
- console.log(JSON.stringify({
946
- relay: 'opencode-anthropic-auth',
947
- transport: 'relay',
948
- mode: payload.mode,
949
- revision: payload.revision,
950
- affinity: String(payload.affinity).slice(0, 12),
951
- bodyBytes: body.length,
952
- }))
953
-
954
- return { body, stateWrite }
955
- }
956
-
957
- function resolveBodyFromState(state, payload) {
958
- if (payload.mode === 'full_sync') return payload.body
959
- if (payload.mode === 'patch') {
960
- if (!state || state.hash !== payload.base_hash) {
961
- return { error: 'state mismatch', status: 409 }
962
- }
963
- return applyPatchSet(state.body, payload.patch)
964
- }
965
- return { error: 'unknown mode', status: 400 }
966
- }
967
-
968
- function deferWorkerTask(task) {
969
- return new Promise((resolve) => setTimeout(resolve, 0)).then(task)
970
- }
971
-
972
- const WEBSOCKET_STREAM_FLUSH_BYTES = 1024
973
- const WEBSOCKET_STREAM_FLUSH_MS = 20
974
-
975
- function createWebSocketStreamCoalescer(socket, options = {}) {
976
- const flushBytes = options.flushBytes || WEBSOCKET_STREAM_FLUSH_BYTES
977
- const flushMs = options.flushMs ?? WEBSOCKET_STREAM_FLUSH_MS
978
- let chunks = []
979
- let byteLength = 0
980
- let flushTimer = null
981
- const stats = {
982
- upstreamChunks: 0,
983
- upstreamBytes: 0,
984
- frames: 0,
985
- frameBytes: 0,
986
- }
987
-
988
- const clearFlushTimer = () => {
989
- if (flushTimer === null) return
990
- clearTimeout(flushTimer)
991
- flushTimer = null
992
- }
993
-
994
- const mergeChunks = () => {
995
- if (chunks.length === 1) return chunks[0]
996
- const frame = new Uint8Array(byteLength)
997
- let offset = 0
998
- for (const chunk of chunks) {
999
- frame.set(chunk, offset)
1000
- offset += chunk.byteLength
1001
- }
1002
- return frame
1003
- }
1004
-
1005
- const flush = () => {
1006
- clearFlushTimer()
1007
- if (byteLength === 0) return
1008
- const frame = mergeChunks()
1009
- chunks = []
1010
- byteLength = 0
1011
- socket.send(frame)
1012
- stats.frames += 1
1013
- stats.frameBytes += frame.byteLength
1014
- }
1015
-
1016
- const scheduleFlush = () => {
1017
- if (flushTimer !== null) return
1018
- flushTimer = setTimeout(flush, flushMs)
1019
- }
1020
-
1021
- return {
1022
- push(value) {
1023
- const chunk = value instanceof Uint8Array ? value : new Uint8Array(value)
1024
- if (chunk.byteLength === 0) return
1025
- stats.upstreamChunks += 1
1026
- stats.upstreamBytes += chunk.byteLength
1027
- chunks.push(chunk)
1028
- byteLength += chunk.byteLength
1029
- if (byteLength >= flushBytes) flush()
1030
- else scheduleFlush()
1031
- },
1032
- flush,
1033
- stats() {
1034
- return { ...stats, pendingBytes: byteLength }
1035
- },
1036
- }
1037
- }
1038
-
1039
- function checkpointWebSocketState(env, payload, body, nextState) {
1040
- return deferWorkerTask(async () => {
1041
- try {
1042
- if ((await hashBody(body)) !== payload.next_hash) {
1043
- throw new Error('hash mismatch')
1044
- }
1045
- await writeState(env, payload.affinity, nextState)
1046
- } catch (error) {
1047
- console.log(JSON.stringify({
1048
- relay: 'opencode-anthropic-auth',
1049
- transport: 'websocket',
1050
- event: 'checkpoint_write_failed',
1051
- affinity: String(payload.affinity).slice(0, 12),
1052
- message: error instanceof Error ? error.message : String(error),
1053
- }))
1054
- }
1055
- })
1056
- }
1057
-
1058
- async function prepareWebSocketUpstream(env, state, payload) {
1059
- if (payload.protocol !== 2 || payload.type !== 'request' || !payload.id || !payload.affinity || !payload.upstream?.url || !payload.next_hash) {
1060
- return { error: 'invalid payload', status: 400 }
1061
- }
1062
-
1063
- const body = resolveBodyFromState(state, payload)
1064
- if (body && typeof body === 'object' && 'error' in body) return body
1065
- if (typeof body !== 'string') return { error: 'invalid body', status: 400 }
1066
- if ((await hashBody(body)) !== payload.next_hash) {
1067
- return { error: 'hash mismatch', status: 409 }
1068
- }
1069
-
1070
- const nextState = { body, hash: payload.next_hash, revision: payload.revision }
1071
- const checkpoint = checkpointWebSocketState(env, payload, body, nextState)
1072
- const logAccepted = () => console.log(JSON.stringify({
1073
- relay: 'opencode-anthropic-auth',
1074
- transport: 'websocket',
1075
- mode: payload.mode,
1076
- revision: payload.revision,
1077
- affinity: String(payload.affinity).slice(0, 12),
1078
- bodyBytes: body.length,
1079
- }))
1080
-
1081
- return { body, state: nextState, checkpoint, logAccepted }
1082
- }
1083
-
1084
- function headersToObject(headers) {
1085
- const result = {}
1086
- for (const [key, value] of headers.entries()) result[key] = value
1087
- return result
1088
- }
1089
-
1090
- async function handleRelayPayload(env, payload) {
1091
- const prepared = await prepareUpstream(env, payload)
1092
- if (prepared.error) return prepared
1093
- const upstream = await fetch(payload.upstream.url, {
1094
- method: payload.upstream.method || 'POST',
1095
- headers: payload.upstream.headers,
1096
- body: prepared.body,
1097
- })
1098
- return { upstream, stateWrite: prepared.stateWrite }
1099
- }
1100
-
1101
- async function handleWebSocket(socket, env, ctx, payload, getState, setState) {
1102
- const heartbeat = setInterval(() => {
1103
- try {
1104
- socket.send(JSON.stringify({ protocol: 2, type: 'keepalive' }))
1105
- } catch {
1106
- clearInterval(heartbeat)
1107
- }
1108
- }, 15000)
1109
- let streamCoalescer = null
1110
- try {
1111
- const result = await prepareWebSocketUpstream(env, getState(), payload)
1112
- if (result.error) {
1113
- socket.send(JSON.stringify({ protocol: 2, type: 'error', id: payload.id, status: result.status, message: result.error }))
1114
- return
1115
- }
1116
-
1117
- setState(result.state)
1118
- socket.send(JSON.stringify({ protocol: 2, type: 'accepted', id: payload.id, hash: result.state.hash, revision: result.state.revision }))
1119
- ctx?.waitUntil?.(deferWorkerTask(result.logAccepted))
1120
-
1121
- const upstreamPromise = fetch(payload.upstream.url, {
1122
- method: payload.upstream.method || 'POST',
1123
- headers: payload.upstream.headers,
1124
- body: result.body,
1125
- })
1126
- ctx?.waitUntil?.(result.checkpoint)
1127
- const upstream = await upstreamPromise
1128
- socket.send(JSON.stringify({
1129
- protocol: 2,
1130
- type: 'response_start',
1131
- id: payload.id,
1132
- status: upstream.status,
1133
- statusText: upstream.statusText,
1134
- headers: headersToObject(upstream.headers),
1135
- }))
1136
-
1137
- const reader = upstream.body?.getReader()
1138
- if (reader) {
1139
- streamCoalescer = createWebSocketStreamCoalescer(socket)
1140
- while (true) {
1141
- const { done, value } = await reader.read()
1142
- if (done) break
1143
- streamCoalescer.push(value)
1144
- }
1145
- streamCoalescer.flush()
1146
- }
1147
- const stats = streamCoalescer?.stats?.() || {}
1148
- socket.send(JSON.stringify({ protocol: 2, type: 'done', id: payload.id, ...stats }))
1149
- } catch (error) {
1150
- try {
1151
- streamCoalescer?.flush?.()
1152
- } catch {}
1153
- socket.send(JSON.stringify({ protocol: 2, type: 'error', id: payload.id, status: 500, message: error instanceof Error ? error.message : String(error) }))
1154
- } finally {
1155
- clearInterval(heartbeat)
1156
- }
1157
- }
1158
-
1159
- export default {
1160
- async fetch(request, env, ctx) {
1161
- if (request.headers.get('Upgrade') === 'websocket') {
1162
- const url = new URL(request.url)
1163
- const token = url.searchParams.get('token')
1164
- const affinity = url.searchParams.get('affinity')
1165
- if (token !== env.RELAY_TOKEN) return new Response('unauthorized', { status: 401 })
1166
- if (!affinity) return new Response('missing affinity', { status: 400 })
1167
- const pair = new WebSocketPair()
1168
- const client = pair[0]
1169
- const server = pair[1]
1170
- server.binaryType = 'arraybuffer'
1171
- let state = null
1172
- let ready = false
1173
- server.accept()
1174
- const loadState = readState(env, affinity).then((loadedState) => {
1175
- state = loadedState
1176
- ready = true
1177
- server.send(JSON.stringify({
1178
- protocol: 2,
1179
- type: 'ready',
1180
- state: state ? { hash: state.hash, revision: state.revision } : null,
1181
- }))
1182
- }).catch((error) => {
1183
- server.send(JSON.stringify({ protocol: 2, type: 'error', status: 500, message: error instanceof Error ? error.message : String(error) }))
1184
- server.close(1011, 'state load failed')
1185
- })
1186
- ctx?.waitUntil?.(loadState)
1187
- if (!ctx?.waitUntil) void loadState
1188
- let busy = false
1189
- server.addEventListener('message', (event) => {
1190
- if (!ready) {
1191
- server.send(JSON.stringify({ protocol: 2, type: 'error', status: 425, message: 'relay state is not ready' }))
1192
- return
1193
- }
1194
- if (busy) {
1195
- server.send(JSON.stringify({ protocol: 2, type: 'error', status: 429, message: 'request already in flight' }))
1196
- return
1197
- }
1198
- let payload
1199
- try {
1200
- payload = JSON.parse(event.data)
1201
- } catch {
1202
- server.send(JSON.stringify({ protocol: 2, type: 'error', status: 400, message: 'invalid JSON payload' }))
1203
- return
1204
- }
1205
- payload.affinity = affinity
1206
- busy = true
1207
- const run = handleWebSocket(server, env, ctx, payload, () => state, (nextState) => { state = nextState }).finally(() => { busy = false })
1208
- ctx?.waitUntil?.(run)
1209
- if (!ctx?.waitUntil) void run
1210
- })
1211
- return new Response(null, { status: 101, webSocket: client })
1212
- }
1213
-
1214
- if (request.method === 'GET') {
1215
- return Response.json({ status: 'ok', transports: ['http', 'websocket'] })
1216
- }
1217
- if (request.method !== 'POST') return new Response('method not allowed', { status: 405 })
1218
- if (request.headers.get('x-relay-token') !== env.RELAY_TOKEN) {
1219
- return new Response('unauthorized', { status: 401 })
1220
- }
1221
-
1222
- try {
1223
- const payload = await request.json()
1224
- const result = await handleRelayPayload(env, payload)
1225
- if (result.error) return Response.json({ error: result.error }, { status: result.status })
1226
-
1227
- if (result.stateWrite) ctx.waitUntil(result.stateWrite)
1228
-
1229
- const upstream = result.upstream
1230
- return new Response(upstream.body, {
1231
- status: upstream.status,
1232
- statusText: upstream.statusText,
1233
- headers: upstream.headers,
1234
- })
1235
- } catch (error) {
1236
- return Response.json(
1237
- { error: error instanceof Error ? error.message : 'internal relay error' },
1238
- { status: 502 },
1239
- )
1240
- }
1241
- },
1242
- }
1243
- `;