codexui-android 0.1.96 → 0.1.98
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/assets/DirectoryHub-CAcNjRYF.css +1 -0
- package/dist/assets/DirectoryHub-D6h4lJqw.js +2 -0
- package/dist/assets/{ReviewPane-DuPX5OZA.css → ReviewPane-BJuXPOZb.css} +1 -1
- package/dist/assets/ReviewPane-BwO2MHvI.js +1 -0
- package/dist/assets/ThreadConversation-CsHUh1AP.js +39 -0
- package/dist/assets/{ThreadConversation-3WaRKicR.css → ThreadConversation-J3tJ8-9X.css} +1 -1
- package/dist/assets/ThreadTerminalPanel-BTHfTRnz.js +38 -0
- package/dist/assets/{ThreadTerminalPanel-CGTJQ1BI.css → ThreadTerminalPanel-pVx2Zv0U.css} +1 -1
- package/dist/assets/index-23FodFfK.js +64 -0
- package/dist/assets/index-Wz7G59hk.css +1 -0
- package/dist/index.html +2 -2
- package/dist-cli/index.js +285 -52
- package/dist-cli/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/assets/ReviewPane-CUrlR8zS.js +0 -1
- package/dist/assets/SkillsHub-BxltnLjE.js +0 -2
- package/dist/assets/SkillsHub-CTnWejwn.css +0 -1
- package/dist/assets/ThreadConversation-BNvqi1ws.js +0 -39
- package/dist/assets/ThreadTerminalPanel-DjP4TSjB.js +0 -38
- package/dist/assets/index-Bqk3qCGk.js +0 -64
- package/dist/assets/index-CfRa3ma1.css +0 -1
package/dist/index.html
CHANGED
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
<link rel="icon" type="image/png" sizes="192x192" href="/icons/pwa-192x192.png" />
|
|
13
13
|
<link rel="apple-touch-icon" sizes="180x180" href="/icons/apple-touch-icon.png" />
|
|
14
14
|
<title>Codex Web</title>
|
|
15
|
-
<script type="module" crossorigin src="/assets/index-
|
|
16
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
15
|
+
<script type="module" crossorigin src="/assets/index-23FodFfK.js"></script>
|
|
16
|
+
<link rel="stylesheet" crossorigin href="/assets/index-Wz7G59hk.css">
|
|
17
17
|
</head>
|
|
18
18
|
<body class="bg-slate-950">
|
|
19
19
|
<div id="app"></div>
|
package/dist-cli/index.js
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli/index.ts
|
|
4
4
|
import { createServer as createServer2 } from "http";
|
|
5
|
-
import { chmodSync as chmodSync2, createWriteStream, existsSync as
|
|
5
|
+
import { chmodSync as chmodSync2, createWriteStream, existsSync as existsSync6, mkdirSync as mkdirSync2 } from "fs";
|
|
6
6
|
import { readFile as readFile5, stat as stat7, writeFile as writeFile6 } from "fs/promises";
|
|
7
|
-
import { homedir as
|
|
8
|
-
import { isAbsolute as isAbsolute4, join as
|
|
7
|
+
import { homedir as homedir7, networkInterfaces } from "os";
|
|
8
|
+
import { isAbsolute as isAbsolute4, join as join10, resolve as resolve3 } from "path";
|
|
9
9
|
import { spawn as spawn5 } from "child_process";
|
|
10
10
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
11
11
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
12
|
-
import { dirname as
|
|
12
|
+
import { dirname as dirname6 } from "path";
|
|
13
13
|
import { get as httpsGet } from "https";
|
|
14
14
|
import { Command } from "commander";
|
|
15
15
|
import qrcode from "qrcode-terminal";
|
|
@@ -216,8 +216,8 @@ function parseApprovalPolicy(value) {
|
|
|
216
216
|
|
|
217
217
|
// src/server/httpServer.ts
|
|
218
218
|
import { fileURLToPath } from "url";
|
|
219
|
-
import { dirname as
|
|
220
|
-
import { existsSync as
|
|
219
|
+
import { dirname as dirname5, extname as extname3, isAbsolute as isAbsolute3, join as join9 } from "path";
|
|
220
|
+
import { existsSync as existsSync5 } from "fs";
|
|
221
221
|
import { writeFile as writeFile5, stat as stat6 } from "fs/promises";
|
|
222
222
|
import express from "express";
|
|
223
223
|
|
|
@@ -4607,12 +4607,17 @@ function loadOptionalTerminalSpawn(spawn6) {
|
|
|
4607
4607
|
return { spawn: loadTerminalSpawn(), reason: null };
|
|
4608
4608
|
} catch (error) {
|
|
4609
4609
|
const message = error instanceof Error ? error.message : String(error);
|
|
4610
|
+
const suffix = message.includes("Cannot find module") ? "Native PTY support is not installed." : sanitizeUnavailableReason(message);
|
|
4610
4611
|
return {
|
|
4611
4612
|
spawn: null,
|
|
4612
|
-
reason: `Integrated terminal is unavailable on this host
|
|
4613
|
+
reason: `Integrated terminal is unavailable on this host. ${suffix}`
|
|
4613
4614
|
};
|
|
4614
4615
|
}
|
|
4615
4616
|
}
|
|
4617
|
+
function sanitizeUnavailableReason(message) {
|
|
4618
|
+
const firstLine = message.split("\n")[0]?.trim() || "";
|
|
4619
|
+
return firstLine ? firstLine : "Native PTY support could not be loaded.";
|
|
4620
|
+
}
|
|
4616
4621
|
function normalizeDimension(value, fallback) {
|
|
4617
4622
|
const parsed = typeof value === "number" ? value : Number(value);
|
|
4618
4623
|
if (!Number.isFinite(parsed)) return fallback;
|
|
@@ -4825,13 +4830,42 @@ function asRecord5(value) {
|
|
|
4825
4830
|
function isInlineDataUrl(value) {
|
|
4826
4831
|
return /^data:/iu.test(value.trim());
|
|
4827
4832
|
}
|
|
4833
|
+
function inferImageMimeTypeFromBytes(bytes) {
|
|
4834
|
+
if (bytes.length >= 8 && bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71 && bytes[4] === 13 && bytes[5] === 10 && bytes[6] === 26 && bytes[7] === 10) {
|
|
4835
|
+
return "image/png";
|
|
4836
|
+
}
|
|
4837
|
+
if (bytes.length >= 3 && bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) {
|
|
4838
|
+
return "image/jpeg";
|
|
4839
|
+
}
|
|
4840
|
+
if (bytes.length >= 12 && bytes[0] === 82 && bytes[1] === 73 && bytes[2] === 70 && bytes[3] === 70 && bytes[8] === 87 && bytes[9] === 69 && bytes[10] === 66 && bytes[11] === 80) {
|
|
4841
|
+
return "image/webp";
|
|
4842
|
+
}
|
|
4843
|
+
if (bytes.length >= 6 && bytes[0] === 71 && bytes[1] === 73 && bytes[2] === 70 && bytes[3] === 56 && (bytes[4] === 55 || bytes[4] === 57) && bytes[5] === 97) {
|
|
4844
|
+
return "image/gif";
|
|
4845
|
+
}
|
|
4846
|
+
return null;
|
|
4847
|
+
}
|
|
4848
|
+
function inferImageMimeTypeFromBase64(value) {
|
|
4849
|
+
const compact = value.trim().replace(/\s+/gu, "");
|
|
4850
|
+
if (compact.length < 32 || !/^[A-Za-z0-9+/]+={0,2}$/u.test(compact)) return null;
|
|
4851
|
+
try {
|
|
4852
|
+
return inferImageMimeTypeFromBytes(Buffer.from(compact.slice(0, 64), "base64"));
|
|
4853
|
+
} catch {
|
|
4854
|
+
return null;
|
|
4855
|
+
}
|
|
4856
|
+
}
|
|
4828
4857
|
function normalizeBase64ImageDataUrl(value, mimeType) {
|
|
4829
4858
|
const trimmed = value.trim();
|
|
4830
4859
|
if (!trimmed) return null;
|
|
4831
|
-
if (isInlineDataUrl(trimmed))
|
|
4860
|
+
if (isInlineDataUrl(trimmed)) {
|
|
4861
|
+
return /^data:image\//iu.test(trimmed) ? trimmed : null;
|
|
4862
|
+
}
|
|
4832
4863
|
const compact = trimmed.replace(/\s+/gu, "");
|
|
4833
|
-
|
|
4834
|
-
return
|
|
4864
|
+
const inferredMimeType = inferImageMimeTypeFromBase64(compact);
|
|
4865
|
+
if (!inferredMimeType) return null;
|
|
4866
|
+
const normalizedMimeType = mimeType.trim().toLowerCase();
|
|
4867
|
+
const finalMimeType = normalizedMimeType.startsWith("image/") && normalizedMimeType !== "image/*" ? normalizedMimeType : inferredMimeType;
|
|
4868
|
+
return `data:${finalMimeType};base64,${compact}`;
|
|
4835
4869
|
}
|
|
4836
4870
|
function extensionFromMimeType(mimeType) {
|
|
4837
4871
|
const normalized = mimeType.trim().toLowerCase();
|
|
@@ -4883,6 +4917,30 @@ async function persistInlineDataUrlToLocalFile(dataUrl, baseName) {
|
|
|
4883
4917
|
function toLocalImageProxyUrl(path) {
|
|
4884
4918
|
return `/codex-local-image?path=${encodeURIComponent(path)}`;
|
|
4885
4919
|
}
|
|
4920
|
+
var INLINE_IMAGE_FIELD_NAMES = /* @__PURE__ */ new Set([
|
|
4921
|
+
"b64_json",
|
|
4922
|
+
"image",
|
|
4923
|
+
"image_url",
|
|
4924
|
+
"images",
|
|
4925
|
+
"result",
|
|
4926
|
+
"url"
|
|
4927
|
+
]);
|
|
4928
|
+
function isPotentialInlineImageField(fieldName) {
|
|
4929
|
+
return typeof fieldName === "string" && INLINE_IMAGE_FIELD_NAMES.has(fieldName);
|
|
4930
|
+
}
|
|
4931
|
+
async function sanitizeInlineImageString(value, context) {
|
|
4932
|
+
if (!isPotentialInlineImageField(context.fieldName)) {
|
|
4933
|
+
return { value, changed: false };
|
|
4934
|
+
}
|
|
4935
|
+
const dataUrl = normalizeBase64ImageDataUrl(value, "image/*");
|
|
4936
|
+
if (!dataUrl) return { value, changed: false };
|
|
4937
|
+
const localUrl = await persistInlineDataUrlToLocalFile(
|
|
4938
|
+
dataUrl,
|
|
4939
|
+
`inline-image-${context.turnId}-${context.itemId}-${context.fieldName}-${String(context.blockIndex)}`
|
|
4940
|
+
);
|
|
4941
|
+
if (!localUrl) return { value, changed: false };
|
|
4942
|
+
return { value: toLocalImageProxyUrl(localUrl), changed: true };
|
|
4943
|
+
}
|
|
4886
4944
|
async function sanitizeInlineUserContentBlock(block, context) {
|
|
4887
4945
|
const record = asRecord5(block);
|
|
4888
4946
|
if (!record) return block;
|
|
@@ -4891,10 +4949,16 @@ async function sanitizeInlineUserContentBlock(block, context) {
|
|
|
4891
4949
|
if (imageUrl && isInlineDataUrl(imageUrl)) {
|
|
4892
4950
|
const localUrl = await persistInlineDataUrlToLocalFile(imageUrl, `inline-image-${context.turnId}-${context.itemId}-${String(context.blockIndex)}`);
|
|
4893
4951
|
if (localUrl) {
|
|
4952
|
+
const nextRecord = { ...record };
|
|
4953
|
+
if (typeof record.url === "string") {
|
|
4954
|
+
nextRecord.url = toLocalImageProxyUrl(localUrl);
|
|
4955
|
+
}
|
|
4956
|
+
if (typeof record.image_url === "string") {
|
|
4957
|
+
nextRecord.image_url = toLocalImageProxyUrl(localUrl);
|
|
4958
|
+
}
|
|
4894
4959
|
return {
|
|
4895
|
-
...
|
|
4896
|
-
type: "image"
|
|
4897
|
-
url: toLocalImageProxyUrl(localUrl)
|
|
4960
|
+
...nextRecord,
|
|
4961
|
+
type: "image"
|
|
4898
4962
|
};
|
|
4899
4963
|
}
|
|
4900
4964
|
const target = toAttachmentLinkTarget(record, `inline-image/${context.turnId}/${context.itemId}/${String(context.blockIndex)}`);
|
|
@@ -4942,6 +5006,9 @@ async function sanitizeInlinePayloadDeep(value, context) {
|
|
|
4942
5006
|
if (maybeBlock !== value) {
|
|
4943
5007
|
return { value: maybeBlock, changed: true };
|
|
4944
5008
|
}
|
|
5009
|
+
if (typeof value === "string") {
|
|
5010
|
+
return sanitizeInlineImageString(value, context);
|
|
5011
|
+
}
|
|
4945
5012
|
if (Array.isArray(value)) {
|
|
4946
5013
|
let changed2 = false;
|
|
4947
5014
|
const nextArray = [];
|
|
@@ -4949,7 +5016,8 @@ async function sanitizeInlinePayloadDeep(value, context) {
|
|
|
4949
5016
|
const nested = await sanitizeInlinePayloadDeep(value[index], {
|
|
4950
5017
|
turnId: context.turnId,
|
|
4951
5018
|
itemId: context.itemId,
|
|
4952
|
-
blockIndex: index
|
|
5019
|
+
blockIndex: index,
|
|
5020
|
+
fieldName: context.fieldName
|
|
4953
5021
|
});
|
|
4954
5022
|
if (nested.changed) changed2 = true;
|
|
4955
5023
|
nextArray.push(nested.value);
|
|
@@ -4964,7 +5032,8 @@ async function sanitizeInlinePayloadDeep(value, context) {
|
|
|
4964
5032
|
const nested = await sanitizeInlinePayloadDeep(nestedValue, {
|
|
4965
5033
|
turnId: context.turnId,
|
|
4966
5034
|
itemId: context.itemId,
|
|
4967
|
-
blockIndex: context.blockIndex
|
|
5035
|
+
blockIndex: context.blockIndex,
|
|
5036
|
+
fieldName: key
|
|
4968
5037
|
});
|
|
4969
5038
|
if (nested.changed) changed = true;
|
|
4970
5039
|
nextRecord[key] = nested.value;
|
|
@@ -6646,6 +6715,46 @@ async function proxyTranscribe(body, contentType, authToken, accountId) {
|
|
|
6646
6715
|
}
|
|
6647
6716
|
return result;
|
|
6648
6717
|
}
|
|
6718
|
+
function parseConnectorLogoUrl(rawUrl) {
|
|
6719
|
+
const trimmed = rawUrl.trim();
|
|
6720
|
+
if (!trimmed.startsWith("connectors://")) return null;
|
|
6721
|
+
const rest = trimmed.slice("connectors://".length);
|
|
6722
|
+
const connectorId = (rest.split(/[/?#]/u)[0] ?? "").trim();
|
|
6723
|
+
if (!connectorId) return null;
|
|
6724
|
+
const query = rest.includes("?") ? rest.slice(rest.indexOf("?") + 1).split("#")[0] ?? "" : "";
|
|
6725
|
+
const theme = new URLSearchParams(query).get("theme")?.toLowerCase() === "dark" ? "dark" : "light";
|
|
6726
|
+
return { connectorId, theme };
|
|
6727
|
+
}
|
|
6728
|
+
async function fetchConnectorLogo(rawUrl) {
|
|
6729
|
+
const parsed = parseConnectorLogoUrl(rawUrl);
|
|
6730
|
+
if (!parsed) throw new Error("Unsupported connector logo URL");
|
|
6731
|
+
const auth = await readCodexAuth();
|
|
6732
|
+
if (!auth) throw new Error("No auth token available for connector logo");
|
|
6733
|
+
const endpoint = `https://chatgpt.com/backend-api/aip/connectors/${encodeURIComponent(parsed.connectorId)}/logo?theme=${parsed.theme}`;
|
|
6734
|
+
const response = await fetch(endpoint, {
|
|
6735
|
+
headers: {
|
|
6736
|
+
Authorization: `Bearer ${auth.accessToken}`,
|
|
6737
|
+
originator: "Codex Desktop",
|
|
6738
|
+
"User-Agent": `Codex Desktop/0.1.0 (${process.platform}; ${process.arch})`,
|
|
6739
|
+
...auth.accountId ? { "ChatGPT-Account-Id": auth.accountId } : {}
|
|
6740
|
+
},
|
|
6741
|
+
signal: AbortSignal.timeout(1e4)
|
|
6742
|
+
});
|
|
6743
|
+
if (!response.ok) throw new Error(`Connector logo fetch failed (${response.status})`);
|
|
6744
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
6745
|
+
if (contentType.includes("application/json")) {
|
|
6746
|
+
const payload = asRecord5(await response.json());
|
|
6747
|
+
const body = asRecord5(payload?.body);
|
|
6748
|
+
const base64 = readNonEmptyString(body?.base64);
|
|
6749
|
+
const nestedContentType = readNonEmptyString(body?.contentType) ?? readNonEmptyString(body?.content_type);
|
|
6750
|
+
if (!base64 || !nestedContentType) throw new Error("Connector logo response was missing image data");
|
|
6751
|
+
return { contentType: nestedContentType, body: Buffer.from(base64, "base64") };
|
|
6752
|
+
}
|
|
6753
|
+
return {
|
|
6754
|
+
contentType: contentType || "image/png",
|
|
6755
|
+
body: Buffer.from(await response.arrayBuffer())
|
|
6756
|
+
};
|
|
6757
|
+
}
|
|
6649
6758
|
var STREAM_EVENT_BUFFER_LIMIT = 400;
|
|
6650
6759
|
var MERGEABLE_ITEM_TYPES = /* @__PURE__ */ new Set([
|
|
6651
6760
|
"commandExecution",
|
|
@@ -7539,6 +7648,11 @@ function createCodexBridgeMiddleware() {
|
|
|
7539
7648
|
return;
|
|
7540
7649
|
}
|
|
7541
7650
|
if (req.method === "POST" && url.pathname === "/codex-api/thread-terminal/attach") {
|
|
7651
|
+
const availability = terminalManager.getAvailability();
|
|
7652
|
+
if (!availability.available) {
|
|
7653
|
+
setJson4(res, 503, { error: availability.reason || "Integrated terminal is unavailable on this host" });
|
|
7654
|
+
return;
|
|
7655
|
+
}
|
|
7542
7656
|
const body = asRecord5(await readJsonBody(req));
|
|
7543
7657
|
const threadId = readNonEmptyString(body?.threadId);
|
|
7544
7658
|
const cwd = readNonEmptyString(body?.cwd);
|
|
@@ -7558,6 +7672,11 @@ function createCodexBridgeMiddleware() {
|
|
|
7558
7672
|
return;
|
|
7559
7673
|
}
|
|
7560
7674
|
if (req.method === "POST" && url.pathname === "/codex-api/thread-terminal/input") {
|
|
7675
|
+
const availability = terminalManager.getAvailability();
|
|
7676
|
+
if (!availability.available) {
|
|
7677
|
+
setJson4(res, 503, { error: availability.reason || "Integrated terminal is unavailable on this host" });
|
|
7678
|
+
return;
|
|
7679
|
+
}
|
|
7561
7680
|
const body = asRecord5(await readJsonBody(req));
|
|
7562
7681
|
const sessionId = readNonEmptyString(body?.sessionId);
|
|
7563
7682
|
const data = typeof body?.data === "string" ? body.data : "";
|
|
@@ -7570,6 +7689,11 @@ function createCodexBridgeMiddleware() {
|
|
|
7570
7689
|
return;
|
|
7571
7690
|
}
|
|
7572
7691
|
if (req.method === "POST" && url.pathname === "/codex-api/thread-terminal/resize") {
|
|
7692
|
+
const availability = terminalManager.getAvailability();
|
|
7693
|
+
if (!availability.available) {
|
|
7694
|
+
setJson4(res, 503, { error: availability.reason || "Integrated terminal is unavailable on this host" });
|
|
7695
|
+
return;
|
|
7696
|
+
}
|
|
7573
7697
|
const body = asRecord5(await readJsonBody(req));
|
|
7574
7698
|
const sessionId = readNonEmptyString(body?.sessionId);
|
|
7575
7699
|
if (!sessionId) {
|
|
@@ -7581,6 +7705,11 @@ function createCodexBridgeMiddleware() {
|
|
|
7581
7705
|
return;
|
|
7582
7706
|
}
|
|
7583
7707
|
if (req.method === "POST" && url.pathname === "/codex-api/thread-terminal/close") {
|
|
7708
|
+
const availability = terminalManager.getAvailability();
|
|
7709
|
+
if (!availability.available) {
|
|
7710
|
+
setJson4(res, 503, { error: availability.reason || "Integrated terminal is unavailable on this host" });
|
|
7711
|
+
return;
|
|
7712
|
+
}
|
|
7584
7713
|
const body = asRecord5(await readJsonBody(req));
|
|
7585
7714
|
const sessionId = readNonEmptyString(body?.sessionId);
|
|
7586
7715
|
if (!sessionId) {
|
|
@@ -7819,6 +7948,23 @@ function createCodexBridgeMiddleware() {
|
|
|
7819
7948
|
res.end(upstream.body);
|
|
7820
7949
|
return;
|
|
7821
7950
|
}
|
|
7951
|
+
if (req.method === "GET" && url.pathname === "/codex-api/connector-logo") {
|
|
7952
|
+
const src = url.searchParams.get("src")?.trim() ?? "";
|
|
7953
|
+
if (!src) {
|
|
7954
|
+
setJson4(res, 400, { error: "Missing src" });
|
|
7955
|
+
return;
|
|
7956
|
+
}
|
|
7957
|
+
try {
|
|
7958
|
+
const logo = await fetchConnectorLogo(src);
|
|
7959
|
+
res.statusCode = 200;
|
|
7960
|
+
res.setHeader("Content-Type", logo.contentType);
|
|
7961
|
+
res.setHeader("Cache-Control", "private, max-age=3600");
|
|
7962
|
+
res.end(logo.body);
|
|
7963
|
+
} catch (error) {
|
|
7964
|
+
setJson4(res, 502, { error: getErrorMessage5(error, "Failed to fetch connector logo") });
|
|
7965
|
+
}
|
|
7966
|
+
return;
|
|
7967
|
+
}
|
|
7822
7968
|
if (req.method === "POST" && url.pathname === "/codex-api/server-requests/respond") {
|
|
7823
7969
|
const payload = await readJsonBody(req);
|
|
7824
7970
|
await appServer.respondToServerRequest(payload);
|
|
@@ -8492,7 +8638,13 @@ data: ${JSON.stringify({ ok: true })}
|
|
|
8492
8638
|
|
|
8493
8639
|
// src/server/authMiddleware.ts
|
|
8494
8640
|
import { randomBytes as randomBytes2, timingSafeEqual } from "crypto";
|
|
8641
|
+
import { existsSync as existsSync4, mkdirSync, readFileSync as readFileSync3, renameSync, writeFileSync as writeFileSync2 } from "fs";
|
|
8642
|
+
import { homedir as homedir6 } from "os";
|
|
8643
|
+
import { dirname as dirname3, join as join7 } from "path";
|
|
8495
8644
|
var TOKEN_COOKIE = "portal_session";
|
|
8645
|
+
var SESSION_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
8646
|
+
var SESSION_STORE_FILE = "webui-auth-sessions.json";
|
|
8647
|
+
var MAX_PERSISTED_TOKENS = 128;
|
|
8496
8648
|
function constantTimeCompare(a, b) {
|
|
8497
8649
|
const bufA = Buffer.from(a);
|
|
8498
8650
|
const bufB = Buffer.from(b);
|
|
@@ -8540,6 +8692,69 @@ function isTrustedTailscaleIPv6(remote) {
|
|
|
8540
8692
|
function isTrustedTailscaleRemote(remote) {
|
|
8541
8693
|
return isTrustedTailscaleIPv4(remote) || isTrustedTailscaleIPv6(remote);
|
|
8542
8694
|
}
|
|
8695
|
+
function getCodexHomeDir4() {
|
|
8696
|
+
const codexHome = process.env.CODEX_HOME?.trim();
|
|
8697
|
+
return codexHome && codexHome.length > 0 ? codexHome : join7(homedir6(), ".codex");
|
|
8698
|
+
}
|
|
8699
|
+
function getSessionStorePath() {
|
|
8700
|
+
return join7(getCodexHomeDir4(), SESSION_STORE_FILE);
|
|
8701
|
+
}
|
|
8702
|
+
function readPersistedSessions() {
|
|
8703
|
+
const sessionStorePath = getSessionStorePath();
|
|
8704
|
+
if (!existsSync4(sessionStorePath)) return /* @__PURE__ */ new Map();
|
|
8705
|
+
try {
|
|
8706
|
+
const raw = readFileSync3(sessionStorePath, "utf8");
|
|
8707
|
+
const parsed = JSON.parse(raw);
|
|
8708
|
+
const now = Date.now();
|
|
8709
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
8710
|
+
for (const entry of parsed.tokens ?? []) {
|
|
8711
|
+
const token = typeof entry?.value === "string" ? entry.value : "";
|
|
8712
|
+
const expiresAt = typeof entry?.expiresAt === "number" ? entry.expiresAt : 0;
|
|
8713
|
+
if (!token || !Number.isFinite(expiresAt) || expiresAt <= now) continue;
|
|
8714
|
+
sessions.set(token, expiresAt);
|
|
8715
|
+
}
|
|
8716
|
+
return sessions;
|
|
8717
|
+
} catch {
|
|
8718
|
+
return /* @__PURE__ */ new Map();
|
|
8719
|
+
}
|
|
8720
|
+
}
|
|
8721
|
+
function persistSessions(validTokens) {
|
|
8722
|
+
const sessionStorePath = getSessionStorePath();
|
|
8723
|
+
mkdirSync(dirname3(sessionStorePath), { recursive: true });
|
|
8724
|
+
const tokens = Array.from(validTokens.entries()).sort((left, right) => right[1] - left[1]).slice(0, MAX_PERSISTED_TOKENS).map(([value, expiresAt]) => ({ value, expiresAt }));
|
|
8725
|
+
const tmpPath = `${sessionStorePath}.tmp`;
|
|
8726
|
+
writeFileSync2(tmpPath, `${JSON.stringify({ tokens }, null, 2)}
|
|
8727
|
+
`, { encoding: "utf8", mode: 384 });
|
|
8728
|
+
renameSync(tmpPath, sessionStorePath);
|
|
8729
|
+
}
|
|
8730
|
+
function tryPersistSessions(validTokens) {
|
|
8731
|
+
try {
|
|
8732
|
+
persistSessions(validTokens);
|
|
8733
|
+
} catch (error) {
|
|
8734
|
+
console.warn("[auth] failed to persist login sessions:", error);
|
|
8735
|
+
}
|
|
8736
|
+
}
|
|
8737
|
+
function pruneExpiredSessions(validTokens) {
|
|
8738
|
+
const now = Date.now();
|
|
8739
|
+
let changed = false;
|
|
8740
|
+
for (const [token, expiresAt] of validTokens.entries()) {
|
|
8741
|
+
if (expiresAt > now) continue;
|
|
8742
|
+
validTokens.delete(token);
|
|
8743
|
+
changed = true;
|
|
8744
|
+
}
|
|
8745
|
+
return changed;
|
|
8746
|
+
}
|
|
8747
|
+
function buildSessionCookie(token, expiresAt) {
|
|
8748
|
+
const maxAgeSeconds = Math.max(0, Math.floor((expiresAt - Date.now()) / 1e3));
|
|
8749
|
+
return [
|
|
8750
|
+
`${TOKEN_COOKIE}=${token}`,
|
|
8751
|
+
"Path=/",
|
|
8752
|
+
"HttpOnly",
|
|
8753
|
+
"SameSite=Lax",
|
|
8754
|
+
`Max-Age=${String(maxAgeSeconds)}`,
|
|
8755
|
+
`Expires=${new Date(expiresAt).toUTCString()}`
|
|
8756
|
+
].join("; ");
|
|
8757
|
+
}
|
|
8543
8758
|
function isAuthorizedByRequestLike(remoteAddress, hostHeader, cookieHeader, validTokens) {
|
|
8544
8759
|
const remote = remoteAddress ?? "";
|
|
8545
8760
|
if (isLocalhostRemote(remote) && isLocalhostHost(hostHeader ?? "")) {
|
|
@@ -8550,7 +8765,9 @@ function isAuthorizedByRequestLike(remoteAddress, hostHeader, cookieHeader, vali
|
|
|
8550
8765
|
}
|
|
8551
8766
|
const cookies = parseCookies(cookieHeader);
|
|
8552
8767
|
const token = cookies[TOKEN_COOKIE];
|
|
8553
|
-
|
|
8768
|
+
if (!token) return false;
|
|
8769
|
+
const expiresAt = validTokens.get(token);
|
|
8770
|
+
return typeof expiresAt === "number" && expiresAt > Date.now();
|
|
8554
8771
|
}
|
|
8555
8772
|
var LOGIN_PAGE_HTML = `<!DOCTYPE html>
|
|
8556
8773
|
<html lang="en">
|
|
@@ -8594,8 +8811,14 @@ form.addEventListener('submit',async e=>{
|
|
|
8594
8811
|
</body>
|
|
8595
8812
|
</html>`;
|
|
8596
8813
|
function createAuthSession(password) {
|
|
8597
|
-
const validTokens =
|
|
8814
|
+
const validTokens = readPersistedSessions();
|
|
8815
|
+
if (pruneExpiredSessions(validTokens)) {
|
|
8816
|
+
tryPersistSessions(validTokens);
|
|
8817
|
+
}
|
|
8598
8818
|
const middleware = (req, res, next) => {
|
|
8819
|
+
if (pruneExpiredSessions(validTokens)) {
|
|
8820
|
+
tryPersistSessions(validTokens);
|
|
8821
|
+
}
|
|
8599
8822
|
if (isAuthorizedByRequestLike(req.socket.remoteAddress, req.headers.host, req.headers.cookie, validTokens)) {
|
|
8600
8823
|
next();
|
|
8601
8824
|
return;
|
|
@@ -8607,19 +8830,27 @@ function createAuthSession(password) {
|
|
|
8607
8830
|
body += chunk;
|
|
8608
8831
|
});
|
|
8609
8832
|
req.on("end", () => {
|
|
8833
|
+
let parsed;
|
|
8834
|
+
try {
|
|
8835
|
+
parsed = JSON.parse(body);
|
|
8836
|
+
} catch {
|
|
8837
|
+
res.status(400).json({ error: "Invalid request body" });
|
|
8838
|
+
return;
|
|
8839
|
+
}
|
|
8840
|
+
const provided = typeof parsed.password === "string" ? parsed.password : "";
|
|
8841
|
+
if (!constantTimeCompare(provided, password)) {
|
|
8842
|
+
res.status(401).json({ error: "Invalid password" });
|
|
8843
|
+
return;
|
|
8844
|
+
}
|
|
8610
8845
|
try {
|
|
8611
|
-
const parsed = JSON.parse(body);
|
|
8612
|
-
const provided = typeof parsed.password === "string" ? parsed.password : "";
|
|
8613
|
-
if (!constantTimeCompare(provided, password)) {
|
|
8614
|
-
res.status(401).json({ error: "Invalid password" });
|
|
8615
|
-
return;
|
|
8616
|
-
}
|
|
8617
8846
|
const token = randomBytes2(32).toString("hex");
|
|
8618
|
-
|
|
8619
|
-
|
|
8847
|
+
const expiresAt = Date.now() + SESSION_TTL_MS;
|
|
8848
|
+
validTokens.set(token, expiresAt);
|
|
8849
|
+
tryPersistSessions(validTokens);
|
|
8850
|
+
res.setHeader("Set-Cookie", buildSessionCookie(token, expiresAt));
|
|
8620
8851
|
res.json({ ok: true });
|
|
8621
8852
|
} catch {
|
|
8622
|
-
res.status(
|
|
8853
|
+
res.status(500).json({ error: "Failed to create login session" });
|
|
8623
8854
|
}
|
|
8624
8855
|
});
|
|
8625
8856
|
return;
|
|
@@ -8628,8 +8859,10 @@ function createAuthSession(password) {
|
|
|
8628
8859
|
const provided = req.path.slice("/password=".length);
|
|
8629
8860
|
if (constantTimeCompare(provided, password)) {
|
|
8630
8861
|
const token = randomBytes2(32).toString("hex");
|
|
8631
|
-
|
|
8632
|
-
|
|
8862
|
+
const expiresAt = Date.now() + SESSION_TTL_MS;
|
|
8863
|
+
validTokens.set(token, expiresAt);
|
|
8864
|
+
tryPersistSessions(validTokens);
|
|
8865
|
+
res.setHeader("Set-Cookie", buildSessionCookie(token, expiresAt));
|
|
8633
8866
|
res.redirect(302, "/");
|
|
8634
8867
|
return;
|
|
8635
8868
|
}
|
|
@@ -8644,7 +8877,7 @@ function createAuthSession(password) {
|
|
|
8644
8877
|
}
|
|
8645
8878
|
|
|
8646
8879
|
// src/server/localBrowseUi.ts
|
|
8647
|
-
import { dirname as
|
|
8880
|
+
import { dirname as dirname4, extname as extname2, join as join8 } from "path";
|
|
8648
8881
|
import { open, readFile as readFile4, readdir as readdir3, stat as stat5 } from "fs/promises";
|
|
8649
8882
|
var TEXT_EDITABLE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
8650
8883
|
".txt",
|
|
@@ -8792,7 +9025,7 @@ function escapeForInlineScriptString(value) {
|
|
|
8792
9025
|
async function getDirectoryItems(localPath) {
|
|
8793
9026
|
const entries = await readdir3(localPath, { withFileTypes: true });
|
|
8794
9027
|
const withMeta = await Promise.all(entries.map(async (entry) => {
|
|
8795
|
-
const entryPath =
|
|
9028
|
+
const entryPath = join8(localPath, entry.name);
|
|
8796
9029
|
const entryStat = await stat5(entryPath);
|
|
8797
9030
|
const editable = !entry.isDirectory() && await isTextEditableFile(entryPath);
|
|
8798
9031
|
return {
|
|
@@ -8813,7 +9046,7 @@ async function getDirectoryItems(localPath) {
|
|
|
8813
9046
|
function projectCreationTargetPath(parentPath, newProjectName) {
|
|
8814
9047
|
const normalizedName = normalizeNewProjectName(newProjectName);
|
|
8815
9048
|
if (!normalizedName) return "";
|
|
8816
|
-
return
|
|
9049
|
+
return join8(parentPath, normalizedName);
|
|
8817
9050
|
}
|
|
8818
9051
|
function projectCreationButtonLabel(newProjectName) {
|
|
8819
9052
|
const normalizedName = normalizeNewProjectName(newProjectName);
|
|
@@ -8842,7 +9075,7 @@ async function getLocalDirectoryListing(localPath, options = {}) {
|
|
|
8842
9075
|
const entries = await readdir3(localPath, { withFileTypes: true });
|
|
8843
9076
|
const directories = entries.filter((entry) => entry.isDirectory()).map((entry) => ({
|
|
8844
9077
|
name: entry.name,
|
|
8845
|
-
path:
|
|
9078
|
+
path: join8(localPath, entry.name)
|
|
8846
9079
|
})).filter((entry) => options.showHidden === true || !isHiddenName(entry.name)).sort((a, b) => {
|
|
8847
9080
|
const aHidden = isHiddenName(a.name);
|
|
8848
9081
|
const bHidden = isHiddenName(b.name);
|
|
@@ -8851,14 +9084,14 @@ async function getLocalDirectoryListing(localPath, options = {}) {
|
|
|
8851
9084
|
});
|
|
8852
9085
|
return {
|
|
8853
9086
|
path: localPath,
|
|
8854
|
-
parentPath:
|
|
9087
|
+
parentPath: dirname4(localPath),
|
|
8855
9088
|
entries: directories
|
|
8856
9089
|
};
|
|
8857
9090
|
}
|
|
8858
9091
|
async function createDirectoryListingHtml(localPath, options) {
|
|
8859
9092
|
const newProjectName = normalizeNewProjectName(options?.newProjectName ?? "");
|
|
8860
9093
|
const items = await getDirectoryItems(localPath);
|
|
8861
|
-
const parentPath =
|
|
9094
|
+
const parentPath = dirname4(localPath);
|
|
8862
9095
|
const rows = items.map((item) => {
|
|
8863
9096
|
const suffix = item.isDirectory ? "/" : "";
|
|
8864
9097
|
const editAction = item.editable ? ` <a class="icon-btn" aria-label="Edit ${escapeHtml2(item.name)}" href="${escapeHtml2(toEditHref(item.path, newProjectName))}" title="Edit">\u270F\uFE0F</a>` : "";
|
|
@@ -8964,7 +9197,7 @@ async function createDirectoryListingHtml(localPath, options) {
|
|
|
8964
9197
|
}
|
|
8965
9198
|
async function createTextEditorHtml(localPath) {
|
|
8966
9199
|
const content = await readFile4(localPath, "utf8");
|
|
8967
|
-
const parentPath =
|
|
9200
|
+
const parentPath = dirname4(localPath);
|
|
8968
9201
|
const language = languageForPath(localPath);
|
|
8969
9202
|
const safeContentLiteral = escapeForInlineScriptString(content);
|
|
8970
9203
|
return `<!doctype html>
|
|
@@ -9033,9 +9266,9 @@ async function createTextEditorHtml(localPath) {
|
|
|
9033
9266
|
|
|
9034
9267
|
// src/server/httpServer.ts
|
|
9035
9268
|
import { WebSocketServer } from "ws";
|
|
9036
|
-
var __dirname =
|
|
9037
|
-
var distDir =
|
|
9038
|
-
var spaEntryFile =
|
|
9269
|
+
var __dirname = dirname5(fileURLToPath(import.meta.url));
|
|
9270
|
+
var distDir = join9(__dirname, "..", "dist");
|
|
9271
|
+
var spaEntryFile = join9(distDir, "index.html");
|
|
9039
9272
|
var IMAGE_CONTENT_TYPES = {
|
|
9040
9273
|
".avif": "image/avif",
|
|
9041
9274
|
".bmp": "image/bmp",
|
|
@@ -9204,7 +9437,7 @@ function createServer(options = {}) {
|
|
|
9204
9437
|
res.status(404).json({ error: "File not found." });
|
|
9205
9438
|
}
|
|
9206
9439
|
});
|
|
9207
|
-
const hasFrontendAssets =
|
|
9440
|
+
const hasFrontendAssets = existsSync5(spaEntryFile);
|
|
9208
9441
|
if (hasFrontendAssets) {
|
|
9209
9442
|
app.use(express.static(distDir));
|
|
9210
9443
|
}
|
|
@@ -9274,26 +9507,26 @@ function generatePassword() {
|
|
|
9274
9507
|
|
|
9275
9508
|
// src/cli/index.ts
|
|
9276
9509
|
var program = new Command().name("codexui").description("Web interface for Codex app-server");
|
|
9277
|
-
var __dirname2 =
|
|
9510
|
+
var __dirname2 = dirname6(fileURLToPath2(import.meta.url));
|
|
9278
9511
|
var hasPromptedCloudflaredInstall = false;
|
|
9279
9512
|
function getCodexHomePath() {
|
|
9280
|
-
return process.env.CODEX_HOME?.trim() ||
|
|
9513
|
+
return process.env.CODEX_HOME?.trim() || join10(homedir7(), ".codex");
|
|
9281
9514
|
}
|
|
9282
9515
|
function getCloudflaredPromptMarkerPath() {
|
|
9283
|
-
return
|
|
9516
|
+
return join10(getCodexHomePath(), ".cloudflared-install-prompted");
|
|
9284
9517
|
}
|
|
9285
9518
|
function hasPromptedCloudflaredInstallPersisted() {
|
|
9286
|
-
return
|
|
9519
|
+
return existsSync6(getCloudflaredPromptMarkerPath());
|
|
9287
9520
|
}
|
|
9288
9521
|
async function persistCloudflaredInstallPrompted() {
|
|
9289
9522
|
const codexHome = getCodexHomePath();
|
|
9290
|
-
|
|
9523
|
+
mkdirSync2(codexHome, { recursive: true });
|
|
9291
9524
|
await writeFile6(getCloudflaredPromptMarkerPath(), `${Date.now()}
|
|
9292
9525
|
`, "utf8");
|
|
9293
9526
|
}
|
|
9294
9527
|
async function readCliVersion() {
|
|
9295
9528
|
try {
|
|
9296
|
-
const packageJsonPath =
|
|
9529
|
+
const packageJsonPath = join10(__dirname2, "..", "package.json");
|
|
9297
9530
|
const raw = await readFile5(packageJsonPath, "utf8");
|
|
9298
9531
|
const parsed = JSON.parse(raw);
|
|
9299
9532
|
return typeof parsed.version === "string" ? parsed.version : "unknown";
|
|
@@ -9318,8 +9551,8 @@ function resolveCloudflaredCommand() {
|
|
|
9318
9551
|
if (canRunCommand("cloudflared", ["--version"])) {
|
|
9319
9552
|
return "cloudflared";
|
|
9320
9553
|
}
|
|
9321
|
-
const localCandidate =
|
|
9322
|
-
if (
|
|
9554
|
+
const localCandidate = join10(homedir7(), ".local", "bin", "cloudflared");
|
|
9555
|
+
if (existsSync6(localCandidate) && canRunCommand(localCandidate, ["--version"])) {
|
|
9323
9556
|
return localCandidate;
|
|
9324
9557
|
}
|
|
9325
9558
|
return null;
|
|
@@ -9372,9 +9605,9 @@ async function ensureCloudflaredInstalledLinux() {
|
|
|
9372
9605
|
if (!mappedArch) {
|
|
9373
9606
|
throw new Error(`cloudflared auto-install is not supported for Linux architecture: ${process.arch}`);
|
|
9374
9607
|
}
|
|
9375
|
-
const userBinDir =
|
|
9376
|
-
|
|
9377
|
-
const destination =
|
|
9608
|
+
const userBinDir = join10(homedir7(), ".local", "bin");
|
|
9609
|
+
mkdirSync2(userBinDir, { recursive: true });
|
|
9610
|
+
const destination = join10(userBinDir, "cloudflared");
|
|
9378
9611
|
const downloadUrl = `https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${mappedArch}`;
|
|
9379
9612
|
console.log("\ncloudflared not found. Installing to ~/.local/bin...\n");
|
|
9380
9613
|
await downloadFile(downloadUrl, destination);
|
|
@@ -9425,7 +9658,7 @@ async function resolveCloudflaredForTunnel() {
|
|
|
9425
9658
|
}
|
|
9426
9659
|
function hasCodexAuth() {
|
|
9427
9660
|
const codexHome = getCodexHomePath();
|
|
9428
|
-
return
|
|
9661
|
+
return existsSync6(join10(codexHome, "auth.json"));
|
|
9429
9662
|
}
|
|
9430
9663
|
function ensureCodexInstalled() {
|
|
9431
9664
|
let codexCommand = resolveCodexCommand();
|
|
@@ -9615,7 +9848,7 @@ function listenWithFallback(server, startPort) {
|
|
|
9615
9848
|
}
|
|
9616
9849
|
function getCodexGlobalStatePath2() {
|
|
9617
9850
|
const codexHome = getCodexHomePath();
|
|
9618
|
-
return
|
|
9851
|
+
return join10(codexHome, ".codex-global-state.json");
|
|
9619
9852
|
}
|
|
9620
9853
|
function normalizeUniqueStrings(value) {
|
|
9621
9854
|
if (!Array.isArray(value)) return [];
|