codex-webstrapper 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,320 @@
1
+ import { EventEmitter } from "node:events";
2
+ import net from "node:net";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import crypto from "node:crypto";
6
+
7
+ import { createLogger, toErrorMessage } from "./util.mjs";
8
+
9
+ export const MAX_IPC_FRAME_BYTES = 256 * 1024 * 1024;
10
+ export const MAX_IPC_BUFFER_BYTES = 512 * 1024 * 1024;
11
+
12
+ export function getDefaultUdsSocketPath() {
13
+ if (process.platform === "win32") {
14
+ return path.join("\\\\.\\pipe", "codex-ipc");
15
+ }
16
+ const base = path.join(os.tmpdir(), "codex-ipc");
17
+ const uid = process.getuid?.();
18
+ return path.join(base, uid ? `ipc-${uid}.sock` : "ipc.sock");
19
+ }
20
+
21
+ export function encodeFrame(value) {
22
+ const json = typeof value === "string" ? value : JSON.stringify(value);
23
+ const payload = Buffer.from(json, "utf8");
24
+ const frame = Buffer.alloc(4 + payload.length);
25
+ frame.writeUInt32LE(payload.length, 0);
26
+ payload.copy(frame, 4);
27
+ return frame;
28
+ }
29
+
30
+ export class FrameDecoder {
31
+ constructor({ maxFrameBytes = MAX_IPC_FRAME_BYTES, maxBufferBytes = MAX_IPC_BUFFER_BYTES } = {}) {
32
+ this.maxFrameBytes = maxFrameBytes;
33
+ this.maxBufferBytes = maxBufferBytes;
34
+ this.buffer = Buffer.alloc(0);
35
+ this.currentFrameLength = null;
36
+ }
37
+
38
+ push(chunk) {
39
+ if (!chunk || chunk.length === 0) {
40
+ return [];
41
+ }
42
+
43
+ if (this.buffer.length + chunk.length > this.maxBufferBytes) {
44
+ throw new Error(`IPC buffer exceeded limit (${this.maxBufferBytes} bytes)`);
45
+ }
46
+
47
+ this.buffer = Buffer.concat([this.buffer, chunk]);
48
+ const messages = [];
49
+
50
+ for (;;) {
51
+ if (this.currentFrameLength == null) {
52
+ if (this.buffer.length < 4) {
53
+ break;
54
+ }
55
+
56
+ this.currentFrameLength = this.buffer.readUInt32LE(0);
57
+ this.buffer = this.buffer.subarray(4);
58
+
59
+ if (this.currentFrameLength > this.maxFrameBytes) {
60
+ throw new Error(
61
+ `IPC frame exceeded limit (${this.currentFrameLength} > ${this.maxFrameBytes} bytes)`
62
+ );
63
+ }
64
+ }
65
+
66
+ if (this.currentFrameLength == null || this.buffer.length < this.currentFrameLength) {
67
+ break;
68
+ }
69
+
70
+ const payload = this.buffer.subarray(0, this.currentFrameLength);
71
+ this.buffer = this.buffer.subarray(this.currentFrameLength);
72
+ this.currentFrameLength = null;
73
+
74
+ let parsed;
75
+ try {
76
+ parsed = JSON.parse(payload.toString("utf8"));
77
+ } catch (error) {
78
+ throw new Error(`Invalid IPC JSON frame: ${toErrorMessage(error)}`);
79
+ }
80
+
81
+ messages.push(parsed);
82
+ }
83
+
84
+ return messages;
85
+ }
86
+ }
87
+
88
+ export class UdsIpcClient extends EventEmitter {
89
+ constructor({
90
+ socketPath = getDefaultUdsSocketPath(),
91
+ clientType = "desktop-webstrapper",
92
+ reconnectMs = 1000,
93
+ requestTimeoutMs = 5000,
94
+ logger
95
+ } = {}) {
96
+ super();
97
+ this.socketPath = socketPath;
98
+ this.clientType = clientType;
99
+ this.reconnectMs = reconnectMs;
100
+ this.requestTimeoutMs = requestTimeoutMs;
101
+ this.logger = logger || createLogger("uds-ipc");
102
+
103
+ this.decoder = new FrameDecoder();
104
+ this.socket = null;
105
+ this.connected = false;
106
+ this.initialized = false;
107
+ this.stopped = false;
108
+ this.reconnectTimer = null;
109
+ this.clientId = "initializing-client";
110
+ this.pending = new Map();
111
+ }
112
+
113
+ async start() {
114
+ this.stopped = false;
115
+ await this._connectNow();
116
+ }
117
+
118
+ stop() {
119
+ this.stopped = true;
120
+ if (this.reconnectTimer) {
121
+ clearTimeout(this.reconnectTimer);
122
+ this.reconnectTimer = null;
123
+ }
124
+
125
+ this._rejectAllPending(new Error("ipc client stopped"));
126
+
127
+ if (this.socket) {
128
+ this.socket.destroy();
129
+ this.socket = null;
130
+ }
131
+
132
+ this.connected = false;
133
+ this.initialized = false;
134
+ }
135
+
136
+ isReady() {
137
+ return this.connected && this.initialized;
138
+ }
139
+
140
+ async sendRequest(method, params, options = {}) {
141
+ const requestId = crypto.randomUUID();
142
+ const payload = {
143
+ type: "request",
144
+ requestId,
145
+ sourceClientId: this.clientId,
146
+ method,
147
+ params,
148
+ targetClientId: options.targetClientId
149
+ };
150
+
151
+ return new Promise((resolve, reject) => {
152
+ const timer = setTimeout(() => {
153
+ this.pending.delete(requestId);
154
+ reject(new Error(`IPC request timeout for method ${method}`));
155
+ }, options.timeoutMs || this.requestTimeoutMs);
156
+
157
+ this.pending.set(requestId, {
158
+ resolve,
159
+ reject,
160
+ timer,
161
+ method
162
+ });
163
+
164
+ try {
165
+ this._write(payload);
166
+ } catch (error) {
167
+ clearTimeout(timer);
168
+ this.pending.delete(requestId);
169
+ reject(error);
170
+ }
171
+ });
172
+ }
173
+
174
+ sendBroadcast(method, params) {
175
+ const payload = {
176
+ type: "broadcast",
177
+ method,
178
+ sourceClientId: this.clientId,
179
+ params,
180
+ version: 1
181
+ };
182
+ this._write(payload);
183
+ }
184
+
185
+ _write(payload) {
186
+ if (!this.socket || !this.socket.writable) {
187
+ throw new Error("IPC socket is not connected");
188
+ }
189
+
190
+ const frame = encodeFrame(payload);
191
+ this.socket.write(frame);
192
+ }
193
+
194
+ async _connectNow() {
195
+ if (this.stopped) {
196
+ return;
197
+ }
198
+
199
+ await new Promise((resolve) => {
200
+ const socket = net.connect(this.socketPath, () => {
201
+ this.logger.info("UDS connected", { socketPath: this.socketPath });
202
+ this.socket = socket;
203
+ this.connected = true;
204
+ this.initialized = false;
205
+ this.clientId = "initializing-client";
206
+
207
+ this._initialize()
208
+ .then(() => {
209
+ this.emit("connected", { socketPath: this.socketPath, clientId: this.clientId });
210
+ resolve();
211
+ })
212
+ .catch((error) => {
213
+ this.logger.warn("UDS initialize failed", { error: toErrorMessage(error) });
214
+ socket.destroy();
215
+ resolve();
216
+ });
217
+ });
218
+
219
+ socket.on("data", (chunk) => {
220
+ try {
221
+ const messages = this.decoder.push(chunk);
222
+ for (const message of messages) {
223
+ this._handleIncomingMessage(message);
224
+ }
225
+ } catch (error) {
226
+ this.logger.warn("UDS frame decode failed", { error: toErrorMessage(error) });
227
+ socket.destroy();
228
+ }
229
+ });
230
+
231
+ socket.on("error", (error) => {
232
+ this.logger.debug("UDS socket error", { error: toErrorMessage(error) });
233
+ });
234
+
235
+ socket.on("close", () => {
236
+ this.connected = false;
237
+ this.initialized = false;
238
+ this.socket = null;
239
+ this.clientId = "initializing-client";
240
+ this._rejectAllPending(new Error("ipc socket closed"));
241
+ this.emit("disconnected");
242
+
243
+ if (!this.stopped) {
244
+ this._scheduleReconnect();
245
+ }
246
+ });
247
+
248
+ socket.on("end", () => {
249
+ socket.destroy();
250
+ });
251
+ });
252
+ }
253
+
254
+ async _initialize() {
255
+ const response = await this.sendRequest("initialize", {
256
+ clientType: this.clientType
257
+ });
258
+
259
+ if (response?.resultType === "success" && response?.result?.clientId) {
260
+ this.clientId = response.result.clientId;
261
+ this.initialized = true;
262
+ return;
263
+ }
264
+
265
+ throw new Error(`Unexpected initialize response: ${JSON.stringify(response)}`);
266
+ }
267
+
268
+ _scheduleReconnect() {
269
+ if (this.reconnectTimer || this.stopped) {
270
+ return;
271
+ }
272
+
273
+ this.reconnectTimer = setTimeout(async () => {
274
+ this.reconnectTimer = null;
275
+ await this._connectNow();
276
+ }, this.reconnectMs);
277
+ }
278
+
279
+ _handleIncomingMessage(message) {
280
+ switch (message?.type) {
281
+ case "broadcast": {
282
+ this.emit("broadcast", message);
283
+ break;
284
+ }
285
+ case "response": {
286
+ const pending = this.pending.get(message.requestId);
287
+ if (!pending) {
288
+ return;
289
+ }
290
+ clearTimeout(pending.timer);
291
+ this.pending.delete(message.requestId);
292
+ pending.resolve(message);
293
+ break;
294
+ }
295
+ case "client-discovery-request": {
296
+ this._write({
297
+ type: "client-discovery-response",
298
+ requestId: message.requestId,
299
+ response: { canHandle: false }
300
+ });
301
+ break;
302
+ }
303
+ case "request": {
304
+ this.emit("request", message);
305
+ break;
306
+ }
307
+ default: {
308
+ this.emit("unknown", message);
309
+ }
310
+ }
311
+ }
312
+
313
+ _rejectAllPending(error) {
314
+ for (const pending of this.pending.values()) {
315
+ clearTimeout(pending.timer);
316
+ pending.reject(error);
317
+ }
318
+ this.pending.clear();
319
+ }
320
+ }