codex-webapp 0.1.7 → 0.1.8

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,492 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from "node:child_process";
3
+ import { Dirent } from "node:fs";
4
+ import { readFile, readdir } from "node:fs/promises";
5
+ import { fileURLToPath } from "node:url";
6
+ import path from "node:path";
7
+
8
+ import {
9
+ checkPackEntries,
10
+ findDependencyBoundaryViolations,
11
+ } from "./check-public-package-boundary.mjs";
12
+
13
+ const MIN_NODE_VERSION = [20, 11, 0];
14
+ const DEPENDENCY_FIELDS = [
15
+ "dependencies",
16
+ "devDependencies",
17
+ "optionalDependencies",
18
+ "peerDependencies",
19
+ "bundleDependencies",
20
+ "bundledDependencies",
21
+ ];
22
+ const SOURCE_SCAN_SKIP_DIRS = new Set([
23
+ ".git",
24
+ ".hg",
25
+ ".svn",
26
+ "coverage",
27
+ "dist",
28
+ "node_modules",
29
+ ".next",
30
+ ".turbo",
31
+ ".vite",
32
+ ]);
33
+ const CONTENT_SCAN_EXTENSIONS = new Set([
34
+ ".cjs",
35
+ ".js",
36
+ ".json",
37
+ ".mjs",
38
+ ".ts",
39
+ ".tsx",
40
+ ".yml",
41
+ ".yaml",
42
+ ]);
43
+ const CONTENT_SCAN_ALLOWLIST = [
44
+ /^docs\//,
45
+ /^test\//,
46
+ /^README(?:\.[^.]+)?\.md$/i,
47
+ /^ACKNOWLEDGEMENTS\.md$/i,
48
+ /^CONTRIBUTING\.md$/i,
49
+ /^LICENSE\.md$/i,
50
+ /^SECURITY\.md$/i,
51
+ /^SUPPORT\.md$/i,
52
+ /^scripts\/check-public-package-boundary\.mjs$/,
53
+ /^scripts\/verify-clean-release\.mjs$/,
54
+ ];
55
+
56
+ const PRIVATE_ENGINE_NAME_PATTERNS = [
57
+ /(?:^|[/@._-])penso[._-]?render[._-]?envelope(?:$|[/@._-])/i,
58
+ /(?:^|[/@])@penso-os[/@]render[._-]?envelope(?:$|[/@._-])/i,
59
+ ];
60
+
61
+ const FORBIDDEN_ARTIFACT_PATH_PATTERNS = [
62
+ {
63
+ label: "Codex App archive",
64
+ pattern: /(?:^|[/\\])app\.asar$/i,
65
+ },
66
+ {
67
+ label: "pre-extracted renderer payload",
68
+ pattern: /(?:^|[/\\])webview(?:[/\\]|$)/i,
69
+ },
70
+ {
71
+ label: "Codex/OpenAI binary-looking artifact",
72
+ pattern: /(?:^|[/\\])(?:codex|openai)(?:[-_.]?(?:app|cli|binary|runtime))?\.(?:app|asar|dmg|pkg|exe|bin|zip|tar|tgz|gz)$/i,
73
+ },
74
+ {
75
+ label: "secret environment file",
76
+ pattern: /(?:^|[/\\])\.env(?:\.|$)/i,
77
+ },
78
+ {
79
+ label: "npm credential file",
80
+ pattern: /(?:^|[/\\])\.npmrc$/i,
81
+ },
82
+ {
83
+ label: "token-looking path",
84
+ pattern: /(?:^|[/\\])(?:tokens?|api[-_]?keys?|auth[-_]?store)(?:[/\\.]|$)/i,
85
+ },
86
+ {
87
+ label: "cookie-looking path",
88
+ pattern: /(?:^|[/\\])cookies?(?:[/\\.]|$)/i,
89
+ },
90
+ {
91
+ label: "signed URL-looking path",
92
+ pattern: /(?:^|[/\\])signed[-_]?urls?(?:[/\\.]|$)/i,
93
+ },
94
+ {
95
+ label: "session database-looking path",
96
+ pattern: /(?:^|[/\\])(?:session|sessions|session[-_]?db|state)\.(?:db|sqlite|sqlite3)$/i,
97
+ },
98
+ {
99
+ label: "customer data-looking path",
100
+ pattern: /(?:^|[/\\])customer[-_]?data(?:[/\\.]|$)/i,
101
+ },
102
+ {
103
+ label: "private key-looking path",
104
+ pattern: /(?:^|[/\\])(?:id_rsa|id_ed25519|private[-_]?key|.*\.pem)$/i,
105
+ },
106
+ ];
107
+ const FORBIDDEN_CONTENT_PATTERNS = [
108
+ {
109
+ label: "private engine package name",
110
+ pattern: /(?:^|[^a-z0-9])(?:@penso-os\/render[._-]?envelope|penso[._-]?render[._-]?envelope)(?:$|[^a-z0-9])/i,
111
+ },
112
+ {
113
+ label: "private GitHub package registry URL",
114
+ pattern: /npm\.pkg\.github\.com/i,
115
+ },
116
+ {
117
+ label: "private repo dependency spec",
118
+ pattern: /(?:github:|git\+https:\/\/github\.com\/|git\+ssh:\/\/git@github\.com:|file:\.\.\/)penso-os\/(?:penso[._-]?render[._-]?envelope|render[._-]?envelope)/i,
119
+ },
120
+ {
121
+ label: "OpenAI API key-looking value",
122
+ pattern: /\bsk-[A-Za-z0-9_-]{20,}\b/,
123
+ },
124
+ {
125
+ label: "GitHub token-looking value",
126
+ pattern: /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{20,}\b/,
127
+ },
128
+ {
129
+ label: "private key block",
130
+ pattern: /-----BEGIN [A-Z ]*PRIVATE KEY-----/,
131
+ },
132
+ {
133
+ label: "signed URL-looking value",
134
+ pattern: /https?:\/\/[^\s"'`]+[?&](?:X-Amz-Signature|Signature|sig)=/i,
135
+ },
136
+ ];
137
+
138
+ const PACK_METADATA_FIELDS = [
139
+ "name",
140
+ "version",
141
+ "filename",
142
+ "id",
143
+ "shasum",
144
+ "integrity",
145
+ "unpackedSize",
146
+ ];
147
+
148
+ export function parseArgs(argv) {
149
+ const args = [...argv];
150
+ let privateEngineDir = process.env.PENSO_RENDER_ENVELOPE_DIR || "";
151
+ for (let index = 0; index < args.length; index += 1) {
152
+ const arg = args[index];
153
+ if (arg === "--private-engine-dir") {
154
+ const value = args[index + 1];
155
+ if (!value || value.startsWith("--")) {
156
+ throw new Error("--private-engine-dir requires a path value");
157
+ }
158
+ privateEngineDir = value;
159
+ index += 1;
160
+ continue;
161
+ }
162
+ if (arg.startsWith("--private-engine-dir=")) {
163
+ privateEngineDir = arg.slice("--private-engine-dir=".length);
164
+ continue;
165
+ }
166
+ throw new Error(`Unknown argument: ${arg}`);
167
+ }
168
+ return { privateEngineDir: privateEngineDir || null };
169
+ }
170
+
171
+ export function checkNodeVersion(version = process.versions.node) {
172
+ const actual = version.split(".").map((part) => Number(part));
173
+ for (let index = 0; index < MIN_NODE_VERSION.length; index += 1) {
174
+ const current = actual[index] || 0;
175
+ const required = MIN_NODE_VERSION[index];
176
+ if (current > required) return [];
177
+ if (current < required) {
178
+ return [`Node.js ${version} is below the clean release gate minimum ${MIN_NODE_VERSION.join(".")}`];
179
+ }
180
+ }
181
+ return [];
182
+ }
183
+
184
+ export function inspectManifestAndLock({ manifest, lockfile }) {
185
+ const violations = findDependencyBoundaryViolations({ manifest, lockfile });
186
+ for (const { source, name, spec } of dependencySpecEntries({ manifest, lockfile })) {
187
+ if (looksLikeBundledCodexRuntime(name, spec)) {
188
+ violations.push(`${source} appears to depend on a Codex/OpenAI runtime package: ${name}`);
189
+ }
190
+ }
191
+ return violations;
192
+ }
193
+
194
+ export function inspectPackJson(packJson) {
195
+ const entries = Array.isArray(packJson) ? packJson : [packJson];
196
+ const violations = [];
197
+ for (const entry of entries) {
198
+ for (const field of PACK_METADATA_FIELDS) {
199
+ if (entry?.[field] && hasPrivateEngineName(String(entry[field]))) {
200
+ violations.push(`npm pack metadata ${field} references a private engine package name`);
201
+ }
202
+ }
203
+ violations.push(...checkPackEntries(entry?.files || []));
204
+ violations.push(...inspectRelativePaths((entry?.files || []).map((file) => `package/${file.path || file}`)));
205
+ }
206
+ return uniqueViolations(violations);
207
+ }
208
+
209
+ export function inspectRelativePaths(relativePaths) {
210
+ const violations = [];
211
+ for (const originalPath of relativePaths) {
212
+ const normalized = normalizeForScan(originalPath);
213
+ if (!normalized) continue;
214
+ if (hasPrivateEngineName(normalized)) {
215
+ violations.push(`path appears to include a private engine package name: ${originalPath}`);
216
+ }
217
+ for (const { label, pattern } of FORBIDDEN_ARTIFACT_PATH_PATTERNS) {
218
+ if (pattern.test(normalized)) {
219
+ violations.push(`path appears to include ${label}: ${originalPath}`);
220
+ }
221
+ }
222
+ }
223
+ return uniqueViolations(violations);
224
+ }
225
+
226
+ export async function collectSourcePaths(rootDir) {
227
+ const paths = [];
228
+ await walk(rootDir, "", paths, null);
229
+ return paths;
230
+ }
231
+
232
+ export async function inspectSourceTree(rootDir) {
233
+ const entries = [];
234
+ await walk(rootDir, "", null, entries);
235
+ const sourcePaths = entries.map((entry) => entry.relativePath);
236
+ return uniqueViolations([
237
+ ...inspectRelativePaths(sourcePaths),
238
+ ...await inspectTextFileContents(rootDir, entries),
239
+ ]);
240
+ }
241
+
242
+ export async function verifyCleanRelease({
243
+ rootDir = process.cwd(),
244
+ privateEngineDir = null,
245
+ log = console.log,
246
+ errorLog = console.error,
247
+ } = {}) {
248
+ const failures = [];
249
+ const results = [];
250
+
251
+ failures.push(...checkNodeVersion());
252
+
253
+ const manifest = JSON.parse(await readFile(path.join(rootDir, "package.json"), "utf8"));
254
+ const lockfile = JSON.parse(await readFile(path.join(rootDir, "package-lock.json"), "utf8"));
255
+ failures.push(...inspectManifestAndLock({ manifest, lockfile }));
256
+ failures.push(...await inspectSourceTree(rootDir));
257
+
258
+ results.push(runRequiredCommand({ label: "npm test", command: "npm", args: ["test"], cwd: rootDir, log }));
259
+ results.push(runRequiredCommand({
260
+ label: "npm run check:public-boundary",
261
+ command: "npm",
262
+ args: ["run", "check:public-boundary"],
263
+ cwd: rootDir,
264
+ log,
265
+ }));
266
+
267
+ const packResult = runPackDryRun(rootDir, log);
268
+ results.push(packResult);
269
+ if (packResult.ok && packResult.packJson) {
270
+ failures.push(...inspectPackJson(packResult.packJson));
271
+ }
272
+
273
+ if (manifest.scripts?.["start:dry-run"]) {
274
+ results.push(runRequiredCommand({
275
+ label: "npm run start:dry-run",
276
+ command: "npm",
277
+ args: ["run", "start:dry-run"],
278
+ cwd: rootDir,
279
+ log,
280
+ }));
281
+ } else {
282
+ results.push({ label: "npm run start:dry-run", ok: true, skipped: true, reason: "script is absent" });
283
+ }
284
+
285
+ if (privateEngineDir) {
286
+ const resolvedPrivateEngineDir = path.resolve(privateEngineDir);
287
+ results.push(runRequiredCommand({
288
+ label: `private engine npm test (${resolvedPrivateEngineDir})`,
289
+ command: "npm",
290
+ args: ["test"],
291
+ cwd: resolvedPrivateEngineDir,
292
+ log,
293
+ }));
294
+ results.push(runRequiredCommand({
295
+ label: `private engine npm pack --dry-run (${resolvedPrivateEngineDir})`,
296
+ command: "npm",
297
+ args: ["pack", "--dry-run"],
298
+ cwd: resolvedPrivateEngineDir,
299
+ log,
300
+ }));
301
+ } else {
302
+ results.push({
303
+ label: "private engine checks",
304
+ ok: true,
305
+ skipped: true,
306
+ reason: "set PENSO_RENDER_ENVELOPE_DIR or pass --private-engine-dir to include the private repo",
307
+ });
308
+ }
309
+
310
+ for (const result of results) {
311
+ if (!result.ok) {
312
+ failures.push(`${result.label} failed with exit code ${result.status}`);
313
+ }
314
+ }
315
+
316
+ const uniqueFailures = uniqueViolations(failures);
317
+ printSummary({ results, failures: uniqueFailures, log, errorLog });
318
+ return { ok: uniqueFailures.length === 0, results, failures: uniqueFailures };
319
+ }
320
+
321
+ function runPackDryRun(rootDir, log) {
322
+ log("==> npm pack --dry-run");
323
+ const result = spawnSync("npm", ["pack", "--dry-run", "--json", "--ignore-scripts"], {
324
+ cwd: rootDir,
325
+ encoding: "utf8",
326
+ stdio: ["ignore", "pipe", "inherit"],
327
+ });
328
+ if (result.status !== 0) {
329
+ return { label: "npm pack --dry-run", ok: false, status: result.status };
330
+ }
331
+ try {
332
+ const packJson = JSON.parse(result.stdout);
333
+ const entries = Array.isArray(packJson) ? packJson : [packJson];
334
+ for (const entry of entries) {
335
+ log(`packed ${entry.filename || entry.name || "package"} (${entry.files?.length || 0} files)`);
336
+ }
337
+ return { label: "npm pack --dry-run", ok: true, status: 0, packJson };
338
+ } catch (error) {
339
+ return {
340
+ label: `npm pack --dry-run JSON parse (${error.message})`,
341
+ ok: false,
342
+ status: 1,
343
+ };
344
+ }
345
+ }
346
+
347
+ export function runRequiredCommand({ label, command, args, cwd, log }) {
348
+ log(`==> ${label}`);
349
+ try {
350
+ const result = spawnSync(command, args, {
351
+ cwd,
352
+ stdio: "inherit",
353
+ env: process.env,
354
+ });
355
+ if (result.error) {
356
+ log(`Command could not start: ${result.error.message}`);
357
+ return { label, ok: false, status: "spawn-error" };
358
+ }
359
+ return { label, ok: result.status === 0, status: result.status };
360
+ } catch (error) {
361
+ log(`Command could not start: ${error.message}`);
362
+ return { label, ok: false, status: "spawn-error" };
363
+ }
364
+ }
365
+
366
+ function printSummary({ results, failures, log, errorLog }) {
367
+ log("\nClean release verification summary:");
368
+ for (const result of results) {
369
+ if (result.skipped) {
370
+ log(`- SKIP ${result.label}: ${result.reason}`);
371
+ continue;
372
+ }
373
+ log(`- ${result.ok ? "PASS" : "FAIL"} ${result.label}`);
374
+ }
375
+
376
+ if (failures.length > 0) {
377
+ errorLog("\nClean release verification found bounded release-readiness issues:");
378
+ for (const failure of failures) {
379
+ errorLog(`- ${failure}`);
380
+ }
381
+ return;
382
+ }
383
+ log("\nClean release verification passed. This is a bounded gate, not a guarantee of release safety.");
384
+ }
385
+
386
+ function looksLikeBundledCodexRuntime(name, spec) {
387
+ if (/^@openai\/codex$/i.test(name)) return true;
388
+ if (/^codex(?:-cli)?$/i.test(name) && /openai|codex/i.test(spec)) return true;
389
+ return false;
390
+ }
391
+
392
+ function dependencySpecEntries({ manifest, lockfile }) {
393
+ const entries = [];
394
+ collectDependencySpecs(entries, "package.json", manifest);
395
+ collectDependencySpecs(entries, "package-lock.json root", lockfile?.packages?.[""]);
396
+ for (const [packagePath, packageMeta] of Object.entries(lockfile?.packages || {})) {
397
+ collectDependencySpecs(entries, `package-lock.json ${packagePath || "<root>"}`, packageMeta);
398
+ }
399
+ for (const [name, meta] of Object.entries(lockfile?.dependencies || {})) {
400
+ entries.push({ source: `package-lock.json dependencies ${name}`, name, spec: String(meta?.version || "") });
401
+ }
402
+ return entries;
403
+ }
404
+
405
+ function collectDependencySpecs(entries, source, value) {
406
+ if (!value || typeof value !== "object" || Array.isArray(value)) return;
407
+ for (const field of DEPENDENCY_FIELDS) {
408
+ for (const [name, spec] of Object.entries(value[field] || {})) {
409
+ entries.push({ source: `${source} ${field}`, name, spec: String(spec) });
410
+ }
411
+ }
412
+ }
413
+
414
+ function hasPrivateEngineName(value) {
415
+ return PRIVATE_ENGINE_NAME_PATTERNS.some((pattern) => pattern.test(normalizeForScan(value)));
416
+ }
417
+
418
+ function normalizeForScan(value) {
419
+ return String(value || "").replaceAll("\\", "/").replace(/^\/+/, "");
420
+ }
421
+
422
+ async function walk(rootDir, relativeDir, paths, entriesOut) {
423
+ const dir = path.join(rootDir, relativeDir);
424
+ let entries;
425
+ try {
426
+ entries = await readdir(dir, { withFileTypes: true });
427
+ } catch {
428
+ return;
429
+ }
430
+ for (const entry of entries) {
431
+ if (!(entry instanceof Dirent)) continue;
432
+ if (entry.name === "." || entry.name === "..") continue;
433
+ const relativePath = path.join(relativeDir, entry.name);
434
+ const normalized = normalizeForScan(relativePath);
435
+ if (entry.isDirectory()) {
436
+ if (SOURCE_SCAN_SKIP_DIRS.has(entry.name)) continue;
437
+ paths?.push(`${normalized}/`);
438
+ entriesOut?.push({ relativePath: `${normalized}/`, isFile: false });
439
+ await walk(rootDir, relativePath, paths, entriesOut);
440
+ continue;
441
+ }
442
+ paths?.push(normalized);
443
+ entriesOut?.push({ relativePath: normalized, isFile: true });
444
+ }
445
+ }
446
+
447
+ async function inspectTextFileContents(rootDir, entries) {
448
+ const violations = [];
449
+ for (const entry of entries) {
450
+ if (!entry.isFile || !shouldScanFileContent(entry.relativePath)) continue;
451
+ let text;
452
+ try {
453
+ text = await readFile(path.join(rootDir, entry.relativePath), "utf8");
454
+ } catch {
455
+ continue;
456
+ }
457
+ for (const { label, pattern } of FORBIDDEN_CONTENT_PATTERNS) {
458
+ if (pattern.test(text)) {
459
+ violations.push(`file content appears to include ${label}: ${entry.relativePath}`);
460
+ }
461
+ }
462
+ }
463
+ return violations;
464
+ }
465
+
466
+ function shouldScanFileContent(relativePath) {
467
+ const normalized = normalizeForScan(relativePath);
468
+ if (CONTENT_SCAN_ALLOWLIST.some((pattern) => pattern.test(normalized))) {
469
+ return false;
470
+ }
471
+ return CONTENT_SCAN_EXTENSIONS.has(path.extname(normalized));
472
+ }
473
+
474
+ function uniqueViolations(violations) {
475
+ return [...new Set(violations.filter(Boolean))];
476
+ }
477
+
478
+ const isDirectRun = process.argv[1]
479
+ && fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
480
+
481
+ if (isDirectRun) {
482
+ try {
483
+ const { privateEngineDir } = parseArgs(process.argv.slice(2));
484
+ const result = await verifyCleanRelease({ privateEngineDir });
485
+ if (!result.ok) {
486
+ process.exit(1);
487
+ }
488
+ } catch (error) {
489
+ console.error(`Clean release verification failed to start: ${error.message}`);
490
+ process.exit(1);
491
+ }
492
+ }
@@ -0,0 +1,150 @@
1
+ import { spawn } from "node:child_process";
2
+
3
+ import { parseAppServerMessage, serializeAppServerMessage } from "./appServerMessageCodec.js";
4
+
5
+ const REQUEST_TIMEOUT_MS = 30_000;
6
+ const CLIENT_INFO = {
7
+ name: "codex-webapp",
8
+ title: "Codex WebApp",
9
+ version: "0.1.8",
10
+ };
11
+
12
+ export class CodexAppServerBridge {
13
+ constructor({
14
+ codexPath = "codex",
15
+ cwd = process.cwd(),
16
+ env = process.env,
17
+ timeoutMs = REQUEST_TIMEOUT_MS,
18
+ onNotification = null,
19
+ } = {}) {
20
+ this.codexPath = codexPath || "codex";
21
+ this.cwd = cwd || process.cwd();
22
+ this.env = env;
23
+ this.timeoutMs = timeoutMs;
24
+ this.onNotification = onNotification;
25
+ this.process = null;
26
+ this.buffer = "";
27
+ this.nextId = 1;
28
+ this.pending = new Map();
29
+ this.startPromise = null;
30
+ this.closed = false;
31
+ }
32
+
33
+ async request(method, params = {}) {
34
+ await this.ensureStarted();
35
+ return await this.sendRequest(method, params);
36
+ }
37
+
38
+ async ensureStarted() {
39
+ if (this.startPromise) return await this.startPromise;
40
+ this.startPromise = this.start();
41
+ return await this.startPromise;
42
+ }
43
+
44
+ async start() {
45
+ if (this.closed) throw new Error("Codex app-server bridge is closed");
46
+ this.process = spawn(this.codexPath, ["app-server", "--listen", "stdio://"], {
47
+ cwd: this.cwd,
48
+ env: this.env,
49
+ stdio: ["pipe", "pipe", "pipe"],
50
+ });
51
+ this.process.stdout.setEncoding("utf8");
52
+ this.process.stderr.setEncoding("utf8");
53
+ this.process.stdout.on("data", (chunk) => this.receiveStdout(chunk));
54
+ this.process.once("exit", (code, signal) => {
55
+ const exitStatus = signal || (code ?? "unknown");
56
+ const error = new Error(`codex app-server exited (${exitStatus})`);
57
+ for (const pending of this.pending.values()) pending.reject(error);
58
+ this.pending.clear();
59
+ this.process = null;
60
+ this.startPromise = null;
61
+ });
62
+
63
+ const stderr = [];
64
+ this.process.stderr.on("data", (chunk) => {
65
+ stderr.push(String(chunk));
66
+ if (stderr.join("").length > 16_384) stderr.shift();
67
+ });
68
+
69
+ try {
70
+ await this.sendRequest("initialize", {
71
+ clientInfo: CLIENT_INFO,
72
+ capabilities: { experimentalApi: true },
73
+ });
74
+ this.sendNotification("initialized", {});
75
+ } catch (error) {
76
+ throw new Error(`failed to initialize codex app-server: ${error.message}; ${stderr.join("").trim()}`);
77
+ }
78
+ }
79
+
80
+ sendRequest(method, params = {}) {
81
+ const id = this.nextId++;
82
+ this.write({ id, method, params });
83
+ return new Promise((resolve, reject) => {
84
+ const timer = setTimeout(() => {
85
+ this.pending.delete(id);
86
+ reject(new Error(`timed out waiting for codex app-server response: ${method}`));
87
+ }, this.timeoutMs);
88
+ this.pending.set(id, { method, resolve, reject, timer });
89
+ });
90
+ }
91
+
92
+ sendNotification(method, params = {}) {
93
+ this.write({ method, params });
94
+ }
95
+
96
+ write(message) {
97
+ if (!this.process?.stdin?.writable) {
98
+ throw new Error("codex app-server stdin is not writable");
99
+ }
100
+ this.process.stdin.write(serializeAppServerMessage(message));
101
+ }
102
+
103
+ receiveStdout(chunk) {
104
+ this.buffer += chunk;
105
+ let newlineIndex;
106
+ while ((newlineIndex = this.buffer.indexOf("\n")) !== -1) {
107
+ const line = this.buffer.slice(0, newlineIndex);
108
+ this.buffer = this.buffer.slice(newlineIndex + 1);
109
+ if (!line.trim()) continue;
110
+ this.receiveLine(line);
111
+ }
112
+ }
113
+
114
+ receiveLine(line) {
115
+ const message = parseAppServerMessage(line);
116
+ if (!message) return;
117
+ if (message.id && this.pending.has(message.id)) {
118
+ const pending = this.pending.get(message.id);
119
+ this.pending.delete(message.id);
120
+ clearTimeout(pending.timer);
121
+ if (message.error) {
122
+ pending.reject(new Error(errorMessage(message.error)));
123
+ return;
124
+ }
125
+ pending.resolve(message.result ?? {});
126
+ return;
127
+ }
128
+ if (message.method && this.onNotification) {
129
+ this.onNotification(message);
130
+ }
131
+ }
132
+
133
+ close() {
134
+ this.closed = true;
135
+ for (const pending of this.pending.values()) {
136
+ clearTimeout(pending.timer);
137
+ pending.reject(new Error("Codex app-server bridge closed"));
138
+ }
139
+ this.pending.clear();
140
+ if (this.process && this.process.exitCode === null) {
141
+ this.process.kill();
142
+ }
143
+ this.process = null;
144
+ }
145
+ }
146
+
147
+ function errorMessage(error) {
148
+ if (!error || typeof error !== "object") return String(error);
149
+ return error.message || JSON.stringify(error);
150
+ }
@@ -0,0 +1,12 @@
1
+ export function parseAppServerMessage(line) {
2
+ try {
3
+ const message = JSON.parse(line);
4
+ return message && typeof message === "object" ? message : null;
5
+ } catch {
6
+ return null;
7
+ }
8
+ }
9
+
10
+ export function serializeAppServerMessage(message) {
11
+ return `${JSON.stringify(message)}\n`;
12
+ }
@@ -0,0 +1,18 @@
1
+ export function createAuditEvidenceHook(handler = null) {
2
+ return typeof handler === "function" ? handler : null;
3
+ }
4
+
5
+ export function emitAuditEvidence(hook, event) {
6
+ if (!hook) return;
7
+ const emit = () => {
8
+ try {
9
+ hook(event);
10
+ } catch {
11
+ }
12
+ };
13
+ if (typeof queueMicrotask === "function") {
14
+ queueMicrotask(emit);
15
+ } else {
16
+ setImmediate(emit);
17
+ }
18
+ }
@@ -0,0 +1,29 @@
1
+ export function parseBridgeFrame(data) {
2
+ try {
3
+ const frame = JSON.parse(Buffer.isBuffer(data) ? data.toString("utf8") : String(data));
4
+ if (!frame || typeof frame !== "object") return null;
5
+ return {
6
+ id: frame.id,
7
+ kind: frame.kind,
8
+ payload: frame.payload,
9
+ };
10
+ } catch {
11
+ return null;
12
+ }
13
+ }
14
+
15
+ export function bridgeEvent(payload) {
16
+ return { type: "event", payload };
17
+ }
18
+
19
+ export function bridgeResponse(id, result) {
20
+ return { id, ok: true, result };
21
+ }
22
+
23
+ export function bridgeErrorResponse(id, error) {
24
+ return { id, ok: false, error: String(error?.message ?? error) };
25
+ }
26
+
27
+ export function serializeBridgeMessage(message) {
28
+ return JSON.stringify(message);
29
+ }