Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
JavaScript
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465// lith — AC monolith server// Wraps Netlify function handlers in Express routes + serves static files.
// Shim awslambda before anything imports @netlify/functions.// Netlify's stream() calls awslambda.streamifyResponse() at wrap time,// which doesn't exist outside AWS Lambda. This shim adapts the 3-arg// streaming function (event, responseStream, context) back to a normal// 2-arg handler (event, context) that returns {statusCode, headers, body}.import { PassThrough } from "stream";import { Readable } from "stream";
if (typeof globalThis.awslambda === "undefined") { globalThis.awslambda = { streamifyResponse: (wrappedFn) => { // wrappedFn expects (event, responseStream, context). // It calls the real handler(event, context) internally, then pipes // the body to responseStream via pipeline(). We provide a PassThrough // as the responseStream and return it as the response body. return async (event, context) => { const pt = new PassThrough();
// Promise that resolves when HttpResponseStream.from() is called // inside wrappedFn, giving us the response metadata (statusCode, headers). let resolveMetadata; const metadataPromise = new Promise((r) => { resolveMetadata = r; }); pt._resolveMetadata = resolveMetadata;
// Start the pipeline (don't await — data streams to pt asynchronously) wrappedFn(event, pt, context).catch((err) => { if (!pt.destroyed) pt.destroy(err); });
const metadata = await metadataPromise; const webStream = Readable.toWeb(pt);
return { ...metadata, body: webStream }; }; }, HttpResponseStream: { from: (stream, metadata) => { // Signal metadata to the adapter above if (stream._resolveMetadata) stream._resolveMetadata(metadata || {}); return stream; }, }, };}
import express from "express";import { userMediaTarget } from "./media-path.mjs";import { readdirSync, readFileSync, existsSync, mkdirSync, writeFileSync, renameSync, statSync } from "fs";import { join, dirname } from "path";import { fileURLToPath, pathToFileURL } from "url";import { createServer as createHttpsServer } from "https";import { createServer as createHttpServer } from "http";import { resolveFunctionName } from "./route-resolution.mjs";import { provenance } from "../shared/provenance.mjs";import { browserAnalyticsConfig, createEndpointAnalytics,} from "./product-analytics.mjs";
const __dirname = dirname(fileURLToPath(import.meta.url));const SYSTEM = join(__dirname, "..", "system");const PUBLIC = join(SYSTEM, "public");const FN_DIR = join(SYSTEM, "netlify", "functions");
// Load .env from system/ if present (handles special chars in values)const envPath = join(SYSTEM, ".env");if (existsSync(envPath)) { for (const line of readFileSync(envPath, "utf-8").split("\n")) { if (!line || line.startsWith("#")) continue; const idx = line.indexOf("="); if (idx === -1) continue; const key = line.slice(0, idx).trim(); const val = line.slice(idx + 1).trim(); if (key && !process.env[key]) process.env[key] = val; }}
const endpointAnalytics = createEndpointAnalytics();
const PORT = process.env.PORT || 8888;const DEV = process.env.NODE_ENV !== "production";
// Tell functions we're in dev mode (so index.mjs uses cwd instead of /var/task)if (DEV) { process.env.CONTEXT = process.env.CONTEXT || "dev"; process.env.NETLIFY_DEV = process.env.NETLIFY_DEV || "true";}
// Set cwd to system/ so relative paths in functions resolve correctlyprocess.chdir(SYSTEM);
// SSL certs for local dev (same ones Netlify local context uses)const SSL_CERT = join(__dirname, "..", "ssl-dev", "localhost.pem");const SSL_KEY = join(__dirname, "..", "ssl-dev", "localhost-key.pem");const HAS_SSL = existsSync(SSL_CERT) && existsSync(SSL_KEY);
const app = express();const BOOT_TIME = Date.now();
// --- Response cache for hot GET endpoints ---const responseCache = new Map(); // key → { body, headers, statusCode, expires }const CACHE_TTLS = { "handle-colors": 60_000, // 1 min (colors rarely change) "version": 30_000, // 30s (git state) "handles": 60_000, // 1 min "mood": 30_000, // 30s "tv": 30_000, // 30s "keeps-config": 300_000, // 5 min (contract addresses) "kidlisp-count": 60_000, // 1 min "playlist": 60_000, // 1 min "clock": 0, // never cache (it's a clock)};
// Clean expired entries every 30ssetInterval(() => { const now = Date.now(); for (const [k, v] of responseCache) { if (v.expires < now) responseCache.delete(k); }}, 30_000);
// --- Function stats & error log ---const fnStats = {}; // { fnName: { calls, errors, totalMs, lastCall, lastError } }const errorLog = []; // [{ time, fn, status, error, path, method }]const requestLog = []; // [{ time, fn, ms, status, path, method }]const MAX_ERROR_LOG = 500;const MAX_REQUEST_LOG = 1000;
function recordCall(name, ms, status, path, method, error) { if (!fnStats[name]) fnStats[name] = { calls: 0, errors: 0, totalMs: 0, lastCall: null, lastError: null }; const s = fnStats[name]; s.calls++; s.totalMs += ms; s.lastCall = new Date().toISOString();
requestLog.unshift({ time: s.lastCall, fn: name, ms: Math.round(ms), status, path, method }); if (requestLog.length > MAX_REQUEST_LOG) requestLog.length = MAX_REQUEST_LOG;
if (error || status >= 500) { s.errors++; s.lastError = new Date().toISOString(); errorLog.unshift({ time: s.lastError, fn: name, status, error: error || `HTTP ${status}`, path, method }); if (errorLog.length > MAX_ERROR_LOG) errorLog.length = MAX_ERROR_LOG; }
endpointAnalytics.capture(name, ms, status, method);}
function captureRawBody(req, _res, buf) { if (buf?.length) req.rawBody = Buffer.from(buf);}
// --- Body parsing ---app.use(express.json({ limit: "50mb", verify: captureRawBody }));app.use(express.urlencoded({ extended: true, limit: "50mb", verify: captureRawBody }));app.use(express.raw({ type: "*/*", limit: "50mb", verify: captureRawBody }));
// --- CORS (mirrors Netlify _headers) ---app.use((req, res, next) => { res.set("Access-Control-Allow-Origin", "*"); res.set( "Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With", ); res.set( "Access-Control-Expose-Headers", "Content-Length, Content-Disposition, X-AC-OS-Requested-Layout, X-AC-OS-Layout, X-AC-OS-Fallback, X-AC-OS-Fallback-Reason, X-Build, X-Patch", ); res.set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); if (req.method === "OPTIONS") return res.sendStatus(204); next();});
// Provenance, in the fleet-wide shape from shared/provenance.mjs. Under /api/// rather than /health because the root namespace belongs to pieces — a bare// /health would shadow anyone who publishes a piece by that name.//// lith deploys by pulling knot into a real checkout, so it can read its own// commit; scp-deployed services get theirs stamped at deploy time instead.app.get("/api/health", (_req, res) => { res.set("Cache-Control", "no-store"); res.json(provenance("lith"));});
app.get("/api/product-analytics-config", (_req, res) => { const config = browserAnalyticsConfig(); if (!config) return res.status(404).json({ error: "Analytics disabled" }); res.set("Cache-Control", "public, max-age=60"); return res.json(config);});
// Count only reviewed Lith-native product surfaces. The analytics adapter maps// the path to a static name and discards params, queries, headers and bodies.app.use((req, res, next) => { const startedAt = Date.now(); res.once("finish", () => { endpointAnalytics.captureSurface( req.path, Date.now() - startedAt, res.statusCode, req.method, ); }); next();});
// --- Whistlegraph prompt routes ---// commands.json is regenerated with the site model and marks which codes may// safely occupy AC's bare command namespace. Every work also gets the explicit// /wg/<code> route, including names that collide with pieces such as `line`.const WG_COMMANDS_PATH = join(PUBLIC, "whistlegraph.org", "commands.json");let wgCommandCache = { mtime: 0, byCode: new Map() };
function whistlegraphCommand(code) { try { const mtime = statSync(WG_COMMANDS_PATH).mtimeMs; if (mtime !== wgCommandCache.mtime) { const data = JSON.parse(readFileSync(WG_COMMANDS_PATH, "utf8")); const commands = Array.isArray(data) ? data : data.commands || []; wgCommandCache = { mtime, byCode: new Map(commands.map((entry) => [String(entry.code).toLowerCase(), entry])), }; } } catch (err) { return null; // Fail open to the normal AC router during deploys/rebuilds. } return wgCommandCache.byCode.get(String(code || "").toLowerCase()) || null;}
app.use((req, res, next) => { if (req.method !== "GET" && req.method !== "HEAD") return next(); const host = (req.headers.host || "").split(":")[0].toLowerCase(); const isAc = host === "aesthetic.computer" || host === "www.aesthetic.computer"; const isPrompt = host === "prompt.ac" || host === "www.prompt.ac"; if (!isAc && !isPrompt) return next();
const explicit = req.path.match(/^\/wg\/([A-Za-z0-9]+)\/?$/); if (explicit) { const entry = whistlegraphCommand(explicit[1]); if (entry) return res.redirect(302, entry.url || `https://whistlegraph.org/${entry.code}`); }
const bare = req.path.match(/^\/([A-Za-z0-9]+)\/?$/); if (bare) { const entry = whistlegraphCommand(bare[1]); if (entry?.bare) return res.redirect(302, entry.url || `https://whistlegraph.org/${entry.code}`); }
// Caddy sends non-root prompt.ac routes here so the same live resolver owns // both domains. Preserve the old behavior when no Whistlegraph route wins. if (isPrompt && req.path !== "/") { return res.redirect(301, `https://aesthetic.computer${req.originalUrl}`); } next();});
// --- Host-based rewrites that Netlify previously handled ---app.use((req, _res, next) => { const host = (req.headers.host || "").split(":")[0].toLowerCase();
// Preserve branded notepat.com URLs while serving the /notepat piece. if ((host === "notepat.com" || host === "www.notepat.com") && req.path === "/") { req.url = "/notepat" + (req.url === "/" ? "" : req.url.slice(req.path.length)); }
// api.aesthetic.computer / api.prompt.ac → api-docs function (public API reference) if (host === "api.aesthetic.computer" || host === "api.prompt.ac") { if (req.path === "/" || req.path === "") { req.url = "/api-docs" + (req.url.includes("?") ? req.url.slice(req.url.indexOf("?")) : ""); } }
// data.aesthetic.computer → crm function (Linked Open Data / CIDOC CRM). // Every path under the subdomain routes to the `crm` handler, which dispatches // by path internally (landing, /@handle, /painting, /piece, /mood, /sparql). if (host === "data.aesthetic.computer" || host === "data.prompt.ac") { if (!req.path.startsWith("/api/crm")) { req.url = "/api/crm" + (req.url === "/" ? "" : req.url); } }
next();});
// --- Load Netlify functions ---const functions = {};const functionFiles = {}; // name → absolute file path (DEV hot-reload)const functionMtimes = {}; // name → last-loaded mtimeMs (DEV hot-reload)
// Scripts that call process.exit() at import time — not API functions.const SKIP = new Set(["backfill-painting-codes", "test-tv-hits"]);
// Import one function file and register its handler. In DEV, `bust` appends a// cache-busting query so re-imports actually pick up edits (ESM caches by URL,// so without this the first import of e.g. index.mjs is frozen for the life of// the process — which is why boot-canvas edits never showed up locally).async function loadFunction(file, bust = false) { if (!file.endsWith(".mjs") && !file.endsWith(".js")) return; const name = file.replace(/\.(mjs|js)$/, ""); if (SKIP.has(name)) return; try { const full = join(FN_DIR, file); const spec = pathToFileURL(full).href + (bust ? `?v=${Date.now()}` : ""); const mod = await import(spec); let registered = false; if (mod.handler) { // Netlify Functions v1: export { handler } functions[name] = mod.handler; registered = true; } else if (mod.default && typeof mod.default === "function") { // Netlify Functions v2: export default async (req) => { ... } // Wrap v2 handler to match v1 event/context signature const v2fn = mod.default; functions[name] = async (event, context) => { // V2 functions receive a Request-like object; build one from the event const url = event.rawUrl || `http://localhost${event.path || "/"}`; const req = new Request(url, { method: event.httpMethod, headers: event.headers, body: event.httpMethod !== "GET" && event.httpMethod !== "HEAD" ? event.body : undefined, }); req.query = event.queryStringParameters; const resp = await v2fn(req, context); // V2 returns a Web Response object const body = await resp.text(); const headers = {}; resp.headers.forEach((v, k) => { headers[k] = v; }); return { statusCode: resp.status, headers, body }; }; registered = true; } if (registered && DEV) { functionFiles[name] = full; try { functionMtimes[name] = statSync(full).mtimeMs; } catch {} } } catch (err) { console.warn(` skip: ${name} (${err.message})`); }}
for (const file of readdirSync(FN_DIR)) await loadFunction(file);
console.log(`Loaded ${Object.keys(functions).length} functions`);
// 🔁 DEV hot-reload: re-import the requested function before dispatching so// source edits (e.g. index.mjs boot HTML) show up without restarting. We reload// on a short throttle rather than trusting fs.watch/inotify OR file mtime —// BOTH are unreliable on WSL2 / Docker bind mounts (events don't fire, mtime// lags). Throttling bounds the cost: a burst of requests re-imports a given// function at most ~twice/sec, and only the function actually being hit.const lastReloadAt = {};async function freshHandler(name) { if (DEV && functionFiles[name]) { const now = Date.now(); if (now - (lastReloadAt[name] || 0) > 500) { lastReloadAt[name] = now; try { await loadFunction(functionFiles[name].slice(FN_DIR.length + 1), true); } catch {} } } return functions[name];}if (DEV) console.log("🔁 DEV: netlify functions hot-reload (per-request, throttled)");
// --- Netlify event adapter ---function toEvent(req) { // Reconstruct body as string (Netlify handlers expect string or null). // Prefer rawBody when available — it preserves the exact bytes the client // sent, which is critical for webhook signature verification (Stripe, etc.). let body = null; if (req.rawBody) { body = Buffer.isBuffer(req.rawBody) ? req.rawBody.toString("utf-8") : String(req.rawBody); } else if (req.body) { const contentType = (req.headers["content-type"] || "").toLowerCase(); body = typeof req.body === "string" ? req.body : Buffer.isBuffer(req.body) ? req.body.toString("utf-8") // Preserve HTML form posts as urlencoded strings so legacy handlers // using URLSearchParams(event.body) continue to work after lith. : contentType.includes("application/x-www-form-urlencoded") ? new URLSearchParams( Object.entries(req.body).flatMap(([key, value]) => Array.isArray(value) ? value.map((item) => [key, item]) : [[key, value]], ), ).toString() : JSON.stringify(req.body); }
return { httpMethod: req.method, headers: req.headers, body, rawBody: req.rawBody ?? req.body, queryStringParameters: req.query || {}, path: req.path, rawUrl: `${req.protocol}://${req.get("host")}${req.originalUrl}`, isBase64Encoded: false, };}
// --- Function handler ---async function handleFunction(req, res) { const name = req.params.fn; const handler = await freshHandler(name); if (!handler) { recordCall(name || "unknown", 0, 404, req.path, req.method, "Function not found"); return res.status(404).send("Function not found: " + name); }
// Netlify-style background functions: filename ends in `-background`. The // Netlify runtime responds 202 immediately and keeps the handler running // asynchronously. lith must do the same or callers that `await` the // invocation (e.g. keep-prepare → keep-prepare-background) hang until the // full pipeline completes, blocking the client request. if (name.endsWith("-background")) { const event = toEvent(req); const context = { clientContext: {} }; const t0 = Date.now(); res.status(202).send(""); handler(event, context) .then((result) => { recordCall(name, Date.now() - t0, result?.statusCode || 202, req.path, req.method, null); }) .catch((err) => { recordCall(name, Date.now() - t0, 500, req.path, req.method, err?.message); console.error(`fn/${name} background error:`, err); }); return; }
// Check response cache (GET only, with matching query string) const ttl = CACHE_TTLS[name]; if (ttl && req.method === "GET") { const cacheKey = `${name}:${req.originalUrl}`; const cached = responseCache.get(cacheKey); if (cached && cached.expires > Date.now()) { recordCall(name, 0, cached.statusCode, req.path, req.method, null); if (cached.headers) res.set(cached.headers); res.set("X-Lith-Cache", "HIT"); return res.status(cached.statusCode).send(cached.body); } }
const t0 = Date.now(); try { const event = toEvent(req); const context = { clientContext: {} }; const result = await handler(event, context);
const statusCode = result.statusCode || 200; const ms = Date.now() - t0; recordCall(name, ms, statusCode, req.path, req.method, statusCode >= 500 ? result.body : null);
if (result.headers) res.set(result.headers); if (result.multiValueHeaders) { for (const [k, vals] of Object.entries(result.multiValueHeaders)) { for (const v of vals) res.append(k, v); } }
// Handle ReadableStream bodies (from streaming functions like ask, keep-mint) if (result.body && typeof result.body === "object" && typeof result.body.getReader === "function") { res.status(statusCode); const reader = result.body.getReader(); const pump = async () => { while (true) { const { done, value } = await reader.read(); if (done) { res.end(); return; } res.write(value); } }; return pump().catch((err) => { console.error(`fn/${name} stream error:`, err); res.end(); }); }
// Store in cache if cacheable if (ttl && req.method === "GET" && statusCode < 400) { const cacheKey = `${name}:${req.originalUrl}`; responseCache.set(cacheKey, { body: result.isBase64Encoded ? Buffer.from(result.body, "base64") : result.body, headers: result.headers, statusCode, expires: Date.now() + ttl, }); }
if (result.isBase64Encoded) { res.status(statusCode).send(Buffer.from(result.body, "base64")); } else { res.status(statusCode).send(result.body); } } catch (err) { const ms = Date.now() - t0; recordCall(name, ms, 500, req.path, req.method, err.message); console.error(`fn/${name} error:`, err); res.status(500).send("Internal Server Error"); }}
// Resolve function name from URL paramsfunction resolveFunction(req) { return resolveFunctionName(req.params.fn, req.params.rest, functions);}
// --- Function handler (updated to use resolveFunction) ---async function handleFunctionResolved(req, res) { req.params.fn = resolveFunction(req); return handleFunction(req, res);}
// --- Deploy webhook (POST /lith/deploy?secret=...) ---import { execFile, spawnSync } from "child_process";import { createHmac, timingSafeEqual } from "crypto";const DEPLOY_SECRET = process.env.DEPLOY_SECRET || "";const DEPLOY_BRANCHES = (process.env.DEPLOY_BRANCHES || process.env.DEPLOY_BRANCH || "main,master") .split(",") .map((branch) => branch.trim()) .filter(Boolean);const DEFAULT_DEPLOY_BRANCH = DEPLOY_BRANCHES[0] || "main";let deployInProgress = false;let queuedDeployBranch = null;
function normalizeDeployBranch(branch) { if (typeof branch !== "string") return null; const trimmed = branch.trim(); if (!trimmed) return null; if (!/^[A-Za-z0-9._/-]+$/.test(trimmed)) return null; return trimmed;}
function branchFromRef(ref) { if (typeof ref !== "string") return null; const prefix = "refs/heads/"; if (!ref.startsWith(prefix)) return null; return normalizeDeployBranch(ref.slice(prefix.length));}
function requestedDeployBranch(req) { const fromRef = branchFromRef(req.body?.ref); if (fromRef) return fromRef; return ( normalizeDeployBranch(req.query.branch) || normalizeDeployBranch(req.headers["x-deploy-branch"]) || DEFAULT_DEPLOY_BRANCH );}
function verifyDeploy(req) { // GitHub HMAC signature (webhook secret) const sig = req.headers["x-hub-signature-256"]; if (sig && DEPLOY_SECRET) { const rawBody = Buffer.isBuffer(req.rawBody) ? req.rawBody : Buffer.from( typeof req.body === "string" ? req.body : JSON.stringify(req.body ?? {}), "utf8", ); const hmac = createHmac("sha256", DEPLOY_SECRET) .update(rawBody) .digest("hex"); const expected = `sha256=${hmac}`; if (sig.length === expected.length && timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) { return true; } } // Fallback: query param or header (manual triggers) const plain = req.query.secret || req.headers["x-deploy-secret"]; return plain === DEPLOY_SECRET;}
function runDeploy(branch) { deployInProgress = true; console.log(`[deploy] starting branch=${branch}`);
execFile( "/opt/ac/lith/webhook.sh", { timeout: 120000, env: { ...process.env, DEPLOY_BRANCH: branch }, }, (err, stdout, stderr) => { deployInProgress = false;
if (stdout?.trim()) { console.log(`[deploy][${branch}] ${stdout.trim()}`); } if (stderr?.trim()) { console.error(`[deploy][${branch}] ${stderr.trim()}`); } if (err) { console.error(`[deploy] failed for ${branch}:`, err.message); }
if (queuedDeployBranch) { const nextBranch = queuedDeployBranch; queuedDeployBranch = null; setImmediate(() => runDeploy(nextBranch)); } }, );}
app.post("/lith/deploy", (req, res) => { if (!DEPLOY_SECRET || !verifyDeploy(req)) { return res.status(401).send("Unauthorized"); }
const githubEvent = req.headers["x-github-event"]; if (githubEvent === "ping") { return res.send("pong"); } if (githubEvent && githubEvent !== "push") { return res.send(`Ignored GitHub event: ${githubEvent}`); }
const ref = req.body?.ref; const branch = requestedDeployBranch(req); if (!DEPLOY_BRANCHES.includes(branch)) { const detail = ref || branch; return res.send(`Ignored non-deploy branch: ${detail}`); }
if (deployInProgress) { queuedDeployBranch = branch; return res.status(202).send(`Deploy queued for ${branch}`); }
runDeploy(branch); res.status(202).send(`Deploy started for ${branch}`);});
// --- gym.anthonyzollo.com single-file publisher ---// The site lives outside the git checkout so routine lith deploys cannot// overwrite it. Anthony publishes a complete HTML document with a dedicated// bearer token; the temporary-file rename keeps readers from seeing a partial// upload.const GYM_SITE_DIR = process.env.GYM_SITE_DIR || "/var/lib/aesthetic-computer/gym.anthonyzollo.com";const GYM_HISTORY_DIR = process.env.GYM_HISTORY_DIR || "/var/lib/aesthetic-computer/gym-history.git";const GYM_PUBLISH_TOKEN = process.env.GYM_PUBLISH_TOKEN || "";
function authorizeGym(req, res) { const host = (req.headers.host || "").split(":")[0].toLowerCase(); if (host !== "gym.anthonyzollo.com") { res.status(404).send("Not found"); return false; }
const authorization = req.get("authorization") || ""; if (!GYM_PUBLISH_TOKEN || authorization !== `Bearer ${GYM_PUBLISH_TOKEN}`) { res.status(401).set("WWW-Authenticate", "Bearer").json({ error: "Unauthorized" }); return false; } return true;}
function gymGit(args) { const result = spawnSync("git", [ `--git-dir=${GYM_HISTORY_DIR}`, `--work-tree=${GYM_SITE_DIR}`, ...args, ], { encoding: "utf8" }); if (result.status !== 0) throw new Error((result.stderr || result.stdout || "git command failed").trim()); return result.stdout.trim();}
function ensureGymHistory() { if (!existsSync(GYM_HISTORY_DIR)) { mkdirSync(dirname(GYM_HISTORY_DIR), { recursive: true }); const initialized = spawnSync("git", ["init", "--bare", "--initial-branch=main", GYM_HISTORY_DIR], { encoding: "utf8" }); if (initialized.status !== 0) throw new Error((initialized.stderr || "git init failed").trim()); } gymGit(["config", "user.name", "gym.anthonyzollo.com publisher"]); gymGit(["config", "user.email", "publisher@gym.anthonyzollo.com"]);}
function commitGymHtml(message) { ensureGymHistory(); gymGit(["add", "--", "index.html"]); const changed = spawnSync("git", [ `--git-dir=${GYM_HISTORY_DIR}`, `--work-tree=${GYM_SITE_DIR}`, "diff", "--cached", "--quiet", ]); if (changed.status === 1) gymGit(["commit", "-m", message]); else if (changed.status !== 0) throw new Error("git diff failed"); return gymGit(["rev-parse", "--short", "HEAD"]);}
function installGymHtml(html) { mkdirSync(GYM_SITE_DIR, { recursive: true }); const temporary = join(GYM_SITE_DIR, `.index.html.${process.pid}.tmp`); writeFileSync(temporary, html, { mode: 0o644 }); renameSync(temporary, join(GYM_SITE_DIR, "index.html"));}
app.put("/api/publish-gym", (req, res) => { if (!authorizeGym(req, res)) return;
const html = Buffer.isBuffer(req.body) ? req.body : req.rawBody; if (!html?.length) return res.status(400).json({ error: "Send index.html as the request body" }); if (html.length > 5 * 1024 * 1024) return res.status(413).json({ error: "HTML exceeds 5 MB" });
const publishedAt = new Date().toISOString(); installGymHtml(html); let revision; try { revision = commitGymHtml(`Publish ${publishedAt} (${html.length} bytes)`); } catch (error) { console.error("[gym-publish] history commit failed:", error.message); return res.status(500).json({ error: "Page published, but history commit failed" }); }
console.log(`[gym-publish] ${html.length} bytes published as ${revision}`); res.set("Cache-Control", "no-store").json({ ok: true, bytes: html.length, revision, url: "https://gym.anthonyzollo.com/", publishedAt, });});
app.get("/api/history-gym", (req, res) => { if (!authorizeGym(req, res)) return; try { ensureGymHistory(); const output = gymGit(["log", "-50", "--date=iso-strict", "--pretty=format:%h%x09%aI%x09%s"]); const history = output ? output.split("\n").map((line) => { const [revision, publishedAt, ...message] = line.split("\t"); return { revision, publishedAt, message: message.join("\t") }; }) : []; res.set("Cache-Control", "no-store").json({ history }); } catch (error) { if (error.message.includes("does not have any commits")) return res.json({ history: [] }); console.error("[gym-history]", error.message); res.status(500).json({ error: "Could not read history" }); }});
app.post("/api/rewind-gym", (req, res) => { if (!authorizeGym(req, res)) return; const revision = String(req.body?.revision || ""); if (!/^(?:HEAD(?:~[1-9][0-9]*)?|[0-9a-f]{7,40})$/.test(revision)) { return res.status(400).json({ error: "revision must be HEAD~N or a 7–40 character commit hash" }); } try { ensureGymHistory(); const shown = spawnSync("git", [ `--git-dir=${GYM_HISTORY_DIR}`, "show", `${revision}:index.html`, ], { maxBuffer: 6 * 1024 * 1024 }); if (shown.status !== 0) return res.status(404).json({ error: "Revision not found" }); installGymHtml(shown.stdout); const restoredAs = commitGymHtml(`Rewind to ${revision} at ${new Date().toISOString()}`); res.set("Cache-Control", "no-store").json({ ok: true, restoredFrom: revision, revision: restoredAs, url: "https://gym.anthonyzollo.com/", }); } catch (error) { console.error("[gym-rewind]", error.message); res.status(500).json({ error: "Could not rewind site" }); }});
// --- Routes ---
app.get(["/lith", "/lith/"], (_req, res) => { res.redirect(302, "/lith/stats");});
// --- Lith stats API (consumed by silo dashboard) ---app.get("/lith/stats", (req, res) => { const uptime = Math.floor((Date.now() - BOOT_TIME) / 1000); const mem = process.memoryUsage(); const sorted = Object.entries(fnStats) .map(([name, s]) => ({ name, ...s, avgMs: s.calls ? Math.round(s.totalMs / s.calls) : 0 })) .sort((a, b) => b.calls - a.calls);
res.json({ uptime, boot: new Date(BOOT_TIME).toISOString(), functionsLoaded: Object.keys(functions).length, memory: { rss: Math.round(mem.rss / 1048576), heap: Math.round(mem.heapUsed / 1048576) }, totals: { calls: sorted.reduce((s, f) => s + f.calls, 0), errors: sorted.reduce((s, f) => s + f.errors, 0), }, functions: sorted, });});
app.get("/lith/errors", (req, res) => { const limit = Math.min(parseInt(req.query.limit) || 100, MAX_ERROR_LOG); res.json({ errors: errorLog.slice(0, limit), total: errorLog.length });});
app.get("/lith/requests", (req, res) => { const limit = Math.min(parseInt(req.query.limit) || 100, MAX_REQUEST_LOG); const fn = req.query.fn; const filtered = fn ? requestLog.filter((r) => r.fn === fn) : requestLog; res.json({ requests: filtered.slice(0, limit), total: filtered.length });});
// --- Caddy access log summary (for silo dashboard) ---app.get("/lith/traffic", async (req, res) => { try { const logPath = "/var/log/caddy/access.log"; const lines = readFileSync(logPath, "utf-8").trim().split("\n").filter(Boolean); const recent = lines.slice(-500); // last 500 entries const byPath = {}, byHost = {}, byStatus = {}; let total = 0;
for (const line of recent) { try { const d = JSON.parse(line); const r = d.request || {}; const uri = (r.uri || "/").split("?")[0]; const host = r.host || "unknown"; const status = String(d.status || 0); // Aggregate by first path segment const seg = "/" + (uri.split("/")[1] || ""); byPath[seg] = (byPath[seg] || 0) + 1; byHost[host] = (byHost[host] || 0) + 1; byStatus[status] = (byStatus[status] || 0) + 1; total++; } catch {} }
const sortDesc = (obj) => Object.entries(obj).sort((a, b) => b[1] - a[1]); res.json({ total, logLines: lines.length, byPath: sortDesc(byPath).slice(0, 30), byHost: sortDesc(byHost).slice(0, 20), byStatus: sortDesc(byStatus), }); } catch (err) { res.json({ total: 0, error: err.message }); }});
// --- Farcaster Frame endpoint for KidLisp pieces ---app.get("/frame/:piece", async (req, res) => { const piece = req.params.piece.startsWith("$") ? req.params.piece : `$${req.params.piece}`; const code = piece.slice(1); const base = "https://aesthetic.computer"; const pieceUrl = `${base}/${piece}`; const keepUrl = `https://keep.kidlisp.com/${code}`;
// Try to get thumbnail from oven cache const thumbUrl = `https://oven.aesthetic.computer/grab/webp/600/400/${piece}`; // Fallback OG image const ogImage = `https://oven.aesthetic.computer/kidlisp-og.png`;
const frameEmbed = JSON.stringify({ version: "1", imageUrl: thumbUrl, button: { title: `View ${piece}`, action: { type: "launch_frame", url: pieceUrl, name: `KidLisp ${piece}`, splashImageUrl: "https://assets.aesthetic.computer/kidlisp-favicon.gif", splashBackgroundColor: "#000000", }, }, });
res.setHeader("Content-Type", "text/html; charset=utf-8"); res.send(`<!DOCTYPE html><html><head> <meta charset="utf-8"> <meta property="og:title" content="${piece} — KidLisp" /> <meta property="og:description" content="A KidLisp piece on Aesthetic Computer" /> <meta property="og:image" content="${thumbUrl}" /> <meta property="og:url" content="${pieceUrl}" /> <meta property="fc:frame" content='${frameEmbed.replace(/'/g, "'")}' /> <meta name="fc:frame" content='${frameEmbed.replace(/'/g, "'")}' /> <title>${piece} — KidLisp</title></head><body> <h1>${piece}</h1> <p><a href="${pieceUrl}">View on Aesthetic Computer</a></p> <p><a href="${keepUrl}">Keep on KidLisp</a></p></body></html>`);});
// --- /api/os-release-upload (ports Netlify edge function os-release-upload.js) ---app.post("/api/os-release-upload", async (req, res) => { const { createHmac } = await import("crypto");
// Auth: verify AC token const authHeader = req.headers["authorization"] || ""; const token = authHeader.startsWith("Bearer ") ? authHeader.slice(7).trim() : ""; if (!token) return res.status(401).json({ error: "Missing Authorization: Bearer <ac_token>" });
let user; try { const uiRes = await fetch("https://hi.aesthetic.computer/userinfo", { headers: { Authorization: `Bearer ${token}` }, }); if (!uiRes.ok) throw new Error(`Auth0 ${uiRes.status}`); user = await uiRes.json(); } catch (err) { return res.status(401).json({ error: `Auth failed: ${err.message}` }); }
const userSub = user.sub || "unknown"; const userName = user.name || user.nickname || userSub;
const accessKey = process.env.DO_SPACES_KEY || process.env.ART_KEY; const secretKey = process.env.DO_SPACES_SECRET || process.env.ART_SECRET; if (!accessKey || !secretKey) return res.status(503).json({ error: "Spaces creds not configured" });
const bucket = "releases-aesthetic-computer"; const host = `${bucket}.sfo3.digitaloceanspaces.com`;
const buildName = req.headers["x-build-name"] || `upload-${Date.now()}`; const gitHash = req.headers["x-git-hash"] || "unknown"; const buildTs = req.headers["x-build-ts"] || new Date().toISOString().slice(0, 16); const commitMsg = req.headers["x-commit-msg"] || ""; const version = `${buildName} ${gitHash}-${buildTs}`;
function presignUrl(key, contentType, expiresSec = 900) { const expires = Math.floor(Date.now() / 1000) + expiresSec; const stringToSign = `PUT\n\n${contentType}\n${expires}\nx-amz-acl:public-read\n/${bucket}/${key}`; const sig = createHmac("sha1", secretKey).update(stringToSign).digest("base64"); return `https://${host}/${key}?AWSAccessKeyId=${encodeURIComponent(accessKey)}&Expires=${expires}&Signature=${encodeURIComponent(sig)}&x-amz-acl=public-read`; }
async function s3Put(key, body, contentType) { const dateStr = new Date().toUTCString(); const stringToSign = `PUT\n\n${contentType}\n${dateStr}\nx-amz-acl:public-read\n/${bucket}/${key}`; const sig = createHmac("sha1", secretKey).update(stringToSign).digest("base64"); const putRes = await fetch(`https://${host}/${key}`, { method: "PUT", headers: { Date: dateStr, "Content-Type": contentType, "x-amz-acl": "public-read", Authorization: `AWS ${accessKey}:${sig}` }, body: typeof body === "string" ? body : body, }); if (!putRes.ok) { const text = await putRes.text(); throw new Error(`S3 PUT ${key}: ${putRes.status} ${text.slice(0, 200)}`); } }
async function loadMachineTokenSecret() { try { const connStr = process.env.MONGODB_CONNECTION_STRING; if (!connStr) return null; const { MongoClient } = await import("mongodb"); const client = new MongoClient(connStr); await client.connect(); const dbName = process.env.MONGODB_NAME || "aesthetic"; const doc = await client.db(dbName).collection("secrets").findOne({ _id: "machine-token" }); await client.close(); return doc?.secret || null; } catch (e) { console.error("[os-release-upload] Failed to load machine-token secret:", e.message); return null; } }
async function generateDeviceToken(sub, handle) { const secret = await loadMachineTokenSecret(); if (!secret) return null; const payload = { sub, handle, iat: Math.floor(Date.now() / 1000) }; const payloadB64 = Buffer.from(JSON.stringify(payload)).toString("base64url"); const sigB64 = createHmac("sha256", secret).update(payloadB64).digest("base64url"); return `${payloadB64}.${sigB64}`; }
const isFinalize = req.headers["x-finalize"] === "true";
if (isFinalize) { const sha256 = req.headers["x-sha256"] || "unknown"; const size = parseInt(req.headers["x-size"] || "0", 10); try { const versionWithSize = `${version}\n${size}`; await Promise.all([ s3Put("os/native-notepat-latest.version", versionWithSize, "text/plain"), s3Put("os/native-notepat-latest.sha256", sha256, "text/plain"), ]); let releases = { releases: [] }; try { const existing = await fetch(`https://${host}/os/releases.json`); if (existing.ok) releases = await existing.json(); } catch { /* first release */ } const userHandle = req.headers["x-handle"] || user.nickname || user.name || userName; releases.releases = releases.releases || []; for (const r of releases.releases) r.deprecated = true; releases.releases.unshift({ version, name: buildName, sha256, size, git_hash: gitHash, build_ts: buildTs, commit_msg: commitMsg, user: userSub, handle: userHandle, url: `https://${host}/os/native-notepat-latest.vmlinuz`, archive_url: `https://${host}/os/builds/${buildName}.vmlinuz`, }); releases.releases = releases.releases.slice(0, 50); releases.latest = version; releases.latest_name = buildName; const deviceToken = await generateDeviceToken(userSub, userHandle); if (deviceToken) releases.device_token = deviceToken; await s3Put("os/releases.json", JSON.stringify(releases, null, 2), "application/json"); return res.json({ ok: true, name: buildName, version, sha256, size, url: `https://${host}/os/native-notepat-latest.vmlinuz`, user: userSub, userName, deviceToken: !!deviceToken }); } catch (err) { return res.status(500).json({ error: `Finalize failed: ${err.message}` }); } }
if (req.headers["x-versioned-upload"] === "true") { try { const versionedKey = req.headers["x-versioned-key"] || `os/builds/${buildName}.vmlinuz`; return res.json({ step: "versioned-upload", versioned_put_url: presignUrl(versionedKey, "application/octet-stream", 1800), key: versionedKey, user: userSub }); } catch (err) { return res.status(500).json({ error: `Versioned presign failed: ${err.message}` }); } }
if (req.headers["x-manifest-upload"] === "true") { try { return res.json({ step: "manifest-upload", manifest_put_url: presignUrl("os/latest-manifest.json", "application/json"), user: userSub }); } catch (err) { return res.status(500).json({ error: `Manifest presign failed: ${err.message}` }); } }
if (req.headers["x-template-upload"] === "true") { try { return res.json({ step: "template-upload", image_put_url: presignUrl("os/native-notepat-latest.img", "application/octet-stream"), user: userSub }); } catch (err) { return res.status(500).json({ error: `Template presign failed: ${err.message}` }); } }
// Step 1: Return presigned URL for vmlinuz upload try { return res.json({ step: "upload", vmlinuz_put_url: presignUrl("os/native-notepat-latest.vmlinuz", "application/octet-stream"), version, user: userSub, userName }); } catch (err) { return res.status(500).json({ error: `Presign failed: ${err.message}` }); }});
// --- /api/os-image (ports Netlify edge function os-image.js) ---app.get("/api/os-image", async (req, res) => { const authHeader = req.headers["authorization"] || ""; if (!authHeader) return res.status(401).json({ error: "Authorization required. Log in at aesthetic.computer first." });
try { const search = new URLSearchParams(req.query || {}).toString(); const ovenUrl = "https://oven.aesthetic.computer/os-image" + (search ? `?${search}` : ""); const ovenRes = await fetch(ovenUrl, { headers: { Authorization: authHeader }, }); res.status(ovenRes.status); res.set("Content-Type", ovenRes.headers.get("content-type") || "application/octet-stream"); if (ovenRes.headers.get("content-disposition")) res.set("Content-Disposition", ovenRes.headers.get("content-disposition")); if (ovenRes.headers.get("content-length")) res.set("Content-Length", ovenRes.headers.get("content-length")); if (ovenRes.headers.get("x-ac-os-requested-layout")) res.set("X-AC-OS-Requested-Layout", ovenRes.headers.get("x-ac-os-requested-layout")); if (ovenRes.headers.get("x-ac-os-layout")) res.set("X-AC-OS-Layout", ovenRes.headers.get("x-ac-os-layout")); if (ovenRes.headers.get("x-ac-os-fallback")) res.set("X-AC-OS-Fallback", ovenRes.headers.get("x-ac-os-fallback")); if (ovenRes.headers.get("x-ac-os-fallback-reason")) res.set("X-AC-OS-Fallback-Reason", ovenRes.headers.get("x-ac-os-fallback-reason")); if (ovenRes.headers.get("x-build")) res.set("X-Build", ovenRes.headers.get("x-build")); if (ovenRes.headers.get("x-patch")) res.set("X-Patch", ovenRes.headers.get("x-patch")); res.set("Access-Control-Allow-Origin", "*"); const { Readable } = await import("stream"); Readable.fromWeb(ovenRes.body).pipe(res); } catch (err) { return res.status(502).json({ error: `Oven unavailable: ${err.message}` }); }});
// --- /api/pack-html, /api/bundle-html, /api/os (proxy to oven) ---// netlify.toml used status=200 rewrites for these; on lith we must proxy// explicitly since no `pack-html` / `os` netlify function exists. The// ableton.mjs offline-amxd builder + `pack` / `m4d` prompt commands all// depend on pack-html reaching oven's /pack-html?format=m4d endpoint.async function proxyToOven(ovenPath, req, res) { try { const search = new URLSearchParams(req.query || {}).toString(); const ovenUrl = `https://oven.aesthetic.computer${ovenPath}` + (search ? `?${search}` : ""); const fwdHeaders = {}; if (req.headers.authorization) fwdHeaders.Authorization = req.headers.authorization; if (req.headers.accept) fwdHeaders.Accept = req.headers.accept; const ovenRes = await fetch(ovenUrl, { method: "GET", headers: fwdHeaders }); res.status(ovenRes.status); const forward = [ "content-type", "content-disposition", "content-length", "cache-control", "etag", "last-modified", "x-ac-os-requested-layout", "x-ac-os-layout", "x-ac-os-fallback", "x-ac-os-fallback-reason", "x-build", "x-patch", ]; for (const h of forward) { const v = ovenRes.headers.get(h); if (v) res.set(h, v); } res.set("Access-Control-Allow-Origin", "*"); if (!ovenRes.body) return res.end(); const { Readable } = await import("stream"); Readable.fromWeb(ovenRes.body).pipe(res); } catch (err) { console.error(`proxyToOven ${ovenPath} error:`, err.message); if (!res.headersSent) res.status(502).json({ error: `Oven unavailable: ${err.message}` }); }}app.get("/api/pack-html", (req, res) => proxyToOven("/pack-html", req, res));app.get("/api/bundle-html", (req, res) => proxyToOven("/bundle-html", req, res));app.get("/api/os", (req, res) => proxyToOven("/os", req, res));
// --- /media/* handler (ports Netlify edge function media.js) ---app.all("/media/*rest", async (req, res) => { const parts = req.path.split("/").filter(Boolean); // ["media", ...] parts.shift(); // remove "media" const resourcePath = parts.join("/");
if (!resourcePath) return res.status(404).send("Missing media path");
// Content type from extension const ext = resourcePath.split(".").pop()?.toLowerCase(); const ctMap = { png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", gif: "image/gif", webp: "image/webp", zip: "application/zip", mp4: "video/mp4", json: "application/json", mjs: "text/javascript", svg: "image/svg+xml" };
// Helper: build a clean event for calling functions internally function mediaEvent(path, query) { return { httpMethod: "GET", headers: req.headers, body: null, queryStringParameters: query, path, rawUrl: `${req.protocol}://${req.get("host")}${path}`, isBase64Encoded: false, }; }
// /media/tapes/CODE → get-tape function → redirect to DO Spaces. // The backing store is .mp4 for video-based tapes (kind "mp4") and .zip for // frame-based recordings (kind "zip", the default for legacy tapes). An // explicit extension on the request (CODE.mp4 / CODE.zip) wins over kind. if (parts[0] === "tapes" && parts[1]) { const reqExt = /\.(mp4|zip)$/.exec(parts[1])?.[1]; const code = parts[1].replace(/\.(mp4|zip)$/, ""); try { const result = await functions["get-tape"](mediaEvent("/api/get-tape", { code }), {}); if (result.statusCode === 200) { const tape = JSON.parse(result.body); const bucket = tape.bucket || "art-aesthetic-computer"; const ext = reqExt || (tape.kind === "mp4" ? "mp4" : "zip"); const slug = tape.slug; // Slug may already be fully-qualified (e.g. "auth0|.../TS" when the // upload pipeline returned the storage key as the slug). Only prepend // the user prefix when it's a bare slug, otherwise we double the // prefix and 404. Mirrors the /media/paintings/ dedup above. const key = tape.user && !slug.startsWith(`${tape.user}/`) ? `${tape.user}/${slug}.${ext}` : `${slug}.${ext}`; return res.redirect(302, `https://${bucket}.sfo3.digitaloceanspaces.com/${key}`); } } catch {} res.set("Cache-Control", "no-store, max-age=0"); return res.status(404).send("Tape not found"); }
// /media/paintings/CODE → get-painting function → redirect if (parts[0] === "paintings" && parts[1]) { const requestedExt = /\.(png|zip)$/.exec(parts[1])?.[1] || "png"; const code = parts[1].replace(/\.(png|zip)$/, ""); try { // painting.mjs requests media by slug (e.g. qwfV8wDk), but short codes // (e.g. eou) also reach here — try code first, then fall back to slug // so anon/slug-addressed paintings resolve instead of 404ing. let result = await functions["get-painting"]?.(mediaEvent("/api/get-painting", { code }), {}); if (result?.statusCode !== 200) { result = await functions["get-painting"]?.(mediaEvent("/api/get-painting", { slug: code }), {}); } if (result?.statusCode === 200) { const painting = JSON.parse(result.body); const bucket = painting.user ? "user-aesthetic-computer" : "art-aesthetic-computer"; const slug = painting.slug?.split(":")[0] || painting.slug; // Slug may already be fully-qualified (e.g. "auth0|.../painting/TS"). // Only prepend user when it's a bare slug, otherwise we double the prefix. const key = painting.user && !slug.startsWith(`${painting.user}/`) ? `${painting.user}/${slug}.png` : `${slug}.png`; return res.redirect(302, `https://${bucket}.sfo3.digitaloceanspaces.com/${key}`); } } catch {} // Legacy anonymous recording ZIPs have object slugs but no tape row. if (requestedExt === "zip" && /^[A-Za-z0-9_-]{6,64}$/.test(code)) { return res.redirect(302, `https://art-aesthetic-computer.sfo3.digitaloceanspaces.com/${code}.zip`); } // A freshly uploaded object and its Mongo record become visible on // separate network hops. Never let Cloudflare turn that brief race into a // cached one-hour failure for a valid new #code. res.set("Cache-Control", "no-store, max-age=0"); return res.status(404).send("Painting not found"); }
// /media/@handle/type/slug → resolve user ID → redirect to DO Spaces if (parts[0]?.startsWith("@") || parts[0]?.match(/^ac[a-z0-9]+$/i)) { const userIdentifier = parts[0]; const subPath = parts.slice(1).join("/");
// Resolve user ID via user function directly try { const query = userIdentifier.match(/^ac[a-z0-9]+$/i) ? { code: userIdentifier } : { from: userIdentifier }; const event = { httpMethod: "GET", headers: req.headers, body: null, queryStringParameters: query, path: "/user", rawUrl: `${req.protocol}://${req.get("host")}/user`, isBase64Encoded: false, }; const result = await functions["user"](event, {}); if (result.statusCode === 200) { const user = JSON.parse(result.body); const userId = user.sub; if (userId) { return res.redirect(302, userMediaTarget({ userId, subPath, extension: ext, })); } } } catch (err) { console.error("media user resolve error:", err.message); } return res.status(404).send("User media not found"); }
// Direct file path → proxy to DO Spaces const baseUrl = ext === "mjs" ? "https://user-aesthetic-computer.sfo3.digitaloceanspaces.com" : "https://user.aesthetic.computer"; const encoded = resourcePath.split("/").map(encodeURIComponent).join("/"); return res.redirect(302, `${baseUrl}/${encoded}`);});
// API functions (matches Netlify redirect rules)// Bare `/api` serves the public API reference (same function that backs api.aesthetic.computer).app.all("/api", directFn("api-docs"));app.all("/api/:fn", handleFunctionResolved);app.all("/api/:fn/*rest", handleFunctionResolved);app.all("/.netlify/functions/:fn", handleFunction);
// Non-/api/ function routes (from netlify.toml)function directFn(fnName) { return (req, res) => { req.params = { fn: fnName }; return handleFunction(req, res); };}app.all("/handle", directFn("handle"));app.all("/user", directFn("user"));app.all("/run", directFn("run"));app.all("/reload/*rest", directFn("reload"));app.all("/session/*rest", directFn("session"));app.all("/authorized", directFn("authorized"));app.all("/handles", directFn("handles"));app.all("/redirect-proxy", directFn("redirect-proxy"));app.all("/redirect-proxy-sotce", directFn("redirect-proxy"));// Local dev upload fallback (used when S3 credentials are missing).app.all("/local-upload/:filename", (req, res) => { if (req.method === "OPTIONS") return res.sendStatus(204); const body = req.rawBody || req.body; if (!body || body.length === 0) { console.error("❌ Local upload: empty body for", req.params.filename); return res.status(400).send("Empty body"); } const dir = join(dirname(fileURLToPath(import.meta.url)), "..", "local-uploads"); mkdirSync(dir, { recursive: true }); const filepath = join(dir, req.params.filename); writeFileSync(filepath, body); console.log("📁 Local upload saved:", filepath, `(${body.length} bytes)`); res.status(200).send("OK");});app.use("/local-uploads", express.static(join(dirname(fileURLToPath(import.meta.url)), "..", "local-uploads")));app.all("/presigned-upload-url/*rest", directFn("presigned-url"));app.all("/presigned-download-url/*rest", directFn("presigned-url"));app.all("/docs", directFn("docs"));app.all("/docs.json", directFn("docs"));app.all("/docs/*rest", directFn("docs"));app.all("/pop", directFn("pop"));app.all("/pop/*rest", directFn("pop"));app.all("/api-docs", directFn("api-docs"));app.all("/api-docs.json", directFn("api-docs"));app.all("/media-collection", directFn("media-collection"));app.all("/media-collection/*rest", directFn("media-collection"));app.all("/device-login", directFn("device-login"));app.all("/device-auth", directFn("device-auth"));app.all("/mcp", directFn("mcp-remote"));app.all("/m4l-plugins", directFn("m4l-plugins"));app.all("/slash", directFn("slash"));app.all("/sotce-blog/*rest", directFn("sotce-blog"));app.all("/profile/*rest", directFn("profile"));app.all("/client/*rest", directFn("client-media"));
// Menu Band crash-log intake → MongoDB collection "menuband-logs". Body is// the raw .ips text; metadata comes from headers. The text-body parser// runs only for this route so other routes' JSON parsing stays untouched.app.post("/menuband-logs", express.text({ type: "*/*", limit: "5mb" }), directFn("menuband-logs"));
// Static filesapp.use(express.static(PUBLIC, { extensions: ["html"], dotfiles: "allow" }));
// --- keeps-social: SSR meta tags for social crawlers on keep/buy.kidlisp.com ---const CRAWLER_RE = /twitterbot|facebookexternalhit|linkedinbot|slackbot|discordbot|telegrambot|whatsapp|applebot/i;const OBJKT_GRAPHQL = "https://data.objkt.com/v3/graphql";
async function keepsSocialMiddleware(req, res, next) { const host = (req.headers.host || "").split(":")[0].toLowerCase(); const isBuy = host.includes("buy.kidlisp.com"); const isKeep = host.includes("keep.kidlisp.com"); if (!isBuy && !isKeep) return next();
const seg = req.path.replace(/^\/+/, "").split("/")[0]; if (!seg.startsWith("$") || seg.length < 2) return next();
const ua = req.headers["user-agent"] || ""; if (!CRAWLER_RE.test(ua)) return next();
const code = seg.slice(1); try { const [tokenData, ogImage] = await Promise.all([ fetchKeepsTokenData(code), resolveKeepsImageUrl(`https://oven.aesthetic.computer/preview/1200x630/$${code}.png`), ]);
// Get the HTML from the index function if (!functions["index"]) return next(); const event = toEvent(req); const result = await functions["index"](event, { clientContext: {} }); let html = result.body || "";
const title = `$${code}`; const subdomain = isBuy ? "buy" : "keep"; const description = buildKeepsDescription(tokenData, isBuy); const permalink = `https://${subdomain}.kidlisp.com/$${code}`;
html = html.replace(/<meta property="og:url"[^>]*\/>/, `<meta property="og:url" content="${permalink}" />`); html = html.replace(/<meta property="og:title"[^>]*\/>/, `<meta property="og:title" content="${escapeAttr(title)}" />`); html = html.replace(/<meta property="og:description"[^>]*\/>/, `<meta property="og:description" content="${escapeAttr(description)}" />`); html = html.replace(/<meta property="og:image" content="[^"]*"[^>]*\/>/, `<meta property="og:image" content="${ogImage}" />`); html = html.replace(/<meta name="twitter:title"[^>]*\/>/, `<meta name="twitter:title" content="${escapeAttr(title)}" />`); html = html.replace(/<meta name="twitter:description"[^>]*\/>/, `<meta name="twitter:description" content="${escapeAttr(description)}" />`); html = html.replace(/<meta name="twitter:image" content="[^"]*"[^>]*\/>/, `<meta name="twitter:image" content="${ogImage}" />`);
res.set("Content-Type", "text/html; charset=utf-8"); res.set("Cache-Control", "public, max-age=3600"); return res.status(200).send(html); } catch (err) { console.error("[keeps-social] error:", err); return next(); }}
async function fetchKeepsTokenData(code) { const contract = "KT1Q1irsjSZ7EfUN4qHzAB2t7xLBPsAWYwBB"; const query = `query { token(where: { fa_contract: { _eq: "${contract}" } name: { _eq: "$${code}" } }) { token_id name thumbnail_uri } listing_active(where: { fa_contract: { _eq: "${contract}" } token: { name: { _eq: "$${code}" } } } order_by: { price_xtz: asc } limit: 1) { price_xtz seller_address } }`; const r = await fetch(OBJKT_GRAPHQL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ query }) }); if (!r.ok) return null; const json = await r.json(); const tokens = json?.data?.token || []; if (tokens.length === 0) return null; return { token: tokens[0], listing: (json?.data?.listing_active || [])[0] || null };}
function buildKeepsDescription(tokenData, isBuy) { if (!tokenData) return isBuy ? "Buy KidLisp generative art on Tezos." : "KidLisp generative art preserved on Tezos."; const { listing } = tokenData; if (listing) { const xtz = (Number(listing.price_xtz) / 1_000_000).toFixed(2); return isBuy ? `Buy now — ${xtz} XTZ | KidLisp generative art on Tezos` : `For Sale — ${xtz} XTZ | KidLisp generative art on Tezos`; } return isBuy ? "Buy KidLisp generative art on Tezos." : "KidLisp generative art preserved on Tezos.";}
function escapeAttr(str) { return str.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");}
async function resolveKeepsImageUrl(url) { try { const r = await fetch(url, { method: "HEAD", redirect: "follow" }); if (r.ok && r.url) return r.url; } catch (e) { console.error("[keeps-social] image resolve error:", e); } return url;}
app.use(keepsSocialMiddleware);
// SPA fallback → index functionapp.use(async (req, res) => { if (functions["index"]) { req.params = { fn: "index" }; return handleFunction(req, res); } res.status(404).send("Not found");});
// --- Start server ---let server;if (DEV && HAS_SSL) { const opts = { cert: readFileSync(SSL_CERT), key: readFileSync(SSL_KEY), }; server = createHttpsServer(opts, app).listen(PORT, () => { console.log(`lith listening on https://localhost:${PORT}`); });} else { server = createHttpServer(app).listen(PORT, () => { console.log(`lith listening on http://localhost:${PORT}`); });}
// --- Graceful shutdown ---// On SIGTERM (sent by systemctl restart), stop accepting new connections// and wait for in-flight requests to finish before exiting.const DRAIN_TIMEOUT = 10_000; // 10s max wait
function gracefulShutdown(signal) { console.log(`[lith] ${signal} received, draining connections...`); server.close(() => { console.log("[lith] all connections drained, exiting"); process.exit(0); }); // Sever Caddy's idle keep-alive sockets cleanly — a request reused on a // dying keep-alive can ECONNRESET mid-flight, which the proxy's dial // retry (lb_try_duration) can't cover for non-idempotent methods. server.closeIdleConnections?.(); // Force exit if connections don't drain in time setTimeout(() => { console.warn("[lith] drain timeout, forcing exit"); process.exit(1); }, DRAIN_TIMEOUT).unref();}
process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));process.on("SIGINT", () => gracefulShutdown("SIGINT"));