@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.
- package/README.md +22 -2
- package/SKILL.md +137 -15
- package/bin/ldm.js +608 -75
- package/lib/deploy.mjs +36 -1
- package/lib/registry-migrations.mjs +296 -0
- package/package.json +20 -3
- package/scripts/test-boot-dir-truth.mjs +158 -0
- package/scripts/test-boot-hook-registration.mjs +136 -0
- package/scripts/test-boot-payload-trim.mjs +200 -0
- package/scripts/test-crc-agentid-tenant-boundary.mjs +2 -2
- package/scripts/test-crc-e2ee-key-persistence.mjs +150 -0
- package/scripts/test-crc-e2ee-session-route.mjs +9 -2
- package/scripts/test-crc-pair-login-flow.mjs +76 -1
- package/scripts/test-crc-pair-relink-audit-and-rotation.mjs +164 -0
- package/scripts/test-crc-websocket-abuse-limits.mjs +128 -0
- package/scripts/test-deploy-hook-ownership.mjs +160 -0
- package/scripts/test-doctor-hook-dedupe.mjs +188 -0
- package/scripts/test-install-prompt-policy.mjs +84 -0
- package/scripts/test-installer-target-self-update.mjs +131 -0
- package/scripts/test-kaleidoscope-onboarding-copy.mjs +170 -0
- package/scripts/test-kaleidoscope-public-stats-baseline.mjs +129 -0
- package/scripts/test-kaleidoscope-qr-authenticator-confirmation.mjs +89 -0
- package/scripts/test-ldm-status-concurrency.mjs +118 -0
- package/scripts/test-ldm-status-timeout.mjs +96 -0
- package/scripts/test-legacy-npm-sources-migration.mjs +460 -0
- package/scripts/test-readme-install-prompt.mjs +66 -0
- package/shared/boot/boot-config.json +18 -8
- package/shared/docs/dev-guide-wipcomputerinc.md.tmpl +5 -4
- package/shared/rules/security.md +4 -0
- package/shared/templates/install-prompt.md +20 -2
- package/src/boot/README.md +24 -1
- package/src/boot/boot-config.json +18 -8
- package/src/boot/boot-hook.mjs +118 -28
- package/src/boot/installer.mjs +33 -10
- package/src/hosted-mcp/.env.example +4 -0
- package/src/hosted-mcp/README.md +37 -0
- package/src/hosted-mcp/app/footer.js +2 -2
- package/src/hosted-mcp/app/kaleidoscope-login.html +489 -42
- package/src/hosted-mcp/app/pair.html +21 -3
- package/src/hosted-mcp/app/wip-logo.png +0 -0
- package/src/hosted-mcp/codex-relay-e2ee-registry.mjs +208 -0
- package/src/hosted-mcp/codex-relay-ws-abuse-limits.mjs +140 -0
- package/src/hosted-mcp/demo/footer.js +2 -2
- package/src/hosted-mcp/demo/index.html +166 -44
- package/src/hosted-mcp/demo/login.html +87 -23
- package/src/hosted-mcp/demo/privacy.html +4 -214
- package/src/hosted-mcp/demo/tos.html +4 -189
- package/src/hosted-mcp/deploy.sh +2 -0
- package/src/hosted-mcp/docs/self-host.md +268 -0
- package/src/hosted-mcp/legal/internet-services/kaleidoscope/index.html +257 -0
- package/src/hosted-mcp/legal/internet-services/terms/site.html +224 -168
- package/src/hosted-mcp/legal/legal-footer.js +75 -0
- package/src/hosted-mcp/legal/legal.css +166 -0
- package/src/hosted-mcp/legal/privacy/en-ww/index.html +4 -221
- package/src/hosted-mcp/legal/privacy/index.html +253 -0
- 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, {
|
|
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,
|
|
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, {
|
|
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.
|
|
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>
|
|
1723
|
-
// desktop status returns the full login response
|
|
1724
|
-
// next) so the desktop poll can authenticate and
|
|
1725
|
-
// own. The phone also gets next via approve, so both
|
|
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
|
|
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
|
|
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
|
-
// (
|
|
1787
|
-
//
|
|
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
|
|
1829
|
-
//
|
|
1830
|
-
//
|
|
1831
|
-
// is handled in
|
|
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 =
|
|
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:
|
|
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[
|
|
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:
|
|
1949
|
+
let wallet = await prisma.wallet.findFirst({ where: { userId: walletUserId } });
|
|
1868
1950
|
if (!wallet) {
|
|
1869
1951
|
wallet = await prisma.wallet.create({
|
|
1870
|
-
data: { userId:
|
|
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[
|
|
1883
|
-
w[
|
|
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[
|
|
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
|
|
1982
|
-
|
|
1983
|
-
|
|
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
|
-
|
|
1995
|
-
|
|
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`.
|
|
2597
|
-
// the daemon and
|
|
2598
|
-
//
|
|
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
|
-
//
|
|
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, {
|
|
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
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
p.
|
|
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
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
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, {
|
|
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 =
|
|
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
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
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
|
|
3119
|
-
const
|
|
3120
|
-
|
|
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
|
|
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(
|
|
3983
|
+
removeCodexWebClient(webKey, ws);
|
|
3138
3984
|
});
|
|
3139
3985
|
ws.on("error", (err) => {
|
|
3140
3986
|
console.error("codex-relay web ws error:", err.message);
|