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/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-Bqk3qCGk.js"></script>
16
- <link rel="stylesheet" crossorigin href="/assets/index-CfRa3ma1.css">
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 existsSync5, mkdirSync } from "fs";
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 homedir6, networkInterfaces } from "os";
8
- import { isAbsolute as isAbsolute4, join as join9, resolve as resolve3 } from "path";
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 dirname5 } from "path";
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 dirname4, extname as extname3, isAbsolute as isAbsolute3, join as join8 } from "path";
220
- import { existsSync as existsSync4 } from "fs";
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: ${message}`
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)) return trimmed;
4860
+ if (isInlineDataUrl(trimmed)) {
4861
+ return /^data:image\//iu.test(trimmed) ? trimmed : null;
4862
+ }
4832
4863
  const compact = trimmed.replace(/\s+/gu, "");
4833
- if (!/^[A-Za-z0-9+/]+={0,2}$/u.test(compact)) return null;
4834
- return `data:${mimeType};base64,${compact}`;
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
- ...record,
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
- return Boolean(token && validTokens.has(token));
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 = /* @__PURE__ */ new Set();
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
- validTokens.add(token);
8619
- res.setHeader("Set-Cookie", `${TOKEN_COOKIE}=${token}; Path=/; HttpOnly; SameSite=Strict`);
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(400).json({ error: "Invalid request body" });
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
- validTokens.add(token);
8632
- res.setHeader("Set-Cookie", `${TOKEN_COOKIE}=${token}; Path=/; HttpOnly; SameSite=Strict`);
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 dirname3, extname as extname2, join as join7 } from "path";
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 = join7(localPath, entry.name);
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 join7(parentPath, normalizedName);
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: join7(localPath, entry.name)
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: dirname3(localPath),
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 = dirname3(localPath);
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 = dirname3(localPath);
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 = dirname4(fileURLToPath(import.meta.url));
9037
- var distDir = join8(__dirname, "..", "dist");
9038
- var spaEntryFile = join8(distDir, "index.html");
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 = existsSync4(spaEntryFile);
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 = dirname5(fileURLToPath2(import.meta.url));
9510
+ var __dirname2 = dirname6(fileURLToPath2(import.meta.url));
9278
9511
  var hasPromptedCloudflaredInstall = false;
9279
9512
  function getCodexHomePath() {
9280
- return process.env.CODEX_HOME?.trim() || join9(homedir6(), ".codex");
9513
+ return process.env.CODEX_HOME?.trim() || join10(homedir7(), ".codex");
9281
9514
  }
9282
9515
  function getCloudflaredPromptMarkerPath() {
9283
- return join9(getCodexHomePath(), ".cloudflared-install-prompted");
9516
+ return join10(getCodexHomePath(), ".cloudflared-install-prompted");
9284
9517
  }
9285
9518
  function hasPromptedCloudflaredInstallPersisted() {
9286
- return existsSync5(getCloudflaredPromptMarkerPath());
9519
+ return existsSync6(getCloudflaredPromptMarkerPath());
9287
9520
  }
9288
9521
  async function persistCloudflaredInstallPrompted() {
9289
9522
  const codexHome = getCodexHomePath();
9290
- mkdirSync(codexHome, { recursive: true });
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 = join9(__dirname2, "..", "package.json");
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 = join9(homedir6(), ".local", "bin", "cloudflared");
9322
- if (existsSync5(localCandidate) && canRunCommand(localCandidate, ["--version"])) {
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 = join9(homedir6(), ".local", "bin");
9376
- mkdirSync(userBinDir, { recursive: true });
9377
- const destination = join9(userBinDir, "cloudflared");
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 existsSync5(join9(codexHome, "auth.json"));
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 join9(codexHome, ".codex-global-state.json");
9851
+ return join10(codexHome, ".codex-global-state.json");
9619
9852
  }
9620
9853
  function normalizeUniqueStrings(value) {
9621
9854
  if (!Array.isArray(value)) return [];