@wipcomputer/wip-ldm-os 0.4.85-alpha.30 → 0.4.85-alpha.32

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 (32) hide show
  1. package/bin/ldm.js +103 -0
  2. package/package.json +8 -2
  3. package/scripts/test-boot-hook-registration.mjs +136 -0
  4. package/scripts/test-boot-payload-trim.mjs +200 -0
  5. package/scripts/test-crc-pair-login-flow.mjs +76 -1
  6. package/scripts/test-doctor-hook-dedupe.mjs +188 -0
  7. package/scripts/test-kaleidoscope-onboarding-copy.mjs +170 -0
  8. package/scripts/test-kaleidoscope-public-stats-baseline.mjs +129 -0
  9. package/scripts/test-kaleidoscope-qr-authenticator-confirmation.mjs +89 -0
  10. package/shared/boot/boot-config.json +18 -8
  11. package/shared/docs/dev-guide-wipcomputerinc.md.tmpl +5 -4
  12. package/shared/rules/security.md +4 -0
  13. package/src/boot/README.md +24 -1
  14. package/src/boot/boot-config.json +18 -8
  15. package/src/boot/boot-hook.mjs +118 -28
  16. package/src/boot/installer.mjs +33 -10
  17. package/src/hosted-mcp/.env.example +4 -0
  18. package/src/hosted-mcp/app/footer.js +2 -2
  19. package/src/hosted-mcp/app/kaleidoscope-login.html +486 -42
  20. package/src/hosted-mcp/app/wip-logo.png +0 -0
  21. package/src/hosted-mcp/demo/footer.js +2 -2
  22. package/src/hosted-mcp/demo/index.html +166 -44
  23. package/src/hosted-mcp/demo/login.html +87 -23
  24. package/src/hosted-mcp/demo/privacy.html +4 -214
  25. package/src/hosted-mcp/demo/tos.html +4 -189
  26. package/src/hosted-mcp/legal/internet-services/kaleidoscope/index.html +257 -0
  27. package/src/hosted-mcp/legal/internet-services/terms/site.html +224 -168
  28. package/src/hosted-mcp/legal/legal-footer.js +75 -0
  29. package/src/hosted-mcp/legal/legal.css +166 -0
  30. package/src/hosted-mcp/legal/privacy/en-ww/index.html +4 -221
  31. package/src/hosted-mcp/legal/privacy/index.html +253 -0
  32. package/src/hosted-mcp/server.mjs +662 -35
@@ -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";
@@ -1706,7 +1706,7 @@ function handleAgentAuthApprove(req, res) {
1706
1706
 
1707
1707
  // ---------- QR Login (Chrome fallback) ----------
1708
1708
 
1709
- // `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
1710
1710
  // land the user on a known phone-side surface after successful sign-in.
1711
1711
  // Anything else is silently dropped. `next` is NOT a general redirect
1712
1712
  // primitive.
@@ -1722,8 +1722,12 @@ function handleAgentAuthApprove(req, res) {
1722
1722
  // Kaleidoscope phone-side remote-control thread surface. Standard
1723
1723
  // ?next semantics; allowed on both desktop and mobile (this is
1724
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.
1725
1728
  const PAIR_NEXT_REGEX = /^\/pair\/[ABCDEFGHJKLMNPQRSTUVWXYZ23456789]{6}$/;
1726
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$/;
1727
1731
 
1728
1732
  function sanitizeCrcPairNext(raw) {
1729
1733
  if (typeof raw !== "string") return null;
@@ -1732,7 +1736,7 @@ function sanitizeCrcPairNext(raw) {
1732
1736
  try { decoded = decodeURIComponent(raw); } catch { return null; }
1733
1737
  // Catch double-encoded payloads.
1734
1738
  if (decoded !== raw && /%/.test(decoded)) return null;
1735
- 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;
1736
1740
  return decoded;
1737
1741
  }
1738
1742
 
@@ -1747,10 +1751,11 @@ async function handleQrLoginStart(req, res) {
1747
1751
  //
1748
1752
  // Only /pair/<CODE> next triggers pair-mode (C6 strip on desktop
1749
1753
  // status, C8 desktop-no-redirect, the "phone is the actor" model).
1750
- // /codex-remote-control/<UUID> is a normal post-login continuation:
1751
- // desktop status returns the full login response (apiKey, handle,
1752
- // next) so the desktop poll can authenticate and redirect on its
1753
- // 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.
1754
1759
  const next = sanitizeCrcPairNext(body && body.next);
1755
1760
  const purpose = (next && PAIR_NEXT_REGEX.test(next)) ? "pair" : null;
1756
1761
  const sessionId = randomUUID();
@@ -1764,7 +1769,7 @@ async function handleQrLoginStart(req, res) {
1764
1769
  handle: handle || null,
1765
1770
  expires: Date.now() + QR_LOGIN_EXPIRY_MS,
1766
1771
  purpose, // "pair" | null
1767
- next: next || null, // sanitized `/pair/<CODE>` or null
1772
+ next: next || null, // sanitized `/pair/<CODE>`, `/codex-remote-control/<UUID>`, `/demo`, or null
1768
1773
  };
1769
1774
  console.log("QR login: created session " + sessionId.slice(0, 8) + "..." + (purpose === "pair" ? " (pair-mode)" : ""));
1770
1775
  json(res, 200, { sessionId, qrUrl: "/api/qr-login/qr?s=" + sessionId });
@@ -1805,17 +1810,18 @@ function handleQrLoginStatus(req, res) {
1805
1810
  // C6 round 4. Desktop never becomes the pairing authority.
1806
1811
  json(res, 200, { status: "approved", agentId: entry.agentId });
1807
1812
  } else {
1808
- // Legacy login mode OR codex-remote-control continuation
1813
+ // Legacy login mode OR standard login continuation
1809
1814
  // (purpose === null). Desktop gets full identity to render the
1810
1815
  // welcome view OR redirect to next on its own poll.
1811
1816
  // credentialLabel matches the saved-passkey label (see
1812
1817
  // register-verify / auth-verify). next is included only if a
1813
1818
  // sanitized non-pair-mode next was set on the session
1814
- // (currently /codex-remote-control/<UUID>); legacy login
1815
- // sessions without next get next === null.
1819
+ // (/codex-remote-control/<UUID> or /demo); legacy login sessions
1820
+ // without next get next === null.
1816
1821
  json(res, 200, {
1817
1822
  status: "approved",
1818
1823
  agentId: entry.agentId,
1824
+ tenantId: entry.tenantId || null,
1819
1825
  apiKey: entry.apiKey,
1820
1826
  credentialLabel: entry.credentialLabel || null,
1821
1827
  next: entry.next || null,
@@ -1834,7 +1840,7 @@ function handleQrLoginStatus(req, res) {
1834
1840
  // {ok: true} unchanged.
1835
1841
  function handleQrLoginApprove(req, res) {
1836
1842
  readBody(req).then(function(body) {
1837
- const { sessionId, agentId, apiKey, credentialLabel } = body || {};
1843
+ const { sessionId, agentId, apiKey, tenantId, credentialLabel } = body || {};
1838
1844
  const entry = qrLoginSessions[sessionId];
1839
1845
  if (!entry || Date.now() > entry.expires) {
1840
1846
  json(res, 404, { error: "Session not found or expired" });
@@ -1847,16 +1853,19 @@ function handleQrLoginApprove(req, res) {
1847
1853
  entry.status = "approved";
1848
1854
  entry.agentId = agentId;
1849
1855
  entry.apiKey = apiKey;
1856
+ const verifiedIdentity = identityForApiKey(apiKey);
1857
+ entry.tenantId = verifiedIdentity?.tenantId || (isInternalTenantId(tenantId) ? tenantId : null);
1850
1858
  // Phone-side passes the label it received from register-verify /
1851
1859
  // auth-verify so the desktop can show the same string the user
1852
1860
  // just saved on their phone. Optional for back-compat.
1853
1861
  entry.credentialLabel = (typeof credentialLabel === "string" && credentialLabel.length <= 64) ? credentialLabel : null;
1854
1862
  console.log("QR login: approved session " + sessionId.slice(0, 8) + "... for '" + agentId + "'" + (entry.purpose === "pair" ? " (pair-mode)" : (entry.next ? " (next=" + entry.next + ")" : "")));
1855
1863
  // Phone receives next on approve regardless of purpose, so the
1856
- // phone can redirect to either /pair/<CODE> (pair-mode, phone is
1857
- // the actor) or /codex-remote-control/<UUID> (continuation, phone
1858
- // can act). Desktop's separate behavior (strip vs full response)
1859
- // 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.
1860
1869
  if (entry.next) {
1861
1870
  json(res, 200, { ok: true, next: entry.next });
1862
1871
  } else {
@@ -1870,32 +1879,77 @@ function handleQrLoginApprove(req, res) {
1870
1879
  // ---------- Demo API handlers ----------
1871
1880
 
1872
1881
  // ── Wallet tracking (per agent) ──
1873
- const IMAGE_COST_CENTS = 1; // $0.01
1874
- 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
1875
1884
 
1876
1885
  // JSON fallback for wallets
1877
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");
1878
1888
  function loadWalletsFromFile() { try { return JSON.parse(readFileSync(WALLET_FILE, "utf8")); } catch { return {}; } }
1879
1889
  function saveWalletsToFile(w) { try { writeFileSync(WALLET_FILE, JSON.stringify(w, null, 2) + "\n"); } catch {} }
1880
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
+
1881
1933
  async function getBalance(agentId) {
1934
+ const walletUserId = walletUserIdForAgent(agentId);
1882
1935
  if (usePrisma) {
1883
1936
  try {
1884
- const wallet = await prisma.wallet.findFirst({ where: { userId: agentId } });
1937
+ const wallet = await prisma.wallet.findFirst({ where: { userId: walletUserId } });
1885
1938
  return wallet ? wallet.balance : INITIAL_BALANCE_CENTS;
1886
1939
  } catch {}
1887
1940
  }
1888
1941
  const w = loadWalletsFromFile();
1889
- return w[agentId] !== undefined ? w[agentId] : INITIAL_BALANCE_CENTS;
1942
+ return w[walletUserId] !== undefined ? w[walletUserId] : INITIAL_BALANCE_CENTS;
1890
1943
  }
1891
1944
 
1892
1945
  async function deductBalance(agentId, cents) {
1946
+ const walletUserId = walletUserIdForAgent(agentId);
1893
1947
  if (usePrisma) {
1894
1948
  try {
1895
- let wallet = await prisma.wallet.findFirst({ where: { userId: agentId } });
1949
+ let wallet = await prisma.wallet.findFirst({ where: { userId: walletUserId } });
1896
1950
  if (!wallet) {
1897
1951
  wallet = await prisma.wallet.create({
1898
- data: { userId: agentId, balance: INITIAL_BALANCE_CENTS },
1952
+ data: { userId: walletUserId, balance: INITIAL_BALANCE_CENTS },
1899
1953
  });
1900
1954
  }
1901
1955
  const newBalance = Math.max(0, wallet.balance - cents);
@@ -1907,13 +1961,515 @@ async function deductBalance(agentId, cents) {
1907
1961
  }
1908
1962
  // JSON fallback
1909
1963
  const w = loadWalletsFromFile();
1910
- if (w[agentId] === undefined) w[agentId] = INITIAL_BALANCE_CENTS;
1911
- 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);
1912
1966
  saveWalletsToFile(w);
1913
- return w[agentId];
1967
+ return w[walletUserId];
1914
1968
  }
1915
1969
  function formatCents(c) { return "$" + (c / 100).toFixed(2); }
1916
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
+
1917
2473
  // POST /demo/api/analyze-photo
1918
2474
  // Sends a base64 image to OpenAI GPT-4o vision to extract colors/mood.
1919
2475
  async function handleDemoAnalyzePhoto(req, res) {
@@ -1986,6 +2542,7 @@ async function handleDemoImagine(req, res) {
1986
2542
  try {
1987
2543
  const body = await readBody(req);
1988
2544
  const prompt = body?.prompt || "kaleidoscope";
2545
+ const liveWallKind = classifyLiveWallPrompt(prompt);
1989
2546
 
1990
2547
  const XAI_KEY = process.env.XAI_API_KEY || "";
1991
2548
  if (!XAI_KEY) {
@@ -1993,34 +2550,57 @@ async function handleDemoImagine(req, res) {
1993
2550
  return;
1994
2551
  }
1995
2552
 
2553
+ const imageModel = process.env.XAI_IMAGE_MODEL || "grok-imagine-image-quality";
2554
+ const imageRequestBody = {
2555
+ model: imageModel,
2556
+ prompt: prompt,
2557
+ };
1996
2558
  const grokRes = await fetch("https://api.x.ai/v1/images/generations", {
1997
2559
  method: "POST",
1998
2560
  headers: {
1999
2561
  "Content-Type": "application/json",
2000
2562
  "Authorization": "Bearer " + XAI_KEY,
2001
2563
  },
2002
- body: JSON.stringify({
2003
- model: "grok-imagine-image",
2004
- prompt: prompt,
2005
- n: 1,
2006
- }),
2564
+ body: JSON.stringify(imageRequestBody),
2007
2565
  });
2008
2566
 
2009
- const grokData = await grokRes.json();
2010
- if (grokData.error) {
2011
- 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 });
2012
2586
  return;
2013
2587
  }
2014
2588
 
2015
2589
  const imageUrl = grokData.data?.[0]?.url;
2016
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
+ }));
2017
2596
  json(res, 502, { error: "No image returned" });
2018
2597
  return;
2019
2598
  }
2020
2599
 
2021
2600
  const newBalance = await deductBalance(identity.agentId, IMAGE_COST_CENTS);
2022
- console.log("Demo: generated image for agent '" + identity.agentId + "' (balance: " + formatCents(newBalance) + ")");
2023
- 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) });
2024
2604
  } catch (err) {
2025
2605
  console.error("Demo imagine error:", err.message);
2026
2606
  json(res, 500, { error: "Internal error" });
@@ -2266,6 +2846,11 @@ const httpServer = createServer(async (req, res) => {
2266
2846
 
2267
2847
  // --- Shared assets (Kaleidoscope template system) ---
2268
2848
 
2849
+ if (req.method === "GET" && path.startsWith(LIVE_WALL_MEDIA_ROUTE)) {
2850
+ serveKaleidoscopeGeneratedMedia(path, res);
2851
+ return;
2852
+ }
2853
+
2269
2854
  if (req.method === "GET" && path.startsWith("/shared/")) {
2270
2855
  const filePath = join(__dirname, path);
2271
2856
  try {
@@ -2337,6 +2922,33 @@ const httpServer = createServer(async (req, res) => {
2337
2922
 
2338
2923
  // --- Legal pages ---
2339
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
+
2340
2952
  if (req.method === "GET" && (path === "/legal/privacy/en-ww/" || path === "/legal/privacy/en-ww")) {
2341
2953
  try {
2342
2954
  const html = readFileSync(join(__dirname, "legal", "privacy", "en-ww", "index.html"), "utf8");
@@ -2355,6 +2967,15 @@ const httpServer = createServer(async (req, res) => {
2355
2967
  return;
2356
2968
  }
2357
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
+
2358
2979
  // --- WebAuthn API ---
2359
2980
 
2360
2981
  if (req.method === "POST" && path === "/webauthn/register-options") {
@@ -2501,6 +3122,12 @@ const httpServer = createServer(async (req, res) => {
2501
3122
  return;
2502
3123
  }
2503
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
+
2504
3131
  if (req.method === "POST" && path === "/demo/api/analyze-photo") {
2505
3132
  await handleDemoAnalyzePhoto(req, res);
2506
3133
  return;