codexapp 0.1.8 → 0.1.10

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
@@ -10,6 +10,7 @@ import { spawn as spawn2, spawnSync } from "child_process";
10
10
  import { fileURLToPath as fileURLToPath2 } from "url";
11
11
  import { dirname as dirname2 } from "path";
12
12
  import { Command } from "commander";
13
+ import qrcode from "qrcode-terminal";
13
14
 
14
15
  // src/server/httpServer.ts
15
16
  import { fileURLToPath } from "url";
@@ -1163,13 +1164,9 @@ function createCodexBridgeMiddleware() {
1163
1164
  res.setHeader("Cache-Control", "no-cache, no-transform");
1164
1165
  res.setHeader("Connection", "keep-alive");
1165
1166
  res.setHeader("X-Accel-Buffering", "no");
1166
- const unsubscribe = appServer.onNotification((notification) => {
1167
+ const unsubscribe = middleware.subscribeNotifications((notification) => {
1167
1168
  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)}
1169
+ res.write(`data: ${JSON.stringify(notification)}
1173
1170
 
1174
1171
  `);
1175
1172
  });
@@ -1200,20 +1197,20 @@ data: ${JSON.stringify({ ok: true })}
1200
1197
  middleware.dispose = () => {
1201
1198
  appServer.dispose();
1202
1199
  };
1200
+ middleware.subscribeNotifications = (listener) => {
1201
+ return appServer.onNotification((notification) => {
1202
+ listener({
1203
+ ...notification,
1204
+ atIso: (/* @__PURE__ */ new Date()).toISOString()
1205
+ });
1206
+ });
1207
+ };
1203
1208
  return middleware;
1204
1209
  }
1205
1210
 
1206
1211
  // src/server/authMiddleware.ts
1207
1212
  import { randomBytes, timingSafeEqual } from "crypto";
1208
1213
  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
1214
  function constantTimeCompare(a, b) {
1218
1215
  const bufA = Buffer.from(a);
1219
1216
  const bufB = Buffer.from(b);
@@ -1232,6 +1229,22 @@ function parseCookies(header) {
1232
1229
  }
1233
1230
  return cookies;
1234
1231
  }
1232
+ function isLocalhostRemote(remote) {
1233
+ return remote === "127.0.0.1" || remote === "::1" || remote === "::ffff:127.0.0.1";
1234
+ }
1235
+ function isLocalhostHost(host) {
1236
+ const normalized = host.toLowerCase();
1237
+ return normalized.startsWith("localhost:") || normalized === "localhost" || normalized.startsWith("127.0.0.1:");
1238
+ }
1239
+ function isAuthorizedByRequestLike(remoteAddress, hostHeader, cookieHeader, validTokens) {
1240
+ const remote = remoteAddress ?? "";
1241
+ if (isLocalhostRemote(remote) || isLocalhostHost(hostHeader ?? "")) {
1242
+ return true;
1243
+ }
1244
+ const cookies = parseCookies(cookieHeader);
1245
+ const token = cookies[TOKEN_COOKIE];
1246
+ return Boolean(token && validTokens.has(token));
1247
+ }
1235
1248
  var LOGIN_PAGE_HTML = `<!DOCTYPE html>
1236
1249
  <html lang="en">
1237
1250
  <head>
@@ -1273,10 +1286,10 @@ form.addEventListener('submit',async e=>{
1273
1286
  </script>
1274
1287
  </body>
1275
1288
  </html>`;
1276
- function createAuthMiddleware(password) {
1289
+ function createAuthSession(password) {
1277
1290
  const validTokens = /* @__PURE__ */ new Set();
1278
- return (req, res, next) => {
1279
- if (isLocalhostRequest(req)) {
1291
+ const middleware = (req, res, next) => {
1292
+ if (isAuthorizedByRequestLike(req.socket.remoteAddress, req.headers.host, req.headers.cookie, validTokens)) {
1280
1293
  next();
1281
1294
  return;
1282
1295
  }
@@ -1294,9 +1307,9 @@ function createAuthMiddleware(password) {
1294
1307
  res.status(401).json({ error: "Invalid password" });
1295
1308
  return;
1296
1309
  }
1297
- const token2 = randomBytes(32).toString("hex");
1298
- validTokens.add(token2);
1299
- res.setHeader("Set-Cookie", `${TOKEN_COOKIE}=${token2}; Path=/; HttpOnly; SameSite=Strict`);
1310
+ const token = randomBytes(32).toString("hex");
1311
+ validTokens.add(token);
1312
+ res.setHeader("Set-Cookie", `${TOKEN_COOKIE}=${token}; Path=/; HttpOnly; SameSite=Strict`);
1300
1313
  res.json({ ok: true });
1301
1314
  } catch {
1302
1315
  res.status(400).json({ error: "Invalid request body" });
@@ -1304,18 +1317,17 @@ function createAuthMiddleware(password) {
1304
1317
  });
1305
1318
  return;
1306
1319
  }
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
1320
  res.setHeader("Content-Type", "text/html; charset=utf-8");
1314
1321
  res.status(200).send(LOGIN_PAGE_HTML);
1315
1322
  };
1323
+ return {
1324
+ middleware,
1325
+ isRequestAuthorized: (req) => isAuthorizedByRequestLike(req.socket.remoteAddress, req.headers.host, req.headers.cookie, validTokens)
1326
+ };
1316
1327
  }
1317
1328
 
1318
1329
  // src/server/httpServer.ts
1330
+ import { WebSocketServer } from "ws";
1319
1331
  var __dirname = dirname(fileURLToPath(import.meta.url));
1320
1332
  var distDir = join2(__dirname, "..", "dist");
1321
1333
  var IMAGE_CONTENT_TYPES = {
@@ -1343,8 +1355,9 @@ function normalizeLocalImagePath(rawPath) {
1343
1355
  function createServer(options = {}) {
1344
1356
  const app = express();
1345
1357
  const bridge = createCodexBridgeMiddleware();
1346
- if (options.password) {
1347
- app.use(createAuthMiddleware(options.password));
1358
+ const authSession = options.password ? createAuthSession(options.password) : null;
1359
+ if (authSession) {
1360
+ app.use(authSession.middleware);
1348
1361
  }
1349
1362
  app.use(bridge);
1350
1363
  app.get("/codex-local-image", (req, res) => {
@@ -1372,7 +1385,33 @@ function createServer(options = {}) {
1372
1385
  });
1373
1386
  return {
1374
1387
  app,
1375
- dispose: () => bridge.dispose()
1388
+ dispose: () => bridge.dispose(),
1389
+ attachWebSocket: (server) => {
1390
+ const wss = new WebSocketServer({ noServer: true });
1391
+ server.on("upgrade", (req, socket, head) => {
1392
+ const url = new URL(req.url ?? "", "http://localhost");
1393
+ if (url.pathname !== "/codex-api/ws") {
1394
+ return;
1395
+ }
1396
+ if (authSession && !authSession.isRequestAuthorized(req)) {
1397
+ socket.write("HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n");
1398
+ socket.destroy();
1399
+ return;
1400
+ }
1401
+ wss.handleUpgrade(req, socket, head, (ws) => {
1402
+ wss.emit("connection", ws, req);
1403
+ });
1404
+ });
1405
+ wss.on("connection", (ws) => {
1406
+ ws.send(JSON.stringify({ method: "ready", params: { ok: true }, atIso: (/* @__PURE__ */ new Date()).toISOString() }));
1407
+ const unsubscribe = bridge.subscribeNotifications((notification) => {
1408
+ if (ws.readyState !== 1) return;
1409
+ ws.send(JSON.stringify(notification));
1410
+ });
1411
+ ws.on("close", unsubscribe);
1412
+ ws.on("error", unsubscribe);
1413
+ });
1414
+ }
1376
1415
  };
1377
1416
  }
1378
1417
 
@@ -1515,6 +1554,49 @@ function openBrowser(url) {
1515
1554
  });
1516
1555
  child.unref();
1517
1556
  }
1557
+ function parseCloudflaredUrl(chunk) {
1558
+ const urlMatch = chunk.match(/https:\/\/[a-zA-Z0-9-]+\.trycloudflare\.com/g);
1559
+ if (!urlMatch || urlMatch.length === 0) {
1560
+ return null;
1561
+ }
1562
+ return urlMatch[urlMatch.length - 1] ?? null;
1563
+ }
1564
+ async function startCloudflaredTunnel(localPort) {
1565
+ return new Promise((resolve2, reject) => {
1566
+ const child = spawn2("cloudflared", ["tunnel", "--url", `http://localhost:${String(localPort)}`], {
1567
+ stdio: ["ignore", "pipe", "pipe"]
1568
+ });
1569
+ const timeout = setTimeout(() => {
1570
+ child.kill("SIGTERM");
1571
+ reject(new Error("Timed out waiting for cloudflared tunnel URL"));
1572
+ }, 2e4);
1573
+ const handleData = (value) => {
1574
+ const text = String(value);
1575
+ const parsedUrl = parseCloudflaredUrl(text);
1576
+ if (!parsedUrl) {
1577
+ return;
1578
+ }
1579
+ clearTimeout(timeout);
1580
+ child.stdout?.off("data", handleData);
1581
+ child.stderr?.off("data", handleData);
1582
+ resolve2({ process: child, url: parsedUrl });
1583
+ };
1584
+ const onError = (error) => {
1585
+ clearTimeout(timeout);
1586
+ reject(new Error(`Failed to start cloudflared: ${error.message}`));
1587
+ };
1588
+ child.once("error", onError);
1589
+ child.stdout?.on("data", handleData);
1590
+ child.stderr?.on("data", handleData);
1591
+ child.once("exit", (code) => {
1592
+ if (code === 0) {
1593
+ return;
1594
+ }
1595
+ clearTimeout(timeout);
1596
+ reject(new Error(`cloudflared exited before providing a URL (code ${String(code)})`));
1597
+ });
1598
+ });
1599
+ }
1518
1600
  function listenWithFallback(server, startPort) {
1519
1601
  return new Promise((resolve2, reject) => {
1520
1602
  const attempt = (port) => {
@@ -1546,13 +1628,28 @@ async function startServer(options) {
1546
1628
  }
1547
1629
  const requestedPort = parseInt(options.port, 10);
1548
1630
  const password = resolvePassword(options.password);
1549
- const { app, dispose } = createServer({ password });
1631
+ const { app, dispose, attachWebSocket } = createServer({ password });
1550
1632
  const server = createServer2(app);
1633
+ attachWebSocket(server);
1551
1634
  const port = await listenWithFallback(server, requestedPort);
1635
+ let tunnelChild = null;
1636
+ let tunnelUrl = null;
1637
+ if (options.tunnel) {
1638
+ try {
1639
+ const tunnel = await startCloudflaredTunnel(port);
1640
+ tunnelChild = tunnel.process;
1641
+ tunnelUrl = tunnel.url;
1642
+ } catch (error) {
1643
+ const message = error instanceof Error ? error.message : String(error);
1644
+ console.warn(`
1645
+ [cloudflared] Tunnel not started: ${message}`);
1646
+ }
1647
+ }
1552
1648
  const lines = [
1553
1649
  "",
1554
1650
  "Codex Web Local is running!",
1555
1651
  ` Version: ${version}`,
1652
+ " GitHub: https://github.com/friuns2/codexui",
1556
1653
  "",
1557
1654
  ` Local: http://localhost:${String(port)}`
1558
1655
  ];
@@ -1562,12 +1659,25 @@ async function startServer(options) {
1562
1659
  if (password) {
1563
1660
  lines.push(` Password: ${password}`);
1564
1661
  }
1662
+ if (tunnelUrl) {
1663
+ lines.push(` Tunnel: ${tunnelUrl}`);
1664
+ lines.push("");
1665
+ lines.push(" Tunnel QR code:");
1666
+ lines.push(` URL: ${tunnelUrl}`);
1667
+ }
1565
1668
  printTermuxKeepAlive(lines);
1566
1669
  lines.push("");
1567
1670
  console.log(lines.join("\n"));
1671
+ if (tunnelUrl) {
1672
+ qrcode.generate(tunnelUrl, { small: true });
1673
+ console.log("");
1674
+ }
1568
1675
  openBrowser(`http://localhost:${String(port)}`);
1569
1676
  function shutdown() {
1570
1677
  console.log("\nShutting down...");
1678
+ if (tunnelChild && !tunnelChild.killed) {
1679
+ tunnelChild.kill("SIGTERM");
1680
+ }
1571
1681
  server.close(() => {
1572
1682
  dispose();
1573
1683
  process.exit(0);
@@ -1585,7 +1695,7 @@ async function runLogin() {
1585
1695
  console.log("\nStarting `codex login`...\n");
1586
1696
  runOrFail(codexCommand, ["login"], "Codex login");
1587
1697
  }
1588
- 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) => {
1698
+ 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) => {
1589
1699
  await startServer(opts);
1590
1700
  });
1591
1701
  program.command("login").description("Install/check Codex CLI and run `codex login`").action(runLogin);