@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.
- package/bin/ldm.js +103 -0
- package/package.json +8 -2
- package/scripts/test-boot-hook-registration.mjs +136 -0
- package/scripts/test-boot-payload-trim.mjs +200 -0
- package/scripts/test-crc-pair-login-flow.mjs +76 -1
- package/scripts/test-doctor-hook-dedupe.mjs +188 -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/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/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/app/footer.js +2 -2
- package/src/hosted-mcp/app/kaleidoscope-login.html +486 -42
- package/src/hosted-mcp/app/wip-logo.png +0 -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/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 +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.
|
|
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>
|
|
1751
|
-
// desktop status returns the full login response
|
|
1752
|
-
// next) so the desktop poll can authenticate and
|
|
1753
|
-
// 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.
|
|
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
|
|
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
|
|
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
|
-
// (
|
|
1815
|
-
//
|
|
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
|
|
1857
|
-
//
|
|
1858
|
-
//
|
|
1859
|
-
// 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.
|
|
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 =
|
|
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:
|
|
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[
|
|
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:
|
|
1949
|
+
let wallet = await prisma.wallet.findFirst({ where: { userId: walletUserId } });
|
|
1896
1950
|
if (!wallet) {
|
|
1897
1951
|
wallet = await prisma.wallet.create({
|
|
1898
|
-
data: { userId:
|
|
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[
|
|
1911
|
-
w[
|
|
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[
|
|
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
|
|
2010
|
-
|
|
2011
|
-
|
|
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
|
-
|
|
2023
|
-
|
|
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;
|