Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

silo: public /sidecar proxy for lith → Datomic sidecar

The sidecar binds to silo's 127.0.0.1:8891, so lith had no way to reach
it during the kidlisp cutover. This adds a pass-through proxy at
/sidecar/* on silo that forwards everything to the sidecar with the
client-secret header carried through. The admin surface stays locked
behind requireAdmin at /api/datomic/*; /sidecar/* is the server-to-
server data plane that only lith uses.

Auth is the shared CLIENT_SECRET — held by lith and the sidecar, never
exposed to browsers. The sidecar rejects anything without it.

With this in place the cutover is live: lith's DATOMIC_SIDECAR_URL
points at https://silo.aesthetic.computer/sidecar and all kidlisp
reads/writes go to Datomic. Mongo kidlisp stays frozen as the pre-
cutover archive.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+35
+35
silo/server.mjs
··· 1387 1387 app.get("/api/datomic/backups", requireAdmin, (req, res) => 1388 1388 datomicProxy(req, res, { method: "GET", path: "/admin/backups" })); 1389 1389 1390 + // ─────────────────── Datomic sidecar — public kidlisp proxy ─────────────────── 1391 + // 1392 + // Server-to-server surface for lith (which runs store-kidlisp-datomic.mjs). 1393 + // Authenticates via the sidecar's CLIENT_SECRET header — a shared secret 1394 + // held by lith and the sidecar only, never exposed to browsers. This 1395 + // route transparently forwards /sidecar/<anything> to the sidecar's 1396 + // /<anything> endpoint so the Node client can use a clean base URL. 1397 + 1398 + app.all("/sidecar/*", async (req, res) => { 1399 + // Forward the client secret header as-is; sidecar rejects if missing/wrong. 1400 + const subpath = req.originalUrl.replace(/^\/sidecar/, "") || "/"; 1401 + const clientSecret = req.headers["x-sidecar-secret"]; 1402 + if (!clientSecret) { 1403 + return res.status(401).json({ error: "missing x-sidecar-secret" }); 1404 + } 1405 + try { 1406 + const hasBody = req.method !== "GET" && req.method !== "HEAD"; 1407 + const resp = await fetch(`${DATOMIC_SIDECAR_URL}${subpath}`, { 1408 + method: req.method, 1409 + headers: { 1410 + "content-type": "application/json", 1411 + "x-sidecar-secret": clientSecret, 1412 + }, 1413 + body: hasBody ? JSON.stringify(req.body ?? {}) : undefined, 1414 + signal: AbortSignal.timeout(15000), 1415 + }); 1416 + const text = await resp.text(); 1417 + res.status(resp.status); 1418 + res.setHeader("content-type", resp.headers.get("content-type") || "application/json"); 1419 + res.send(text); 1420 + } catch (err) { 1421 + res.status(502).json({ error: err.message }); 1422 + } 1423 + }); 1424 + 1390 1425 app.get("/api/firehose/history", async (req, res) => { 1391 1426 if (!db) return res.json([]); 1392 1427 const limit = Math.min(parseInt(req.query.limit) || 100, 1000);