@landh93/web-codex-client 0.1.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js ADDED
@@ -0,0 +1,2999 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { parseArgs } from "node:util";
5
+ import { resolve as resolve4 } from "node:path";
6
+ import { readFile as readFile3 } from "node:fs/promises";
7
+
8
+ // src/probe.ts
9
+ import { mkdirSync, writeFileSync, renameSync } from "node:fs";
10
+ import { dirname } from "node:path";
11
+ import cliProgress from "cli-progress";
12
+
13
+ // src/rpc.ts
14
+ import { EventEmitter } from "node:events";
15
+ import { homedir } from "node:os";
16
+ import { join } from "node:path";
17
+ import { lstat } from "node:fs/promises";
18
+ import { spawn } from "node:child_process";
19
+ import WebSocket from "ws";
20
+ var BridgeError = class extends Error {
21
+ constructor(code, message = code, retryable = false) {
22
+ super(message);
23
+ this.code = code;
24
+ this.retryable = retryable;
25
+ }
26
+ };
27
+ var RpcError = class extends Error {
28
+ constructor(code, message) {
29
+ super(message);
30
+ this.code = code;
31
+ }
32
+ };
33
+ function controlSocketPath(config) {
34
+ return config.socket ?? join(config.codexHome ?? process.env.CODEX_HOME ?? join(homedir(), ".codex"), "app-server-control", "app-server-control.sock");
35
+ }
36
+ var CodexRpc = class _CodexRpc extends EventEmitter {
37
+ available = false;
38
+ version = "unknown";
39
+ child;
40
+ ws;
41
+ next = 0;
42
+ ended = false;
43
+ pending = /* @__PURE__ */ new Map();
44
+ static async connect(config) {
45
+ const rpc = new _CodexRpc();
46
+ try {
47
+ await rpc.open(config);
48
+ const result = await rpc.call("initialize", {
49
+ clientInfo: {
50
+ name: "web_codex_client",
51
+ title: "Web Codex Client",
52
+ version: "0.1.0"
53
+ },
54
+ capabilities: { experimentalApi: true }
55
+ });
56
+ rpc.version = result.userAgent ?? "unknown";
57
+ rpc.send({ method: "initialized" });
58
+ rpc.available = true;
59
+ return rpc;
60
+ } catch (e) {
61
+ rpc.close();
62
+ throw e;
63
+ }
64
+ }
65
+ async open(c) {
66
+ if (c.mode === "websocket" || c.mode === "unix") {
67
+ const socketPath = c.mode === "unix" ? controlSocketPath(c) : void 0;
68
+ if (socketPath) {
69
+ if (!socketPath.startsWith("/") || socketPath.includes(":")) throw Error("UPSTREAM_SOCKET_INVALID");
70
+ const info = await lstat(socketPath);
71
+ if (!info.isSocket() || info.uid !== process.getuid?.() || (info.mode & 63) !== 0)
72
+ throw Error("UPSTREAM_SOCKET_PERMISSIONS");
73
+ }
74
+ const url = socketPath ? "ws+unix://" + socketPath + ":/" : new URL(c.url);
75
+ if (url instanceof URL && (url.protocol !== "ws:" || !["127.0.0.1", "[::1]", "localhost"].includes(url.hostname)))
76
+ throw Error("UPSTREAM_MUST_BE_LOOPBACK");
77
+ this.ws = new WebSocket(url, {
78
+ maxPayload: 8 * 1024 * 1024,
79
+ perMessageDeflate: false,
80
+ handshakeTimeout: 1e4
81
+ });
82
+ await new Promise((resolve5, reject) => {
83
+ this.ws.once("open", resolve5);
84
+ this.ws.once("error", reject);
85
+ });
86
+ this.ws.on("message", (data) => this.parse(data.toString()));
87
+ this.ws.on("error", () => this.fail());
88
+ this.ws.on("close", () => this.fail());
89
+ return;
90
+ }
91
+ const args = c.mode === "proxy" ? ["app-server", "proxy", ...c.socket ? ["--sock", c.socket] : []] : ["app-server", "--listen", "stdio://"];
92
+ if (c.mode === "stdio" && !c.codexHome)
93
+ throw Error("STANDALONE_REQUIRES_EXPLICIT_CODEX_HOME");
94
+ this.child = spawn(c.binary ?? "codex", args, {
95
+ env: {
96
+ ...process.env,
97
+ ...c.codexHome ? { CODEX_HOME: c.codexHome } : {}
98
+ },
99
+ stdio: ["pipe", "pipe", "pipe"]
100
+ });
101
+ let buffer = Buffer.alloc(0);
102
+ this.child.stdout.on("data", (chunk) => {
103
+ buffer = Buffer.concat([buffer, chunk]);
104
+ let i;
105
+ while ((i = buffer.indexOf(10)) >= 0) {
106
+ if (i > 8 * 1024 * 1024) {
107
+ this.fail();
108
+ return;
109
+ }
110
+ const line = buffer.subarray(0, i).toString();
111
+ buffer = buffer.subarray(i + 1);
112
+ if (line) this.parse(line);
113
+ }
114
+ if (buffer.length > 8 * 1024 * 1024) this.fail();
115
+ });
116
+ this.child.stderr.resume();
117
+ this.child.on("error", () => this.fail());
118
+ this.child.on("exit", () => this.fail());
119
+ }
120
+ parse(line) {
121
+ try {
122
+ const m = JSON.parse(line);
123
+ if (m.method) {
124
+ this.emit(m.id === void 0 ? "notification" : "serverRequest", m);
125
+ return;
126
+ }
127
+ const key = String(m.id);
128
+ const p = this.pending.get(key);
129
+ if (!p) return;
130
+ this.pending.delete(key);
131
+ clearTimeout(p.timer);
132
+ if (m.error) p.reject(new RpcError(m.error.code, m.error.message));
133
+ else p.resolve(m.result);
134
+ } catch {
135
+ this.fail();
136
+ }
137
+ }
138
+ send(value) {
139
+ if (this.ended) throw new BridgeError("UPSTREAM_UNAVAILABLE");
140
+ const data = JSON.stringify(value);
141
+ if (this.ws) {
142
+ if (this.ws.bufferedAmount > 1024 * 1024) throw new BridgeError("BUSY");
143
+ this.ws.send(data);
144
+ } else {
145
+ if (!this.child || this.child.stdin.writableLength > 1024 * 1024)
146
+ throw new BridgeError("BUSY");
147
+ this.child.stdin.write(data + "\n");
148
+ }
149
+ }
150
+ call(method, params) {
151
+ if (this.pending.size >= 32) return Promise.reject(new BridgeError("BUSY"));
152
+ return new Promise((resolve5, reject) => {
153
+ const id2 = "bridge-" + ++this.next;
154
+ const timer = setTimeout(() => {
155
+ this.pending.delete(id2);
156
+ reject(
157
+ new BridgeError("OUTCOME_UNKNOWN", "Upstream response timed out")
158
+ );
159
+ }, 15e3);
160
+ this.pending.set(id2, { resolve: resolve5, reject, timer });
161
+ try {
162
+ this.send({ id: id2, method, params });
163
+ } catch (e) {
164
+ clearTimeout(timer);
165
+ this.pending.delete(id2);
166
+ reject(e);
167
+ }
168
+ });
169
+ }
170
+ respond(id2, result, error) {
171
+ this.send(error ? { id: id2, error } : { id: id2, result });
172
+ }
173
+ fail() {
174
+ if (this.ended) return;
175
+ this.ended = true;
176
+ this.available = false;
177
+ for (const p of this.pending.values()) {
178
+ clearTimeout(p.timer);
179
+ p.reject(new BridgeError("UPSTREAM_UNAVAILABLE"));
180
+ }
181
+ this.pending.clear();
182
+ this.ws?.terminate();
183
+ this.child?.stdin.destroy();
184
+ this.child?.kill("SIGTERM");
185
+ this.emit("disconnect");
186
+ }
187
+ close() {
188
+ this.fail();
189
+ }
190
+ };
191
+
192
+ // src/probe.ts
193
+ async function probe(config, quiet = false, report) {
194
+ const result = {
195
+ cliBaseline: "0.155.1",
196
+ transport: config.mode,
197
+ readOnly: true,
198
+ checks: {}
199
+ };
200
+ const checks = result.checks;
201
+ const start = Date.now();
202
+ let completed = 0;
203
+ let rpc;
204
+ let threadId;
205
+ const bar = new cliProgress.SingleBar(
206
+ {
207
+ format: "Codex probe [{bar}] {percentage}% | {value}/{total} | elapsed {duration_formatted} | ETA {eta_formatted} | {rate}/s | {phase}",
208
+ noTTYOutput: !quiet,
209
+ notTTYSchedule: 1e3
210
+ },
211
+ cliProgress.Presets.shades_classic
212
+ );
213
+ const persist = () => {
214
+ if (report) {
215
+ mkdirSync(dirname(report), { recursive: true });
216
+ writeFileSync(report + ".tmp", JSON.stringify(result, null, 2) + "\n");
217
+ renameSync(report + ".tmp", report);
218
+ }
219
+ };
220
+ const mark = (phase) => {
221
+ result.completedUnits = ++completed;
222
+ result.elapsedMs = Date.now() - start;
223
+ persist();
224
+ if (!quiet)
225
+ bar.update(completed, {
226
+ phase,
227
+ rate: (completed / Math.max(1e-3, (Date.now() - start) / 1e3)).toFixed(1)
228
+ });
229
+ };
230
+ if (!quiet) bar.start(6, 0, { phase: "initialize", rate: "0" });
231
+ try {
232
+ try {
233
+ rpc = await CodexRpc.connect(config);
234
+ result.upstreamVersion = rpc.version;
235
+ checks.initialize = { status: "PASS" };
236
+ mark("initialize");
237
+ } catch (e) {
238
+ checks.initialize = {
239
+ status: "FAIL",
240
+ code: e instanceof Error ? e.message : "UNAVAILABLE"
241
+ };
242
+ mark("initialize failed");
243
+ throw e;
244
+ }
245
+ for (const method of [
246
+ "project/list",
247
+ "thread/list",
248
+ "thread/read",
249
+ "thread/turns/list",
250
+ "thread/items/list"
251
+ ]) {
252
+ if (method.startsWith("thread/") && method !== "thread/list" && !threadId) {
253
+ checks[method] = {
254
+ status: "NOT_RUN",
255
+ reason: "No thread returned by sampled list"
256
+ };
257
+ mark(method + " skipped");
258
+ continue;
259
+ }
260
+ try {
261
+ const params = method === "thread/list" ? {
262
+ limit: 5,
263
+ useStateDbOnly: true,
264
+ sourceKinds: [
265
+ "cli",
266
+ "vscode",
267
+ "exec",
268
+ "appServer",
269
+ "subAgent",
270
+ "subAgentReview",
271
+ "subAgentCompact",
272
+ "subAgentThreadSpawn",
273
+ "subAgentOther",
274
+ "unknown"
275
+ ]
276
+ } : method === "project/list" ? { limit: 5 } : method === "thread/read" ? { threadId, includeTurns: false } : { threadId, limit: 1 };
277
+ const r = await rpc.call(method, params);
278
+ if (method === "thread/list") threadId = r.data[0]?.id;
279
+ checks[method] = {
280
+ status: "PASS",
281
+ ...Array.isArray(r.data) ? { count: r.data.length, hasMore: !!r.nextCursor } : {},
282
+ ...method === "thread/read" ? { threadStatus: r.thread.status?.type ?? null } : {}
283
+ };
284
+ } catch (e) {
285
+ if (method === "thread/items/list" && e instanceof RpcError && e.code === -32601) {
286
+ try {
287
+ const page2 = await rpc.call("thread/turns/list", { threadId, limit: 1, sortDirection: "desc", itemsView: "full" });
288
+ if (!Array.isArray(page2.data) || page2.data.some((turn) => turn.itemsView && turn.itemsView !== "full")) throw Error("FULL_TURN_ITEMS_UNAVAILABLE");
289
+ checks[method] = { status: "PASS", nativeStatus: "UNSUPPORTED", fallback: "thread/turns/list:full", count: page2.data.reduce((n, t) => n + (t.items?.length ?? 0), 0), hasMore: !!page2.nextCursor };
290
+ mark(method + " bounded fallback");
291
+ continue;
292
+ } catch {
293
+ }
294
+ }
295
+ checks[method] = {
296
+ status: "FAIL",
297
+ code: e instanceof RpcError ? e.code : e instanceof Error ? e.message : "UNAVAILABLE"
298
+ };
299
+ }
300
+ mark(method);
301
+ }
302
+ } finally {
303
+ bar.stop();
304
+ rpc?.close();
305
+ persist();
306
+ console.log(JSON.stringify(result, null, 2));
307
+ }
308
+ if (Object.values(checks).some((check) => check.status === "FAIL"))
309
+ throw new Error("Codex probe failed; inspect the check report");
310
+ return result;
311
+ }
312
+
313
+ // src/config.ts
314
+ import { z as z4 } from "zod";
315
+ import {
316
+ readFile as readFile2,
317
+ writeFile,
318
+ rename,
319
+ stat,
320
+ mkdir,
321
+ realpath
322
+ } from "node:fs/promises";
323
+ import { dirname as dirname2, resolve } from "node:path";
324
+ import { randomUUID } from "node:crypto";
325
+
326
+ // sdk/src/node.ts
327
+ import { readFile } from "node:fs/promises";
328
+
329
+ // sdk/wasm/pkg/web_codex_noise.js
330
+ var wasm;
331
+ var cachedUint8ArrayMemory0 = null;
332
+ function getUint8ArrayMemory0() {
333
+ if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) {
334
+ cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer);
335
+ }
336
+ return cachedUint8ArrayMemory0;
337
+ }
338
+ function getArrayU8FromWasm0(ptr, len) {
339
+ ptr = ptr >>> 0;
340
+ return getUint8ArrayMemory0().subarray(ptr / 1, ptr / 1 + len);
341
+ }
342
+ function addToExternrefTable0(obj2) {
343
+ const idx = wasm.__externref_table_alloc();
344
+ wasm.__wbindgen_export_2.set(idx, obj2);
345
+ return idx;
346
+ }
347
+ function handleError(f, args) {
348
+ try {
349
+ return f.apply(this, args);
350
+ } catch (e) {
351
+ const idx = addToExternrefTable0(e);
352
+ wasm.__wbindgen_exn_store(idx);
353
+ }
354
+ }
355
+ var cachedTextDecoder = typeof TextDecoder !== "undefined" ? new TextDecoder("utf-8", { ignoreBOM: true, fatal: true }) : { decode: () => {
356
+ throw Error("TextDecoder not available");
357
+ } };
358
+ if (typeof TextDecoder !== "undefined") {
359
+ cachedTextDecoder.decode();
360
+ }
361
+ function getStringFromWasm0(ptr, len) {
362
+ ptr = ptr >>> 0;
363
+ return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
364
+ }
365
+ function takeFromExternrefTable0(idx) {
366
+ const value = wasm.__wbindgen_export_2.get(idx);
367
+ wasm.__externref_table_dealloc(idx);
368
+ return value;
369
+ }
370
+ function generate_identity() {
371
+ const ret = wasm.generate_identity();
372
+ if (ret[3]) {
373
+ throw takeFromExternrefTable0(ret[2]);
374
+ }
375
+ var v1 = getArrayU8FromWasm0(ret[0], ret[1]).slice();
376
+ wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
377
+ return v1;
378
+ }
379
+ var WASM_VECTOR_LEN = 0;
380
+ function passArray8ToWasm0(arg, malloc) {
381
+ const ptr = malloc(arg.length * 1, 1) >>> 0;
382
+ getUint8ArrayMemory0().set(arg, ptr / 1);
383
+ WASM_VECTOR_LEN = arg.length;
384
+ return ptr;
385
+ }
386
+ var CipherPairFinalization = typeof FinalizationRegistry === "undefined" ? { register: () => {
387
+ }, unregister: () => {
388
+ } } : new FinalizationRegistry((ptr) => wasm.__wbg_cipherpair_free(ptr >>> 0, 1));
389
+ var CipherPair = class _CipherPair {
390
+ static __wrap(ptr) {
391
+ ptr = ptr >>> 0;
392
+ const obj2 = Object.create(_CipherPair.prototype);
393
+ obj2.__wbg_ptr = ptr;
394
+ CipherPairFinalization.register(obj2, obj2.__wbg_ptr, obj2);
395
+ return obj2;
396
+ }
397
+ __destroy_into_raw() {
398
+ const ptr = this.__wbg_ptr;
399
+ this.__wbg_ptr = 0;
400
+ CipherPairFinalization.unregister(this);
401
+ return ptr;
402
+ }
403
+ free() {
404
+ const ptr = this.__destroy_into_raw();
405
+ wasm.__wbg_cipherpair_free(ptr, 0);
406
+ }
407
+ /**
408
+ * @returns {RecordCipher}
409
+ */
410
+ take_send() {
411
+ const ret = wasm.cipherpair_take_send(this.__wbg_ptr);
412
+ if (ret[2]) {
413
+ throw takeFromExternrefTable0(ret[1]);
414
+ }
415
+ return RecordCipher.__wrap(ret[0]);
416
+ }
417
+ /**
418
+ * @returns {RecordCipher}
419
+ */
420
+ take_recv() {
421
+ const ret = wasm.cipherpair_take_recv(this.__wbg_ptr);
422
+ if (ret[2]) {
423
+ throw takeFromExternrefTable0(ret[1]);
424
+ }
425
+ return RecordCipher.__wrap(ret[0]);
426
+ }
427
+ };
428
+ var HandshakeFinalization = typeof FinalizationRegistry === "undefined" ? { register: () => {
429
+ }, unregister: () => {
430
+ } } : new FinalizationRegistry((ptr) => wasm.__wbg_handshake_free(ptr >>> 0, 1));
431
+ var Handshake = class {
432
+ __destroy_into_raw() {
433
+ const ptr = this.__wbg_ptr;
434
+ this.__wbg_ptr = 0;
435
+ HandshakeFinalization.unregister(this);
436
+ return ptr;
437
+ }
438
+ free() {
439
+ const ptr = this.__destroy_into_raw();
440
+ wasm.__wbg_handshake_free(ptr, 0);
441
+ }
442
+ /**
443
+ * @param {boolean} initiator
444
+ * @param {Uint8Array} prologue
445
+ * @param {Uint8Array} private_key
446
+ */
447
+ constructor(initiator, prologue2, private_key) {
448
+ const ptr0 = passArray8ToWasm0(prologue2, wasm.__wbindgen_malloc);
449
+ const len0 = WASM_VECTOR_LEN;
450
+ const ptr1 = passArray8ToWasm0(private_key, wasm.__wbindgen_malloc);
451
+ const len1 = WASM_VECTOR_LEN;
452
+ const ret = wasm.handshake_new(initiator, ptr0, len0, ptr1, len1);
453
+ if (ret[2]) {
454
+ throw takeFromExternrefTable0(ret[1]);
455
+ }
456
+ this.__wbg_ptr = ret[0] >>> 0;
457
+ HandshakeFinalization.register(this, this.__wbg_ptr, this);
458
+ return this;
459
+ }
460
+ /**
461
+ * @returns {number}
462
+ */
463
+ action() {
464
+ const ret = wasm.handshake_action(this.__wbg_ptr);
465
+ if (ret[2]) {
466
+ throw takeFromExternrefTable0(ret[1]);
467
+ }
468
+ return ret[0] >>> 0;
469
+ }
470
+ /**
471
+ * @param {Uint8Array} payload
472
+ * @returns {Uint8Array}
473
+ */
474
+ write(payload) {
475
+ const ptr0 = passArray8ToWasm0(payload, wasm.__wbindgen_malloc);
476
+ const len0 = WASM_VECTOR_LEN;
477
+ const ret = wasm.handshake_write(this.__wbg_ptr, ptr0, len0);
478
+ if (ret[3]) {
479
+ throw takeFromExternrefTable0(ret[2]);
480
+ }
481
+ var v2 = getArrayU8FromWasm0(ret[0], ret[1]).slice();
482
+ wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
483
+ return v2;
484
+ }
485
+ /**
486
+ * @param {Uint8Array} message
487
+ * @returns {Uint8Array}
488
+ */
489
+ read(message) {
490
+ const ptr0 = passArray8ToWasm0(message, wasm.__wbindgen_malloc);
491
+ const len0 = WASM_VECTOR_LEN;
492
+ const ret = wasm.handshake_read(this.__wbg_ptr, ptr0, len0);
493
+ if (ret[3]) {
494
+ throw takeFromExternrefTable0(ret[2]);
495
+ }
496
+ var v2 = getArrayU8FromWasm0(ret[0], ret[1]).slice();
497
+ wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
498
+ return v2;
499
+ }
500
+ /**
501
+ * @returns {Uint8Array}
502
+ */
503
+ hash() {
504
+ const ret = wasm.handshake_hash(this.__wbg_ptr);
505
+ if (ret[3]) {
506
+ throw takeFromExternrefTable0(ret[2]);
507
+ }
508
+ var v1 = getArrayU8FromWasm0(ret[0], ret[1]).slice();
509
+ wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
510
+ return v1;
511
+ }
512
+ /**
513
+ * @returns {Uint8Array}
514
+ */
515
+ remote_key() {
516
+ const ret = wasm.handshake_remote_key(this.__wbg_ptr);
517
+ if (ret[3]) {
518
+ throw takeFromExternrefTable0(ret[2]);
519
+ }
520
+ var v1 = getArrayU8FromWasm0(ret[0], ret[1]).slice();
521
+ wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
522
+ return v1;
523
+ }
524
+ /**
525
+ * @returns {CipherPair}
526
+ */
527
+ split() {
528
+ const ret = wasm.handshake_split(this.__wbg_ptr);
529
+ if (ret[2]) {
530
+ throw takeFromExternrefTable0(ret[1]);
531
+ }
532
+ return CipherPair.__wrap(ret[0]);
533
+ }
534
+ };
535
+ var RecordCipherFinalization = typeof FinalizationRegistry === "undefined" ? { register: () => {
536
+ }, unregister: () => {
537
+ } } : new FinalizationRegistry((ptr) => wasm.__wbg_recordcipher_free(ptr >>> 0, 1));
538
+ var RecordCipher = class _RecordCipher {
539
+ static __wrap(ptr) {
540
+ ptr = ptr >>> 0;
541
+ const obj2 = Object.create(_RecordCipher.prototype);
542
+ obj2.__wbg_ptr = ptr;
543
+ RecordCipherFinalization.register(obj2, obj2.__wbg_ptr, obj2);
544
+ return obj2;
545
+ }
546
+ __destroy_into_raw() {
547
+ const ptr = this.__wbg_ptr;
548
+ this.__wbg_ptr = 0;
549
+ RecordCipherFinalization.unregister(this);
550
+ return ptr;
551
+ }
552
+ free() {
553
+ const ptr = this.__destroy_into_raw();
554
+ wasm.__wbg_recordcipher_free(ptr, 0);
555
+ }
556
+ /**
557
+ * @param {Uint8Array} ad
558
+ * @param {Uint8Array} plaintext
559
+ * @returns {Uint8Array}
560
+ */
561
+ encrypt(ad, plaintext) {
562
+ const ptr0 = passArray8ToWasm0(ad, wasm.__wbindgen_malloc);
563
+ const len0 = WASM_VECTOR_LEN;
564
+ const ptr1 = passArray8ToWasm0(plaintext, wasm.__wbindgen_malloc);
565
+ const len1 = WASM_VECTOR_LEN;
566
+ const ret = wasm.recordcipher_encrypt(this.__wbg_ptr, ptr0, len0, ptr1, len1);
567
+ if (ret[3]) {
568
+ throw takeFromExternrefTable0(ret[2]);
569
+ }
570
+ var v3 = getArrayU8FromWasm0(ret[0], ret[1]).slice();
571
+ wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
572
+ return v3;
573
+ }
574
+ /**
575
+ * @param {Uint8Array} ad
576
+ * @param {Uint8Array} ciphertext
577
+ * @returns {Uint8Array}
578
+ */
579
+ decrypt(ad, ciphertext) {
580
+ const ptr0 = passArray8ToWasm0(ad, wasm.__wbindgen_malloc);
581
+ const len0 = WASM_VECTOR_LEN;
582
+ const ptr1 = passArray8ToWasm0(ciphertext, wasm.__wbindgen_malloc);
583
+ const len1 = WASM_VECTOR_LEN;
584
+ const ret = wasm.recordcipher_decrypt(this.__wbg_ptr, ptr0, len0, ptr1, len1);
585
+ if (ret[3]) {
586
+ throw takeFromExternrefTable0(ret[2]);
587
+ }
588
+ var v3 = getArrayU8FromWasm0(ret[0], ret[1]).slice();
589
+ wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
590
+ return v3;
591
+ }
592
+ };
593
+ async function __wbg_load(module, imports) {
594
+ if (typeof Response === "function" && module instanceof Response) {
595
+ if (typeof WebAssembly.instantiateStreaming === "function") {
596
+ try {
597
+ return await WebAssembly.instantiateStreaming(module, imports);
598
+ } catch (e) {
599
+ if (module.headers.get("Content-Type") != "application/wasm") {
600
+ console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);
601
+ } else {
602
+ throw e;
603
+ }
604
+ }
605
+ }
606
+ const bytes = await module.arrayBuffer();
607
+ return await WebAssembly.instantiate(bytes, imports);
608
+ } else {
609
+ const instance = await WebAssembly.instantiate(module, imports);
610
+ if (instance instanceof WebAssembly.Instance) {
611
+ return { instance, module };
612
+ } else {
613
+ return instance;
614
+ }
615
+ }
616
+ }
617
+ function __wbg_get_imports() {
618
+ const imports = {};
619
+ imports.wbg = {};
620
+ imports.wbg.__wbg_getRandomValues_1c61fac11405ffdc = function() {
621
+ return handleError(function(arg0, arg1) {
622
+ globalThis.crypto.getRandomValues(getArrayU8FromWasm0(arg0, arg1));
623
+ }, arguments);
624
+ };
625
+ imports.wbg.__wbindgen_init_externref_table = function() {
626
+ const table = wasm.__wbindgen_export_2;
627
+ const offset = table.grow(4);
628
+ table.set(0, void 0);
629
+ table.set(offset + 0, void 0);
630
+ table.set(offset + 1, null);
631
+ table.set(offset + 2, true);
632
+ table.set(offset + 3, false);
633
+ ;
634
+ };
635
+ imports.wbg.__wbindgen_string_new = function(arg0, arg1) {
636
+ const ret = getStringFromWasm0(arg0, arg1);
637
+ return ret;
638
+ };
639
+ imports.wbg.__wbindgen_throw = function(arg0, arg1) {
640
+ throw new Error(getStringFromWasm0(arg0, arg1));
641
+ };
642
+ return imports;
643
+ }
644
+ function __wbg_init_memory(imports, memory) {
645
+ }
646
+ function __wbg_finalize_init(instance, module) {
647
+ wasm = instance.exports;
648
+ __wbg_init.__wbindgen_wasm_module = module;
649
+ cachedUint8ArrayMemory0 = null;
650
+ wasm.__wbindgen_start();
651
+ return wasm;
652
+ }
653
+ function initSync(module) {
654
+ if (wasm !== void 0) return wasm;
655
+ if (typeof module !== "undefined") {
656
+ if (Object.getPrototypeOf(module) === Object.prototype) {
657
+ ({ module } = module);
658
+ } else {
659
+ console.warn("using deprecated parameters for `initSync()`; pass a single object instead");
660
+ }
661
+ }
662
+ const imports = __wbg_get_imports();
663
+ __wbg_init_memory(imports);
664
+ if (!(module instanceof WebAssembly.Module)) {
665
+ module = new WebAssembly.Module(module);
666
+ }
667
+ const instance = new WebAssembly.Instance(module, imports);
668
+ return __wbg_finalize_init(instance, module);
669
+ }
670
+ async function __wbg_init(module_or_path) {
671
+ if (wasm !== void 0) return wasm;
672
+ if (typeof module_or_path !== "undefined") {
673
+ if (Object.getPrototypeOf(module_or_path) === Object.prototype) {
674
+ ({ module_or_path } = module_or_path);
675
+ } else {
676
+ console.warn("using deprecated parameters for the initialization function; pass a single object instead");
677
+ }
678
+ }
679
+ if (typeof module_or_path === "undefined") {
680
+ module_or_path = new URL("web_codex_noise_bg.wasm", import.meta.url);
681
+ }
682
+ const imports = __wbg_get_imports();
683
+ if (typeof module_or_path === "string" || typeof Request === "function" && module_or_path instanceof Request || typeof URL === "function" && module_or_path instanceof URL) {
684
+ module_or_path = fetch(module_or_path);
685
+ }
686
+ __wbg_init_memory(imports);
687
+ const { instance, module } = await __wbg_load(await module_or_path, imports);
688
+ return __wbg_finalize_init(instance, module);
689
+ }
690
+
691
+ // sdk/src/relay.ts
692
+ import { z } from "zod";
693
+ var MAX_FRAME_BYTES = 128 * 1024;
694
+ var MAX_MESSAGE_BYTES = 1024 * 1024;
695
+ var CHUNK_BYTES = 32 * 1024;
696
+ var SUITE = "Noise_XX_25519_AESGCM_SHA256";
697
+ var id = z.string().min(1).max(256);
698
+ var decimal = z.string().regex(/^(0|[1-9][0-9]{0,19})$/);
699
+ var secureFrameSchema = z.strictObject({
700
+ type: z.literal("secure"),
701
+ wireVersion: z.literal(1),
702
+ routeId: id,
703
+ sessionId: z.string().regex(/^[A-Za-z0-9_-]{22,128}$/),
704
+ phase: z.enum(["handshake", "transport"]),
705
+ direction: z.enum(["browserToClient", "clientToBrowser"]),
706
+ recordNo: decimal,
707
+ payload: z.string().max(9e4).regex(/^[A-Za-z0-9_-]*$/)
708
+ });
709
+ function decodeFrame(text) {
710
+ if (new TextEncoder().encode(text).length > MAX_FRAME_BYTES)
711
+ throw Error("FRAME_TOO_LARGE");
712
+ return secureFrameSchema.parse(JSON.parse(text));
713
+ }
714
+ function encodeFrame(frame) {
715
+ const text = JSON.stringify(secureFrameSchema.parse(frame));
716
+ if (new TextEncoder().encode(text).length > MAX_FRAME_BYTES)
717
+ throw Error("FRAME_TOO_LARGE");
718
+ return text;
719
+ }
720
+ var utf8 = (s) => new TextEncoder().encode(s);
721
+ function prologue(c) {
722
+ return utf8(
723
+ JSON.stringify([
724
+ "web-codex",
725
+ 1,
726
+ SUITE,
727
+ c.deviceId,
728
+ c.routeId,
729
+ c.sessionId,
730
+ "browser:initiator",
731
+ "client:responder"
732
+ ])
733
+ );
734
+ }
735
+ function aad(f) {
736
+ return utf8(
737
+ JSON.stringify([
738
+ "secure",
739
+ f.wireVersion,
740
+ f.routeId,
741
+ f.sessionId,
742
+ f.phase,
743
+ f.direction,
744
+ f.recordNo
745
+ ])
746
+ );
747
+ }
748
+
749
+ // sdk/src/wasm-integrity.ts
750
+ var WASM_SHA256 = "1ac97442f8b0fd8ef20b31d46d2e4cbc30b6d877346f0cf2438eb08026330490";
751
+
752
+ // sdk/src/noise-provider.ts
753
+ async function initializeNoise(bytes) {
754
+ const digest = Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", new Uint8Array(bytes))), (n) => n.toString(16).padStart(2, "0")).join("");
755
+ if (digest !== WASM_SHA256) throw Error("CRYPTO_ARTIFACT_MISMATCH");
756
+ initSync({ module: bytes });
757
+ return {
758
+ constants: { NOISE_DH_CURVE25519: 1, NOISE_ROLE_INITIATOR: 1, NOISE_ROLE_RESPONDER: 2, NOISE_ACTION_WRITE_MESSAGE: 1, NOISE_ACTION_READ_MESSAGE: 2, NOISE_ACTION_SPLIT: 3 },
759
+ CreateKeyPair(algorithm) {
760
+ if (algorithm !== 1) throw Error("UNSUPPORTED_SUITE");
761
+ const keys = generate_identity();
762
+ try {
763
+ return [keys.slice(0, 32), keys.slice(32)];
764
+ } finally {
765
+ keys.fill(0);
766
+ }
767
+ },
768
+ HandshakeState: class {
769
+ constructor(suite, role) {
770
+ this.role = role;
771
+ if (suite !== SUITE || ![1, 2].includes(role)) throw Error("UNSUPPORTED_SUITE");
772
+ }
773
+ inner;
774
+ Initialize(prologue2, key) {
775
+ if (this.inner) throw Error("HANDSHAKE_STATE");
776
+ this.inner = new Handshake(this.role === 1, prologue2, key);
777
+ }
778
+ get state() {
779
+ if (!this.inner) throw Error("HANDSHAKE_STATE");
780
+ return this.inner;
781
+ }
782
+ WriteMessage() {
783
+ return this.state.write(new Uint8Array());
784
+ }
785
+ ReadMessage(message) {
786
+ return this.state.read(message);
787
+ }
788
+ GetAction() {
789
+ return this.state.action();
790
+ }
791
+ GetHandshakeHash() {
792
+ return this.state.hash();
793
+ }
794
+ GetRemotePublicKey() {
795
+ return this.state.remote_key();
796
+ }
797
+ Split() {
798
+ const pair = this.state.split();
799
+ try {
800
+ const wrap = (c) => ({ EncryptWithAd: (ad, p) => c.encrypt(ad, p), DecryptWithAd: (ad, cipher) => c.decrypt(ad, cipher), free: () => c.free() });
801
+ return [wrap(pair.take_send()), wrap(pair.take_recv())];
802
+ } finally {
803
+ pair.free();
804
+ this.free();
805
+ }
806
+ }
807
+ free() {
808
+ this.inner?.free();
809
+ this.inner = void 0;
810
+ }
811
+ }
812
+ };
813
+ }
814
+
815
+ // sdk/src/types.ts
816
+ import { z as z2 } from "zod";
817
+ var obj = z2.record(z2.string(), z2.unknown());
818
+ var page = {
819
+ cursor: id.nullable().optional(),
820
+ limit: z2.number().int().min(1).max(100).default(40)
821
+ };
822
+ var thread = { threadId: id };
823
+ var methodSchemas = {
824
+ "bridge.capabilities": z2.strictObject({}),
825
+ "projects.list": z2.strictObject(page),
826
+ "projects.read": z2.strictObject({ projectId: id }),
827
+ "threads.list": z2.strictObject({
828
+ ...page,
829
+ projectId: id,
830
+ archived: z2.boolean().default(false),
831
+ sourceKinds: z2.array(
832
+ z2.enum([
833
+ "cli",
834
+ "vscode",
835
+ "exec",
836
+ "appServer",
837
+ "subAgent",
838
+ "subAgentReview",
839
+ "subAgentCompact",
840
+ "subAgentThreadSpawn",
841
+ "subAgentOther",
842
+ "unknown"
843
+ ])
844
+ ).max(10).optional()
845
+ }),
846
+ "threads.read": z2.strictObject(thread),
847
+ "turns.list": z2.strictObject({ ...thread, ...page }),
848
+ "items.list": z2.strictObject({ ...thread, ...page, turnId: id.optional() }),
849
+ "threads.subscribe": z2.strictObject(thread),
850
+ "threads.unsubscribe": z2.strictObject({ subscriptionId: id }),
851
+ "subscriptions.resume": z2.strictObject({
852
+ subscriptionId: id,
853
+ connectionEpoch: id,
854
+ eventSeq: decimal
855
+ }),
856
+ "threads.start": z2.strictObject({ projectId: id, connectionEpoch: id }),
857
+ "threads.resume": z2.strictObject({ ...thread, connectionEpoch: id }),
858
+ "turns.start": z2.strictObject({
859
+ ...thread,
860
+ connectionEpoch: id,
861
+ text: z2.string().min(1).max(128 * 1024)
862
+ }),
863
+ "turns.interrupt": z2.strictObject({
864
+ ...thread,
865
+ turnId: id,
866
+ connectionEpoch: id
867
+ }),
868
+ "leases.acquire": z2.strictObject({
869
+ ...thread,
870
+ takeover: z2.boolean().default(false)
871
+ }),
872
+ "leases.release": z2.strictObject(thread),
873
+ "files.list": z2.strictObject({
874
+ projectId: id,
875
+ path: z2.string().max(4096).default(".")
876
+ }),
877
+ "files.open": z2.strictObject({
878
+ projectId: id,
879
+ path: z2.string().min(1).max(4096)
880
+ }),
881
+ "resources.read": z2.strictObject({
882
+ resourceId: id,
883
+ offset: z2.number().int().nonnegative(),
884
+ length: z2.number().int().min(1).max(32 * 1024)
885
+ }),
886
+ "resources.close": z2.strictObject({ resourceId: id })
887
+ };
888
+ var errorSchema = z2.strictObject({
889
+ code: id,
890
+ message: z2.string().max(4096),
891
+ retryable: z2.boolean()
892
+ });
893
+ var messageSchema = z2.union([
894
+ z2.strictObject({
895
+ kind: z2.literal("request"),
896
+ requestId: id,
897
+ method: z2.enum(Object.keys(methodSchemas)),
898
+ params: obj
899
+ }),
900
+ z2.strictObject({
901
+ kind: z2.literal("response"),
902
+ requestId: id,
903
+ result: z2.unknown()
904
+ }),
905
+ z2.strictObject({
906
+ kind: z2.literal("response"),
907
+ requestId: id,
908
+ error: errorSchema
909
+ }),
910
+ z2.strictObject({
911
+ kind: z2.literal("event"),
912
+ connectionEpoch: id,
913
+ subscriptionId: id,
914
+ eventSeq: decimal,
915
+ event: id,
916
+ payload: z2.unknown()
917
+ }),
918
+ z2.strictObject({
919
+ kind: z2.literal("serverRequest"),
920
+ serverRequestId: id,
921
+ method: z2.enum(["approvals.request", "userInput.request"]),
922
+ params: obj
923
+ }),
924
+ z2.strictObject({
925
+ kind: z2.literal("serverResponse"),
926
+ serverRequestId: id,
927
+ result: obj
928
+ }),
929
+ z2.strictObject({
930
+ kind: z2.literal("pairing"),
931
+ status: z2.enum(["pending", "confirmed", "ready", "rejected"]),
932
+ fingerprint: z2.string().regex(/^[a-f0-9]{64}$/)
933
+ }),
934
+ z2.strictObject({
935
+ kind: z2.literal("ack"),
936
+ subscriptionId: id,
937
+ connectionEpoch: id,
938
+ eventSeq: decimal
939
+ })
940
+ ]);
941
+ function parseMessage(value) {
942
+ const m = messageSchema.parse(value);
943
+ if (m.kind === "request") methodSchemas[m.method].parse(m.params);
944
+ return m;
945
+ }
946
+ function encodeMessage(value) {
947
+ const bytes = new TextEncoder().encode(JSON.stringify(parseMessage(value)));
948
+ if (bytes.length > MAX_MESSAGE_BYTES) throw Error("RESOURCE_TOO_LARGE");
949
+ return bytes;
950
+ }
951
+ var opaque = z2.object({ id: z2.string() }).passthrough();
952
+ var projectDto = z2.strictObject({
953
+ id: z2.string(),
954
+ name: z2.string(),
955
+ cwd: z2.string(),
956
+ source: z2.enum(["codex", "derived"])
957
+ });
958
+ var pageOf = (entry) => z2.object({
959
+ data: z2.array(entry),
960
+ nextCursor: z2.string().nullable(),
961
+ source: z2.enum(["codex", "bounded-fallback"]).optional()
962
+ }).passthrough();
963
+ var resultSchemas = {
964
+ "bridge.capabilities": z2.object({
965
+ connectionEpoch: id,
966
+ codexAvailable: z2.boolean(),
967
+ codexVersion: z2.string(),
968
+ methods: z2.array(z2.string()),
969
+ limits: z2.strictObject({
970
+ frameBytes: z2.number(),
971
+ messageBytes: z2.number(),
972
+ chunkBytes: z2.number()
973
+ }),
974
+ historyFallback: z2.literal("bounded"),
975
+ activeTakeover: z2.literal("unverified")
976
+ }),
977
+ "projects.list": pageOf(projectDto),
978
+ "projects.read": z2.strictObject({ project: projectDto }),
979
+ "threads.list": pageOf(opaque),
980
+ "threads.read": z2.object({ thread: opaque }),
981
+ "turns.list": pageOf(opaque),
982
+ "items.list": pageOf(
983
+ z2.object({
984
+ turnId: id,
985
+ item: z2.object({ id, type: id }).passthrough()
986
+ })
987
+ ),
988
+ "threads.subscribe": z2.object({
989
+ subscriptionId: id,
990
+ connectionEpoch: id,
991
+ eventSeq: decimal,
992
+ snapshotPolicy: z2.literal("reload-complete-item-on-delta")
993
+ }),
994
+ "subscriptions.resume": z2.object({
995
+ subscriptionId: id,
996
+ connectionEpoch: id,
997
+ eventSeq: decimal
998
+ }),
999
+ "threads.unsubscribe": z2.strictObject({}),
1000
+ "threads.start": z2.object({ thread: opaque }).passthrough(),
1001
+ "threads.resume": z2.object({ thread: opaque }).passthrough(),
1002
+ "turns.start": z2.object({ turn: opaque }).passthrough(),
1003
+ "turns.interrupt": z2.strictObject({}),
1004
+ "leases.acquire": z2.object({
1005
+ threadId: id,
1006
+ holder: id,
1007
+ expiresAt: z2.number()
1008
+ }),
1009
+ "leases.release": z2.strictObject({}),
1010
+ "files.list": pageOf(
1011
+ z2.strictObject({
1012
+ name: z2.string(),
1013
+ type: z2.enum(["file", "directory", "symlink", "other"])
1014
+ })
1015
+ ),
1016
+ "files.open": z2.strictObject({
1017
+ resourceId: id,
1018
+ name: z2.string(),
1019
+ size: z2.number().int().nonnegative(),
1020
+ mime: z2.string(),
1021
+ expiresAt: z2.number()
1022
+ }),
1023
+ "resources.read": z2.strictObject({
1024
+ resourceId: id,
1025
+ offset: z2.number().int().nonnegative(),
1026
+ data: z2.string(),
1027
+ sha256: z2.string().regex(/^[a-f0-9]{64}$/),
1028
+ eof: z2.boolean()
1029
+ }),
1030
+ "resources.close": z2.strictObject({})
1031
+ };
1032
+
1033
+ // sdk/src/secure.ts
1034
+ import { z as z3 } from "zod";
1035
+ var b64 = (b) => {
1036
+ let s = "";
1037
+ for (let i = 0; i < b.length; i += 8192)
1038
+ s += String.fromCharCode(...b.subarray(i, i + 8192));
1039
+ return btoa(s).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
1040
+ };
1041
+ function unb64(s) {
1042
+ if (!/^[A-Za-z0-9_-]*$/.test(s)) throw Error("BASE64");
1043
+ const b = Uint8Array.from(
1044
+ atob(s.replaceAll("-", "+").replaceAll("_", "/")),
1045
+ (c) => c.charCodeAt(0)
1046
+ );
1047
+ if (b64(b) !== s) throw Error("BASE64");
1048
+ return b;
1049
+ }
1050
+ var hex = (b) => Array.from(b, (n) => n.toString(16).padStart(2, "0")).join("");
1051
+ var randomId = () => b64(crypto.getRandomValues(new Uint8Array(24)));
1052
+ function createIdentity(noise) {
1053
+ const [privateKey, publicKey] = noise.CreateKeyPair(
1054
+ noise.constants.NOISE_DH_CURVE25519
1055
+ );
1056
+ return { privateKey, publicKey };
1057
+ }
1058
+ var fragment = z3.strictObject({
1059
+ id: z3.string().min(1).max(64),
1060
+ index: z3.number().int().min(0).max(31),
1061
+ total: z3.number().int().min(1).max(32),
1062
+ size: z3.number().int().min(1).max(MAX_MESSAGE_BYTES),
1063
+ data: z3.string().max(44e3)
1064
+ });
1065
+ var SecureSession = class {
1066
+ constructor(noise, role, context, identity, pinnedPeer) {
1067
+ this.noise = noise;
1068
+ this.role = role;
1069
+ this.context = context;
1070
+ this.pinnedPeer = pinnedPeer;
1071
+ if (identity.privateKey.length !== 32) throw Error("IDENTITY");
1072
+ this.hs = new noise.HandshakeState(
1073
+ SUITE,
1074
+ noise.constants[role === "browser" ? "NOISE_ROLE_INITIATOR" : "NOISE_ROLE_RESPONDER"]
1075
+ );
1076
+ this.hs.Initialize(prologue(context), identity.privateKey);
1077
+ }
1078
+ hs;
1079
+ sendCipher;
1080
+ recvCipher;
1081
+ sendHs = 0n;
1082
+ recvHs = 0n;
1083
+ sendNo = 0n;
1084
+ recvNo = 0n;
1085
+ sent = 0;
1086
+ received = 0;
1087
+ born = Date.now();
1088
+ closed = false;
1089
+ fragmentTimer;
1090
+ incoming;
1091
+ fingerprint = "";
1092
+ peerPublicKey = "";
1093
+ ready = false;
1094
+ trusted = false;
1095
+ guard() {
1096
+ if (this.closed) throw Error("SESSION_CLOSED");
1097
+ if (Date.now() - this.born > 36e5 || this.sent >= 2 ** 30 || this.received >= 2 ** 30) {
1098
+ this.close();
1099
+ throw Error("SESSION_ROTATE");
1100
+ }
1101
+ }
1102
+ header(phase, n) {
1103
+ return {
1104
+ type: "secure",
1105
+ wireVersion: 1,
1106
+ ...{ routeId: this.context.routeId, sessionId: this.context.sessionId },
1107
+ phase,
1108
+ direction: this.role === "browser" ? "browserToClient" : "clientToBrowser",
1109
+ recordNo: String(n)
1110
+ };
1111
+ }
1112
+ finish() {
1113
+ if (this.hs?.GetAction() !== this.noise.constants.NOISE_ACTION_SPLIT)
1114
+ return;
1115
+ this.fingerprint = hex(this.hs.GetHandshakeHash());
1116
+ this.peerPublicKey = b64(this.hs.GetRemotePublicKey());
1117
+ if (this.pinnedPeer && this.pinnedPeer !== this.peerPublicKey)
1118
+ throw Error("PEER_KEY_CHANGED");
1119
+ [this.sendCipher, this.recvCipher] = this.hs.Split();
1120
+ this.hs = void 0;
1121
+ this.ready = true;
1122
+ this.trusted = !!this.pinnedPeer;
1123
+ }
1124
+ writeHandshake() {
1125
+ const payload = this.hs.WriteMessage();
1126
+ const f = {
1127
+ ...this.header("handshake", this.sendHs++),
1128
+ payload: b64(payload)
1129
+ };
1130
+ this.finish();
1131
+ return f;
1132
+ }
1133
+ start() {
1134
+ this.guard();
1135
+ if (this.role !== "browser" || this.sendHs !== 0n)
1136
+ throw Error("HANDSHAKE_STATE");
1137
+ return this.writeHandshake();
1138
+ }
1139
+ receiveHandshake(value) {
1140
+ try {
1141
+ this.guard();
1142
+ const f = this.validate(value, "handshake", this.recvHs);
1143
+ if (!this.hs) throw Error("HANDSHAKE_STATE");
1144
+ const payload = this.hs.ReadMessage(unb64(f.payload), true);
1145
+ if (payload?.length) throw Error("EARLY_DATA");
1146
+ this.recvHs++;
1147
+ if (this.hs.GetAction() === this.noise.constants.NOISE_ACTION_WRITE_MESSAGE)
1148
+ return this.writeHandshake();
1149
+ this.finish();
1150
+ return void 0;
1151
+ } catch (e) {
1152
+ this.close();
1153
+ throw e;
1154
+ }
1155
+ }
1156
+ trust(fingerprint) {
1157
+ if (!this.ready || fingerprint !== this.fingerprint)
1158
+ throw Error("FINGERPRINT_MISMATCH");
1159
+ this.trusted = true;
1160
+ }
1161
+ validate(value, phase, n) {
1162
+ const f = secureFrameSchema.parse(value);
1163
+ if (f.routeId !== this.context.routeId || f.sessionId !== this.context.sessionId || f.phase !== phase || f.recordNo !== String(n) || f.direction === (this.role === "browser" ? "browserToClient" : "clientToBrowser"))
1164
+ throw Error("RECORD_MISMATCH");
1165
+ return f;
1166
+ }
1167
+ *seal(message) {
1168
+ this.guard();
1169
+ if (!this.ready || !this.trusted && message.kind !== "pairing")
1170
+ throw Error("NOT_PAIRED");
1171
+ const bytes = encodeMessage(message);
1172
+ const total = Math.ceil(bytes.length / CHUNK_BYTES);
1173
+ const id2 = randomId();
1174
+ for (let index = 0; index < total; index++) {
1175
+ this.guard();
1176
+ const body = new TextEncoder().encode(
1177
+ JSON.stringify({
1178
+ id: id2,
1179
+ index,
1180
+ total,
1181
+ size: bytes.length,
1182
+ data: b64(
1183
+ bytes.subarray(index * CHUNK_BYTES, (index + 1) * CHUNK_BYTES)
1184
+ )
1185
+ })
1186
+ );
1187
+ const h = this.header("transport", this.sendNo);
1188
+ try {
1189
+ const cipher = this.sendCipher.EncryptWithAd(aad(h), body);
1190
+ this.sendNo++;
1191
+ this.sent += cipher.length;
1192
+ yield { ...h, payload: b64(cipher) };
1193
+ } catch (e) {
1194
+ this.close();
1195
+ throw e;
1196
+ }
1197
+ }
1198
+ }
1199
+ open(value) {
1200
+ try {
1201
+ this.guard();
1202
+ if (!this.ready) throw Error("HANDSHAKE_REQUIRED");
1203
+ const f = this.validate(value, "transport", this.recvNo);
1204
+ const cipher = unb64(f.payload);
1205
+ const clear = this.recvCipher.DecryptWithAd(aad(f), cipher);
1206
+ this.recvNo++;
1207
+ this.received += cipher.length;
1208
+ const p = fragment.parse(
1209
+ JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(clear))
1210
+ );
1211
+ if (!this.incoming) {
1212
+ if (p.index !== 0) throw Error("FRAGMENT_ORDER");
1213
+ this.incoming = {
1214
+ id: p.id,
1215
+ total: p.total,
1216
+ size: p.size,
1217
+ index: 0,
1218
+ bytes: 0,
1219
+ parts: [],
1220
+ deadline: Date.now() + 15e3
1221
+ };
1222
+ this.fragmentTimer = setTimeout(() => this.close(), 15e3);
1223
+ }
1224
+ const m = this.incoming;
1225
+ if (Date.now() > m.deadline || p.id !== m.id || p.total !== m.total || p.size !== m.size || p.index !== m.index)
1226
+ throw Error("FRAGMENT_ORDER");
1227
+ const chunk = unb64(p.data);
1228
+ if (chunk.length > CHUNK_BYTES || p.index < p.total - 1 && chunk.length !== CHUNK_BYTES)
1229
+ throw Error("FRAGMENT_SIZE");
1230
+ m.bytes += chunk.length;
1231
+ if (m.bytes > m.size) throw Error("FRAGMENT_SIZE");
1232
+ m.parts.push(chunk);
1233
+ m.index++;
1234
+ if (m.index !== m.total) return;
1235
+ if (m.bytes !== m.size) throw Error("FRAGMENT_TRUNCATED");
1236
+ const out = new Uint8Array(m.bytes);
1237
+ let offset = 0;
1238
+ for (const part of m.parts) {
1239
+ out.set(part, offset);
1240
+ offset += part.length;
1241
+ }
1242
+ this.incoming = void 0;
1243
+ clearTimeout(this.fragmentTimer);
1244
+ this.fragmentTimer = void 0;
1245
+ const message = parseMessage(
1246
+ JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(out))
1247
+ );
1248
+ if (!this.trusted && message.kind !== "pairing")
1249
+ throw Error("NOT_PAIRED");
1250
+ return message;
1251
+ } catch (e) {
1252
+ this.close();
1253
+ throw e;
1254
+ }
1255
+ }
1256
+ close() {
1257
+ if (this.closed) return;
1258
+ this.closed = true;
1259
+ clearTimeout(this.fragmentTimer);
1260
+ this.fragmentTimer = void 0;
1261
+ this.hs?.free();
1262
+ this.hs = void 0;
1263
+ this.sendCipher?.free();
1264
+ this.recvCipher?.free();
1265
+ this.sendCipher = this.recvCipher = void 0;
1266
+ this.incoming = void 0;
1267
+ this.trusted = false;
1268
+ }
1269
+ };
1270
+
1271
+ // sdk/src/node.ts
1272
+ var pending;
1273
+ function loadNoise() {
1274
+ return pending ??= (async () => {
1275
+ const file = import.meta.url.endsWith("/src/node.ts") ? new URL("../dist/assets/noise.wasm", import.meta.url) : new URL("./assets/noise.wasm", import.meta.url);
1276
+ return initializeNoise(new Uint8Array(await readFile(file)));
1277
+ })().catch((error) => {
1278
+ pending = void 0;
1279
+ throw error;
1280
+ });
1281
+ }
1282
+
1283
+ // src/config.ts
1284
+ var configSchema = z4.strictObject({
1285
+ relayUrl: z4.url().refine((s) => {
1286
+ if (!URL.canParse(s)) return false;
1287
+ const u = new URL(s);
1288
+ return u.protocol === "wss:" && !u.username && !u.password && !u.search && !u.hash && u.pathname === "/";
1289
+ }, "Relay requires WSS without URL credentials"),
1290
+ deviceTokenFile: z4.string(),
1291
+ stateDir: z4.string(),
1292
+ experimentalCrypto: z4.literal(true).optional(),
1293
+ cryptoPolicy: z4.enum(["local-reviewed-v1", "remote-wss-v1"]).optional(),
1294
+ upstream: z4.strictObject({
1295
+ mode: z4.enum(["proxy", "stdio", "websocket", "unix"]).default("unix"),
1296
+ socket: z4.string().optional(),
1297
+ url: z4.string().optional(),
1298
+ codexHome: z4.string().optional(),
1299
+ binary: z4.string().optional()
1300
+ })
1301
+ }).superRefine((c, ctx) => {
1302
+ if (c.cryptoPolicy === "local-reviewed-v1") {
1303
+ if (!URL.canParse(c.relayUrl) || !["localhost", "127.0.0.1", "[::1]"].includes(new URL(c.relayUrl).hostname))
1304
+ ctx.addIssue({ code: "custom", message: "Local crypto policy requires a loopback relay" });
1305
+ } else if (c.cryptoPolicy !== "remote-wss-v1" && !c.experimentalCrypto) {
1306
+ ctx.addIssue({ code: "custom", message: "Select local-reviewed-v1 or remote-wss-v1 explicitly" });
1307
+ }
1308
+ });
1309
+ async function privateRead(path) {
1310
+ const s = await stat(path);
1311
+ if ((s.mode & 63) !== 0 || s.uid !== process.getuid?.())
1312
+ throw Error("PRIVATE_FILE_PERMISSIONS");
1313
+ return readFile2(path, "utf8");
1314
+ }
1315
+ async function atomicPrivate(path, value) {
1316
+ await mkdir(dirname2(path), { recursive: true, mode: 448 });
1317
+ const tmp = path + "." + randomUUID();
1318
+ await writeFile(tmp, JSON.stringify(value, null, 2) + "\n", {
1319
+ mode: 384,
1320
+ flag: "wx"
1321
+ });
1322
+ await rename(tmp, path);
1323
+ }
1324
+ async function readConfig(path) {
1325
+ const c = configSchema.parse(JSON.parse(await readFile2(path, "utf8")));
1326
+ const base = dirname2(resolve(path));
1327
+ c.stateDir = resolve(base, c.stateDir);
1328
+ c.deviceTokenFile = resolve(base, c.deviceTokenFile);
1329
+ return c;
1330
+ }
1331
+ async function loadState(dir) {
1332
+ await mkdir(dir, { recursive: true, mode: 448 });
1333
+ const s = await stat(dir);
1334
+ if ((s.mode & 63) !== 0 || s.uid !== process.getuid?.())
1335
+ throw Error("STATE_DIRECTORY_PERMISSIONS");
1336
+ const path = dir + "/identity.json";
1337
+ let identity;
1338
+ try {
1339
+ const data = JSON.parse(await privateRead(path));
1340
+ identity = {
1341
+ privateKey: unb64(data.privateKey),
1342
+ publicKey: unb64(data.publicKey)
1343
+ };
1344
+ if (identity.privateKey.length !== 32 || identity.publicKey.length !== 32)
1345
+ throw Error("IDENTITY");
1346
+ } catch (e) {
1347
+ if (e.code !== "ENOENT") throw e;
1348
+ identity = createIdentity(await loadNoise());
1349
+ await atomicPrivate(path, {
1350
+ privateKey: b64(identity.privateKey),
1351
+ publicKey: b64(identity.publicKey)
1352
+ });
1353
+ }
1354
+ let list = [];
1355
+ try {
1356
+ list = z4.array(
1357
+ z4.strictObject({
1358
+ peer: z4.string(),
1359
+ roots: z4.array(z4.string()).min(1).max(32),
1360
+ write: z4.boolean()
1361
+ })
1362
+ ).max(256).parse(JSON.parse(await privateRead(dir + "/grants.json")));
1363
+ } catch (e) {
1364
+ if (e.code !== "ENOENT") throw e;
1365
+ }
1366
+ for (const g of list)
1367
+ g.roots = await Promise.all(g.roots.map((r) => realpath(r)));
1368
+ return { identity, grants: new Map(list.map((g) => [g.peer, g])) };
1369
+ }
1370
+
1371
+ // src/daemon.ts
1372
+ import WebSocket2 from "ws";
1373
+ import {
1374
+ createServer,
1375
+ createConnection
1376
+ } from "node:net";
1377
+ import { chmod, realpath as realpath3, unlink, lstat as lstat2 } from "node:fs/promises";
1378
+ import { z as z5 } from "zod";
1379
+
1380
+ // src/bridge.ts
1381
+ import { randomUUID as randomUUID4 } from "node:crypto";
1382
+ import { basename as basename2 } from "node:path";
1383
+
1384
+ // src/files.ts
1385
+ import { realpath as realpath2, open, opendir } from "node:fs/promises";
1386
+ import { constants } from "node:fs";
1387
+ import { resolve as resolve2, relative, basename, extname, isAbsolute } from "node:path";
1388
+ import { createHash, randomUUID as randomUUID2 } from "node:crypto";
1389
+ function within(root, path) {
1390
+ const rel = relative(root, path);
1391
+ return rel === "" || !rel.startsWith("../") && rel !== ".." && !isAbsolute(rel);
1392
+ }
1393
+ async function authorizedPath(roots, path) {
1394
+ const actual = await realpath2(path).catch(() => {
1395
+ throw new BridgeError("NOT_FOUND");
1396
+ });
1397
+ if (!roots.some((r) => within(r, actual))) throw new BridgeError("FORBIDDEN");
1398
+ return actual;
1399
+ }
1400
+ var FileResources = class {
1401
+ handles = /* @__PURE__ */ new Map();
1402
+ async list(roots, cwd, path) {
1403
+ const target = await authorizedPath(roots, resolve2(cwd, path));
1404
+ const dir = await opendir(target);
1405
+ const entries = [];
1406
+ for await (const entry of dir) {
1407
+ if (entries.length >= 1e3)
1408
+ throw new BridgeError(
1409
+ "RESOURCE_TOO_LARGE",
1410
+ "Directory exceeds 1000 entries"
1411
+ );
1412
+ entries.push({
1413
+ name: entry.name,
1414
+ type: entry.isDirectory() ? "directory" : entry.isSymbolicLink() ? "symlink" : entry.isFile() ? "file" : "other"
1415
+ });
1416
+ }
1417
+ await authorizedPath(roots, target);
1418
+ return {
1419
+ data: entries.sort((a, b) => a.name.localeCompare(b.name)),
1420
+ nextCursor: null
1421
+ };
1422
+ }
1423
+ async open(owner, roots, cwd, path) {
1424
+ await this.expire();
1425
+ if (this.handles.size >= 64) throw new BridgeError("BUSY");
1426
+ const target = await authorizedPath(roots, resolve2(cwd, path));
1427
+ const fd = await open(
1428
+ target,
1429
+ constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK
1430
+ );
1431
+ try {
1432
+ const bound = await realpath2("/proc/self/fd/" + fd.fd);
1433
+ if (!roots.some((r) => within(r, bound)))
1434
+ throw new BridgeError("FORBIDDEN");
1435
+ const s = await fd.stat();
1436
+ if (!s.isFile()) throw new BridgeError("FORBIDDEN");
1437
+ if (s.size > 64 * 1024 * 1024)
1438
+ throw new BridgeError("RESOURCE_TOO_LARGE");
1439
+ const resourceId = randomUUID2(), expiresAt = Date.now() + 6e4;
1440
+ this.handles.set(resourceId, {
1441
+ owner,
1442
+ fd,
1443
+ size: s.size,
1444
+ mtime: s.mtimeMs,
1445
+ ctime: s.ctimeMs,
1446
+ expiresAt,
1447
+ path: target,
1448
+ roots
1449
+ });
1450
+ const mime = {
1451
+ ".png": "image/png",
1452
+ ".jpg": "image/jpeg",
1453
+ ".jpeg": "image/jpeg",
1454
+ ".webp": "image/webp",
1455
+ ".gif": "image/gif",
1456
+ ".txt": "text/plain",
1457
+ ".md": "text/plain",
1458
+ ".ts": "text/plain",
1459
+ ".js": "text/plain",
1460
+ ".json": "application/json"
1461
+ };
1462
+ return {
1463
+ resourceId,
1464
+ name: basename(target),
1465
+ size: s.size,
1466
+ mime: mime[extname(target)] ?? "application/octet-stream",
1467
+ expiresAt
1468
+ };
1469
+ } catch (e) {
1470
+ await fd.close();
1471
+ throw e;
1472
+ }
1473
+ }
1474
+ async get(owner, id2) {
1475
+ await this.expire();
1476
+ const h = this.handles.get(id2);
1477
+ if (!h || h.owner !== owner) throw new BridgeError("NOT_FOUND");
1478
+ return h;
1479
+ }
1480
+ async read(owner, id2, offset, length) {
1481
+ const h = await this.get(owner, id2);
1482
+ if (!Number.isSafeInteger(offset) || offset < 0 || !Number.isInteger(length) || length < 1 || length > 32768 || offset > h.size)
1483
+ throw new BridgeError("FORBIDDEN");
1484
+ const verify = async () => {
1485
+ const s = await h.fd.stat();
1486
+ const path = await realpath2("/proc/self/fd/" + h.fd.fd);
1487
+ if (!h.roots.some((r) => within(r, path)))
1488
+ throw new BridgeError("FORBIDDEN");
1489
+ if (s.size !== h.size || s.mtimeMs !== h.mtime || s.ctimeMs !== h.ctime)
1490
+ throw new BridgeError("RESOURCE_CHANGED");
1491
+ };
1492
+ try {
1493
+ await verify();
1494
+ const bytes = Buffer.alloc(Math.min(length, h.size - offset));
1495
+ const { bytesRead } = await h.fd.read(bytes, 0, bytes.length, offset);
1496
+ await verify();
1497
+ if (bytesRead !== bytes.length) throw new BridgeError("RESOURCE_CHANGED");
1498
+ return {
1499
+ resourceId: id2,
1500
+ offset,
1501
+ data: bytes.toString("base64url"),
1502
+ sha256: createHash("sha256").update(bytes).digest("hex"),
1503
+ eof: offset + bytesRead === h.size
1504
+ };
1505
+ } catch (e) {
1506
+ await this.close(owner, id2);
1507
+ throw e;
1508
+ }
1509
+ }
1510
+ async close(owner, id2) {
1511
+ const h = this.handles.get(id2);
1512
+ if (!h || h.owner !== owner) throw new BridgeError("NOT_FOUND");
1513
+ this.handles.delete(id2);
1514
+ await h.fd.close();
1515
+ return {};
1516
+ }
1517
+ async closeOwner(owner) {
1518
+ for (const [id2, h] of this.handles)
1519
+ if (h.owner === owner) await this.close(owner, id2);
1520
+ }
1521
+ async expire() {
1522
+ for (const [id2, h] of this.handles)
1523
+ if (h.expiresAt < Date.now()) {
1524
+ this.handles.delete(id2);
1525
+ await h.fd.close();
1526
+ }
1527
+ }
1528
+ async dispose() {
1529
+ for (const h of this.handles.values()) await h.fd.close();
1530
+ this.handles.clear();
1531
+ }
1532
+ };
1533
+
1534
+ // src/state.ts
1535
+ import { randomUUID as randomUUID3, createHash as createHash2 } from "node:crypto";
1536
+ var Leases = class {
1537
+ map = /* @__PURE__ */ new Map();
1538
+ acquire(thread2, owner, takeover = false) {
1539
+ const old = this.map.get(thread2);
1540
+ if (old && old.expires > Date.now() && old.owner !== owner && !takeover)
1541
+ throw new BridgeError("BUSY");
1542
+ if (this.map.size >= 1024 && !old) throw new BridgeError("BUSY");
1543
+ this.map.set(thread2, { owner, expires: Date.now() + 3e5 });
1544
+ return { threadId: thread2, holder: owner, expiresAt: Date.now() + 3e5 };
1545
+ }
1546
+ assert(thread2, owner) {
1547
+ const l = this.map.get(thread2);
1548
+ if (!l || l.owner !== owner || l.expires < Date.now())
1549
+ throw new BridgeError("LEASE_REQUIRED");
1550
+ l.expires = Date.now() + 3e5;
1551
+ }
1552
+ holder(thread2) {
1553
+ const l = this.map.get(thread2);
1554
+ return l && l.expires > Date.now() ? l.owner : void 0;
1555
+ }
1556
+ release(thread2, owner) {
1557
+ this.assert(thread2, owner);
1558
+ this.map.delete(thread2);
1559
+ return {};
1560
+ }
1561
+ revoke(owner) {
1562
+ for (const [k, l] of this.map) if (l.owner === owner) this.map.delete(k);
1563
+ }
1564
+ clear() {
1565
+ this.map.clear();
1566
+ }
1567
+ };
1568
+ var Dedup = class {
1569
+ bytes = 0;
1570
+ entries = /* @__PURE__ */ new Map();
1571
+ run(owner, id2, body, fn) {
1572
+ const key = owner + ":" + id2;
1573
+ const digest = createHash2("sha256").update(JSON.stringify(body)).digest("hex");
1574
+ const old = this.entries.get(key);
1575
+ if (old) {
1576
+ if (old.digest !== digest) throw new BridgeError("REQUEST_ID_REUSED");
1577
+ return old.promise;
1578
+ }
1579
+ if (this.entries.size >= 2048 || this.bytes >= 8 * 1024 * 1024)
1580
+ throw new BridgeError(
1581
+ "BUSY",
1582
+ "Dedup budget exhausted; reconnect after a deliberate client restart and reconcile outcomes"
1583
+ );
1584
+ const promise = Promise.resolve().then(fn).then((result) => {
1585
+ const bytes = Buffer.byteLength(JSON.stringify(result));
1586
+ if (bytes > 1024 * 1024 || this.bytes + bytes > 8 * 1024 * 1024)
1587
+ throw new BridgeError(
1588
+ "OUTCOME_UNKNOWN",
1589
+ "Write executed; response exceeds retained dedup budget; reconcile upstream"
1590
+ );
1591
+ this.bytes += bytes;
1592
+ return result;
1593
+ });
1594
+ this.entries.set(key, { digest, promise, at: Date.now() });
1595
+ return promise;
1596
+ }
1597
+ };
1598
+ var Subscriptions = class {
1599
+ constructor(deliver) {
1600
+ this.deliver = deliver;
1601
+ }
1602
+ epoch = randomUUID3();
1603
+ subs = /* @__PURE__ */ new Map();
1604
+ subscribe(owner, thread2) {
1605
+ if (this.subs.size >= 128) throw new BridgeError("BUSY");
1606
+ const subscriptionId = randomUUID3();
1607
+ this.subs.set(subscriptionId, {
1608
+ owner,
1609
+ thread: thread2,
1610
+ seq: 0n,
1611
+ events: [],
1612
+ bytes: 0,
1613
+ floor: 0n,
1614
+ at: Date.now()
1615
+ });
1616
+ return {
1617
+ subscriptionId,
1618
+ connectionEpoch: this.epoch,
1619
+ eventSeq: "0",
1620
+ snapshotPolicy: "reload-complete-item-on-delta"
1621
+ };
1622
+ }
1623
+ unsubscribe(owner, id2) {
1624
+ this.owned(owner, id2);
1625
+ this.subs.delete(id2);
1626
+ return {};
1627
+ }
1628
+ owned(owner, id2) {
1629
+ const s = this.subs.get(id2);
1630
+ if (!s || s.owner !== owner) throw new BridgeError("RESYNC_REQUIRED");
1631
+ return s;
1632
+ }
1633
+ emit(thread2, event, payload) {
1634
+ for (const [id2, s] of this.subs) {
1635
+ if (s.thread !== thread2) continue;
1636
+ const e = {
1637
+ kind: "event",
1638
+ connectionEpoch: this.epoch,
1639
+ subscriptionId: id2,
1640
+ eventSeq: String(++s.seq),
1641
+ event,
1642
+ payload
1643
+ };
1644
+ const size = Buffer.byteLength(JSON.stringify(e));
1645
+ if (size > 512 * 1024) {
1646
+ s.events = [];
1647
+ s.bytes = 0;
1648
+ s.floor = s.seq;
1649
+ this.deliver(s.owner, {
1650
+ ...e,
1651
+ event: "resyncRequired",
1652
+ payload: { reason: "event-too-large" }
1653
+ });
1654
+ continue;
1655
+ }
1656
+ s.events.push({ e, size, at: Date.now() });
1657
+ s.bytes += size;
1658
+ this.trim(s);
1659
+ this.deliver(s.owner, e);
1660
+ }
1661
+ }
1662
+ trim(s) {
1663
+ while (s.events.length && (s.bytes > 1024 * 1024 || Date.now() - s.events[0].at > 6e4)) {
1664
+ const e = s.events.shift();
1665
+ s.bytes -= e.size;
1666
+ s.floor = BigInt(e.e.eventSeq);
1667
+ }
1668
+ }
1669
+ resume(owner, id2, epoch, seq) {
1670
+ const s = this.owned(owner, id2);
1671
+ this.trim(s);
1672
+ const n = BigInt(seq);
1673
+ if (epoch !== this.epoch || n < s.floor || n > s.seq)
1674
+ throw new BridgeError("RESYNC_REQUIRED");
1675
+ s.at = Date.now();
1676
+ for (const { e } of s.events)
1677
+ if (BigInt(e.eventSeq) > n) this.deliver(owner, e);
1678
+ return {
1679
+ subscriptionId: id2,
1680
+ connectionEpoch: this.epoch,
1681
+ eventSeq: String(s.seq)
1682
+ };
1683
+ }
1684
+ ack(owner, id2, epoch, seq) {
1685
+ const s = this.owned(owner, id2);
1686
+ if (epoch !== this.epoch || BigInt(seq) > s.seq)
1687
+ throw new BridgeError("RESYNC_REQUIRED");
1688
+ s.at = Date.now();
1689
+ while (s.events.length && BigInt(s.events[0].e.eventSeq) <= BigInt(seq)) {
1690
+ const e = s.events.shift();
1691
+ s.bytes -= e.size;
1692
+ s.floor = BigInt(e.e.eventSeq);
1693
+ }
1694
+ }
1695
+ revoke(owner) {
1696
+ for (const [id2, s] of this.subs)
1697
+ if (s.owner === owner) this.subs.delete(id2);
1698
+ }
1699
+ clear() {
1700
+ for (const [id2, s] of this.subs)
1701
+ this.deliver(s.owner, {
1702
+ kind: "event",
1703
+ connectionEpoch: this.epoch,
1704
+ subscriptionId: id2,
1705
+ eventSeq: String(++s.seq),
1706
+ event: "resyncRequired",
1707
+ payload: { reason: "upstream-disconnected" }
1708
+ });
1709
+ this.subs.clear();
1710
+ }
1711
+ expire() {
1712
+ for (const [id2, s] of this.subs)
1713
+ if (Date.now() - s.at > 36e5) this.subs.delete(id2);
1714
+ }
1715
+ };
1716
+
1717
+ // src/bridge.ts
1718
+ var sources = [
1719
+ "cli",
1720
+ "vscode",
1721
+ "exec",
1722
+ "appServer",
1723
+ "subAgent",
1724
+ "subAgentReview",
1725
+ "subAgentCompact",
1726
+ "subAgentThreadSpawn",
1727
+ "subAgentOther",
1728
+ "unknown"
1729
+ ];
1730
+ var writes = /* @__PURE__ */ new Set([
1731
+ "threads.start",
1732
+ "threads.resume",
1733
+ "turns.start",
1734
+ "turns.interrupt"
1735
+ ]);
1736
+ var unsupported = (e) => e instanceof RpcError && (e.code === -32601 || /paginated|pagination|history mode/i.test(e.message));
1737
+ var Bridge = class {
1738
+ constructor(upstream, grants, deliver) {
1739
+ this.upstream = upstream;
1740
+ this.grants = grants;
1741
+ this.deliver = deliver;
1742
+ this.subscriptions = new Subscriptions(deliver);
1743
+ upstream.on("notification", (m) => this.notification(m));
1744
+ upstream.on("serverRequest", (m) => this.serverRequest(m));
1745
+ upstream.on("disconnect", () => {
1746
+ this.subscriptions.clear();
1747
+ this.leases.clear();
1748
+ this.approvals.clear();
1749
+ });
1750
+ this.timer = setInterval(() => {
1751
+ void this.files.expire();
1752
+ this.subscriptions.expire();
1753
+ for (const [id2, a] of this.approvals)
1754
+ if (a.expires < Date.now()) this.deny(id2, a);
1755
+ for (const [id2, h] of this.histories)
1756
+ if (h.expires < Date.now()) this.histories.delete(id2);
1757
+ for (const [id2, p] of this.projectCursors)
1758
+ if (p.expires < Date.now()) this.projectCursors.delete(id2);
1759
+ }, 1e3);
1760
+ this.timer.unref();
1761
+ }
1762
+ leases = new Leases();
1763
+ files = new FileResources();
1764
+ dedup = new Dedup();
1765
+ subscriptions;
1766
+ approvals = /* @__PURE__ */ new Map();
1767
+ histories = /* @__PURE__ */ new Map();
1768
+ projectCursors = /* @__PURE__ */ new Map();
1769
+ timer;
1770
+ grant(owner) {
1771
+ const grant = this.grants.get(owner);
1772
+ if (!grant) throw new BridgeError("NOT_PAIRED");
1773
+ return grant;
1774
+ }
1775
+ async project(owner, id2) {
1776
+ const g = this.grant(owner);
1777
+ if (id2.startsWith("cwd:")) {
1778
+ const cwd = Buffer.from(id2.slice(4), "base64url").toString();
1779
+ const actual = await authorizedPath(g.roots, cwd);
1780
+ if (actual !== cwd) throw new BridgeError("FORBIDDEN");
1781
+ return { id: id2, name: basename2(cwd), cwd, source: "derived" };
1782
+ }
1783
+ const { project: p } = await this.upstream.call("project/read", {
1784
+ projectId: id2
1785
+ });
1786
+ const roots = [];
1787
+ for (const root of p.roots ?? [])
1788
+ try {
1789
+ roots.push(await authorizedPath(g.roots, root.path));
1790
+ } catch {
1791
+ }
1792
+ if (!roots.length) throw new BridgeError("FORBIDDEN");
1793
+ return { id: p.id, name: p.name, cwd: roots[0], source: "codex" };
1794
+ }
1795
+ async thread(owner, id2) {
1796
+ const g = this.grant(owner);
1797
+ const r = await this.upstream.call("thread/read", {
1798
+ threadId: id2,
1799
+ includeTurns: false
1800
+ });
1801
+ await authorizedPath(g.roots, r.thread.cwd);
1802
+ return r.thread;
1803
+ }
1804
+ async history(owner, method, p) {
1805
+ if (p.cursor?.startsWith("turn-items:")) {
1806
+ const h = this.histories.get(p.cursor);
1807
+ if (!h || h.owner !== owner || h.thread !== p.threadId || h.kind !== method + ":" + (p.turnId ?? "") || h.expires < Date.now())
1808
+ throw new BridgeError("RESYNC_REQUIRED");
1809
+ return this.turnItems(owner, p, h);
1810
+ }
1811
+ if (p.cursor?.startsWith("fallback:")) {
1812
+ const h = this.histories.get(p.cursor);
1813
+ if (!h || h.owner !== owner || h.thread !== p.threadId || h.kind !== method + ":" + (p.turnId ?? "") || h.expires < Date.now())
1814
+ throw new BridgeError("RESYNC_REQUIRED");
1815
+ const data2 = h.data.slice(h.offset, h.offset + p.limit);
1816
+ h.offset += data2.length;
1817
+ this.histories.delete(p.cursor);
1818
+ const nextCursor2 = h.offset < h.data.length ? "fallback:" + randomUUID4() : null;
1819
+ if (nextCursor2) this.histories.set(nextCursor2, h);
1820
+ return { data: data2, nextCursor: nextCursor2, source: "bounded-fallback" };
1821
+ }
1822
+ try {
1823
+ const r2 = await this.upstream.call(
1824
+ method === "turns.list" ? "thread/turns/list" : "thread/items/list",
1825
+ {
1826
+ ...p,
1827
+ sortDirection: "desc",
1828
+ ...method === "turns.list" ? { itemsView: "summary" } : {}
1829
+ }
1830
+ );
1831
+ return {
1832
+ data: r2.data,
1833
+ nextCursor: r2.nextCursor ?? null,
1834
+ source: "codex"
1835
+ };
1836
+ } catch (e) {
1837
+ if (!unsupported(e)) throw e;
1838
+ }
1839
+ if (p.cursor) throw new BridgeError("RESYNC_REQUIRED");
1840
+ if (method === "items.list") {
1841
+ try {
1842
+ return await this.turnItems(owner, p);
1843
+ } catch (e) {
1844
+ if (!unsupported(e)) throw e;
1845
+ }
1846
+ }
1847
+ const r = await this.upstream.call("thread/read", {
1848
+ threadId: p.threadId,
1849
+ includeTurns: true
1850
+ });
1851
+ const bytes = Buffer.byteLength(JSON.stringify(r));
1852
+ if (bytes > 8 * 1024 * 1024 || Array.from(this.histories.values()).reduce((n, h) => n + h.bytes, 0) + bytes > 8 * 1024 * 1024)
1853
+ throw new BridgeError(
1854
+ "RESOURCE_TOO_LARGE",
1855
+ "Legacy history exceeds bounded fallback budget"
1856
+ );
1857
+ const turns = (r.thread.turns ?? []).slice().reverse();
1858
+ const data = method === "turns.list" ? turns.map((t) => ({ ...t, items: [] })) : turns.filter((t) => !p.turnId || p.turnId === t.id).flatMap(
1859
+ (t) => (t.items ?? []).slice().reverse().map((item) => ({ turnId: t.id, item }))
1860
+ );
1861
+ const nextCursor = data.length > p.limit ? "fallback:" + randomUUID4() : null;
1862
+ if (nextCursor) {
1863
+ if (this.histories.size >= 32) throw new BridgeError("BUSY");
1864
+ this.histories.set(nextCursor, {
1865
+ owner,
1866
+ thread: p.threadId,
1867
+ kind: method + ":" + (p.turnId ?? ""),
1868
+ data,
1869
+ offset: p.limit,
1870
+ bytes,
1871
+ expires: Date.now() + 6e4
1872
+ });
1873
+ }
1874
+ return {
1875
+ data: data.slice(0, p.limit),
1876
+ nextCursor,
1877
+ source: "bounded-fallback"
1878
+ };
1879
+ }
1880
+ /** Older app-server versions paginate full turns but not individual items.
1881
+ * Buffer at most one turn; never load a whole long conversation for this path. */
1882
+ async turnItems(owner, p, previous) {
1883
+ const h = previous ? { ...previous } : { owner, thread: p.threadId, kind: "items.list:" + (p.turnId ?? ""), data: [], offset: 0, bytes: 0, expires: Date.now() + 6e4 };
1884
+ const data = [];
1885
+ let pages = 0;
1886
+ const visited = /* @__PURE__ */ new Set();
1887
+ while (data.length < p.limit) {
1888
+ const part = h.data.slice(h.offset, h.offset + p.limit - data.length);
1889
+ data.push(...part);
1890
+ h.offset += part.length;
1891
+ if (data.length >= p.limit || h.upstream === null || pages >= 10) break;
1892
+ if (h.upstream && visited.has(h.upstream)) throw new BridgeError("RESYNC_REQUIRED");
1893
+ if (h.upstream) visited.add(h.upstream);
1894
+ const r = await this.upstream.call("thread/turns/list", { threadId: p.threadId, cursor: h.upstream, limit: 1, sortDirection: "desc", itemsView: "full" });
1895
+ pages++;
1896
+ const bytes = Buffer.byteLength(JSON.stringify(r));
1897
+ const retained = Array.from(this.histories.values()).reduce((sum, entry) => sum + (entry === previous ? 0 : entry.bytes), 0);
1898
+ if (bytes > 8 * 1024 * 1024 || retained + bytes > 8 * 1024 * 1024) throw new BridgeError("RESOURCE_TOO_LARGE");
1899
+ if (r.data.some((turn) => turn.itemsView && turn.itemsView !== "full")) throw new BridgeError("UNSUPPORTED_CAPABILITY", "Upstream did not return full turn items");
1900
+ h.data = r.data.filter((turn) => !p.turnId || turn.id === p.turnId).flatMap((turn) => (turn.items ?? []).slice().reverse().map((item) => ({ turnId: turn.id, item })));
1901
+ h.offset = 0;
1902
+ h.bytes = bytes;
1903
+ h.upstream = r.nextCursor ?? null;
1904
+ }
1905
+ const more = h.offset < h.data.length || h.upstream !== null;
1906
+ if (more && this.histories.size - (previous ? 1 : 0) >= 32) throw new BridgeError("BUSY");
1907
+ if (previous) this.histories.delete(p.cursor);
1908
+ const nextCursor = more ? "turn-items:" + randomUUID4() : null;
1909
+ if (nextCursor) this.histories.set(nextCursor, h);
1910
+ return { data, nextCursor, source: "bounded-fallback" };
1911
+ }
1912
+ async handle(owner, message) {
1913
+ if (message.kind === "serverResponse") {
1914
+ await this.answer(owner, message.serverRequestId, message.result);
1915
+ return;
1916
+ }
1917
+ if (message.kind === "ack") {
1918
+ this.grant(owner);
1919
+ this.subscriptions.ack(
1920
+ owner,
1921
+ message.subscriptionId,
1922
+ message.connectionEpoch,
1923
+ message.eventSeq
1924
+ );
1925
+ return;
1926
+ }
1927
+ if (message.kind !== "request") throw new BridgeError("FORBIDDEN");
1928
+ try {
1929
+ const g = this.grant(owner);
1930
+ const p = methodSchemas[message.method].parse(message.params);
1931
+ if (writes.has(message.method) && !g.write)
1932
+ throw new BridgeError("FORBIDDEN");
1933
+ const invoke = () => this.execute(owner, message.method, p);
1934
+ const result = writes.has(message.method) ? await this.dedup.run(
1935
+ owner,
1936
+ message.requestId,
1937
+ { method: message.method, params: p },
1938
+ invoke
1939
+ ) : await invoke();
1940
+ if (Buffer.byteLength(JSON.stringify(result)) > MAX_MESSAGE_BYTES - 1024)
1941
+ throw new BridgeError(
1942
+ "RESOURCE_TOO_LARGE",
1943
+ "Response exceeds page budget; request a smaller page"
1944
+ );
1945
+ return { kind: "response", requestId: message.requestId, result };
1946
+ } catch (e) {
1947
+ const code = e instanceof BridgeError ? e.code : e instanceof RpcError ? e.code === -32601 ? "UNSUPPORTED_CAPABILITY" : "UPSTREAM_ERROR" : "INVALID_REQUEST";
1948
+ return {
1949
+ kind: "response",
1950
+ requestId: message.requestId,
1951
+ error: {
1952
+ code,
1953
+ message: e instanceof BridgeError ? e.message : code,
1954
+ retryable: e instanceof BridgeError && e.retryable
1955
+ }
1956
+ };
1957
+ }
1958
+ }
1959
+ async execute(owner, method, p) {
1960
+ const g = this.grant(owner);
1961
+ if (writes.has(method) && p.connectionEpoch !== this.subscriptions.epoch)
1962
+ throw new BridgeError(
1963
+ "OUTCOME_UNKNOWN",
1964
+ "Client epoch changed; reconcile upstream before submitting a new request"
1965
+ );
1966
+ if (method === "bridge.capabilities")
1967
+ return {
1968
+ connectionEpoch: this.subscriptions.epoch,
1969
+ codexAvailable: this.upstream.available,
1970
+ codexVersion: this.upstream.version,
1971
+ methods: Object.keys(methodSchemas).filter(
1972
+ (m) => g.write || !writes.has(m)
1973
+ ),
1974
+ limits: {
1975
+ frameBytes: MAX_FRAME_BYTES,
1976
+ messageBytes: MAX_MESSAGE_BYTES,
1977
+ chunkBytes: CHUNK_BYTES
1978
+ },
1979
+ historyFallback: "bounded",
1980
+ activeTakeover: "unverified"
1981
+ };
1982
+ if ("threadId" in p) await this.thread(owner, p.threadId);
1983
+ if (writes.has(method) && "threadId" in p) {
1984
+ this.leases.assert(p.threadId, owner);
1985
+ if ((method === "turns.start" || method === "turns.interrupt") && p.connectionEpoch !== this.subscriptions.epoch)
1986
+ throw new BridgeError(
1987
+ "OUTCOME_UNKNOWN",
1988
+ "Client epoch changed; reconcile upstream before submitting a new request"
1989
+ );
1990
+ }
1991
+ switch (method) {
1992
+ case "projects.list": {
1993
+ const previous = p.cursor?.startsWith("derived:") ? this.projectCursors.get(p.cursor) : void 0;
1994
+ if (p.cursor?.startsWith("derived:") && (!previous || previous.owner !== owner || previous.expires < Date.now()))
1995
+ throw new BridgeError("RESYNC_REQUIRED");
1996
+ try {
1997
+ if (previous)
1998
+ throw new RpcError(-32601, "Use bounded derived-project cursor");
1999
+ const r = await this.upstream.call("project/list", p);
2000
+ if (!p.cursor && r.data.length === 0 && !r.nextCursor)
2001
+ throw new RpcError(-32601, "Empty project catalog; discover history roots");
2002
+ const data = [];
2003
+ for (const item of r.data) {
2004
+ for (const root of item.roots ?? []) {
2005
+ try {
2006
+ const cwd = await authorizedPath(g.roots, root.path);
2007
+ data.push({
2008
+ id: item.id,
2009
+ name: item.name,
2010
+ cwd,
2011
+ source: "codex"
2012
+ });
2013
+ break;
2014
+ } catch {
2015
+ }
2016
+ }
2017
+ }
2018
+ return { data, nextCursor: r.nextCursor ?? null };
2019
+ } catch (e) {
2020
+ if (!unsupported(e)) throw e;
2021
+ const data = [];
2022
+ const seen = new Set(previous?.seen);
2023
+ let upstreamCursor = previous?.upstream ?? p.cursor;
2024
+ const visited = /* @__PURE__ */ new Set();
2025
+ for (let page2 = 0; page2 < 10; page2++) {
2026
+ if (upstreamCursor) {
2027
+ if (visited.has(upstreamCursor)) throw new BridgeError("RESYNC_REQUIRED");
2028
+ visited.add(upstreamCursor);
2029
+ }
2030
+ const r = await this.upstream.call("thread/list", {
2031
+ cursor: upstreamCursor,
2032
+ limit: (p.limit ?? 50) - data.length,
2033
+ useStateDbOnly: true,
2034
+ sourceKinds: sources,
2035
+ sortKey: "updated_at",
2036
+ sortDirection: "desc"
2037
+ });
2038
+ for (const t of r.data) {
2039
+ let cwd;
2040
+ try {
2041
+ cwd = await authorizedPath(g.roots, t.cwd);
2042
+ } catch {
2043
+ continue;
2044
+ }
2045
+ if (!seen.has(cwd)) {
2046
+ seen.add(cwd);
2047
+ data.push({
2048
+ id: "cwd:" + Buffer.from(cwd).toString("base64url"),
2049
+ name: basename2(cwd),
2050
+ cwd,
2051
+ source: "derived"
2052
+ });
2053
+ }
2054
+ }
2055
+ upstreamCursor = r.nextCursor ?? void 0;
2056
+ if (!upstreamCursor || data.length >= (p.limit ?? 50)) break;
2057
+ }
2058
+ if (seen.size > 1024 || this.projectCursors.size >= 128)
2059
+ throw new BridgeError("RESOURCE_TOO_LARGE");
2060
+ if (previous) this.projectCursors.delete(p.cursor);
2061
+ const nextCursor = upstreamCursor ? "derived:" + randomUUID4() : null;
2062
+ if (nextCursor)
2063
+ this.projectCursors.set(nextCursor, {
2064
+ owner,
2065
+ upstream: upstreamCursor,
2066
+ seen,
2067
+ expires: Date.now() + 6e4
2068
+ });
2069
+ return { data, nextCursor };
2070
+ }
2071
+ }
2072
+ case "projects.read":
2073
+ return { project: await this.project(owner, p.projectId) };
2074
+ case "threads.list": {
2075
+ const project = await this.project(owner, p.projectId);
2076
+ const r = await this.upstream.call("thread/list", {
2077
+ cursor: p.cursor,
2078
+ limit: p.limit,
2079
+ archived: p.archived,
2080
+ sourceKinds: p.sourceKinds ?? sources,
2081
+ cwd: project.cwd,
2082
+ useStateDbOnly: true,
2083
+ sortKey: "updated_at",
2084
+ sortDirection: "desc"
2085
+ });
2086
+ const data = [];
2087
+ for (const t of r.data)
2088
+ try {
2089
+ await authorizedPath(g.roots, t.cwd);
2090
+ data.push(t);
2091
+ } catch {
2092
+ }
2093
+ return { data, nextCursor: r.nextCursor ?? null };
2094
+ }
2095
+ case "threads.read":
2096
+ return { thread: await this.thread(owner, p.threadId) };
2097
+ case "turns.list":
2098
+ case "items.list":
2099
+ return this.history(owner, method, p);
2100
+ case "threads.subscribe":
2101
+ return this.subscriptions.subscribe(owner, p.threadId);
2102
+ case "threads.unsubscribe":
2103
+ return this.subscriptions.unsubscribe(owner, p.subscriptionId);
2104
+ case "subscriptions.resume":
2105
+ return this.subscriptions.resume(
2106
+ owner,
2107
+ p.subscriptionId,
2108
+ p.connectionEpoch,
2109
+ p.eventSeq
2110
+ );
2111
+ case "leases.acquire": {
2112
+ if (!g.write) throw new BridgeError("FORBIDDEN");
2113
+ const lease = this.leases.acquire(p.threadId, owner, p.takeover);
2114
+ this.subscriptions.emit(p.threadId, "lease.changed", lease);
2115
+ return lease;
2116
+ }
2117
+ case "leases.release": {
2118
+ const result = this.leases.release(p.threadId, owner);
2119
+ this.subscriptions.emit(p.threadId, "lease.changed", {
2120
+ threadId: p.threadId,
2121
+ holder: null
2122
+ });
2123
+ return result;
2124
+ }
2125
+ case "threads.start": {
2126
+ const project = await this.project(owner, p.projectId);
2127
+ const r = await this.upstream.call("thread/start", {
2128
+ cwd: project.cwd,
2129
+ sandbox: "workspace-write",
2130
+ approvalPolicy: "on-request",
2131
+ ...project.source === "codex" ? { projectId: project.id } : {}
2132
+ });
2133
+ this.leases.acquire(r.thread.id, owner);
2134
+ return r;
2135
+ }
2136
+ case "threads.resume":
2137
+ return this.upstream.call("thread/resume", {
2138
+ threadId: p.threadId,
2139
+ excludeTurns: true,
2140
+ sandbox: "workspace-write",
2141
+ approvalPolicy: "on-request"
2142
+ });
2143
+ case "turns.start":
2144
+ return this.upstream.call("turn/start", {
2145
+ threadId: p.threadId,
2146
+ input: [{ type: "text", text: p.text, text_elements: [] }]
2147
+ });
2148
+ case "turns.interrupt":
2149
+ return this.upstream.call("turn/interrupt", {
2150
+ threadId: p.threadId,
2151
+ turnId: p.turnId
2152
+ });
2153
+ case "files.list": {
2154
+ const project = await this.project(owner, p.projectId);
2155
+ return this.files.list(g.roots, project.cwd, p.path);
2156
+ }
2157
+ case "files.open": {
2158
+ const project = await this.project(owner, p.projectId);
2159
+ return this.files.open(owner, g.roots, project.cwd, p.path);
2160
+ }
2161
+ case "resources.read":
2162
+ return this.files.read(owner, p.resourceId, p.offset, p.length);
2163
+ case "resources.close":
2164
+ return this.files.close(owner, p.resourceId);
2165
+ }
2166
+ }
2167
+ notification(m) {
2168
+ const thread2 = m.params?.threadId ?? m.params?.thread?.id;
2169
+ if (typeof thread2 !== "string") return;
2170
+ if (m.method.endsWith("/delta"))
2171
+ this.subscriptions.emit(thread2, "item.invalidated", {
2172
+ threadId: thread2,
2173
+ turnId: m.params.turnId,
2174
+ itemId: m.params.itemId,
2175
+ requiresReload: true
2176
+ });
2177
+ else if ([
2178
+ "item/started",
2179
+ "item/completed",
2180
+ "turn/started",
2181
+ "turn/completed",
2182
+ "thread/status/changed",
2183
+ "turn/plan/updated",
2184
+ "turn/diff/updated"
2185
+ ].includes(m.method))
2186
+ this.subscriptions.emit(thread2, m.method, m.params);
2187
+ }
2188
+ serverRequest(m) {
2189
+ const thread2 = m.params?.threadId, owner = this.leases.holder(thread2);
2190
+ const supported = [
2191
+ "item/commandExecution/requestApproval",
2192
+ "item/fileChange/requestApproval",
2193
+ "item/permissions/requestApproval",
2194
+ "item/tool/requestUserInput"
2195
+ ];
2196
+ if (!owner || !this.grants.get(owner)?.write || !supported.includes(m.method) || this.approvals.size >= 128) {
2197
+ this.upstream.respond(m.id, null, {
2198
+ code: -32001,
2199
+ message: "No authorized bridge controller"
2200
+ });
2201
+ return;
2202
+ }
2203
+ const id2 = randomUUID4();
2204
+ const a = {
2205
+ owner,
2206
+ thread: thread2,
2207
+ rpcId: m.id,
2208
+ method: m.method,
2209
+ params: m.params,
2210
+ expires: Date.now() + 12e4
2211
+ };
2212
+ this.approvals.set(id2, a);
2213
+ this.deliver(owner, {
2214
+ kind: "serverRequest",
2215
+ serverRequestId: id2,
2216
+ method: m.method.endsWith("requestUserInput") ? "userInput.request" : "approvals.request",
2217
+ params: { ...m.params, upstreamMethod: m.method, expiresAt: a.expires }
2218
+ });
2219
+ }
2220
+ deny(id2, a) {
2221
+ this.approvals.delete(id2);
2222
+ try {
2223
+ this.upstream.respond(a.rpcId, null, {
2224
+ code: -32001,
2225
+ message: "Approval expired or controller disconnected"
2226
+ });
2227
+ } catch {
2228
+ }
2229
+ this.subscriptions.emit(a.thread, "approval.closed", {
2230
+ serverRequestId: id2,
2231
+ status: "expired"
2232
+ });
2233
+ }
2234
+ async answer(owner, id2, result) {
2235
+ const g = this.grant(owner), a = this.approvals.get(id2);
2236
+ if (!a || a.owner !== owner || a.expires < Date.now())
2237
+ throw new BridgeError("APPROVAL_EXPIRED");
2238
+ if (!g.write) throw new BridgeError("FORBIDDEN");
2239
+ this.leases.assert(a.thread, owner);
2240
+ await this.thread(owner, a.thread);
2241
+ if (this.approvals.get(id2) !== a) throw new BridgeError("APPROVAL_EXPIRED");
2242
+ this.grant(owner);
2243
+ this.leases.assert(a.thread, owner);
2244
+ let response;
2245
+ if (a.method.endsWith("requestUserInput")) {
2246
+ const answers = result.answers;
2247
+ if (!answers || typeof answers !== "object" || Array.isArray(answers))
2248
+ throw new BridgeError("INVALID_REQUEST");
2249
+ const out = {};
2250
+ for (const q of a.params.questions ?? []) {
2251
+ const value = answers[q.id];
2252
+ if (!value || !Array.isArray(value.answers) || value.answers.length > 16 || value.answers.some(
2253
+ (s) => typeof s !== "string" || s.length > 8192
2254
+ ))
2255
+ throw new BridgeError("INVALID_REQUEST");
2256
+ out[q.id] = { answers: value.answers };
2257
+ }
2258
+ response = { answers: out };
2259
+ } else if (a.method.endsWith("permissions/requestApproval")) {
2260
+ if (result.decision !== "decline")
2261
+ throw new BridgeError(
2262
+ "UNSUPPORTED_CAPABILITY",
2263
+ "Permission expansion requires local Codex approval"
2264
+ );
2265
+ response = { permissions: {}, scope: "turn" };
2266
+ } else {
2267
+ if (!["accept", "decline", "cancel"].includes(result.decision))
2268
+ throw new BridgeError("INVALID_REQUEST");
2269
+ response = { decision: result.decision };
2270
+ }
2271
+ this.approvals.delete(id2);
2272
+ this.upstream.respond(a.rpcId, response);
2273
+ this.subscriptions.emit(a.thread, "approval.closed", {
2274
+ serverRequestId: id2,
2275
+ status: "answered"
2276
+ });
2277
+ }
2278
+ disconnect(owner) {
2279
+ for (const [id2, a] of this.approvals)
2280
+ if (a.owner === owner) this.deny(id2, a);
2281
+ void this.files.closeOwner(owner);
2282
+ this.leases.revoke(owner);
2283
+ }
2284
+ revoke(owner) {
2285
+ this.grants.delete(owner);
2286
+ this.disconnect(owner);
2287
+ this.subscriptions.revoke(owner);
2288
+ for (const [id2, h] of this.histories)
2289
+ if (h.owner === owner) this.histories.delete(id2);
2290
+ }
2291
+ async close() {
2292
+ clearInterval(this.timer);
2293
+ for (const [id2, a] of this.approvals) this.deny(id2, a);
2294
+ await this.files.dispose();
2295
+ this.upstream.close();
2296
+ }
2297
+ };
2298
+
2299
+ // src/daemon.ts
2300
+ var ClientDaemon = class {
2301
+ constructor(config, identity, grants, connectUpstream = () => CodexRpc.connect(config.upstream)) {
2302
+ this.config = config;
2303
+ this.identity = identity;
2304
+ this.grants = grants;
2305
+ this.connectUpstream = connectUpstream;
2306
+ }
2307
+ ws;
2308
+ bridge;
2309
+ deviceId = "";
2310
+ stopped = false;
2311
+ routes = /* @__PURE__ */ new Map();
2312
+ pairUntil = 0;
2313
+ admin;
2314
+ timer;
2315
+ retry;
2316
+ attempts = 0;
2317
+ noise;
2318
+ token = "";
2319
+ alive = true;
2320
+ connecting;
2321
+ async start() {
2322
+ this.noise = await loadNoise();
2323
+ this.token = (await privateRead(this.config.deviceTokenFile)).trim();
2324
+ if (!this.token || /[\r\n]/.test(this.token)) throw Error("DEVICE_TOKEN");
2325
+ await this.startAdmin();
2326
+ this.connect();
2327
+ this.timer = setInterval(() => {
2328
+ if (this.ws?.readyState === WebSocket2.OPEN) {
2329
+ if (!this.alive) this.ws.terminate();
2330
+ else {
2331
+ this.alive = false;
2332
+ this.ws.ping();
2333
+ }
2334
+ }
2335
+ for (const [id2, r] of this.routes)
2336
+ if (Date.now() > r.expires && !r.owner) this.closeRoute(id2);
2337
+ }, 1e4);
2338
+ this.timer.unref();
2339
+ }
2340
+ async ensureUpstream() {
2341
+ if (this.bridge?.upstream.available) return;
2342
+ if (this.connecting) return this.connecting;
2343
+ this.connecting = (async () => {
2344
+ await this.bridge?.close();
2345
+ this.bridge = new Bridge(
2346
+ await this.connectUpstream(),
2347
+ this.grants,
2348
+ (owner, m) => {
2349
+ for (const [id2, r] of this.routes)
2350
+ if (r.owner === owner) this.send(id2, m);
2351
+ }
2352
+ );
2353
+ })().finally(() => {
2354
+ this.connecting = void 0;
2355
+ });
2356
+ return this.connecting;
2357
+ }
2358
+ connect() {
2359
+ if (this.stopped) return;
2360
+ this.ws = new WebSocket2(new URL("/ws/v1/client", this.config.relayUrl), {
2361
+ headers: { Authorization: "Bearer " + this.token },
2362
+ perMessageDeflate: false,
2363
+ maxPayload: 128 * 1024,
2364
+ handshakeTimeout: 1e4
2365
+ });
2366
+ this.ws.on("open", () => {
2367
+ this.alive = true;
2368
+ this.attempts = 0;
2369
+ });
2370
+ this.ws.on("pong", () => this.alive = true);
2371
+ this.ws.on("error", () => {
2372
+ });
2373
+ this.ws.on("message", (data) => {
2374
+ try {
2375
+ this.receive(data.toString());
2376
+ } catch {
2377
+ this.ws?.close(1008, "Protocol violation");
2378
+ }
2379
+ });
2380
+ this.ws.on("close", () => {
2381
+ this.deviceId = "";
2382
+ for (const id2 of this.routes.keys()) this.closeRoute(id2, false);
2383
+ if (!this.stopped) {
2384
+ const delay = Math.min(3e4, 500 * 2 ** Math.min(this.attempts++, 7)) * (0.5 + Math.random());
2385
+ this.retry = setTimeout(() => this.connect(), delay);
2386
+ }
2387
+ });
2388
+ }
2389
+ control(value) {
2390
+ if (this.ws?.readyState === WebSocket2.OPEN)
2391
+ this.ws.send(JSON.stringify(value));
2392
+ }
2393
+ receive(text) {
2394
+ if (Buffer.byteLength(text) > 128 * 1024) throw Error("FRAME_TOO_LARGE");
2395
+ const m = JSON.parse(text);
2396
+ if (m.type === "device.ready") {
2397
+ if (typeof m.deviceId !== "string" || this.deviceId)
2398
+ throw Error("DEVICE_STATE");
2399
+ this.deviceId = m.deviceId;
2400
+ return;
2401
+ }
2402
+ if (m.type === "route.opened") {
2403
+ if (!this.deviceId || m.deviceId !== this.deviceId || typeof m.routeId !== "string" || this.routes.has(m.routeId) || this.routes.size >= 32) {
2404
+ this.control({ type: "route.close", routeId: m.routeId });
2405
+ return;
2406
+ }
2407
+ this.routes.set(m.routeId, {
2408
+ confirmed: false,
2409
+ expires: Date.now() + 12e4,
2410
+ queue: Promise.resolve(),
2411
+ pending: 0,
2412
+ out: [],
2413
+ bytes: 0,
2414
+ sending: false
2415
+ });
2416
+ return;
2417
+ }
2418
+ if (m.type === "route.closed") {
2419
+ this.closeRoute(m.routeId, false);
2420
+ return;
2421
+ }
2422
+ if (m.type !== "secure") return;
2423
+ const frame = decodeFrame(text);
2424
+ const r = this.routes.get(frame.routeId);
2425
+ if (!r) return;
2426
+ if (++r.pending > 64) {
2427
+ this.closeRoute(frame.routeId);
2428
+ return;
2429
+ }
2430
+ r.queue = r.queue.then(async () => {
2431
+ if (!this.routes.has(frame.routeId)) return;
2432
+ if (!r.session) {
2433
+ if (frame.phase !== "handshake" || frame.recordNo !== "0")
2434
+ throw Error("HANDSHAKE_REQUIRED");
2435
+ r.session = new SecureSession(
2436
+ this.noise,
2437
+ "client",
2438
+ {
2439
+ deviceId: this.deviceId,
2440
+ routeId: frame.routeId,
2441
+ sessionId: frame.sessionId
2442
+ },
2443
+ this.identity
2444
+ );
2445
+ }
2446
+ if (frame.phase === "handshake") {
2447
+ const reply = r.session.receiveHandshake(frame);
2448
+ if (reply) this.ws.send(encodeFrame(reply));
2449
+ if (r.session.ready) {
2450
+ const grant = this.grants.get(r.session.peerPublicKey);
2451
+ if (grant) {
2452
+ r.session.trust(r.session.fingerprint);
2453
+ r.owner = grant.peer;
2454
+ this.send(frame.routeId, {
2455
+ kind: "pairing",
2456
+ status: "ready",
2457
+ fingerprint: r.session.fingerprint
2458
+ });
2459
+ } else {
2460
+ if (Date.now() > this.pairUntil)
2461
+ throw new BridgeError("NOT_PAIRED");
2462
+ this.send(frame.routeId, {
2463
+ kind: "pairing",
2464
+ status: "pending",
2465
+ fingerprint: r.session.fingerprint
2466
+ });
2467
+ }
2468
+ }
2469
+ return;
2470
+ }
2471
+ const message = r.session.open(frame);
2472
+ if (!message) return;
2473
+ if (message.kind === "pairing") {
2474
+ if (message.fingerprint !== r.session.fingerprint || message.status !== "confirmed" || Date.now() > r.expires)
2475
+ throw Error("PAIRING_MISMATCH");
2476
+ r.confirmed = true;
2477
+ return;
2478
+ }
2479
+ if (!r.owner || !this.grants.has(r.owner))
2480
+ throw new BridgeError("NOT_PAIRED");
2481
+ await this.ensureUpstream();
2482
+ const response = await this.bridge.handle(r.owner, message);
2483
+ if (response) this.send(frame.routeId, response);
2484
+ }).catch(() => this.closeRoute(frame.routeId)).finally(() => r.pending--);
2485
+ }
2486
+ send(id2, message) {
2487
+ const r = this.routes.get(id2);
2488
+ if (!r?.session?.ready) return;
2489
+ const bytes = Buffer.byteLength(JSON.stringify(message));
2490
+ if (r.bytes + bytes > 1024 * 1024 || this.ws.bufferedAmount > 4 * 1024 * 1024) {
2491
+ this.closeRoute(id2);
2492
+ return;
2493
+ }
2494
+ r.out.push(message);
2495
+ r.bytes += bytes;
2496
+ if (!r.sending) void this.flush(id2, r);
2497
+ }
2498
+ async flush(id2, r) {
2499
+ r.sending = true;
2500
+ try {
2501
+ while (r.out.length && this.routes.has(id2)) {
2502
+ const m = r.out.shift();
2503
+ r.bytes -= Buffer.byteLength(JSON.stringify(m));
2504
+ for (const frame of r.session.seal(m)) {
2505
+ if (this.ws?.readyState !== WebSocket2.OPEN)
2506
+ throw Error("DISCONNECTED");
2507
+ await new Promise(
2508
+ (resolve5, reject) => this.ws.send(
2509
+ encodeFrame(frame),
2510
+ (e) => e ? reject(e) : resolve5()
2511
+ )
2512
+ );
2513
+ }
2514
+ }
2515
+ } catch {
2516
+ this.closeRoute(id2);
2517
+ } finally {
2518
+ r.sending = false;
2519
+ }
2520
+ }
2521
+ closeRoute(id2, notify = true) {
2522
+ const r = this.routes.get(id2);
2523
+ if (!r) return;
2524
+ this.routes.delete(id2);
2525
+ r.session?.close();
2526
+ r.out = [];
2527
+ if (r.owner && !Array.from(this.routes.values()).some((x) => x.owner === r.owner))
2528
+ this.bridge?.disconnect(r.owner);
2529
+ if (notify) this.control({ type: "route.close", routeId: id2 });
2530
+ }
2531
+ async adminCommand(value) {
2532
+ const m = z5.object({ command: z5.string() }).passthrough().parse(value);
2533
+ switch (m.command) {
2534
+ case "status":
2535
+ return {
2536
+ relayOnline: !!this.deviceId,
2537
+ codexAvailable: this.bridge?.upstream.available ?? false,
2538
+ handshakes: Array.from(this.routes.values()).filter(
2539
+ (r) => r.session?.ready
2540
+ ).length,
2541
+ authorizedSessions: Array.from(this.routes.values()).filter(
2542
+ (r) => r.owner
2543
+ ).length,
2544
+ pairingUntil: this.pairUntil
2545
+ };
2546
+ case "pair-open":
2547
+ this.pairUntil = Date.now() + 12e4;
2548
+ return { expiresAt: this.pairUntil };
2549
+ case "pair-list":
2550
+ return {
2551
+ pending: Array.from(this.routes.entries()).filter(([, r]) => r.session?.ready && !r.owner).map(([routeId, r]) => ({
2552
+ routeId,
2553
+ fingerprint: r.session.fingerprint,
2554
+ peer: r.session.peerPublicKey,
2555
+ browserConfirmed: r.confirmed,
2556
+ expiresAt: r.expires
2557
+ }))
2558
+ };
2559
+ case "pair-approve": {
2560
+ const p = z5.object({
2561
+ fingerprint: z5.string().regex(/^[a-f0-9]{64}$/),
2562
+ roots: z5.array(z5.string()).min(1).max(32),
2563
+ write: z5.boolean().default(false)
2564
+ }).parse(m);
2565
+ const found = Array.from(this.routes.entries()).find(
2566
+ ([, r2]) => r2.session?.fingerprint === p.fingerprint
2567
+ );
2568
+ if (!found || !found[1].confirmed || Date.now() > this.pairUntil || Date.now() > found[1].expires)
2569
+ throw Error("PAIRING_EXPIRED_OR_UNCONFIRMED");
2570
+ if (this.grants.size >= 256) throw Error("GRANT_LIMIT");
2571
+ const [id2, r] = found;
2572
+ const peer = r.session.peerPublicKey;
2573
+ const grant = {
2574
+ peer,
2575
+ roots: await Promise.all(p.roots.map((x) => realpath3(x))),
2576
+ write: p.write
2577
+ };
2578
+ this.grants.set(peer, grant);
2579
+ try {
2580
+ await atomicPrivate(
2581
+ this.config.stateDir + "/grants.json",
2582
+ Array.from(this.grants.values())
2583
+ );
2584
+ } catch (e) {
2585
+ this.grants.delete(peer);
2586
+ throw e;
2587
+ }
2588
+ r.session.trust(p.fingerprint);
2589
+ r.owner = peer;
2590
+ this.pairUntil = 0;
2591
+ this.send(id2, {
2592
+ kind: "pairing",
2593
+ status: "ready",
2594
+ fingerprint: p.fingerprint
2595
+ });
2596
+ return { peer, approved: true };
2597
+ }
2598
+ case "revoke": {
2599
+ const peer = z5.string().parse(m.peer);
2600
+ this.bridge?.revoke(peer);
2601
+ this.grants.delete(peer);
2602
+ for (const [id2, r] of this.routes)
2603
+ if (r.owner === peer || r.session?.peerPublicKey === peer)
2604
+ this.closeRoute(id2);
2605
+ await atomicPrivate(
2606
+ this.config.stateDir + "/grants.json",
2607
+ Array.from(this.grants.values())
2608
+ );
2609
+ return { revoked: true };
2610
+ }
2611
+ default:
2612
+ throw Error("ADMIN_COMMAND");
2613
+ }
2614
+ }
2615
+ async startAdmin() {
2616
+ const path = this.config.stateDir + "/control.sock";
2617
+ try {
2618
+ const entry = await lstat2(path);
2619
+ if (!entry.isSocket() || entry.uid !== process.getuid?.())
2620
+ throw Error("UNSAFE_CONTROL_PATH");
2621
+ const active = await new Promise((resolve5, reject) => {
2622
+ const socket = createConnection(path);
2623
+ socket.setTimeout(1e3, () => {
2624
+ socket.destroy();
2625
+ reject(Error("CONTROL_SOCKET_BUSY"));
2626
+ });
2627
+ socket.once("connect", () => {
2628
+ socket.destroy();
2629
+ resolve5(true);
2630
+ });
2631
+ socket.once("error", (e) => {
2632
+ if (e.code === "ECONNREFUSED" || e.code === "ENOENT") resolve5(false);
2633
+ else reject(e);
2634
+ });
2635
+ });
2636
+ if (active) throw Error("CLIENT_ALREADY_RUNNING");
2637
+ const current = await lstat2(path);
2638
+ if (current.ino !== entry.ino) throw Error("CONTROL_SOCKET_CHANGED");
2639
+ await unlink(path);
2640
+ } catch (e) {
2641
+ if (e.code !== "ENOENT") throw e;
2642
+ }
2643
+ this.admin = createServer((socket) => {
2644
+ socket.setTimeout(5e3, () => socket.destroy());
2645
+ let data = "";
2646
+ socket.on("data", (chunk) => {
2647
+ data += chunk.toString();
2648
+ if (data.length > 16384) {
2649
+ socket.destroy();
2650
+ return;
2651
+ }
2652
+ if (!data.includes("\n")) return;
2653
+ socket.pause();
2654
+ void Promise.resolve().then(() => this.adminCommand(JSON.parse(data))).then(
2655
+ (result) => socket.end(JSON.stringify({ result }) + "\n"),
2656
+ () => socket.end(
2657
+ JSON.stringify({ error: "ADMIN_REQUEST_REJECTED" }) + "\n"
2658
+ )
2659
+ );
2660
+ });
2661
+ socket.on("error", () => {
2662
+ });
2663
+ });
2664
+ await new Promise((resolve5, reject) => {
2665
+ this.admin.once("error", reject);
2666
+ this.admin.listen(path, () => resolve5());
2667
+ });
2668
+ await chmod(path, 384);
2669
+ }
2670
+ async stop() {
2671
+ this.stopped = true;
2672
+ clearInterval(this.timer);
2673
+ clearTimeout(this.retry);
2674
+ for (const id2 of this.routes.keys()) this.closeRoute(id2);
2675
+ this.ws?.terminate();
2676
+ await this.bridge?.close();
2677
+ await new Promise(
2678
+ (resolve5) => this.admin ? this.admin.close(() => resolve5()) : resolve5()
2679
+ );
2680
+ await unlink(this.config.stateDir + "/control.sock").catch(() => {
2681
+ });
2682
+ this.identity.privateKey.fill(0);
2683
+ }
2684
+ };
2685
+
2686
+ // src/control.ts
2687
+ import { createConnection as createConnection2 } from "node:net";
2688
+ import { join as join2 } from "node:path";
2689
+ function control(stateDir, request) {
2690
+ return new Promise((resolve5, reject) => {
2691
+ const socket = createConnection2(join2(stateDir, "control.sock"));
2692
+ let data = "";
2693
+ socket.setTimeout(5e3, () => socket.destroy(Error("ADMIN_TIMEOUT")));
2694
+ socket.on("connect", () => socket.write(JSON.stringify(request) + "\n"));
2695
+ socket.on("data", (chunk) => {
2696
+ data += chunk.toString();
2697
+ if (Buffer.byteLength(data) > 65536) socket.destroy(Error("ADMIN_TOO_LARGE"));
2698
+ });
2699
+ socket.on("error", reject);
2700
+ socket.on("end", () => {
2701
+ try {
2702
+ resolve5(JSON.parse(data));
2703
+ } catch {
2704
+ reject(Error("ADMIN_INVALID_RESPONSE"));
2705
+ }
2706
+ });
2707
+ });
2708
+ }
2709
+
2710
+ // src/setup.ts
2711
+ import { homedir as homedir2 } from "node:os";
2712
+ import { dirname as dirname3, join as join3, resolve as resolve3, basename as basename3 } from "node:path";
2713
+ import { mkdir as mkdir2, lstat as lstat3, writeFile as writeFile2, unlink as unlink2 } from "node:fs/promises";
2714
+ import { randomUUID as randomUUID5 } from "node:crypto";
2715
+ import { createInterface } from "node:readline/promises";
2716
+ import { Writable } from "node:stream";
2717
+ function defaultConfigPath() {
2718
+ return resolve3(process.env.WEB_CODEX_CLIENT_CONFIG ?? join3(process.env.XDG_CONFIG_HOME ?? join3(homedir2(), ".config"), "web-codex-client", "config.json"));
2719
+ }
2720
+ function validateToken(value) {
2721
+ const token = value.trim();
2722
+ if (!token || token.length > 4096 || /[^\x21-\x7e]/.test(token)) throw Error("DEVICE_TOKEN_INVALID: expected a single token without whitespace");
2723
+ return token;
2724
+ }
2725
+ async function privateDirectory(path) {
2726
+ await mkdir2(path, { recursive: true, mode: 448 });
2727
+ const info = await lstat3(path);
2728
+ if (!info.isDirectory() || info.isSymbolicLink() || info.uid !== process.getuid?.() || info.mode & 63) throw Error("CONFIG_DIRECTORY_PERMISSIONS: use a private directory owned by this user (0700)");
2729
+ }
2730
+ async function tokenFromStdin() {
2731
+ if (process.stdin.isTTY) throw Error("TOKEN_STDIN_REQUIRES_PIPE");
2732
+ let value = "";
2733
+ for await (const chunk of process.stdin) {
2734
+ value += chunk.toString();
2735
+ if (Buffer.byteLength(value) > 4098) throw Error("DEVICE_TOKEN_TOO_LARGE");
2736
+ }
2737
+ return validateToken(value);
2738
+ }
2739
+ async function question(prompt, secret = false) {
2740
+ if (!process.stdin.isTTY || !process.stdout.isTTY) throw Error("INTERACTIVE_TERMINAL_REQUIRED: use --relay and --token-stdin");
2741
+ const output = new Writable({ write(chunk, _encoding, done) {
2742
+ if (!secret) process.stdout.write(chunk);
2743
+ done();
2744
+ } });
2745
+ const rl = createInterface({ input: process.stdin, output, terminal: true });
2746
+ const abort = new AbortController();
2747
+ rl.on("SIGINT", () => abort.abort());
2748
+ if (secret) process.stdout.write(prompt);
2749
+ try {
2750
+ return await rl.question(secret ? "" : prompt, { signal: abort.signal });
2751
+ } finally {
2752
+ rl.close();
2753
+ if (secret) process.stdout.write("\n");
2754
+ }
2755
+ }
2756
+ async function saveConfiguration(path, relayUrl, token, socket) {
2757
+ path = resolve3(path);
2758
+ const directory = dirname3(path);
2759
+ const parsed = configSchema.safeParse({ relayUrl, deviceTokenFile: "placeholder", stateDir: "./state", cryptoPolicy: "remote-wss-v1", upstream: { mode: "unix", ...socket ? { socket: resolve3(socket) } : {} } });
2760
+ if (!parsed.success) throw Error("RELAY_URL_INVALID: use a WSS origin without path, credentials, query or fragment");
2761
+ token = validateToken(token);
2762
+ await privateDirectory(directory);
2763
+ let existing;
2764
+ try {
2765
+ existing = await readConfig(path);
2766
+ } catch (e) {
2767
+ if (e.code !== "ENOENT") throw Error("CONFIG_INVALID: repair the existing configuration before replacing it");
2768
+ }
2769
+ if (existing) {
2770
+ try {
2771
+ await control(existing.stateDir, { command: "status" });
2772
+ throw Error("CLIENT_RUNNING: stop the client before reconfiguring");
2773
+ } catch (e) {
2774
+ if (!["ENOENT", "ECONNREFUSED"].includes(e.code ?? "")) throw e;
2775
+ }
2776
+ }
2777
+ const host = new URL(relayUrl).hostname;
2778
+ const stateDir = existing?.stateDir ?? join3(directory, "state");
2779
+ await loadState(stateDir);
2780
+ const tokenFile = join3(directory, "device-token-" + randomUUID5());
2781
+ await writeFile2(tokenFile, token + "\n", { mode: 384, flag: "wx" });
2782
+ try {
2783
+ await atomicPrivate(path, {
2784
+ ...parsed.data,
2785
+ relayUrl: new URL(relayUrl).origin,
2786
+ deviceTokenFile: "./" + tokenFile.slice(directory.length + 1),
2787
+ stateDir,
2788
+ cryptoPolicy: ["localhost", "127.0.0.1", "[::1]"].includes(host) ? "local-reviewed-v1" : "remote-wss-v1",
2789
+ upstream: socket ? parsed.data.upstream : existing?.upstream ?? parsed.data.upstream
2790
+ });
2791
+ } catch (e) {
2792
+ await unlink2(tokenFile);
2793
+ throw e;
2794
+ }
2795
+ if (existing && dirname3(existing.deviceTokenFile) === directory && /^device-token-[0-9a-f-]{36}$/.test(basename3(existing.deviceTokenFile))) await unlink2(existing.deviceTokenFile).catch(() => {
2796
+ });
2797
+ return { path, socket: controlSocketPath(socket ? parsed.data.upstream : existing?.upstream ?? parsed.data.upstream) };
2798
+ }
2799
+
2800
+ // src/doctor.ts
2801
+ import { join as join4, dirname as dirname4 } from "node:path";
2802
+ import WebSocket3 from "ws";
2803
+ import { SingleBar, Presets } from "cli-progress";
2804
+ async function authenticateRelay(relayUrl, token) {
2805
+ await new Promise((resolve5, reject) => {
2806
+ const ws = new WebSocket3(new URL("/ws/v1/client", relayUrl), {
2807
+ headers: { Authorization: "Bearer " + token },
2808
+ perMessageDeflate: false,
2809
+ maxPayload: 4096,
2810
+ handshakeTimeout: 1e4
2811
+ });
2812
+ const timer = setTimeout(() => finish(Error("RELAY_TIMEOUT")), 12e3);
2813
+ let finished = false;
2814
+ function finish(error) {
2815
+ if (finished) return;
2816
+ finished = true;
2817
+ clearTimeout(timer);
2818
+ ws.terminate();
2819
+ if (error) reject(error);
2820
+ else resolve5();
2821
+ }
2822
+ ws.on("error", () => finish(Error("RELAY_TLS_OR_NETWORK: check DNS, certificate trust and reachability")));
2823
+ ws.on("unexpected-response", (_request, response) => {
2824
+ response.resume();
2825
+ finish(Error(response.statusCode === 401 ? "RELAY_UNAUTHORIZED: replace the device token" : "RELAY_HTTP_REJECTED: check the reverse proxy"));
2826
+ });
2827
+ ws.on("close", () => finish(Error("RELAY_CLOSED")));
2828
+ ws.on("message", (bytes) => {
2829
+ try {
2830
+ const frame = JSON.parse(bytes.toString());
2831
+ if (frame.type === "device.ready" && typeof frame.deviceId === "string") finish();
2832
+ else finish(Error("RELAY_PROTOCOL"));
2833
+ } catch {
2834
+ finish(Error("RELAY_PROTOCOL"));
2835
+ }
2836
+ });
2837
+ });
2838
+ }
2839
+ async function doctor(configPath, quiet = false, reportPath = join4(dirname4(configPath), "doctor-report.json")) {
2840
+ const results = [];
2841
+ const started = Date.now();
2842
+ const bar = new SingleBar({ format: "Doctor [{bar}] {percentage}% | {value}/{total} | elapsed {duration_formatted} | ETA {eta_formatted} | {rate}/s | {phase}", noTTYOutput: true }, Presets.shades_classic);
2843
+ await privateDirectory(dirname4(reportPath));
2844
+ if (!quiet) bar.start(4, 0, { phase: "configuration", rate: "0" });
2845
+ async function check(phase, fn) {
2846
+ try {
2847
+ const detail = await fn();
2848
+ results.push({ phase, status: "PASS", ...detail ? { detail } : {} });
2849
+ } catch (error) {
2850
+ const message = error instanceof Error ? error.message : "";
2851
+ const detail = message.startsWith("RELAY_UNAUTHORIZED:") ? "device token rejected; register or rotate the device credential" : message.startsWith("RELAY_TLS_OR_NETWORK:") ? "check DNS, certificate trust, port and WSS reverse proxy" : message === "CLIENT_OFFLINE" ? "existing local client is offline; inspect its relay configuration" : "check configuration, permissions and service availability";
2852
+ results.push({ phase, status: "FAIL", detail });
2853
+ await atomicPrivate(reportPath, { results, elapsedMs: Date.now() - started });
2854
+ throw Error("DOCTOR_FAILED: " + phase + ": " + detail);
2855
+ }
2856
+ await atomicPrivate(reportPath, { results, elapsedMs: Date.now() - started });
2857
+ if (!quiet) bar.update(results.length, { phase, rate: (results.length / Math.max(1e-3, (Date.now() - started) / 1e3)).toFixed(2) });
2858
+ }
2859
+ let config, token;
2860
+ try {
2861
+ await check("configuration and private token", async () => {
2862
+ config = await readConfig(configPath);
2863
+ token = validateToken(await privateRead(config.deviceTokenFile));
2864
+ await privateDirectory(config.stateDir);
2865
+ });
2866
+ await check("locked encryption WASM", async () => {
2867
+ await loadNoise();
2868
+ });
2869
+ await check("relay TLS and device authentication", async () => {
2870
+ try {
2871
+ const running = await control(config.stateDir, { command: "status" });
2872
+ if (running.result?.relayOnline) return "existing client is authenticated; no duplicate connection";
2873
+ throw Error("CLIENT_OFFLINE");
2874
+ } catch (e) {
2875
+ if (!["ENOENT", "ECONNREFUSED"].includes(e.code ?? "")) throw e;
2876
+ }
2877
+ await authenticateRelay(config.relayUrl, token);
2878
+ });
2879
+ await check("Codex initialization (no model request)", async () => {
2880
+ const rpc = await CodexRpc.connect(config.upstream);
2881
+ try {
2882
+ if (!rpc.available) throw Error("CODEX_UNAVAILABLE");
2883
+ } finally {
2884
+ rpc.close();
2885
+ }
2886
+ });
2887
+ } finally {
2888
+ bar.stop();
2889
+ }
2890
+ console.log("PASS: configuration, encryption, authenticated WSS and Codex initialization.");
2891
+ }
2892
+
2893
+ // src/cli.ts
2894
+ var { values, positionals } = parseArgs({
2895
+ allowPositionals: true,
2896
+ options: {
2897
+ config: { type: "string" },
2898
+ state: { type: "string" },
2899
+ socket: { type: "string" },
2900
+ binary: { type: "string" },
2901
+ standalone: { type: "boolean" },
2902
+ transport: { type: "string" },
2903
+ "codex-home": { type: "string" },
2904
+ fingerprint: { type: "string" },
2905
+ peer: { type: "string" },
2906
+ root: { type: "string", multiple: true },
2907
+ write: { type: "boolean" },
2908
+ quiet: { type: "boolean" },
2909
+ report: { type: "string" },
2910
+ relay: { type: "string" },
2911
+ "token-stdin": { type: "boolean" },
2912
+ help: { type: "boolean", short: "h" },
2913
+ version: { type: "boolean", short: "v" }
2914
+ }
2915
+ });
2916
+ var command = positionals[0] ?? "help";
2917
+ async function main() {
2918
+ if (values.version) {
2919
+ console.log(JSON.parse(await readFile3(new URL("../package.json", import.meta.url), "utf8")).version);
2920
+ return;
2921
+ }
2922
+ if (command === "help" || values.help) {
2923
+ console.log(
2924
+ "web-codex-client configure [--config FILE] [--relay wss://HOST --token-stdin] [--socket PATH]\nweb-codex-client doctor [--config FILE] [--quiet] [--report FILE]\nweb-codex-client run [--config FILE]\nweb-codex-client init [--state DIR]\nweb-codex-client status|pair-open|pair-list [--state DIR] [--config FILE]\nweb-codex-client pair-approve --fingerprint FULL_HASH --root PATH [--write] [--state DIR]\nweb-codex-client revoke --peer PUBLIC_KEY [--state DIR]\nweb-codex-client probe [--transport unix|proxy] [--socket PATH] [--binary PATH] [--quiet] [--report FILE] (default: unix; --standalone starts an isolated stdio server)\nDefault config: " + defaultConfigPath()
2925
+ );
2926
+ return;
2927
+ }
2928
+ if (process.platform !== "linux" || Number(process.versions.node.split(".")[0]) !== 24) throw Error("SUPPORTED_RUNTIME: Linux with Node.js 24 required");
2929
+ if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === "0") throw Error("TLS_VERIFICATION_REQUIRED: remove NODE_TLS_REJECT_UNAUTHORIZED=0");
2930
+ const configPath = resolve4(values.config ?? defaultConfigPath());
2931
+ if (command === "configure") {
2932
+ const relay = values.relay ?? (await question("Relay WSS origin (e.g. wss://relay.example.com): ")).trim();
2933
+ if (values["token-stdin"] && !values.relay) throw Error("--token-stdin requires --relay");
2934
+ const socket = values.socket ?? (values["token-stdin"] ? void 0 : (await question("Codex socket [" + controlSocketPath({ mode: "unix" }) + "]: ")).trim() || void 0);
2935
+ const token = values["token-stdin"] ? await tokenFromStdin() : await question("Device token (hidden): ", true);
2936
+ const result2 = await saveConfiguration(configPath, relay, token, socket);
2937
+ console.log("Configuration saved: " + result2.path + "\nRun web-codex-client doctor, then web-codex-client run. Pairing and directory approval are still required.");
2938
+ return;
2939
+ }
2940
+ if (command === "doctor") {
2941
+ await doctor(configPath, values.quiet, values.report ? resolve4(values.report) : void 0);
2942
+ return;
2943
+ }
2944
+ if (command === "init") {
2945
+ await loadState(values.state ? resolve4(values.state) : (await readConfig(configPath)).stateDir);
2946
+ console.log("Client identity initialized in private state directory.");
2947
+ return;
2948
+ }
2949
+ if (command === "probe") {
2950
+ if (values.transport && !["unix", "proxy"].includes(values.transport)) throw Error("probe --transport must be unix or proxy");
2951
+ if (values.standalone && values.transport) throw Error("Do not combine --standalone and --transport");
2952
+ await probe(
2953
+ {
2954
+ mode: values.standalone ? "stdio" : values.transport ?? "unix",
2955
+ socket: values.socket,
2956
+ binary: values.binary,
2957
+ codexHome: values["codex-home"]
2958
+ },
2959
+ values.quiet,
2960
+ values.report
2961
+ );
2962
+ return;
2963
+ }
2964
+ if (command === "run") {
2965
+ const config = await readConfig(configPath);
2966
+ const state = await loadState(config.stateDir);
2967
+ const daemon = new ClientDaemon(config, state.identity, state.grants);
2968
+ await daemon.start();
2969
+ console.log(
2970
+ "Client running; use local status/pair-open/pair-list commands."
2971
+ );
2972
+ let stopping = false;
2973
+ const stop = () => {
2974
+ if (stopping) return;
2975
+ stopping = true;
2976
+ void daemon.stop().then(() => process.exit(0));
2977
+ };
2978
+ process.on("SIGINT", stop);
2979
+ process.on("SIGTERM", stop);
2980
+ return;
2981
+ }
2982
+ if (!["status", "pair-open", "pair-list", "pair-approve", "revoke"].includes(command)) throw Error("UNKNOWN_COMMAND: use --help");
2983
+ const stateDir = values.state ? resolve4(values.state) : (await readConfig(configPath)).stateDir;
2984
+ const request = {
2985
+ command,
2986
+ fingerprint: values.fingerprint,
2987
+ peer: values.peer,
2988
+ roots: values.root,
2989
+ write: values.write ?? false
2990
+ };
2991
+ const result = await control(stateDir, request);
2992
+ console.log(JSON.stringify(result));
2993
+ if (result.error) process.exitCode = 1;
2994
+ }
2995
+ main().catch((e) => {
2996
+ console.error(e?.code === "ENOENT" ? "CONFIG_OR_FILE_MISSING: run configure; check the configured paths" : e?.name === "ZodError" ? "CONFIG_INVALID: check the configuration fields" : e instanceof Error ? e.message : "CLIENT_FAILED");
2997
+ process.exitCode = 1;
2998
+ });
2999
+ //# sourceMappingURL=cli.js.map