@wipcomputer/wip-ldm-os 0.4.85-alpha.9 → 0.4.86

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.
Files changed (56) hide show
  1. package/README.md +22 -2
  2. package/SKILL.md +137 -15
  3. package/bin/ldm.js +608 -75
  4. package/lib/deploy.mjs +36 -1
  5. package/lib/registry-migrations.mjs +296 -0
  6. package/package.json +20 -3
  7. package/scripts/test-boot-dir-truth.mjs +158 -0
  8. package/scripts/test-boot-hook-registration.mjs +136 -0
  9. package/scripts/test-boot-payload-trim.mjs +200 -0
  10. package/scripts/test-crc-agentid-tenant-boundary.mjs +2 -2
  11. package/scripts/test-crc-e2ee-key-persistence.mjs +150 -0
  12. package/scripts/test-crc-e2ee-session-route.mjs +9 -2
  13. package/scripts/test-crc-pair-login-flow.mjs +76 -1
  14. package/scripts/test-crc-pair-relink-audit-and-rotation.mjs +164 -0
  15. package/scripts/test-crc-websocket-abuse-limits.mjs +128 -0
  16. package/scripts/test-deploy-hook-ownership.mjs +160 -0
  17. package/scripts/test-doctor-hook-dedupe.mjs +188 -0
  18. package/scripts/test-install-prompt-policy.mjs +84 -0
  19. package/scripts/test-installer-target-self-update.mjs +131 -0
  20. package/scripts/test-kaleidoscope-onboarding-copy.mjs +170 -0
  21. package/scripts/test-kaleidoscope-public-stats-baseline.mjs +129 -0
  22. package/scripts/test-kaleidoscope-qr-authenticator-confirmation.mjs +89 -0
  23. package/scripts/test-ldm-status-concurrency.mjs +118 -0
  24. package/scripts/test-ldm-status-timeout.mjs +96 -0
  25. package/scripts/test-legacy-npm-sources-migration.mjs +460 -0
  26. package/scripts/test-readme-install-prompt.mjs +66 -0
  27. package/shared/boot/boot-config.json +18 -8
  28. package/shared/docs/dev-guide-wipcomputerinc.md.tmpl +5 -4
  29. package/shared/rules/security.md +4 -0
  30. package/shared/templates/install-prompt.md +20 -2
  31. package/src/boot/README.md +24 -1
  32. package/src/boot/boot-config.json +18 -8
  33. package/src/boot/boot-hook.mjs +118 -28
  34. package/src/boot/installer.mjs +33 -10
  35. package/src/hosted-mcp/.env.example +4 -0
  36. package/src/hosted-mcp/README.md +37 -0
  37. package/src/hosted-mcp/app/footer.js +2 -2
  38. package/src/hosted-mcp/app/kaleidoscope-login.html +489 -42
  39. package/src/hosted-mcp/app/pair.html +21 -3
  40. package/src/hosted-mcp/app/wip-logo.png +0 -0
  41. package/src/hosted-mcp/codex-relay-e2ee-registry.mjs +208 -0
  42. package/src/hosted-mcp/codex-relay-ws-abuse-limits.mjs +140 -0
  43. package/src/hosted-mcp/demo/footer.js +2 -2
  44. package/src/hosted-mcp/demo/index.html +166 -44
  45. package/src/hosted-mcp/demo/login.html +87 -23
  46. package/src/hosted-mcp/demo/privacy.html +4 -214
  47. package/src/hosted-mcp/demo/tos.html +4 -189
  48. package/src/hosted-mcp/deploy.sh +2 -0
  49. package/src/hosted-mcp/docs/self-host.md +268 -0
  50. package/src/hosted-mcp/legal/internet-services/kaleidoscope/index.html +257 -0
  51. package/src/hosted-mcp/legal/internet-services/terms/site.html +224 -168
  52. package/src/hosted-mcp/legal/legal-footer.js +75 -0
  53. package/src/hosted-mcp/legal/legal.css +166 -0
  54. package/src/hosted-mcp/legal/privacy/en-ww/index.html +4 -221
  55. package/src/hosted-mcp/legal/privacy/index.html +253 -0
  56. package/src/hosted-mcp/server.mjs +920 -74
@@ -5,7 +5,7 @@
5
5
  // WebAuthn: Passkey-based signup/login (replaces agent name text form).
6
6
 
7
7
  import { randomUUID, randomBytes, createHash } from "node:crypto";
8
- import { readFileSync, writeFileSync, existsSync } from "node:fs";
8
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, accessSync, constants as fsConstants } from "node:fs";
9
9
  import { dirname, join } from "node:path";
10
10
  import { fileURLToPath } from "node:url";
11
11
  import { createServer } from "node:http";
@@ -23,6 +23,19 @@ import {
23
23
  import QRCode from "qrcode";
24
24
  import { WebSocketServer } from "ws";
25
25
  import { parse as parseUrlQs } from "node:querystring";
26
+ import {
27
+ buildCodexBootstrapPayload,
28
+ codexDaemonPubkeyFingerprint,
29
+ createCodexDaemonPubkeyRegistry,
30
+ evaluateCodexDaemonReconnectPubkey,
31
+ } from "./codex-relay-e2ee-registry.mjs";
32
+ import {
33
+ codexWsFrameByteLength,
34
+ createCodexWsAbuseLimitConfig,
35
+ createCodexWsConnectionGuard,
36
+ formatCodexWsLimitLog,
37
+ isCodexWsAgentDisabled,
38
+ } from "./codex-relay-ws-abuse-limits.mjs";
26
39
 
27
40
  // ── Settings ─────────────────────────────────────────────────────────
28
41
 
@@ -42,6 +55,7 @@ const WS_ORIGIN_ALLOWLIST = (process.env.LDM_HOSTED_MCP_WS_ORIGIN_ALLOWLIST || "
42
55
  .split(",")
43
56
  .map(s => s.trim())
44
57
  .filter(Boolean);
58
+ const CODEX_WS_ABUSE_LIMITS = createCodexWsAbuseLimitConfig(process.env);
45
59
 
46
60
  function isWsOriginAllowed(origin) {
47
61
  if (!origin) return false;
@@ -771,7 +785,14 @@ async function handleRegisterVerify(req, res) {
771
785
 
772
786
  console.log("WebAuthn: registered passkey for tenant '" + agentId + "' handle '" + credentialLabel + "' (credId: " + cred.id.slice(0, 16) + "...)");
773
787
 
774
- json(res, 200, { success: true, agentId: credentialLabel, tenantId: agentId, apiKey, credentialLabel });
788
+ json(res, 200, {
789
+ success: true,
790
+ agentId: credentialLabel,
791
+ tenantId: agentId,
792
+ apiKey,
793
+ credentialLabel,
794
+ codex_pair_presence_token: generateCodexPairPresenceToken(agentId),
795
+ });
775
796
  }
776
797
 
777
798
  // POST /webauthn/auth-options
@@ -898,12 +919,19 @@ async function handleAuthVerify(req, res) {
898
919
  }
899
920
  entry.apiKey = newKey;
900
921
  entry.handle = credentialLabel;
901
- console.log("WebAuthn: minted recovery key for tenant '" + entry.agentId + "' (key: " + newKey.slice(0, 10) + "...)");
922
+ console.log("WebAuthn: minted recovery key for tenant '" + entry.agentId + "' (key: " + newKey.slice(0, 6) + "...)");
902
923
  }
903
924
 
904
925
  console.log("WebAuthn: authenticated tenant '" + entry.agentId + "' handle '" + credentialLabel + "'");
905
926
 
906
- json(res, 200, { success: true, agentId: credentialLabel, tenantId: entry.agentId, apiKey: entry.apiKey, credentialLabel });
927
+ json(res, 200, {
928
+ success: true,
929
+ agentId: credentialLabel,
930
+ tenantId: entry.agentId,
931
+ apiKey: entry.apiKey,
932
+ credentialLabel,
933
+ codex_pair_presence_token: generateCodexPairPresenceToken(entry.agentId),
934
+ });
907
935
  }
908
936
 
909
937
  // ---------- Page handlers ----------
@@ -1678,7 +1706,7 @@ function handleAgentAuthApprove(req, res) {
1678
1706
 
1679
1707
  // ---------- QR Login (Chrome fallback) ----------
1680
1708
 
1681
- // `next` whitelist for the QR login flow. Two shapes are allowed; both
1709
+ // `next` whitelist for the QR login flow. Three shapes are allowed; each
1682
1710
  // land the user on a known phone-side surface after successful sign-in.
1683
1711
  // Anything else is silently dropped. `next` is NOT a general redirect
1684
1712
  // primitive.
@@ -1694,8 +1722,12 @@ function handleAgentAuthApprove(req, res) {
1694
1722
  // Kaleidoscope phone-side remote-control thread surface. Standard
1695
1723
  // ?next semantics; allowed on both desktop and mobile (this is
1696
1724
  // navigation continuation, not authority transfer).
1725
+ //
1726
+ // 3. DEMO_NEXT_REGEX: /demo for the homepage CTA. Standard post-login
1727
+ // continuation; allowed on both desktop and mobile.
1697
1728
  const PAIR_NEXT_REGEX = /^\/pair\/[ABCDEFGHJKLMNPQRSTUVWXYZ23456789]{6}$/;
1698
1729
  const REMOTE_CONTROL_NEXT_REGEX = /^\/codex-remote-control\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1730
+ const DEMO_NEXT_REGEX = /^\/demo$/;
1699
1731
 
1700
1732
  function sanitizeCrcPairNext(raw) {
1701
1733
  if (typeof raw !== "string") return null;
@@ -1704,7 +1736,7 @@ function sanitizeCrcPairNext(raw) {
1704
1736
  try { decoded = decodeURIComponent(raw); } catch { return null; }
1705
1737
  // Catch double-encoded payloads.
1706
1738
  if (decoded !== raw && /%/.test(decoded)) return null;
1707
- if (!PAIR_NEXT_REGEX.test(decoded) && !REMOTE_CONTROL_NEXT_REGEX.test(decoded)) return null;
1739
+ if (!PAIR_NEXT_REGEX.test(decoded) && !REMOTE_CONTROL_NEXT_REGEX.test(decoded) && !DEMO_NEXT_REGEX.test(decoded)) return null;
1708
1740
  return decoded;
1709
1741
  }
1710
1742
 
@@ -1719,10 +1751,11 @@ async function handleQrLoginStart(req, res) {
1719
1751
  //
1720
1752
  // Only /pair/<CODE> next triggers pair-mode (C6 strip on desktop
1721
1753
  // status, C8 desktop-no-redirect, the "phone is the actor" model).
1722
- // /codex-remote-control/<UUID> is a normal post-login continuation:
1723
- // desktop status returns the full login response (apiKey, handle,
1724
- // next) so the desktop poll can authenticate and redirect on its
1725
- // own. The phone also gets next via approve, so both ends can act.
1754
+ // /codex-remote-control/<UUID> and /demo are normal post-login
1755
+ // continuations: desktop status returns the full login response
1756
+ // (apiKey, handle, next) so the desktop poll can authenticate and
1757
+ // redirect on its own. The phone also gets next via approve, so both
1758
+ // ends can act.
1726
1759
  const next = sanitizeCrcPairNext(body && body.next);
1727
1760
  const purpose = (next && PAIR_NEXT_REGEX.test(next)) ? "pair" : null;
1728
1761
  const sessionId = randomUUID();
@@ -1736,7 +1769,7 @@ async function handleQrLoginStart(req, res) {
1736
1769
  handle: handle || null,
1737
1770
  expires: Date.now() + QR_LOGIN_EXPIRY_MS,
1738
1771
  purpose, // "pair" | null
1739
- next: next || null, // sanitized `/pair/<CODE>` or null
1772
+ next: next || null, // sanitized `/pair/<CODE>`, `/codex-remote-control/<UUID>`, `/demo`, or null
1740
1773
  };
1741
1774
  console.log("QR login: created session " + sessionId.slice(0, 8) + "..." + (purpose === "pair" ? " (pair-mode)" : ""));
1742
1775
  json(res, 200, { sessionId, qrUrl: "/api/qr-login/qr?s=" + sessionId });
@@ -1777,17 +1810,18 @@ function handleQrLoginStatus(req, res) {
1777
1810
  // C6 round 4. Desktop never becomes the pairing authority.
1778
1811
  json(res, 200, { status: "approved", agentId: entry.agentId });
1779
1812
  } else {
1780
- // Legacy login mode OR codex-remote-control continuation
1813
+ // Legacy login mode OR standard login continuation
1781
1814
  // (purpose === null). Desktop gets full identity to render the
1782
1815
  // welcome view OR redirect to next on its own poll.
1783
1816
  // credentialLabel matches the saved-passkey label (see
1784
1817
  // register-verify / auth-verify). next is included only if a
1785
1818
  // sanitized non-pair-mode next was set on the session
1786
- // (currently /codex-remote-control/<UUID>); legacy login
1787
- // sessions without next get next === null.
1819
+ // (/codex-remote-control/<UUID> or /demo); legacy login sessions
1820
+ // without next get next === null.
1788
1821
  json(res, 200, {
1789
1822
  status: "approved",
1790
1823
  agentId: entry.agentId,
1824
+ tenantId: entry.tenantId || null,
1791
1825
  apiKey: entry.apiKey,
1792
1826
  credentialLabel: entry.credentialLabel || null,
1793
1827
  next: entry.next || null,
@@ -1806,7 +1840,7 @@ function handleQrLoginStatus(req, res) {
1806
1840
  // {ok: true} unchanged.
1807
1841
  function handleQrLoginApprove(req, res) {
1808
1842
  readBody(req).then(function(body) {
1809
- const { sessionId, agentId, apiKey, credentialLabel } = body || {};
1843
+ const { sessionId, agentId, apiKey, tenantId, credentialLabel } = body || {};
1810
1844
  const entry = qrLoginSessions[sessionId];
1811
1845
  if (!entry || Date.now() > entry.expires) {
1812
1846
  json(res, 404, { error: "Session not found or expired" });
@@ -1819,16 +1853,19 @@ function handleQrLoginApprove(req, res) {
1819
1853
  entry.status = "approved";
1820
1854
  entry.agentId = agentId;
1821
1855
  entry.apiKey = apiKey;
1856
+ const verifiedIdentity = identityForApiKey(apiKey);
1857
+ entry.tenantId = verifiedIdentity?.tenantId || (isInternalTenantId(tenantId) ? tenantId : null);
1822
1858
  // Phone-side passes the label it received from register-verify /
1823
1859
  // auth-verify so the desktop can show the same string the user
1824
1860
  // just saved on their phone. Optional for back-compat.
1825
1861
  entry.credentialLabel = (typeof credentialLabel === "string" && credentialLabel.length <= 64) ? credentialLabel : null;
1826
1862
  console.log("QR login: approved session " + sessionId.slice(0, 8) + "... for '" + agentId + "'" + (entry.purpose === "pair" ? " (pair-mode)" : (entry.next ? " (next=" + entry.next + ")" : "")));
1827
1863
  // Phone receives next on approve regardless of purpose, so the
1828
- // phone can redirect to either /pair/<CODE> (pair-mode, phone is
1829
- // the actor) or /codex-remote-control/<UUID> (continuation, phone
1830
- // can act). Desktop's separate behavior (strip vs full response)
1831
- // is handled in handleQrLoginStatus.
1864
+ // phone can redirect to /pair/<CODE> (pair-mode, phone is the
1865
+ // actor) or a standard continuation such as
1866
+ // /codex-remote-control/<UUID> or /demo. Desktop's separate
1867
+ // behavior (strip vs full response) is handled in
1868
+ // handleQrLoginStatus.
1832
1869
  if (entry.next) {
1833
1870
  json(res, 200, { ok: true, next: entry.next });
1834
1871
  } else {
@@ -1842,32 +1879,77 @@ function handleQrLoginApprove(req, res) {
1842
1879
  // ---------- Demo API handlers ----------
1843
1880
 
1844
1881
  // ── Wallet tracking (per agent) ──
1845
- const IMAGE_COST_CENTS = 1; // $0.01
1846
- const INITIAL_BALANCE_CENTS = 500; // $5.00
1882
+ const IMAGE_COST_CENTS = 1; // $0.01, launch onboarding image-generation step
1883
+ const INITIAL_BALANCE_CENTS = 1000; // $10.00
1847
1884
 
1848
1885
  // JSON fallback for wallets
1849
1886
  const WALLET_FILE = join(dirname(fileURLToPath(import.meta.url)), "wallets.json");
1887
+ const DEMO_WALLET_RESET_MARKER_FILE = join(dirname(fileURLToPath(import.meta.url)), ".demo-wallet-reset-v0-4-87.json");
1850
1888
  function loadWalletsFromFile() { try { return JSON.parse(readFileSync(WALLET_FILE, "utf8")); } catch { return {}; } }
1851
1889
  function saveWalletsToFile(w) { try { writeFileSync(WALLET_FILE, JSON.stringify(w, null, 2) + "\n"); } catch {} }
1852
1890
 
1891
+ function walletUserIdForAgent(agentId) {
1892
+ return typeof agentId === "string" && agentId.startsWith("acct:") ? agentId.slice("acct:".length) : agentId;
1893
+ }
1894
+
1895
+ function resetAndNormalizeWalletFileEntries(wallets) {
1896
+ const normalizedWallets = {};
1897
+ let resetCount = 0;
1898
+ for (const agentId of Object.keys(wallets)) {
1899
+ normalizedWallets[walletUserIdForAgent(agentId)] = INITIAL_BALANCE_CENTS;
1900
+ resetCount += 1;
1901
+ }
1902
+ return { wallets: normalizedWallets, count: resetCount };
1903
+ }
1904
+
1905
+ async function resetExistingDemoWalletsToStarterBalanceOnce() {
1906
+ if (existsSync(DEMO_WALLET_RESET_MARKER_FILE)) return;
1907
+ let prismaResetCount = 0;
1908
+ let jsonResetCount = 0;
1909
+ if (usePrisma) {
1910
+ try {
1911
+ const result = await prisma.wallet.updateMany({ data: { balance: INITIAL_BALANCE_CENTS } });
1912
+ prismaResetCount = result.count || 0;
1913
+ } catch (err) {
1914
+ console.error("Demo wallet reset migration error:", err.message);
1915
+ }
1916
+ }
1917
+ const normalizedWalletFile = resetAndNormalizeWalletFileEntries(loadWalletsFromFile());
1918
+ jsonResetCount = normalizedWalletFile.count;
1919
+ saveWalletsToFile(normalizedWalletFile.wallets);
1920
+ try {
1921
+ writeFileSync(DEMO_WALLET_RESET_MARKER_FILE, JSON.stringify({
1922
+ resetAt: new Date().toISOString(),
1923
+ balance: INITIAL_BALANCE_CENTS,
1924
+ prismaCount: prismaResetCount,
1925
+ jsonCount: jsonResetCount,
1926
+ }, null, 2) + "\n");
1927
+ } catch (err) {
1928
+ console.error("Demo wallet reset marker write error:", err.message);
1929
+ }
1930
+ console.log("Demo wallet reset migration: set " + (prismaResetCount + jsonResetCount) + " existing wallet balance(s) to " + formatCents(INITIAL_BALANCE_CENTS));
1931
+ }
1932
+
1853
1933
  async function getBalance(agentId) {
1934
+ const walletUserId = walletUserIdForAgent(agentId);
1854
1935
  if (usePrisma) {
1855
1936
  try {
1856
- const wallet = await prisma.wallet.findFirst({ where: { userId: agentId } });
1937
+ const wallet = await prisma.wallet.findFirst({ where: { userId: walletUserId } });
1857
1938
  return wallet ? wallet.balance : INITIAL_BALANCE_CENTS;
1858
1939
  } catch {}
1859
1940
  }
1860
1941
  const w = loadWalletsFromFile();
1861
- return w[agentId] !== undefined ? w[agentId] : INITIAL_BALANCE_CENTS;
1942
+ return w[walletUserId] !== undefined ? w[walletUserId] : INITIAL_BALANCE_CENTS;
1862
1943
  }
1863
1944
 
1864
1945
  async function deductBalance(agentId, cents) {
1946
+ const walletUserId = walletUserIdForAgent(agentId);
1865
1947
  if (usePrisma) {
1866
1948
  try {
1867
- let wallet = await prisma.wallet.findFirst({ where: { userId: agentId } });
1949
+ let wallet = await prisma.wallet.findFirst({ where: { userId: walletUserId } });
1868
1950
  if (!wallet) {
1869
1951
  wallet = await prisma.wallet.create({
1870
- data: { userId: agentId, balance: INITIAL_BALANCE_CENTS },
1952
+ data: { userId: walletUserId, balance: INITIAL_BALANCE_CENTS },
1871
1953
  });
1872
1954
  }
1873
1955
  const newBalance = Math.max(0, wallet.balance - cents);
@@ -1879,13 +1961,515 @@ async function deductBalance(agentId, cents) {
1879
1961
  }
1880
1962
  // JSON fallback
1881
1963
  const w = loadWalletsFromFile();
1882
- if (w[agentId] === undefined) w[agentId] = INITIAL_BALANCE_CENTS;
1883
- w[agentId] = Math.max(0, w[agentId] - cents);
1964
+ if (w[walletUserId] === undefined) w[walletUserId] = INITIAL_BALANCE_CENTS;
1965
+ w[walletUserId] = Math.max(0, w[walletUserId] - cents);
1884
1966
  saveWalletsToFile(w);
1885
- return w[agentId];
1967
+ return w[walletUserId];
1886
1968
  }
1887
1969
  function formatCents(c) { return "$" + (c / 100).toFixed(2); }
1888
1970
 
1971
+ await resetExistingDemoWalletsToStarterBalanceOnce();
1972
+
1973
+ const LIVE_WALL_FILE = process.env.KALEIDOSCOPE_LIVE_WALL_FILE || join(__dirname, "kaleidoscope-live-wall.json");
1974
+ const LIVE_WALL_MEDIA_ROUTE = "/media/kaleidoscope/generated/";
1975
+ const LIVE_WALL_DEFAULT_MEDIA_DIR = DEV_MODE
1976
+ ? join(__dirname, "media", "kaleidoscope", "generated")
1977
+ : "/var/www/wip.computer/public_html/media/kaleidoscope/generated";
1978
+ const LIVE_WALL_MEDIA_DIR = process.env.KALEIDOSCOPE_LIVE_WALL_MEDIA_DIR || LIVE_WALL_DEFAULT_MEDIA_DIR;
1979
+ const LIVE_WALL_MAX_IMAGE_BYTES = Math.max(1, parseInt(process.env.KALEIDOSCOPE_LIVE_WALL_MAX_IMAGE_BYTES || String(10 * 1024 * 1024), 10));
1980
+ const LIVE_WALL_FETCH_TIMEOUT_MS = Math.max(1_000, parseInt(process.env.KALEIDOSCOPE_LIVE_WALL_FETCH_TIMEOUT_MS || "15000", 10));
1981
+ const LIVE_WALL_LIMIT = 240;
1982
+ const KALEIDOSCOPE_PUBLIC_STATS_BASELINE = Object.freeze({
1983
+ timezone: "America/Los_Angeles",
1984
+ date: "2026-05-21",
1985
+ startsAt: "2026-05-21T07:00:00.000Z",
1986
+ counts: Object.freeze({
1987
+ keysCreated: 3,
1988
+ genericKaleidoscopes: 11,
1989
+ imageKaleidoscopes: 3,
1990
+ }),
1991
+ });
1992
+ const LIVE_WALL_SEED_SOURCES = [
1993
+ {
1994
+ sourceUrl: "https://imgen.x.ai/xai-imgen/xai-tmp-imgen-bd814982-276f-438d-bc3c-2ca817982aae.jpeg",
1995
+ createdAt: "2026-05-20T00:00:00.000Z",
1996
+ kind: "generic",
1997
+ },
1998
+ {
1999
+ sourceUrl: "https://imgen.x.ai/xai-imgen/xai-tmp-imgen-226e18a9-7721-4f6c-9ef2-3c26c0ff1293.jpeg",
2000
+ createdAt: "2026-05-20T00:00:01.000Z",
2001
+ kind: "generic",
2002
+ },
2003
+ {
2004
+ sourceUrl: "https://imgen.x.ai/xai-imgen/xai-tmp-imgen-b1b398b5-4982-4ed8-b583-170a4ae4e811.jpeg",
2005
+ createdAt: "2026-05-20T00:00:02.000Z",
2006
+ kind: "generic",
2007
+ },
2008
+ {
2009
+ sourceUrl: "https://imgen.x.ai/xai-imgen/xai-tmp-imgen-5505496e-c436-4047-ab81-630a22ec75ea.jpeg",
2010
+ createdAt: "2026-05-20T00:00:03.000Z",
2011
+ kind: "generic",
2012
+ },
2013
+ ];
2014
+ let liveWallSeedBackfillAttempted = false;
2015
+
2016
+ function ensureLiveWallRuntimeStorage() {
2017
+ try {
2018
+ mkdirSync(dirname(LIVE_WALL_FILE), { recursive: true });
2019
+ mkdirSync(LIVE_WALL_MEDIA_DIR, { recursive: true });
2020
+ accessSync(dirname(LIVE_WALL_FILE), fsConstants.W_OK);
2021
+ accessSync(LIVE_WALL_MEDIA_DIR, fsConstants.W_OK);
2022
+ } catch (err) {
2023
+ console.error("FATAL: Kaleidoscope live wall storage is not writable:", err.message);
2024
+ process.exit(1);
2025
+ }
2026
+ }
2027
+
2028
+ ensureLiveWallRuntimeStorage();
2029
+
2030
+ const LIVE_WALL_GENERIC_PROMPT = "Create an abstract kaleidoscope image with mirrored radial symmetry, analog film grain, warm color bleed, soft exposure, organic imperfections, and no text.";
2031
+ const LIVE_WALL_PHOTO_PROMPT = "Create an abstract kaleidoscope image from the colors, light, and shapes in the user's photo. Use mirrored radial symmetry, analog film grain, warm color bleed, soft exposure, organic imperfections, and no text.";
2032
+ const LIVE_WALL_IMAGE_PROMPT_PREFIX = "Create an abstract kaleidoscope image from these visual details in the user's photo: ";
2033
+ const LIVE_WALL_IMAGE_PROMPT_SUFFIX = ". Use mirrored radial symmetry, analog film grain, warm color bleed, soft exposure, organic imperfections, and no text.";
2034
+
2035
+ function classifyLiveWallPrompt(prompt) {
2036
+ if (prompt === LIVE_WALL_GENERIC_PROMPT) return "generic";
2037
+ if (prompt === LIVE_WALL_PHOTO_PROMPT) return "image";
2038
+ if (typeof prompt === "string"
2039
+ && prompt.startsWith(LIVE_WALL_IMAGE_PROMPT_PREFIX)
2040
+ && prompt.endsWith(LIVE_WALL_IMAGE_PROMPT_SUFFIX)) {
2041
+ return "image";
2042
+ }
2043
+ return null;
2044
+ }
2045
+
2046
+ function parseTimestampMs(value) {
2047
+ if (typeof value !== "string" || !value.trim()) return null;
2048
+ const ms = Date.parse(value);
2049
+ return Number.isFinite(ms) ? ms : null;
2050
+ }
2051
+
2052
+ function createdAtSinceBaseline(item) {
2053
+ const baselineMs = Date.parse(KALEIDOSCOPE_PUBLIC_STATS_BASELINE.startsAt);
2054
+ const createdAtMs = parseTimestampMs(item?.createdAt);
2055
+ return createdAtMs !== null && createdAtMs >= baselineMs;
2056
+ }
2057
+
2058
+ function isWipTestHandle(handle) {
2059
+ return typeof handle === "string" && handle.trim().toLowerCase().startsWith("wiptest-");
2060
+ }
2061
+
2062
+ function publicKeyCreatedSinceBaseline(entry) {
2063
+ return createdAtSinceBaseline(entry) && !isWipTestHandle(entry?.handle);
2064
+ }
2065
+
2066
+ function latestCreatedAt(items) {
2067
+ let latestMs = null;
2068
+ let latest = null;
2069
+ for (const item of Array.isArray(items) ? items : []) {
2070
+ const ms = parseTimestampMs(item?.createdAt);
2071
+ if (ms === null) continue;
2072
+ if (latestMs === null || ms > latestMs) {
2073
+ latestMs = ms;
2074
+ latest = new Date(ms).toISOString();
2075
+ }
2076
+ }
2077
+ return latest;
2078
+ }
2079
+
2080
+ function countCreatedInLast24Hours(items, now = new Date()) {
2081
+ const nowMs = now.getTime();
2082
+ const windowStartMs = nowMs - (24 * 60 * 60 * 1000);
2083
+ return (Array.isArray(items) ? items : []).filter(item => {
2084
+ const ms = parseTimestampMs(item?.createdAt);
2085
+ return ms !== null && ms >= windowStartMs && ms <= nowMs;
2086
+ }).length;
2087
+ }
2088
+
2089
+ function deriveKaleidoscopePublicStats({ images, passkeyEntries, now = new Date() }) {
2090
+ const safeImages = Array.isArray(images) ? images : [];
2091
+ const safePasskeys = Array.isArray(passkeyEntries) ? passkeyEntries : [];
2092
+ const postBaselineGeneric = safeImages
2093
+ .filter(item => item && isPublicWallImageUrl(item.url) && item.kind !== "image" && createdAtSinceBaseline(item))
2094
+ .length;
2095
+ const postBaselineImage = safeImages
2096
+ .filter(item => item && isPublicWallImageUrl(item.url) && item.kind === "image" && createdAtSinceBaseline(item))
2097
+ .length;
2098
+ const publicPasskeys = safePasskeys.filter(item => !isWipTestHandle(item?.handle));
2099
+ const postBaselineKeys = publicPasskeys.filter(createdAtSinceBaseline).length;
2100
+ const newestCreatedAt = latestCreatedAt([...safeImages, ...publicPasskeys]);
2101
+
2102
+ return {
2103
+ genericKaleidoscopes: KALEIDOSCOPE_PUBLIC_STATS_BASELINE.counts.genericKaleidoscopes + postBaselineGeneric,
2104
+ imageKaleidoscopes: KALEIDOSCOPE_PUBLIC_STATS_BASELINE.counts.imageKaleidoscopes + postBaselineImage,
2105
+ keysCreated: KALEIDOSCOPE_PUBLIC_STATS_BASELINE.counts.keysCreated + postBaselineKeys,
2106
+ publicWallImages: safeImages.filter(item => item && isPublicWallImageUrl(item.url)).length,
2107
+ baseline: KALEIDOSCOPE_PUBLIC_STATS_BASELINE,
2108
+ newSinceBaseline: {
2109
+ genericKaleidoscopes: postBaselineGeneric,
2110
+ imageKaleidoscopes: postBaselineImage,
2111
+ keysCreated: postBaselineKeys,
2112
+ total: postBaselineGeneric + postBaselineImage + postBaselineKeys,
2113
+ },
2114
+ last24Hours: {
2115
+ wallImages: countCreatedInLast24Hours(safeImages, now),
2116
+ keysCreated: countCreatedInLast24Hours(publicPasskeys, now),
2117
+ },
2118
+ lastCreated: newestCreatedAt,
2119
+ };
2120
+ }
2121
+
2122
+ function loadLiveWallState() {
2123
+ try {
2124
+ const parsed = JSON.parse(readFileSync(LIVE_WALL_FILE, "utf8"));
2125
+ const saved = Array.isArray(parsed?.images) ? parsed.images : [];
2126
+ return {
2127
+ images: saved,
2128
+ };
2129
+ } catch {
2130
+ return {
2131
+ images: [],
2132
+ };
2133
+ }
2134
+ }
2135
+
2136
+ function saveLiveWallState(state) {
2137
+ try {
2138
+ mkdirSync(dirname(LIVE_WALL_FILE), { recursive: true });
2139
+ writeFileSync(LIVE_WALL_FILE, JSON.stringify({
2140
+ images: Array.isArray(state?.images) ? state.images : [],
2141
+ }, null, 2) + "\n");
2142
+ } catch (err) {
2143
+ console.error("Kaleidoscope live wall save error:", err.message);
2144
+ }
2145
+ }
2146
+
2147
+ function liveWallSourceHash(value) {
2148
+ return createHash("sha256").update(value).digest("hex");
2149
+ }
2150
+
2151
+ function publicLiveWallMediaUrl(filename) {
2152
+ return ISSUER_URL + LIVE_WALL_MEDIA_ROUTE + filename;
2153
+ }
2154
+
2155
+ function liveWallMediaFilenameFromUrl(value) {
2156
+ try {
2157
+ const url = new URL(value, ISSUER_URL);
2158
+ if (url.origin !== ISSUER_URL || !url.pathname.startsWith(LIVE_WALL_MEDIA_ROUTE)) return null;
2159
+ const filename = url.pathname.slice(LIVE_WALL_MEDIA_ROUTE.length);
2160
+ return /^[a-f0-9]{64}\.(jpg|png|webp)$/.test(filename) ? filename : null;
2161
+ } catch {
2162
+ return null;
2163
+ }
2164
+ }
2165
+
2166
+ function liveWallMediaFileExists(value) {
2167
+ const filename = liveWallMediaFilenameFromUrl(value);
2168
+ return Boolean(filename && existsSync(join(LIVE_WALL_MEDIA_DIR, filename)));
2169
+ }
2170
+
2171
+ function liveWallContentTypeForFilename(filename) {
2172
+ if (filename.endsWith(".jpg")) return "image/jpeg";
2173
+ if (filename.endsWith(".png")) return "image/png";
2174
+ if (filename.endsWith(".webp")) return "image/webp";
2175
+ return null;
2176
+ }
2177
+
2178
+ function serveKaleidoscopeGeneratedMedia(path, res) {
2179
+ const filename = path.slice(LIVE_WALL_MEDIA_ROUTE.length);
2180
+ if (!/^[a-f0-9]{64}\.(jpg|png|webp)$/.test(filename)) {
2181
+ json(res, 404, { error: "Not found" });
2182
+ return;
2183
+ }
2184
+ try {
2185
+ const content = readFileSync(join(LIVE_WALL_MEDIA_DIR, filename));
2186
+ const contentType = liveWallContentTypeForFilename(filename) || "application/octet-stream";
2187
+ res.writeHead(200, {
2188
+ "Content-Type": contentType,
2189
+ "Content-Length": content.length,
2190
+ "Cache-Control": "public, max-age=31536000, immutable",
2191
+ });
2192
+ res.end(content);
2193
+ } catch {
2194
+ json(res, 404, { error: "Not found" });
2195
+ }
2196
+ }
2197
+
2198
+ function isLiveWallMediaUrl(value) {
2199
+ if (typeof value !== "string" || value.length > 2048) return false;
2200
+ try {
2201
+ const url = new URL(value, ISSUER_URL);
2202
+ return url.origin === ISSUER_URL && url.pathname.startsWith(LIVE_WALL_MEDIA_ROUTE);
2203
+ } catch {
2204
+ return false;
2205
+ }
2206
+ }
2207
+
2208
+ function isAllowedLiveWallSourceUrl(value) {
2209
+ if (typeof value !== "string" || value.length > 2048) return false;
2210
+ if (value.startsWith("data:")) return false;
2211
+ try {
2212
+ const url = new URL(value);
2213
+ return url.protocol === "https:" && url.hostname === "imgen.x.ai" && url.pathname.startsWith("/xai-imgen/");
2214
+ } catch {
2215
+ return false;
2216
+ }
2217
+ }
2218
+
2219
+ function isPublicWallImageUrl(value) {
2220
+ return isLiveWallMediaUrl(value) || isAllowedLiveWallSourceUrl(value);
2221
+ }
2222
+
2223
+ function normalizeLiveWallContentType(value) {
2224
+ const type = String(value || "").split(";")[0].trim().toLowerCase();
2225
+ if (type === "image/jpg") return "image/jpeg";
2226
+ return type;
2227
+ }
2228
+
2229
+ function liveWallExtensionForContentType(contentType) {
2230
+ if (contentType === "image/jpeg") return "jpg";
2231
+ if (contentType === "image/png") return "png";
2232
+ if (contentType === "image/webp") return "webp";
2233
+ return null;
2234
+ }
2235
+
2236
+ function liveWallBufferMatchesContentType(buffer, contentType) {
2237
+ if (!Buffer.isBuffer(buffer) || buffer.length < 12) return false;
2238
+ if (contentType === "image/jpeg") {
2239
+ return buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff;
2240
+ }
2241
+ if (contentType === "image/png") {
2242
+ return buffer[0] === 0x89 && buffer[1] === 0x50 && buffer[2] === 0x4e && buffer[3] === 0x47
2243
+ && buffer[4] === 0x0d && buffer[5] === 0x0a && buffer[6] === 0x1a && buffer[7] === 0x0a;
2244
+ }
2245
+ if (contentType === "image/webp") {
2246
+ return buffer.subarray(0, 4).toString("ascii") === "RIFF"
2247
+ && buffer.subarray(8, 12).toString("ascii") === "WEBP";
2248
+ }
2249
+ return false;
2250
+ }
2251
+
2252
+ async function readLiveWallImageResponseBody(response) {
2253
+ if (!response.body || typeof response.body.getReader !== "function") {
2254
+ throw new Error("source image response body is not readable");
2255
+ }
2256
+ const reader = response.body.getReader();
2257
+ const chunks = [];
2258
+ let total = 0;
2259
+
2260
+ try {
2261
+ while (true) {
2262
+ const { value, done } = await reader.read();
2263
+ if (done) break;
2264
+ if (!value) continue;
2265
+ total += value.byteLength;
2266
+ if (total > LIVE_WALL_MAX_IMAGE_BYTES) {
2267
+ throw new Error("source image exceeds max byte size");
2268
+ }
2269
+ chunks.push(Buffer.from(value));
2270
+ }
2271
+ } finally {
2272
+ try { reader.releaseLock(); } catch {}
2273
+ }
2274
+
2275
+ if (total < 1) throw new Error("source image byte size is invalid");
2276
+ return Buffer.concat(chunks, total);
2277
+ }
2278
+
2279
+ async function archiveKaleidoscopeGeneratedImage(sourceUrl) {
2280
+ if (!isAllowedLiveWallSourceUrl(sourceUrl)) throw new Error("source image URL is not an allowed HTTPS image source");
2281
+ const source = new URL(sourceUrl);
2282
+
2283
+ const controller = new AbortController();
2284
+ const timeout = setTimeout(() => controller.abort(), LIVE_WALL_FETCH_TIMEOUT_MS);
2285
+ if (typeof timeout.unref === "function") timeout.unref();
2286
+ let imageRes;
2287
+ try {
2288
+ imageRes = await fetch(source.href, { redirect: "follow", signal: controller.signal });
2289
+ if (!imageRes.ok) throw new Error("source image fetch failed with HTTP " + imageRes.status);
2290
+ const finalUrl = new URL(imageRes.url || source.href);
2291
+ if (!isAllowedLiveWallSourceUrl(finalUrl.href)) throw new Error("source image redirected away from allowed image source");
2292
+
2293
+ const contentType = normalizeLiveWallContentType(imageRes.headers.get("content-type"));
2294
+ const ext = liveWallExtensionForContentType(contentType);
2295
+ if (!ext) throw new Error("source image content type is not an allowed raster image");
2296
+
2297
+ const contentLength = Number.parseInt(imageRes.headers.get("content-length") || "0", 10);
2298
+ if (Number.isFinite(contentLength) && contentLength > LIVE_WALL_MAX_IMAGE_BYTES) {
2299
+ throw new Error("source image exceeds max byte size");
2300
+ }
2301
+
2302
+ const buffer = await readLiveWallImageResponseBody(imageRes);
2303
+ if (!liveWallBufferMatchesContentType(buffer, contentType)) {
2304
+ throw new Error("source image bytes do not match content type");
2305
+ }
2306
+
2307
+ const contentHash = createHash("sha256").update(buffer).digest("hex");
2308
+ const sourceUrlHash = liveWallSourceHash(source.href);
2309
+ const filename = contentHash + "." + ext;
2310
+ mkdirSync(LIVE_WALL_MEDIA_DIR, { recursive: true });
2311
+ const filePath = join(LIVE_WALL_MEDIA_DIR, filename);
2312
+ if (!existsSync(filePath)) writeFileSync(filePath, buffer);
2313
+
2314
+ return {
2315
+ url: publicLiveWallMediaUrl(filename),
2316
+ sourceProvider: "xai",
2317
+ sourceUrlHash,
2318
+ contentHash,
2319
+ contentType,
2320
+ byteLength: buffer.length,
2321
+ archivedAt: new Date().toISOString(),
2322
+ };
2323
+ } catch (err) {
2324
+ if (err?.name === "AbortError") throw new Error("source image fetch timed out");
2325
+ throw err;
2326
+ } finally {
2327
+ clearTimeout(timeout);
2328
+ }
2329
+ }
2330
+
2331
+ function liveWallEntryFromArchived({ archived, createdAt, kind, id }) {
2332
+ return {
2333
+ id: id || randomUUID(),
2334
+ url: archived.url,
2335
+ createdAt: createdAt || new Date().toISOString(),
2336
+ kind: kind === "image" ? "image" : "generic",
2337
+ sourceProvider: archived.sourceProvider,
2338
+ sourceUrlHash: archived.sourceUrlHash,
2339
+ contentHash: archived.contentHash,
2340
+ contentType: archived.contentType,
2341
+ byteLength: archived.byteLength,
2342
+ archivedAt: archived.archivedAt,
2343
+ };
2344
+ }
2345
+
2346
+ function liveWallEntrySourceHash(item) {
2347
+ if (!item || typeof item !== "object") return null;
2348
+ if (typeof item.sourceUrlHash === "string" && item.sourceUrlHash) return item.sourceUrlHash;
2349
+ if (isAllowedLiveWallSourceUrl(item.url)) return liveWallSourceHash(item.url);
2350
+ return null;
2351
+ }
2352
+
2353
+ async function ensureLiveWallSeedImages() {
2354
+ if (liveWallSeedBackfillAttempted) return;
2355
+ liveWallSeedBackfillAttempted = true;
2356
+
2357
+ const state = loadLiveWallState();
2358
+ const images = [];
2359
+ const existingSourceHashes = new Set();
2360
+ const existingUrls = new Set(images.map(item => item.url).filter(Boolean));
2361
+ let changed = false;
2362
+
2363
+ for (const item of state.images) {
2364
+ if (!item || typeof item.url !== "string") {
2365
+ changed = true;
2366
+ continue;
2367
+ }
2368
+ if (isLiveWallMediaUrl(item.url) && liveWallMediaFileExists(item.url)) {
2369
+ images.push(item);
2370
+ if (item.sourceUrlHash) existingSourceHashes.add(item.sourceUrlHash);
2371
+ existingUrls.add(item.url);
2372
+ continue;
2373
+ }
2374
+ if (isAllowedLiveWallSourceUrl(item.url)) {
2375
+ try {
2376
+ const archived = await archiveKaleidoscopeGeneratedImage(item.url);
2377
+ if (!existingUrls.has(archived.url)) {
2378
+ images.push(liveWallEntryFromArchived({
2379
+ archived,
2380
+ createdAt: item.createdAt,
2381
+ kind: item.kind,
2382
+ id: item.id,
2383
+ }));
2384
+ existingUrls.add(archived.url);
2385
+ }
2386
+ existingSourceHashes.add(archived.sourceUrlHash);
2387
+ changed = true;
2388
+ } catch (err) {
2389
+ images.push(item);
2390
+ console.error("Kaleidoscope live wall registry image migration failed:", JSON.stringify({
2391
+ sourceUrlHash: liveWallSourceHash(item.url),
2392
+ message: err.message,
2393
+ }));
2394
+ }
2395
+ continue;
2396
+ }
2397
+ changed = true;
2398
+ }
2399
+
2400
+ for (const seed of LIVE_WALL_SEED_SOURCES) {
2401
+ const sourceUrlHash = liveWallSourceHash(seed.sourceUrl);
2402
+ if (existingSourceHashes.has(sourceUrlHash)) continue;
2403
+ try {
2404
+ const archived = await archiveKaleidoscopeGeneratedImage(seed.sourceUrl);
2405
+ if (existingUrls.has(archived.url)) continue;
2406
+ images.push(liveWallEntryFromArchived({
2407
+ archived,
2408
+ createdAt: seed.createdAt,
2409
+ kind: seed.kind,
2410
+ }));
2411
+ existingSourceHashes.add(sourceUrlHash);
2412
+ existingUrls.add(archived.url);
2413
+ changed = true;
2414
+ } catch (err) {
2415
+ console.error("Kaleidoscope live wall seed archive failed:", JSON.stringify({
2416
+ sourceUrlHash,
2417
+ message: err.message,
2418
+ }));
2419
+ }
2420
+ }
2421
+
2422
+ if (changed) saveLiveWallState({ images: images.slice(0, LIVE_WALL_LIMIT) });
2423
+ }
2424
+
2425
+ async function registerKaleidoscopeLiveWallEvent({ kind, imageUrl }) {
2426
+ if (kind !== "generic" && kind !== "image") return;
2427
+ let archived;
2428
+ try {
2429
+ archived = await archiveKaleidoscopeGeneratedImage(imageUrl);
2430
+ } catch (err) {
2431
+ console.error("Kaleidoscope live wall archive failed:", JSON.stringify({
2432
+ kind,
2433
+ message: err.message,
2434
+ }));
2435
+ return;
2436
+ }
2437
+
2438
+ const state = loadLiveWallState();
2439
+ const mediaImages = state.images
2440
+ .filter(item => item && isLiveWallMediaUrl(item.url) && liveWallMediaFileExists(item.url))
2441
+ .filter(item => item.url !== archived.url && item.sourceUrlHash !== archived.sourceUrlHash);
2442
+ const rawImages = state.images
2443
+ .filter(item => item && isAllowedLiveWallSourceUrl(item.url))
2444
+ .filter(item => liveWallEntrySourceHash(item) !== archived.sourceUrlHash);
2445
+ const images = [
2446
+ liveWallEntryFromArchived({
2447
+ archived,
2448
+ createdAt: new Date().toISOString(),
2449
+ kind,
2450
+ }),
2451
+ ...mediaImages,
2452
+ ...rawImages,
2453
+ ];
2454
+ saveLiveWallState({ images: images.slice(0, LIVE_WALL_LIMIT) });
2455
+ return archived.url;
2456
+ }
2457
+
2458
+ async function handleKaleidoscopeLiveWall(req, res) {
2459
+ await ensureLiveWallSeedImages();
2460
+ const state = loadLiveWallState();
2461
+ const images = state.images
2462
+ .filter(item => item && isLiveWallMediaUrl(item.url) && liveWallMediaFileExists(item.url))
2463
+ .slice(0, LIVE_WALL_LIMIT);
2464
+ const stats = deriveKaleidoscopePublicStats({ images, passkeyEntries: passkeys });
2465
+ res.setHeader("Cache-Control", "no-store");
2466
+ json(res, 200, {
2467
+ count: images.length,
2468
+ stats,
2469
+ images: images.map(item => ({ url: item.url, createdAt: item.createdAt || null })),
2470
+ });
2471
+ }
2472
+
1889
2473
  // POST /demo/api/analyze-photo
1890
2474
  // Sends a base64 image to OpenAI GPT-4o vision to extract colors/mood.
1891
2475
  async function handleDemoAnalyzePhoto(req, res) {
@@ -1958,6 +2542,7 @@ async function handleDemoImagine(req, res) {
1958
2542
  try {
1959
2543
  const body = await readBody(req);
1960
2544
  const prompt = body?.prompt || "kaleidoscope";
2545
+ const liveWallKind = classifyLiveWallPrompt(prompt);
1961
2546
 
1962
2547
  const XAI_KEY = process.env.XAI_API_KEY || "";
1963
2548
  if (!XAI_KEY) {
@@ -1965,34 +2550,57 @@ async function handleDemoImagine(req, res) {
1965
2550
  return;
1966
2551
  }
1967
2552
 
2553
+ const imageModel = process.env.XAI_IMAGE_MODEL || "grok-imagine-image-quality";
2554
+ const imageRequestBody = {
2555
+ model: imageModel,
2556
+ prompt: prompt,
2557
+ };
1968
2558
  const grokRes = await fetch("https://api.x.ai/v1/images/generations", {
1969
2559
  method: "POST",
1970
2560
  headers: {
1971
2561
  "Content-Type": "application/json",
1972
2562
  "Authorization": "Bearer " + XAI_KEY,
1973
2563
  },
1974
- body: JSON.stringify({
1975
- model: "grok-imagine-image",
1976
- prompt: prompt,
1977
- n: 1,
1978
- }),
2564
+ body: JSON.stringify(imageRequestBody),
1979
2565
  });
1980
2566
 
1981
- const grokData = await grokRes.json();
1982
- if (grokData.error) {
1983
- json(res, 502, { error: grokData.error.message || "Image generation failed" });
2567
+ const grokText = await grokRes.text();
2568
+ let grokData;
2569
+ try {
2570
+ grokData = grokText ? JSON.parse(grokText) : {};
2571
+ } catch {
2572
+ grokData = { error: { message: "Non-JSON image API response" } };
2573
+ }
2574
+
2575
+ if (!grokRes.ok || grokData.error) {
2576
+ const message = grokData.error?.message || grokRes.statusText || "Image generation failed";
2577
+ console.error("Demo imagine upstream error:", JSON.stringify({
2578
+ status: grokRes.status,
2579
+ model: imageModel,
2580
+ message,
2581
+ type: grokData.error?.type || null,
2582
+ code: grokData.error?.code || null,
2583
+ param: grokData.error?.param || null,
2584
+ }));
2585
+ json(res, 502, { error: message });
1984
2586
  return;
1985
2587
  }
1986
2588
 
1987
2589
  const imageUrl = grokData.data?.[0]?.url;
1988
2590
  if (!imageUrl) {
2591
+ console.error("Demo imagine upstream returned no image URL:", JSON.stringify({
2592
+ status: grokRes.status,
2593
+ model: imageModel,
2594
+ responseKeys: Object.keys(grokData || {}),
2595
+ }));
1989
2596
  json(res, 502, { error: "No image returned" });
1990
2597
  return;
1991
2598
  }
1992
2599
 
1993
2600
  const newBalance = await deductBalance(identity.agentId, IMAGE_COST_CENTS);
1994
- console.log("Demo: generated image for agent '" + identity.agentId + "' (balance: " + formatCents(newBalance) + ")");
1995
- json(res, 200, { url: imageUrl, prompt: prompt, cost: formatCents(IMAGE_COST_CENTS), balance: formatCents(newBalance) });
2601
+ const archivedImageUrl = await registerKaleidoscopeLiveWallEvent({ kind: liveWallKind, imageUrl });
2602
+ console.log("Demo: generated image for agent '" + identity.agentId + "' using " + imageModel + " (balance: " + formatCents(newBalance) + ")");
2603
+ json(res, 200, { url: archivedImageUrl || imageUrl, prompt: prompt, cost: formatCents(IMAGE_COST_CENTS), balance: formatCents(newBalance) });
1996
2604
  } catch (err) {
1997
2605
  console.error("Demo imagine error:", err.message);
1998
2606
  json(res, 500, { error: "Internal error" });
@@ -2238,6 +2846,11 @@ const httpServer = createServer(async (req, res) => {
2238
2846
 
2239
2847
  // --- Shared assets (Kaleidoscope template system) ---
2240
2848
 
2849
+ if (req.method === "GET" && path.startsWith(LIVE_WALL_MEDIA_ROUTE)) {
2850
+ serveKaleidoscopeGeneratedMedia(path, res);
2851
+ return;
2852
+ }
2853
+
2241
2854
  if (req.method === "GET" && path.startsWith("/shared/")) {
2242
2855
  const filePath = join(__dirname, path);
2243
2856
  try {
@@ -2309,6 +2922,33 @@ const httpServer = createServer(async (req, res) => {
2309
2922
 
2310
2923
  // --- Legal pages ---
2311
2924
 
2925
+ if (req.method === "GET" && path === "/legal/legal.css") {
2926
+ try {
2927
+ const css = readFileSync(join(__dirname, "legal", "legal.css"), "utf8");
2928
+ res.writeHead(200, { "Content-Type": "text/css; charset=utf-8" });
2929
+ res.end(css);
2930
+ } catch { json(res, 404, { error: "Not found" }); }
2931
+ return;
2932
+ }
2933
+
2934
+ if (req.method === "GET" && path === "/legal/legal-footer.js") {
2935
+ try {
2936
+ const js = readFileSync(join(__dirname, "legal", "legal-footer.js"), "utf8");
2937
+ res.writeHead(200, { "Content-Type": "text/javascript; charset=utf-8" });
2938
+ res.end(js);
2939
+ } catch { json(res, 404, { error: "Not found" }); }
2940
+ return;
2941
+ }
2942
+
2943
+ if (req.method === "GET" && (path === "/legal/privacy/" || path === "/legal/privacy")) {
2944
+ try {
2945
+ const html = readFileSync(join(__dirname, "legal", "privacy", "index.html"), "utf8");
2946
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
2947
+ res.end(html);
2948
+ } catch { json(res, 404, { error: "Not found" }); }
2949
+ return;
2950
+ }
2951
+
2312
2952
  if (req.method === "GET" && (path === "/legal/privacy/en-ww/" || path === "/legal/privacy/en-ww")) {
2313
2953
  try {
2314
2954
  const html = readFileSync(join(__dirname, "legal", "privacy", "en-ww", "index.html"), "utf8");
@@ -2327,6 +2967,15 @@ const httpServer = createServer(async (req, res) => {
2327
2967
  return;
2328
2968
  }
2329
2969
 
2970
+ if (req.method === "GET" && (path === "/legal/internet-services/kaleidoscope/" || path === "/legal/internet-services/kaleidoscope")) {
2971
+ try {
2972
+ const html = readFileSync(join(__dirname, "legal", "internet-services", "kaleidoscope", "index.html"), "utf8");
2973
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
2974
+ res.end(html);
2975
+ } catch { json(res, 404, { error: "Not found" }); }
2976
+ return;
2977
+ }
2978
+
2330
2979
  // --- WebAuthn API ---
2331
2980
 
2332
2981
  if (req.method === "POST" && path === "/webauthn/register-options") {
@@ -2473,6 +3122,12 @@ const httpServer = createServer(async (req, res) => {
2473
3122
  return;
2474
3123
  }
2475
3124
 
3125
+ if (req.method === "GET" && path === "/demo/api/kaleidoscope-live-wall") {
3126
+ if (!applyRateLimit(req, res, "status")) return;
3127
+ await handleKaleidoscopeLiveWall(req, res);
3128
+ return;
3129
+ }
3130
+
2476
3131
  if (req.method === "POST" && path === "/demo/api/analyze-photo") {
2477
3132
  await handleDemoAnalyzePhoto(req, res);
2478
3133
  return;
@@ -2593,21 +3248,24 @@ const httpServer = createServer(async (req, res) => {
2593
3248
  //
2594
3249
  // In-memory state. Pairing codes: 6-char, 5-min TTL. Daemons indexed by
2595
3250
  // immutable tenant id (one daemon per tenant; new daemon kicks the old one). Web clients
2596
- // indexed by `tenantId:threadId`. Server is a transparent passthrough between
2597
- // the daemon and the matching web client(s); thread routing is enforced
2598
- // purely client-side via session.send/sessionId payloads.
3251
+ // indexed by `tenantId:threadId`. The server is a transport relay between
3252
+ // the daemon and matching web client(s). The relay injects the route thread
3253
+ // into the E2EE handshake, and the daemon enforces that bound route after
3254
+ // decrypting session commands.
2599
3255
 
2600
3256
  const CODEX_PAIR_EXPIRY_MS = 5 * 60 * 1000;
3257
+ const CODEX_PAIR_PRESENCE_TTL_MS = 2 * 60 * 1000;
2601
3258
  const CODEX_PAIR_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
2602
3259
  const codexPairings = {}; // pairing_id -> { code, status, expires, poll_token, poll_token_used?, daemon_info, apiKey?, agentId?, handle?, daemon_public_key?, crypto_versions? }
2603
3260
  const codexPairingByCode = {}; // code -> pairing_id (only while pending)
3261
+ const codexPairPresenceTokens = new Map(); // token -> { agentId, expires, used }
2604
3262
  const codexDaemons = new Map(); // agentId -> ws
2605
3263
  const codexWebClients = new Map(); // `${agentId}:${threadId}` -> Set<ws>
2606
3264
  const codexE2eeSessionRoutes = new Map(); // `${agentId}:${e2eeSession}` -> { threadId, webKey, ws }
2607
3265
 
2608
3266
  // E2EE substrate (Phase 2.5).
2609
3267
  //
2610
- // codexDaemonPubkeys: per tenant id, the most recently paired daemon's
3268
+ // codexDaemonPubkeyRegistry: per tenant id, the most recently paired daemon's
2611
3269
  // public key (P-256 SPKI base64url) + supported crypto versions +
2612
3270
  // registration timestamp. This is what the browser fetches via
2613
3271
  // bootstrap before opening an encrypted session.
@@ -2616,10 +3274,18 @@ const codexE2eeSessionRoutes = new Map(); // `${agentId}:${e2eeSession}` -> { th
2616
3274
  // ?token=ck-... in the browser WebSocket URL. Bound to a specific
2617
3275
  // (agentId, threadId) so a leaked ticket cannot drive a different
2618
3276
  // route, even by the same authenticated user.
2619
- const codexDaemonPubkeys = new Map(); // tenantId -> { pubkey, crypto_versions, registered_at }
2620
3277
  const codexRelayTickets = new Map(); // ticket -> { agentId, threadId, expires, used }
2621
3278
  const CODEX_RELAY_TICKET_TTL_MS = 60 * 1000; // 60s; browser must connect immediately
2622
3279
 
3280
+ const codexDaemonPubkeyRegistry = createCodexDaemonPubkeyRegistry({
3281
+ usePrisma,
3282
+ prisma,
3283
+ devMode: DEV_MODE,
3284
+ logger: console,
3285
+ });
3286
+
3287
+ await codexDaemonPubkeyRegistry.loadFromDb();
3288
+
2623
3289
  function codexRelayKey(agentId, id) {
2624
3290
  return agentId + ":" + id;
2625
3291
  }
@@ -2677,6 +3343,34 @@ function removeCodexE2eeRoutesForWeb(agentId, threadId, ws) {
2677
3343
  }
2678
3344
  }
2679
3345
 
3346
+ function invalidateCodexBrowserSessionsForAgent(agentId, reason) {
3347
+ const prefix = agentId + ":";
3348
+ let closed = 0;
3349
+ for (const routeKey of [...codexE2eeSessionRoutes.keys()]) {
3350
+ if (routeKey.startsWith(prefix)) codexE2eeSessionRoutes.delete(routeKey);
3351
+ }
3352
+ for (const [webKey, clients] of [...codexWebClients]) {
3353
+ if (!webKey.startsWith(prefix)) continue;
3354
+ for (const webWs of clients) {
3355
+ if (webWs.readyState === webWs.OPEN) {
3356
+ closed += 1;
3357
+ try { webWs.close(4001, reason); } catch {}
3358
+ }
3359
+ }
3360
+ codexWebClients.delete(webKey);
3361
+ }
3362
+ return closed;
3363
+ }
3364
+
3365
+ function logCodexWsLimit({ agentId, threadId, connectionId, reason }) {
3366
+ console.warn(formatCodexWsLimitLog({ agentId, threadId, connectionId, reason }));
3367
+ }
3368
+
3369
+ function closeCodexWsForLimit(ws, { agentId, threadId, connectionId }, decision) {
3370
+ logCodexWsLimit({ agentId, threadId, connectionId, reason: decision.reason });
3371
+ try { ws.close(decision.code, decision.reason); } catch {}
3372
+ }
3373
+
2680
3374
  function generateCodexPairingCode() {
2681
3375
  for (let attempt = 0; attempt < 100; attempt += 1) {
2682
3376
  let code = "";
@@ -2693,6 +3387,34 @@ function generateCodexPairPollToken() {
2693
3387
  return "ppt_" + randomBytes(32).toString("base64url");
2694
3388
  }
2695
3389
 
3390
+ function cleanupCodexPairPresenceTokens() {
3391
+ const now = Date.now();
3392
+ for (const [token, entry] of codexPairPresenceTokens) {
3393
+ if (!entry || entry.used || now > entry.expires) codexPairPresenceTokens.delete(token);
3394
+ }
3395
+ }
3396
+
3397
+ function generateCodexPairPresenceToken(agentId) {
3398
+ cleanupCodexPairPresenceTokens();
3399
+ if (typeof agentId !== "string" || !agentId) return null;
3400
+ const token = "cpt_" + randomBytes(32).toString("base64url");
3401
+ codexPairPresenceTokens.set(token, {
3402
+ agentId,
3403
+ expires: Date.now() + CODEX_PAIR_PRESENCE_TTL_MS,
3404
+ used: false,
3405
+ });
3406
+ return token;
3407
+ }
3408
+
3409
+ function consumeCodexPairPresenceToken(token, agentId) {
3410
+ cleanupCodexPairPresenceTokens();
3411
+ const entry = codexPairPresenceTokens.get(token);
3412
+ if (!entry || entry.used || entry.agentId !== agentId || Date.now() > entry.expires) return false;
3413
+ entry.used = true;
3414
+ codexPairPresenceTokens.delete(token);
3415
+ return true;
3416
+ }
3417
+
2696
3418
  function getBearerToken(req) {
2697
3419
  const auth = req.headers["authorization"];
2698
3420
  if (typeof auth !== "string" || !auth.startsWith("Bearer ")) return null;
@@ -2757,7 +3479,12 @@ function handleCodexPairStatus(req, res, pairingId) {
2757
3479
  }
2758
3480
  if (p.status === "completed") {
2759
3481
  p.poll_token_used = true;
2760
- json(res, 200, { status: "completed", api_key: p.apiKey, handle: p.handle || p.agentId });
3482
+ json(res, 200, {
3483
+ status: "completed",
3484
+ api_key: p.apiKey,
3485
+ handle: p.handle || p.agentId,
3486
+ replaced_daemon_key: !!p.replaced_daemon_key,
3487
+ });
2761
3488
  } else {
2762
3489
  json(res, 200, { status: p.status });
2763
3490
  }
@@ -2785,24 +3512,44 @@ async function handleCodexPairComplete(req, res) {
2785
3512
  json(res, 410, { error: "code expired or already used" });
2786
3513
  return;
2787
3514
  }
2788
- p.status = "completed";
2789
- p.apiKey = identity.apiKey;
2790
- p.agentId = identity.agentId;
2791
- p.handle = identity.handle;
3515
+ const pairPresenceToken = body && typeof body.codex_pair_presence_token === "string"
3516
+ ? body.codex_pair_presence_token
3517
+ : "";
3518
+ if (p.daemon_public_key && !consumeCodexPairPresenceToken(pairPresenceToken, identity.agentId)) {
3519
+ json(res, 403, {
3520
+ error: "fresh_presence_required",
3521
+ error_description: "Pairing this daemon requires a fresh passkey confirmation. Sign in again from the pair page.",
3522
+ });
3523
+ return;
3524
+ }
2792
3525
  // Phase 2.5: register the daemon's E2EE public key against the
2793
3526
  // authenticated immutable tenant id. The display handle is returned
2794
3527
  // as metadata only.
3528
+ let daemonKeyResult = null;
2795
3529
  if (p.daemon_public_key) {
2796
- codexDaemonPubkeys.set(identity.agentId, {
2797
- pubkey: p.daemon_public_key,
2798
- crypto_versions: p.crypto_versions && p.crypto_versions.length ? p.crypto_versions : ["e2ee-v1"],
2799
- registered_at: new Date().toISOString(),
2800
- });
2801
- console.log("codex-relay: registered E2EE pubkey for " + identity.agentId);
3530
+ daemonKeyResult = await codexDaemonPubkeyRegistry.register(identity.agentId, p.daemon_public_key, p.crypto_versions, "pair-complete");
3531
+ if (daemonKeyResult?.replaced) {
3532
+ const closed = invalidateCodexBrowserSessionsForAgent(identity.agentId, "daemon key replaced");
3533
+ console.log(
3534
+ "codex-relay: replaced daemon E2EE key for tenant " + identity.agentId
3535
+ + " old=" + daemonKeyResult.old_fingerprint
3536
+ + " new=" + daemonKeyResult.new_fingerprint
3537
+ + " closed_browser_sessions=" + closed
3538
+ );
3539
+ }
2802
3540
  }
3541
+ p.status = "completed";
3542
+ p.apiKey = identity.apiKey;
3543
+ p.agentId = identity.agentId;
3544
+ p.handle = identity.handle;
3545
+ p.replaced_daemon_key = !!daemonKeyResult?.replaced;
2803
3546
  delete codexPairingByCode[code];
2804
3547
  console.log("codex-relay: paired daemon for tenant " + identity.agentId + " handle " + identity.handle);
2805
- json(res, 200, { ok: true, handle: identity.handle });
3548
+ json(res, 200, {
3549
+ ok: true,
3550
+ handle: identity.handle,
3551
+ replaced_daemon_key: !!daemonKeyResult?.replaced,
3552
+ });
2806
3553
  }
2807
3554
 
2808
3555
  function handleCodexRelayState(req, res) {
@@ -2823,16 +3570,8 @@ function handleCodexBootstrap(req, res, threadId) {
2823
3570
  if (!identity) { json(res, 401, { error: "Unauthorized" }); return; }
2824
3571
  if (!threadId) { json(res, 400, { error: "missing threadId" }); return; }
2825
3572
  const daemonOnline = codexDaemons.has(identity.agentId);
2826
- const daemonKey = codexDaemonPubkeys.get(identity.agentId) || null;
2827
- json(res, 200, {
2828
- handle: identity.handle,
2829
- thread_id: threadId,
2830
- daemon_online: daemonOnline,
2831
- daemon_public_key: daemonKey ? daemonKey.pubkey : null,
2832
- daemon_crypto_versions: daemonKey ? daemonKey.crypto_versions : null,
2833
- supported_crypto_versions: ["e2ee-v1"],
2834
- e2ee_available: !!daemonKey,
2835
- });
3573
+ const daemonKey = codexDaemonPubkeyRegistry.get(identity.agentId);
3574
+ json(res, 200, buildCodexBootstrapPayload({ identity, threadId, daemonOnline, daemonKey }));
2836
3575
  }
2837
3576
 
2838
3577
  // POST /api/codex-relay/ws-ticket
@@ -3037,10 +3776,19 @@ httpServer.on("upgrade", (req, socket, head) => {
3037
3776
 
3038
3777
  if (isDaemon) {
3039
3778
  codexRelayWss.handleUpgrade(req, socket, head, (ws) => {
3040
- const previous = codexDaemons.get(identity.agentId);
3041
- if (previous && previous !== ws) try { previous.close(4000, "replaced"); } catch {}
3042
- codexDaemons.set(identity.agentId, ws);
3043
- console.log("codex-relay: daemon online for " + identity.agentId);
3779
+ let daemonIdentityAccepted = false;
3780
+ function activateCodexDaemonWs() {
3781
+ const previous = codexDaemons.get(identity.agentId);
3782
+ if (previous && previous !== ws && previous.readyState === previous.OPEN) {
3783
+ console.warn("codex-relay: rejected duplicate daemon reconnect for online tenant " + identity.agentId);
3784
+ try { ws.close(4004, "daemon already online"); } catch {}
3785
+ return false;
3786
+ }
3787
+ if (previous && previous !== ws) try { previous.close(4000, "replaced"); } catch {}
3788
+ codexDaemons.set(identity.agentId, ws);
3789
+ console.log("codex-relay: daemon online for " + identity.agentId);
3790
+ return true;
3791
+ }
3044
3792
  // F-001 per-thread isolation. Daemon -> web routing must NOT
3045
3793
  // fan out every frame to every same-agent web socket; that
3046
3794
  // breaks isolation when one user has multiple threads open.
@@ -3062,6 +3810,44 @@ httpServer.on("upgrade", (req, socket, head) => {
3062
3810
  const text = data.toString();
3063
3811
  let envelope = null;
3064
3812
  try { envelope = JSON.parse(text); } catch {}
3813
+ if (envelope?.type === "daemon.identity") {
3814
+ const reconnectPolicy = evaluateCodexDaemonReconnectPubkey(
3815
+ codexDaemonPubkeyRegistry.get(identity.agentId),
3816
+ envelope.daemon_public_key,
3817
+ );
3818
+ if (!reconnectPolicy.allowed) {
3819
+ console.warn(
3820
+ "codex-relay: rejected daemon reconnect E2EE key for tenant " + identity.agentId
3821
+ + " reason=" + reconnectPolicy.reason
3822
+ + " old=" + (reconnectPolicy.old_fingerprint || "<none>")
3823
+ + " new=" + (reconnectPolicy.new_fingerprint || codexDaemonPubkeyFingerprint(envelope.daemon_public_key) || "<none>"),
3824
+ );
3825
+ const closeReason = reconnectPolicy.replaced
3826
+ ? "daemon key change requires fresh pair"
3827
+ : "invalid daemon identity";
3828
+ try { ws.close(reconnectPolicy.replaced ? 4003 : 1008, closeReason); } catch {}
3829
+ return;
3830
+ }
3831
+ void codexDaemonPubkeyRegistry.register(
3832
+ identity.agentId,
3833
+ envelope.daemon_public_key,
3834
+ envelope.crypto_versions,
3835
+ "daemon-reconnect",
3836
+ ).then((result) => {
3837
+ if (!result?.registered) {
3838
+ try { ws.close(1011, "daemon identity persistence failed"); } catch {}
3839
+ return;
3840
+ }
3841
+ daemonIdentityAccepted = activateCodexDaemonWs();
3842
+ }).catch(() => {
3843
+ try { ws.close(1011, "daemon identity persistence failed"); } catch {}
3844
+ });
3845
+ return;
3846
+ }
3847
+ if (!daemonIdentityAccepted) {
3848
+ try { ws.close(1008, "daemon identity required"); } catch {}
3849
+ return;
3850
+ }
3065
3851
  const sessionId = envelope?.session || envelope?.sessionId || envelope?.threadId;
3066
3852
  if (sessionId) {
3067
3853
  const targets = resolveCodexWebClientsForDaemonFrame(identity.agentId, sessionId);
@@ -3114,27 +3900,87 @@ httpServer.on("upgrade", (req, socket, head) => {
3114
3900
  socket.destroy();
3115
3901
  return;
3116
3902
  }
3903
+ const webKey = codexRelayKey(identity.agentId, threadId);
3904
+ if (isCodexWsAgentDisabled(CODEX_WS_ABUSE_LIMITS, identity.agentId)) {
3905
+ console.warn(formatCodexWsLimitLog({
3906
+ agentId: identity.agentId,
3907
+ threadId,
3908
+ connectionId: "upgrade",
3909
+ reason: "operator disabled",
3910
+ }));
3911
+ socket.write("HTTP/1.1 503 Service Unavailable\r\nConnection: close\r\n\r\n");
3912
+ socket.destroy();
3913
+ return;
3914
+ }
3915
+ const openBrowserSockets = openCodexWebClientsForKey(webKey).length;
3916
+ if (openBrowserSockets >= CODEX_WS_ABUSE_LIMITS.maxBrowserSocketsPerThread) {
3917
+ console.warn(formatCodexWsLimitLog({
3918
+ agentId: identity.agentId,
3919
+ threadId,
3920
+ connectionId: "upgrade",
3921
+ reason: "too many browser sockets",
3922
+ }));
3923
+ socket.write("HTTP/1.1 429 Too Many Requests\r\nConnection: close\r\n\r\n");
3924
+ socket.destroy();
3925
+ return;
3926
+ }
3117
3927
  codexRelayWss.handleUpgrade(req, socket, head, (ws) => {
3118
- const key = codexRelayKey(identity.agentId, threadId);
3119
- const clientCount = addCodexWebClient(key, ws);
3120
- console.log("codex-relay: web online " + key + " clients=" + clientCount);
3928
+ const connectionId = randomUUID();
3929
+ const guard = createCodexWsConnectionGuard({
3930
+ config: CODEX_WS_ABUSE_LIMITS,
3931
+ agentId: identity.agentId,
3932
+ });
3933
+ const guardContext = { agentId: identity.agentId, threadId, connectionId };
3934
+ const idleIntervalMs = Math.max(1000, Math.min(60_000, Math.floor(CODEX_WS_ABUSE_LIMITS.idleTtlMs / 2)));
3935
+ const idleTimer = setInterval(() => {
3936
+ const decision = guard.observeIdle();
3937
+ if (!decision.ok && ws.readyState === ws.OPEN) {
3938
+ closeCodexWsForLimit(ws, guardContext, decision);
3939
+ }
3940
+ }, idleIntervalMs);
3941
+ const clientCount = addCodexWebClient(webKey, ws);
3942
+ console.log("codex-relay: web online " + webKey + " clients=" + clientCount + " conn=" + connectionId);
3121
3943
  ws.on("message", (data) => {
3122
- const text = data.toString();
3944
+ const frameDecision = guard.observeFrame(codexWsFrameByteLength(data));
3945
+ if (!frameDecision.ok) {
3946
+ closeCodexWsForLimit(ws, guardContext, frameDecision);
3947
+ return;
3948
+ }
3949
+ let text = data.toString();
3123
3950
  let envelope = null;
3124
3951
  try { envelope = JSON.parse(text); } catch {}
3952
+ if (!envelope || typeof envelope !== "object" || Array.isArray(envelope)) {
3953
+ const malformedDecision = guard.observeMalformed();
3954
+ if (!malformedDecision.ok) {
3955
+ closeCodexWsForLimit(ws, guardContext, malformedDecision);
3956
+ }
3957
+ return;
3958
+ }
3125
3959
  if (isCodexE2eeEnvelope(envelope) && envelope.session) {
3960
+ // The browser cannot be allowed to choose this value. The relay
3961
+ // owns the route because it consumed the ticket for this URL
3962
+ // thread. The daemon uses this metadata to bind the encrypted
3963
+ // session before it decrypts any session.* command.
3964
+ envelope.route_thread_id = threadId;
3965
+ text = JSON.stringify(envelope);
3126
3966
  registerCodexE2eeSessionRoute(identity.agentId, envelope.session, threadId, ws);
3127
3967
  }
3128
3968
  const daemonWs = codexDaemons.get(identity.agentId);
3129
3969
  if (daemonWs && daemonWs.readyState === daemonWs.OPEN) {
3970
+ const pendingDecision = guard.observePendingBytes(daemonWs.bufferedAmount || 0);
3971
+ if (!pendingDecision.ok) {
3972
+ closeCodexWsForLimit(ws, guardContext, pendingDecision);
3973
+ return;
3974
+ }
3130
3975
  daemonWs.send(text);
3131
3976
  } else {
3132
3977
  try { ws.send(JSON.stringify({ type: "error", message: "daemon offline" })); } catch {}
3133
3978
  }
3134
3979
  });
3135
3980
  ws.on("close", () => {
3981
+ clearInterval(idleTimer);
3136
3982
  removeCodexE2eeRoutesForWeb(identity.agentId, threadId, ws);
3137
- removeCodexWebClient(key, ws);
3983
+ removeCodexWebClient(webKey, ws);
3138
3984
  });
3139
3985
  ws.on("error", (err) => {
3140
3986
  console.error("codex-relay web ws error:", err.message);