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