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.
- package/LICENSE.md +21 -0
- package/README.md +239 -0
- package/bin/codex-webstrap.sh +63 -0
- package/package.json +27 -0
- package/src/app-server.mjs +289 -0
- package/src/assets.mjs +190 -0
- package/src/auth.mjs +166 -0
- package/src/bridge-shim.js +669 -0
- package/src/ipc-uds.mjs +320 -0
- package/src/message-router.mjs +1857 -0
- package/src/server.mjs +363 -0
- package/src/util.mjs +95 -0
package/src/ipc-uds.mjs
ADDED
|
@@ -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
|
+
}
|