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/README.md +9 -0
- package/dist/assets/index-CKknL24Z.css +1 -0
- package/dist/assets/{index-DrAmX48U.js → index-DMpyfM80.js} +18 -18
- package/dist/index.html +2 -2
- package/dist-cli/index.js +141 -31
- package/dist-cli/index.js.map +1 -1
- package/package.json +6 -2
- package/dist/assets/index-BIm_B5t3.css +0 -1
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-
|
|
8
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
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 =
|
|
1167
|
+
const unsubscribe = middleware.subscribeNotifications((notification) => {
|
|
1167
1168
|
if (res.writableEnded || res.destroyed) return;
|
|
1168
|
-
|
|
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
|
|
1289
|
+
function createAuthSession(password) {
|
|
1277
1290
|
const validTokens = /* @__PURE__ */ new Set();
|
|
1278
|
-
|
|
1279
|
-
if (
|
|
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
|
|
1298
|
-
validTokens.add(
|
|
1299
|
-
res.setHeader("Set-Cookie", `${TOKEN_COOKIE}=${
|
|
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
|
-
|
|
1347
|
-
|
|
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);
|