codexapp 0.1.9 → 0.1.11

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/index.html CHANGED
@@ -4,8 +4,8 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>Codex Web Local</title>
7
- <script type="module" crossorigin src="/assets/index-DrAmX48U.js"></script>
8
- <link rel="stylesheet" crossorigin href="/assets/index-BIm_B5t3.css">
7
+ <script type="module" crossorigin src="/assets/index-DMpyfM80.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/assets/index-CKknL24Z.css">
9
9
  </head>
10
10
  <body class="bg-slate-950">
11
11
  <div id="app"></div>
package/dist-cli/index.js CHANGED
@@ -3,6 +3,7 @@
3
3
  // src/cli/index.ts
4
4
  import { createServer as createServer2 } from "http";
5
5
  import { existsSync } from "fs";
6
+ import { accessSync, constants } from "fs";
6
7
  import { readFile as readFile2 } from "fs/promises";
7
8
  import { homedir as homedir2 } from "os";
8
9
  import { join as join3 } from "path";
@@ -10,6 +11,7 @@ import { spawn as spawn2, spawnSync } from "child_process";
10
11
  import { fileURLToPath as fileURLToPath2 } from "url";
11
12
  import { dirname as dirname2 } from "path";
12
13
  import { Command } from "commander";
14
+ import qrcode from "qrcode-terminal";
13
15
 
14
16
  // src/server/httpServer.ts
15
17
  import { fileURLToPath } from "url";
@@ -1163,13 +1165,9 @@ function createCodexBridgeMiddleware() {
1163
1165
  res.setHeader("Cache-Control", "no-cache, no-transform");
1164
1166
  res.setHeader("Connection", "keep-alive");
1165
1167
  res.setHeader("X-Accel-Buffering", "no");
1166
- const unsubscribe = appServer.onNotification((notification) => {
1168
+ const unsubscribe = middleware.subscribeNotifications((notification) => {
1167
1169
  if (res.writableEnded || res.destroyed) return;
1168
- const payload = {
1169
- ...notification,
1170
- atIso: (/* @__PURE__ */ new Date()).toISOString()
1171
- };
1172
- res.write(`data: ${JSON.stringify(payload)}
1170
+ res.write(`data: ${JSON.stringify(notification)}
1173
1171
 
1174
1172
  `);
1175
1173
  });
@@ -1200,20 +1198,20 @@ data: ${JSON.stringify({ ok: true })}
1200
1198
  middleware.dispose = () => {
1201
1199
  appServer.dispose();
1202
1200
  };
1201
+ middleware.subscribeNotifications = (listener) => {
1202
+ return appServer.onNotification((notification) => {
1203
+ listener({
1204
+ ...notification,
1205
+ atIso: (/* @__PURE__ */ new Date()).toISOString()
1206
+ });
1207
+ });
1208
+ };
1203
1209
  return middleware;
1204
1210
  }
1205
1211
 
1206
1212
  // src/server/authMiddleware.ts
1207
1213
  import { randomBytes, timingSafeEqual } from "crypto";
1208
1214
  var TOKEN_COOKIE = "codex_web_local_token";
1209
- function isLocalhostRequest(req) {
1210
- const remote = req.socket.remoteAddress ?? "";
1211
- if (remote === "127.0.0.1" || remote === "::1" || remote === "::ffff:127.0.0.1") {
1212
- return true;
1213
- }
1214
- const host = (req.headers.host ?? "").toLowerCase();
1215
- return host.startsWith("localhost:") || host === "localhost" || host.startsWith("127.0.0.1:");
1216
- }
1217
1215
  function constantTimeCompare(a, b) {
1218
1216
  const bufA = Buffer.from(a);
1219
1217
  const bufB = Buffer.from(b);
@@ -1232,6 +1230,22 @@ function parseCookies(header) {
1232
1230
  }
1233
1231
  return cookies;
1234
1232
  }
1233
+ function isLocalhostRemote(remote) {
1234
+ return remote === "127.0.0.1" || remote === "::1" || remote === "::ffff:127.0.0.1";
1235
+ }
1236
+ function isLocalhostHost(host) {
1237
+ const normalized = host.toLowerCase();
1238
+ return normalized.startsWith("localhost:") || normalized === "localhost" || normalized.startsWith("127.0.0.1:");
1239
+ }
1240
+ function isAuthorizedByRequestLike(remoteAddress, hostHeader, cookieHeader, validTokens) {
1241
+ const remote = remoteAddress ?? "";
1242
+ if (isLocalhostRemote(remote) || isLocalhostHost(hostHeader ?? "")) {
1243
+ return true;
1244
+ }
1245
+ const cookies = parseCookies(cookieHeader);
1246
+ const token = cookies[TOKEN_COOKIE];
1247
+ return Boolean(token && validTokens.has(token));
1248
+ }
1235
1249
  var LOGIN_PAGE_HTML = `<!DOCTYPE html>
1236
1250
  <html lang="en">
1237
1251
  <head>
@@ -1273,10 +1287,10 @@ form.addEventListener('submit',async e=>{
1273
1287
  </script>
1274
1288
  </body>
1275
1289
  </html>`;
1276
- function createAuthMiddleware(password) {
1290
+ function createAuthSession(password) {
1277
1291
  const validTokens = /* @__PURE__ */ new Set();
1278
- return (req, res, next) => {
1279
- if (isLocalhostRequest(req)) {
1292
+ const middleware = (req, res, next) => {
1293
+ if (isAuthorizedByRequestLike(req.socket.remoteAddress, req.headers.host, req.headers.cookie, validTokens)) {
1280
1294
  next();
1281
1295
  return;
1282
1296
  }
@@ -1294,9 +1308,9 @@ function createAuthMiddleware(password) {
1294
1308
  res.status(401).json({ error: "Invalid password" });
1295
1309
  return;
1296
1310
  }
1297
- const token2 = randomBytes(32).toString("hex");
1298
- validTokens.add(token2);
1299
- res.setHeader("Set-Cookie", `${TOKEN_COOKIE}=${token2}; Path=/; HttpOnly; SameSite=Strict`);
1311
+ const token = randomBytes(32).toString("hex");
1312
+ validTokens.add(token);
1313
+ res.setHeader("Set-Cookie", `${TOKEN_COOKIE}=${token}; Path=/; HttpOnly; SameSite=Strict`);
1300
1314
  res.json({ ok: true });
1301
1315
  } catch {
1302
1316
  res.status(400).json({ error: "Invalid request body" });
@@ -1304,18 +1318,17 @@ function createAuthMiddleware(password) {
1304
1318
  });
1305
1319
  return;
1306
1320
  }
1307
- const cookies = parseCookies(req.headers.cookie);
1308
- const token = cookies[TOKEN_COOKIE];
1309
- if (token && validTokens.has(token)) {
1310
- next();
1311
- return;
1312
- }
1313
1321
  res.setHeader("Content-Type", "text/html; charset=utf-8");
1314
1322
  res.status(200).send(LOGIN_PAGE_HTML);
1315
1323
  };
1324
+ return {
1325
+ middleware,
1326
+ isRequestAuthorized: (req) => isAuthorizedByRequestLike(req.socket.remoteAddress, req.headers.host, req.headers.cookie, validTokens)
1327
+ };
1316
1328
  }
1317
1329
 
1318
1330
  // src/server/httpServer.ts
1331
+ import { WebSocketServer } from "ws";
1319
1332
  var __dirname = dirname(fileURLToPath(import.meta.url));
1320
1333
  var distDir = join2(__dirname, "..", "dist");
1321
1334
  var IMAGE_CONTENT_TYPES = {
@@ -1343,8 +1356,9 @@ function normalizeLocalImagePath(rawPath) {
1343
1356
  function createServer(options = {}) {
1344
1357
  const app = express();
1345
1358
  const bridge = createCodexBridgeMiddleware();
1346
- if (options.password) {
1347
- app.use(createAuthMiddleware(options.password));
1359
+ const authSession = options.password ? createAuthSession(options.password) : null;
1360
+ if (authSession) {
1361
+ app.use(authSession.middleware);
1348
1362
  }
1349
1363
  app.use(bridge);
1350
1364
  app.get("/codex-local-image", (req, res) => {
@@ -1372,7 +1386,33 @@ function createServer(options = {}) {
1372
1386
  });
1373
1387
  return {
1374
1388
  app,
1375
- dispose: () => bridge.dispose()
1389
+ dispose: () => bridge.dispose(),
1390
+ attachWebSocket: (server) => {
1391
+ const wss = new WebSocketServer({ noServer: true });
1392
+ server.on("upgrade", (req, socket, head) => {
1393
+ const url = new URL(req.url ?? "", "http://localhost");
1394
+ if (url.pathname !== "/codex-api/ws") {
1395
+ return;
1396
+ }
1397
+ if (authSession && !authSession.isRequestAuthorized(req)) {
1398
+ socket.write("HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n");
1399
+ socket.destroy();
1400
+ return;
1401
+ }
1402
+ wss.handleUpgrade(req, socket, head, (ws) => {
1403
+ wss.emit("connection", ws, req);
1404
+ });
1405
+ });
1406
+ wss.on("connection", (ws) => {
1407
+ ws.send(JSON.stringify({ method: "ready", params: { ok: true }, atIso: (/* @__PURE__ */ new Date()).toISOString() }));
1408
+ const unsubscribe = bridge.subscribeNotifications((notification) => {
1409
+ if (ws.readyState !== 1) return;
1410
+ ws.send(JSON.stringify(notification));
1411
+ });
1412
+ ws.on("close", unsubscribe);
1413
+ ws.on("error", unsubscribe);
1414
+ });
1415
+ }
1376
1416
  };
1377
1417
  }
1378
1418
 
@@ -1423,6 +1463,42 @@ function runWithStatus(command, args) {
1423
1463
  function getUserNpmPrefix() {
1424
1464
  return join3(homedir2(), ".npm-global");
1425
1465
  }
1466
+ function installGlobalNpmPackageWithFallback(pkg, label) {
1467
+ const npmPrefix = spawnSync("npm", ["config", "get", "prefix"], {
1468
+ stdio: ["ignore", "pipe", "ignore"],
1469
+ encoding: "utf8"
1470
+ }).stdout?.trim();
1471
+ const globalPrefixWritable = Boolean(npmPrefix && (() => {
1472
+ try {
1473
+ accessSync(npmPrefix, constants.W_OK);
1474
+ return true;
1475
+ } catch {
1476
+ return false;
1477
+ }
1478
+ })());
1479
+ if (!globalPrefixWritable && !isTermuxRuntime()) {
1480
+ const userPrefix2 = getUserNpmPrefix();
1481
+ console.log(`
1482
+ Global npm prefix is not writable. Installing with --prefix ${userPrefix2}...
1483
+ `);
1484
+ runOrFail("npm", ["install", "-g", "--prefix", userPrefix2, pkg], `${label} (user prefix)`);
1485
+ process.env.PATH = `${join3(userPrefix2, "bin")}:${process.env.PATH ?? ""}`;
1486
+ return;
1487
+ }
1488
+ const status = runWithStatus("npm", ["install", "-g", pkg]);
1489
+ if (status === 0) {
1490
+ return;
1491
+ }
1492
+ if (isTermuxRuntime()) {
1493
+ throw new Error(`${label} failed with exit code ${String(status)}`);
1494
+ }
1495
+ const userPrefix = getUserNpmPrefix();
1496
+ console.log(`
1497
+ Global npm install requires elevated permissions. Retrying with --prefix ${userPrefix}...
1498
+ `);
1499
+ runOrFail("npm", ["install", "-g", "--prefix", userPrefix, pkg], `${label} (user prefix)`);
1500
+ process.env.PATH = `${join3(userPrefix, "bin")}:${process.env.PATH ?? ""}`;
1501
+ }
1426
1502
  function resolveCodexCommand() {
1427
1503
  if (canRun("codex", ["--version"])) {
1428
1504
  return "codex";
@@ -1448,32 +1524,17 @@ function hasCodexAuth() {
1448
1524
  function ensureCodexInstalled() {
1449
1525
  let codexCommand = resolveCodexCommand();
1450
1526
  if (!codexCommand) {
1451
- const installWithFallback = (pkg, label) => {
1452
- const status = runWithStatus("npm", ["install", "-g", pkg]);
1453
- if (status === 0) {
1454
- return;
1455
- }
1456
- if (isTermuxRuntime()) {
1457
- throw new Error(`${label} failed with exit code ${String(status)}`);
1458
- }
1459
- const userPrefix = getUserNpmPrefix();
1460
- console.log(`
1461
- Global npm install requires elevated permissions. Retrying with --prefix ${userPrefix}...
1462
- `);
1463
- runOrFail("npm", ["install", "-g", "--prefix", userPrefix, pkg], `${label} (user prefix)`);
1464
- process.env.PATH = `${join3(userPrefix, "bin")}:${process.env.PATH ?? ""}`;
1465
- };
1466
1527
  if (isTermuxRuntime()) {
1467
1528
  console.log("\nCodex CLI not found. Installing Termux-compatible Codex CLI from npm...\n");
1468
- installWithFallback("@mmmbuto/codex-cli-termux", "Codex CLI install");
1529
+ installGlobalNpmPackageWithFallback("@mmmbuto/codex-cli-termux", "Codex CLI install");
1469
1530
  codexCommand = resolveCodexCommand();
1470
1531
  if (!codexCommand) {
1471
1532
  console.log("\nTermux npm package did not expose `codex`. Installing official CLI fallback...\n");
1472
- installWithFallback("@openai/codex", "Codex CLI fallback install");
1533
+ installGlobalNpmPackageWithFallback("@openai/codex", "Codex CLI fallback install");
1473
1534
  }
1474
1535
  } else {
1475
1536
  console.log("\nCodex CLI not found. Installing official Codex CLI from npm...\n");
1476
- installWithFallback("@openai/codex", "Codex CLI install");
1537
+ installGlobalNpmPackageWithFallback("@openai/codex", "Codex CLI install");
1477
1538
  }
1478
1539
  codexCommand = resolveCodexCommand();
1479
1540
  if (!codexCommand && !isTermuxRuntime()) {
@@ -1489,6 +1550,37 @@ Global npm install requires elevated permissions. Retrying with --prefix ${userP
1489
1550
  }
1490
1551
  return codexCommand;
1491
1552
  }
1553
+ function resolveCloudflaredCommand() {
1554
+ if (canRun("cloudflared", ["--version"])) {
1555
+ return "cloudflared";
1556
+ }
1557
+ const userCandidate = join3(getUserNpmPrefix(), "bin", "cloudflared");
1558
+ if (existsSync(userCandidate) && canRun(userCandidate, ["--version"])) {
1559
+ return userCandidate;
1560
+ }
1561
+ const prefix = process.env.PREFIX?.trim();
1562
+ if (!prefix) {
1563
+ return null;
1564
+ }
1565
+ const candidate = join3(prefix, "bin", "cloudflared");
1566
+ if (existsSync(candidate) && canRun(candidate, ["--version"])) {
1567
+ return candidate;
1568
+ }
1569
+ return null;
1570
+ }
1571
+ function ensureCloudflaredInstalled() {
1572
+ let cloudflaredCommand = resolveCloudflaredCommand();
1573
+ if (!cloudflaredCommand) {
1574
+ console.log("\ncloudflared not found. Installing from npm...\n");
1575
+ installGlobalNpmPackageWithFallback("cloudflared", "cloudflared install");
1576
+ cloudflaredCommand = resolveCloudflaredCommand();
1577
+ if (!cloudflaredCommand) {
1578
+ throw new Error("cloudflared install completed but binary is still not available in PATH");
1579
+ }
1580
+ console.log("\ncloudflared installed.\n");
1581
+ }
1582
+ return cloudflaredCommand;
1583
+ }
1492
1584
  function resolvePassword(input) {
1493
1585
  if (input === false) {
1494
1586
  return void 0;
@@ -1515,6 +1607,49 @@ function openBrowser(url) {
1515
1607
  });
1516
1608
  child.unref();
1517
1609
  }
1610
+ function parseCloudflaredUrl(chunk) {
1611
+ const urlMatch = chunk.match(/https:\/\/[a-zA-Z0-9-]+\.trycloudflare\.com/g);
1612
+ if (!urlMatch || urlMatch.length === 0) {
1613
+ return null;
1614
+ }
1615
+ return urlMatch[urlMatch.length - 1] ?? null;
1616
+ }
1617
+ async function startCloudflaredTunnel(cloudflaredCommand, localPort) {
1618
+ return new Promise((resolve2, reject) => {
1619
+ const child = spawn2(cloudflaredCommand, ["tunnel", "--url", `http://localhost:${String(localPort)}`], {
1620
+ stdio: ["ignore", "pipe", "pipe"]
1621
+ });
1622
+ const timeout = setTimeout(() => {
1623
+ child.kill("SIGTERM");
1624
+ reject(new Error("Timed out waiting for cloudflared tunnel URL"));
1625
+ }, 2e4);
1626
+ const handleData = (value) => {
1627
+ const text = String(value);
1628
+ const parsedUrl = parseCloudflaredUrl(text);
1629
+ if (!parsedUrl) {
1630
+ return;
1631
+ }
1632
+ clearTimeout(timeout);
1633
+ child.stdout?.off("data", handleData);
1634
+ child.stderr?.off("data", handleData);
1635
+ resolve2({ process: child, url: parsedUrl });
1636
+ };
1637
+ const onError = (error) => {
1638
+ clearTimeout(timeout);
1639
+ reject(new Error(`Failed to start cloudflared: ${error.message}`));
1640
+ };
1641
+ child.once("error", onError);
1642
+ child.stdout?.on("data", handleData);
1643
+ child.stderr?.on("data", handleData);
1644
+ child.once("exit", (code) => {
1645
+ if (code === 0) {
1646
+ return;
1647
+ }
1648
+ clearTimeout(timeout);
1649
+ reject(new Error(`cloudflared exited before providing a URL (code ${String(code)})`));
1650
+ });
1651
+ });
1652
+ }
1518
1653
  function listenWithFallback(server, startPort) {
1519
1654
  return new Promise((resolve2, reject) => {
1520
1655
  const attempt = (port) => {
@@ -1546,9 +1681,28 @@ async function startServer(options) {
1546
1681
  }
1547
1682
  const requestedPort = parseInt(options.port, 10);
1548
1683
  const password = resolvePassword(options.password);
1549
- const { app, dispose } = createServer({ password });
1684
+ const { app, dispose, attachWebSocket } = createServer({ password });
1550
1685
  const server = createServer2(app);
1686
+ attachWebSocket(server);
1551
1687
  const port = await listenWithFallback(server, requestedPort);
1688
+ let tunnelChild = null;
1689
+ let tunnelUrl = null;
1690
+ if (options.tunnel) {
1691
+ if (isTermuxRuntime()) {
1692
+ console.log("\n[cloudflared] Tunnel is disabled on Termux.");
1693
+ } else {
1694
+ try {
1695
+ const cloudflaredCommand = ensureCloudflaredInstalled();
1696
+ const tunnel = await startCloudflaredTunnel(cloudflaredCommand, port);
1697
+ tunnelChild = tunnel.process;
1698
+ tunnelUrl = tunnel.url;
1699
+ } catch (error) {
1700
+ const message = error instanceof Error ? error.message : String(error);
1701
+ console.warn(`
1702
+ [cloudflared] Tunnel not started: ${message}`);
1703
+ }
1704
+ }
1705
+ }
1552
1706
  const lines = [
1553
1707
  "",
1554
1708
  "Codex Web Local is running!",
@@ -1563,12 +1717,25 @@ async function startServer(options) {
1563
1717
  if (password) {
1564
1718
  lines.push(` Password: ${password}`);
1565
1719
  }
1720
+ if (tunnelUrl) {
1721
+ lines.push(` Tunnel: ${tunnelUrl}`);
1722
+ lines.push("");
1723
+ lines.push(" Tunnel QR code:");
1724
+ lines.push(` URL: ${tunnelUrl}`);
1725
+ }
1566
1726
  printTermuxKeepAlive(lines);
1567
1727
  lines.push("");
1568
1728
  console.log(lines.join("\n"));
1729
+ if (tunnelUrl) {
1730
+ qrcode.generate(tunnelUrl, { small: true });
1731
+ console.log("");
1732
+ }
1569
1733
  openBrowser(`http://localhost:${String(port)}`);
1570
1734
  function shutdown() {
1571
1735
  console.log("\nShutting down...");
1736
+ if (tunnelChild && !tunnelChild.killed) {
1737
+ tunnelChild.kill("SIGTERM");
1738
+ }
1572
1739
  server.close(() => {
1573
1740
  dispose();
1574
1741
  process.exit(0);
@@ -1586,7 +1753,7 @@ async function runLogin() {
1586
1753
  console.log("\nStarting `codex login`...\n");
1587
1754
  runOrFail(codexCommand, ["login"], "Codex login");
1588
1755
  }
1589
- program.option("-p, --port <port>", "port to listen on", "5999").option("--password <pass>", "set a specific password").option("--no-password", "disable password protection").action(async (opts) => {
1756
+ program.option("-p, --port <port>", "port to listen on", "5999").option("--password <pass>", "set a specific password").option("--no-password", "disable password protection").option("--tunnel", "start cloudflared tunnel", true).option("--no-tunnel", "disable cloudflared tunnel startup").action(async (opts) => {
1590
1757
  await startServer(opts);
1591
1758
  });
1592
1759
  program.command("login").description("Install/check Codex CLI and run `codex login`").action(runLogin);