remove plumbing

This commit is contained in:
Your Name 2026-08-22 12:19:12 -04:00
parent f6bde50ce3
commit 2954aecdf8
9 changed files with 0 additions and 503 deletions

View file

@ -1,57 +0,0 @@
<article data-bliss-artifact-editor
data-kind="<%= it.kind %>" data-id="<%= it.artifact.id %>"
data-structure-id="<%= it.artifact.structure_id %>" data-method="<%= it.artifact.verb || '' %>"
class="border-2 border-black bg-white text-black shadow-[4px_4px_0_#000]">
<header class="flex items-center gap-2 border-b-4 border-double border-black p-3">
<strong class="min-w-0 flex-1 truncate"><% if (it.kind === 'route') { %><%= it.artifact.verb %> <%= it.artifact.path %><% } else { %><%= it.artifact.name %><% } %></strong>
<span class="text-xs uppercase"><%= it.kind %></span><span data-status class="text-xs"></span>
</header>
<div class="p-3">
<% if (it.kind === 'route' && it.artifact.verb !== 'GET') { %>
<p class="mb-2 border border-amber-700 bg-amber-100 p-2 text-sm">Saving does not replay this <%= it.artifact.verb %> route.</p>
<% } %>
<label class="block text-xs font-bold uppercase tracking-widest"><%= it.kind === 'route' ? 'Handler' : 'Template' %></label>
<div data-editor="<%= it.kind === 'route' ? 'handler' : 'content' %>" data-mode="<%= it.kind === 'route' ? 'javascript' : 'html' %>" class="h-[min(55vh,34rem)] w-full border-2 border-black"><%= it.kind === 'route' ? it.artifact.handler : it.artifact.content %></div>
<% if (it.kind === 'template') { %>
<details class="mt-3" open>
<summary class="cursor-pointer text-xs font-bold uppercase tracking-widest">Rendering test</summary>
<div data-editor="test_object" data-mode="javascript" class="mt-1 h-32 w-full border-2 border-black"><%= it.artifact.test_object || '' %></div>
<iframe data-template-test title="Template rendering test" src="/workshop/<%= it.artifact.structure_id %>/template/<%= it.artifact.id %>/preview" class="mt-2 h-64 w-full border-2 border-black"></iframe>
</details>
<% } %>
<button data-save type="button" class="mt-3 border-2 border-black bg-white px-3 py-2 font-bold shadow-[3px_3px_0_#000]">Save</button>
</div>
</article>
<script>
(function () {
const card = document.currentScript.previousElementSibling;
if (!card || card.dataset.ready) return;
card.dataset.ready = "true";
const editors = Object.fromEntries([...card.querySelectorAll("[data-editor]")].map((node) => {
const editor = ace.edit(node);
editor.setTheme("ace/theme/monokai");
editor.session.setMode(`ace/mode/${node.dataset.mode}`);
return [node.dataset.editor, editor];
}));
card.querySelector("[data-save]").onclick = async () => {
const status = card.querySelector("[data-status]");
const { kind, id, structureId } = card.dataset;
status.textContent = "Saving…";
const body = kind === "route"
? { handler: editors.handler.getValue() }
: { content: editors.content.getValue(), test_object: editors.test_object.getValue() };
const response = await fetch(`/workshop/${structureId}/${kind}/${id}`, {
method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body),
});
if (!response.ok) return void (status.textContent = "Save failed");
status.textContent = "Saved";
if (kind === "template") {
const frame = card.querySelector("[data-template-test]");
frame.src = `${frame.src.split("?")[0]}?t=${Date.now()}`;
}
document.dispatchEvent(new CustomEvent("bliss:source-saved", {
detail: { kind, id, method: kind === "route" ? card.dataset.verb : undefined },
}));
};
})();
</script>

View file

@ -1,3 +0,0 @@
function handler(req, res) {
res.render("inspector/slideout", {});
}

View file

@ -1,80 +0,0 @@
#!/usr/bin/env node
// Install/update the live editor using Bliss's public CLI workflow. This file
// intentionally does not open the Bliss database directly.
const { spawnSync } = require("node:child_process");
const path = require("node:path");
const repo = path.resolve(__dirname, "../..");
const cli = path.join(repo, "bliss-cli/bliss");
function bliss(...args) {
const result = spawnSync(process.execPath, [cli, ...args], {
cwd: repo,
env: process.env,
encoding: "utf8",
});
if (result.status !== 0) throw new Error(result.stderr || result.stdout);
return result.stdout.trim();
}
function json(...args) {
return JSON.parse(bliss(...args));
}
const structures = json("structures");
let structure = structures.find((item) => item.name === "__bliss_live_editor");
if (!structure) {
const created = json("create-structure", "__bliss_live_editor");
structure = { id: created.id };
}
const structureId = String(structure.id);
bliss("update-settings", structureId, "--route-prefix", "/_bliss");
let state = json("structure", structureId);
// No db is attached: the editor reads structure sources through the /plumbing
// API (owner-checked), not by mounting the prime db — which is no longer
// attachable to other structures anyway.
const templates = [
["inspector/slideout", "slideout.eta"],
["inspector/route_editor", "route-editor.eta"],
["inspector/template_editor", "template-editor.eta"],
["inspector/artifact_editor", "artifact-editor.eta"],
];
for (const [name, filename] of templates) {
let template = state.templates.find((item) => item.name === name);
if (!template) {
template = json("create-template", structureId, name);
}
bliss(
"update-template",
structureId,
String(template.id),
"--content-file",
path.join(__dirname, filename),
);
}
const routes = [
["/inspector", "inspector.js"],
["/editor/route/:id", "route-editor.js"],
["/editor/template/:id", "template-editor.js"],
];
for (const [routePath, filename] of routes) {
let route = state.routes.find(
(item) => item.verb === "GET" && item.path === routePath,
);
if (!route) {
route = json("create-route", structureId, "GET", routePath);
}
bliss(
"update-route",
structureId,
String(route.id),
"--handler-file",
path.join(__dirname, filename),
);
}
console.log(`live editor installed as Structure ${structureId}`);

View file

@ -1,28 +0,0 @@
<div data-bliss-ui>
<div class="mb-2 flex items-center justify-between border-b-4 border-double border-black pb-2"><strong><%= it.sourceRoute.verb %> <%= it.sourceRoute.path %></strong><span data-status class="text-xs"></span></div>
<% if (it.sourceRoute.verb !== "GET") { %>
<div class="mb-2 rounded border border-amber-600 bg-amber-950 p-2 text-amber-200">Rerunning this <%= it.sourceRoute.verb %> request can repeat writes or external side effects. Bliss will not rerun it automatically.</div>
<% } %>
<div data-source class="h-[55vh] w-full border-2 border-black"><%= it.sourceRoute.handler %></div>
<button data-save type="button" class="mt-2 border-2 border-black bg-white px-3 py-2 font-bold shadow-[3px_3px_0_#000] active:translate-x-0.5 active:translate-y-0.5 active:shadow-none">Save Route</button>
</div>
<script>
(function () {
const box = document.currentScript.previousElementSibling;
if (!box) return;
const editor = ace.edit(box.querySelector("[data-source]"));
editor.setTheme("ace/theme/monokai");
editor.session.setMode("ace/mode/javascript");
box.querySelector("[data-save]").onclick = async () => {
const status = box.querySelector("[data-status]");
status.textContent = "Saving…";
const response = await fetch("/workshop/<%= it.sourceRoute.structure_id %>/route/<%= it.sourceRoute.id %>", {
method: "PUT", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ handler: editor.getValue() })
});
if (!response.ok) return void (status.textContent = "Save failed");
status.textContent = "Saved";
document.dispatchEvent(new CustomEvent("bliss:source-saved", { detail: { kind: "route", id: "<%= it.sourceRoute.id %>", method: "<%= it.sourceRoute.verb %>" } }));
};
})();
</script>

View file

@ -1,14 +0,0 @@
// Read a route's source through the plumbing API rather than mounting the prime
// db directly. The prime db is no longer attachable to other structures, and
// going through /plumbing means this read is owner-checked: forwarding the
// caller's session cookie, plumbing 404s any route in a structure they can't
// see, so the live editor can only open what the user is already allowed to edit.
async function handler(req, res) {
const base = `${req.protocol}://${req.get("host")}`;
const response = await fetch(`${base}/plumbing/routes/${req.params.id}`, {
headers: { cookie: req.headers.cookie || "" },
});
if (!response.ok) return res.status(response.status).send("Route not found");
const artifact = await response.json();
res.render("inspector/artifact_editor", { kind: "route", artifact });
}

View file

@ -1,200 +0,0 @@
<aside id="bliss-live-editor" data-bliss-ui
class="fixed inset-y-0 left-0 z-[2147483646] w-[min(28rem,90vw)] overflow-auto border-r-4 border-black bg-white text-black shadow-[8px_0_0_#000]"
style="font-family:ui-sans-serif,system-ui">
<header class="sticky top-0 flex items-center justify-between border-b-4 border-double border-black bg-white p-3">
<div><div class="text-lg font-black tracking-tight">Live Page</div><div class="text-xs">Objects currently on this card</div></div>
<button type="button" data-bliss-close class="h-7 w-7 border-2 border-black bg-white text-lg font-black leading-none shadow-[2px_2px_0_#000] active:translate-x-0.5 active:translate-y-0.5 active:shadow-none">×</button>
</header>
<div class="grid min-h-[calc(100vh-61px)] grid-rows-[auto_1fr]">
<nav data-bliss-tree class="border-b-4 border-double border-black p-2"></nav>
<section data-bliss-editor class="min-h-0 p-3 text-sm">Select a route or template to edit it, or use the ✏️ 📄 📋 👯 buttons on the page.</section>
</div>
</aside>
<script>
(function () {
const root = document.getElementById("bliss-live-editor");
if (!root) return;
// Re-running this script (the slideout is remounted on every open) must not
// pile up document listeners or leave a stale overlay behind.
if (window.__blissLive) window.__blissLive();
const cleanups = [];
const on = (target, type, handler, opts) => {
target.addEventListener(type, handler, opts);
cleanups.push(() => target.removeEventListener(type, handler, opts));
};
window.__blissLive = () => { for (const c of cleanups.splice(0)) c(); window.__blissLive = null; };
const tree = root.querySelector("[data-bliss-tree]");
const editor = root.querySelector("[data-bliss-editor]");
root.querySelector("[data-bliss-close]").onclick = () =>
document.dispatchEvent(new CustomEvent("bliss:inspector-close"));
const highlight = (kind, id, method, state) =>
document.dispatchEvent(new CustomEvent("bliss:highlight", { detail: { kind, id, method, on: state } }));
function openEditor(kind, id) {
editor.textContent = "Loading…";
htmx.ajax("GET", `/_bliss/editor/${kind}/${id}`, { target: editor, swap: "innerHTML" });
}
// Clone runs through the workshop's own modal, dropped into a full-screen host
// we manage so it stacks above the slideout and closes on backdrop click.
function openModal(url) {
const host = document.createElement("div");
host.dataset.blissUi = "";
host.style.cssText = "position:fixed;inset:0;z-index:2147483646;background:rgba(0,0,0,.45)";
host.onclick = (event) => { if (event.target === host) host.remove(); };
on(document, "bliss:inspector-close", () => host.remove());
document.body.append(host);
htmx.ajax("GET", url, { target: host, swap: "innerHTML" });
}
// T R E E -- the list of everything the current page rendered.
function sourceButton(label, kind, id, count) {
const button = document.createElement("button");
button.type = "button";
button.className = "block w-full border border-transparent px-2 py-1 text-left hover:border-black hover:bg-black hover:text-white";
button.textContent = label + (count > 1 ? ` (${count})` : "");
button.onmouseenter = () => highlight(kind, id, null, true);
button.onmouseleave = () => highlight(kind, id, null, false);
if (kind !== "structure") button.onclick = () => openEditor(kind, id);
return button;
}
function renderTree(items) {
tree.innerHTML = "";
const structures = new Map();
for (const item of items) {
if (!structures.has(item.structureId)) structures.set(item.structureId, { name: item.structureName, routes: new Map(), templates: new Map(), count: 0 });
const structure = structures.get(item.structureId);
structure.count++;
if (!structure.routes.has(item.routeId)) structure.routes.set(item.routeId, { name: item.routeName, count: 0 });
structure.routes.get(item.routeId).count++;
if (item.templateId) {
if (!structure.templates.has(item.templateId)) structure.templates.set(item.templateId, { name: item.templateName, count: 0 });
structure.templates.get(item.templateId).count++;
}
}
for (const [structureId, structure] of structures) {
const group = document.createElement("details");
group.open = true;
const heading = document.createElement("summary");
heading.className = "cursor-pointer list-none font-semibold";
heading.append(sourceButton(structure.name, "structure", structureId, structure.count));
group.append(heading);
for (const [label, kind, entries] of [["Routes", "route", structure.routes], ["Templates", "template", structure.templates]]) {
if (!entries.size) continue;
const section = document.createElement("details");
section.open = true;
section.className = "ml-3";
const summary = document.createElement("summary");
summary.className = "cursor-pointer py-1 text-xs font-bold uppercase tracking-widest";
summary.textContent = label;
section.append(summary);
for (const [id, entry] of entries) section.append(sourceButton(entry.name, kind, id, entry.count));
group.append(section);
}
tree.append(group);
}
}
// O V E R L A Y -- little edit/copy/clone toolbars drawn on the page itself,
// one per rendered element, anchored to its top-left corner.
const layer = document.createElement("div");
layer.dataset.blissUi = "";
layer.style.cssText = "position:fixed;inset:0;z-index:2147483640;pointer-events:none";
root.parentNode.append(layer); // lives inside the inspector root, so it is torn down on close
const bars = new Map();
function sourceEls() {
return [...document.querySelectorAll("[data-bliss-structure-id],[data-bliss-template-id]")]
.filter((element) => !element.closest("[data-bliss-ui]"));
}
function templatesOf(element) {
try { const list = JSON.parse(element.dataset.blissTemplates || "[]"); if (list.length) return list; } catch (_) {}
return element.dataset.blissTemplateId
? [{ id: element.dataset.blissTemplateId, name: element.dataset.blissTemplateName }]
: [];
}
function iconButton(glyph, title, kind, id, method, action) {
const button = document.createElement("button");
button.type = "button";
button.textContent = glyph;
button.title = title;
button.style.cssText = "pointer-events:auto;border:2px solid #000;background:#fff;border-radius:6px;min-width:20px;height:20px;padding:0 2px;font-size:11px;line-height:16px;cursor:pointer;box-shadow:1px 1px 0 #000";
if (kind) {
button.onmouseenter = () => highlight(kind, id, method, true);
button.onmouseleave = () => highlight(kind, id, method, false);
}
button.onclick = (event) => { event.preventDefault(); event.stopPropagation(); action(button); };
return button;
}
function buildBar(element) {
const wrap = document.createElement("div");
wrap.dataset.blissUi = "";
wrap.style.cssText = "position:absolute;display:flex;gap:3px;pointer-events:none";
if (element.dataset.blissRouteId) {
wrap.append(iconButton("✏️", "Edit route " + (element.dataset.blissRouteName || ""),
"route", element.dataset.blissRouteId, element.dataset.blissMethod,
() => openEditor("route", element.dataset.blissRouteId)));
}
for (const template of templatesOf(element)) {
wrap.append(iconButton("📄", "Edit template " + (template.name || ""),
"template", template.id, null, () => openEditor("template", template.id)));
}
if (element.dataset.blissCopy) {
wrap.append(iconButton("📋", "Copy embed snippet", null, null, null, async (button) => {
try { await navigator.clipboard.writeText(element.dataset.blissCopy); button.textContent = "✅"; }
catch (_) { button.textContent = "❌"; }
setTimeout(() => (button.textContent = "📋"), 1000);
}));
}
if (element.dataset.blissClone) {
wrap.append(iconButton("👯", "Clone structure", null, null, null,
() => openModal(element.dataset.blissClone)));
}
return wrap;
}
function place(element, wrap) {
const rect = element.getBoundingClientRect();
if (!rect.width && !rect.height) { wrap.style.display = "none"; return; }
wrap.style.display = "flex";
// Keep the toolbar clear of the slideout: the page root (<body>) is not
// itself shifted, so its top-left would otherwise hide behind the panel.
const guard = root.getBoundingClientRect().right + 2;
const top = rect.top - 24;
wrap.style.left = Math.max(guard, rect.left + 2) + "px";
wrap.style.top = (top < 2 ? rect.top + 2 : top) + "px";
}
function syncBars() {
const live = new Set();
for (const element of sourceEls()) {
live.add(element);
let wrap = bars.get(element);
if (!wrap || !wrap.isConnected) { wrap = buildBar(element); bars.set(element, wrap); layer.append(wrap); }
place(element, wrap);
}
for (const [element, wrap] of bars) {
if (!live.has(element)) { wrap.remove(); bars.delete(element); }
}
}
let frame = 0;
function reposition() {
cancelAnimationFrame(frame);
frame = requestAnimationFrame(() => { for (const [element, wrap] of bars) place(element, wrap); });
}
on(window, "scroll", reposition, true);
on(window, "resize", reposition);
on(document, "bliss:inventory", (event) => { renderTree(event.detail); syncBars(); });
on(document, "bliss:inspector-close", () => window.__blissLive && window.__blissLive());
document.dispatchEvent(new CustomEvent("bliss:inventory-request"));
})();
</script>

View file

@ -1,25 +0,0 @@
<div data-bliss-ui>
<div class="mb-2 flex items-center justify-between border-b-4 border-double border-black pb-2"><strong><%= it.sourceTemplate.name %></strong><span data-status class="text-xs"></span></div>
<div data-source class="h-[55vh] w-full border-2 border-black"><%= it.sourceTemplate.content %></div>
<button data-save type="button" class="mt-2 border-2 border-black bg-white px-3 py-2 font-bold shadow-[3px_3px_0_#000] active:translate-x-0.5 active:translate-y-0.5 active:shadow-none">Save Template</button>
</div>
<script>
(function () {
const box = document.currentScript.previousElementSibling;
if (!box) return;
const editor = ace.edit(box.querySelector("[data-source]"));
editor.setTheme("ace/theme/monokai");
editor.session.setMode("ace/mode/html");
box.querySelector("[data-save]").onclick = async () => {
const status = box.querySelector("[data-status]");
status.textContent = "Saving…";
const response = await fetch("/workshop/<%= it.sourceTemplate.structure_id %>/template/<%= it.sourceTemplate.id %>", {
method: "PUT", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ content: editor.getValue() })
});
if (!response.ok) return void (status.textContent = "Save failed");
status.textContent = "Saved";
document.dispatchEvent(new CustomEvent("bliss:source-saved", { detail: { kind: "template", id: "<%= it.sourceTemplate.id %>" } }));
};
})();
</script>

View file

@ -1,13 +0,0 @@
// Read a template's source through the plumbing API rather than mounting the
// prime db directly (see route-editor.js for the why). Forwarding the caller's
// session cookie keeps the read owner-checked: plumbing 404s any template in a
// structure the user can't see.
async function handler(req, res) {
const base = `${req.protocol}://${req.get("host")}`;
const response = await fetch(`${base}/plumbing/templates/${req.params.id}`, {
headers: { cookie: req.headers.cookie || "" },
});
if (!response.ok) return res.status(response.status).send("Template not found");
const artifact = await response.json();
res.render("inspector/artifact_editor", { kind: "template", artifact });
}

View file

@ -1,83 +0,0 @@
// Proof: the /plumbing read API only ever hands out an artifact to a caller who
// may see the structure it BELONGS TO — including via the id-only endpoints the
// live editor uses, and even when a private db is aliased into a public one.
//
// It mounts the REAL plumbing.js router on a throwaway express app (temp db),
// with a tiny middleware that fakes a logged-in session from an ?as= query, and
// makes real HTTP requests. So the assertions exercise the shipped access logic.
const fs = require("fs");
const os = require("os");
const path = require("path");
const express = require("express");
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "bliss-plumbing-"));
fs.mkdirSync(path.join(tmp, "dbs"));
process.chdir(tmp);
const PROJECT = "/home/happy/code/bliss";
const model = require(path.join(PROJECT, "db.js"));
model.applyMigrations();
// ---- scenario ------------------------------------------------------------
const alice = model.createUser("alice", "x"); // member of the private structure
const bob = model.createUser("bob", "x"); // outsider
// PRIVATE structure owned by alice: its own route, template, and db.
const priv = model.createStructure("private-vault", alice);
const privRoute = model.createRoute("GET", "/secret", priv, "handler(){}");
const privTemplate = model.createTemplate(priv, "secret_tpl", "top secret", "");
const secretsDbId = model.createDb(priv, "secrets");
model.setStructurePrivacy(priv, true, alice);
// PUBLIC structure owned by bob that ALIASES alice's private db under "secrets".
const pub = model.createStructure("public-front", bob);
model.attachDb(pub, secretsDbId, "secrets");
// ---- app: real plumbing router, faked session from ?as= ------------------
const app = express();
app.use((req, _res, next) => {
const as = req.query.as;
req.session = { userId: as === "alice" ? alice : as === "bob" ? bob : null };
next();
});
app.use("/plumbing", require(path.join(PROJECT, "plumbing.js")));
const server = app.listen(0);
const port = server.address().port;
async function get(url) {
const res = await fetch(`http://127.0.0.1:${port}${url}`);
return res.status;
}
function expect(label, actual, wanted) {
const ok = actual === wanted;
console.log(` ${ok ? "PASS" : "FAIL"} | ${label} (got ${actual}, want ${wanted})`);
return ok;
}
(async () => {
const results = [];
console.log("\nid-only endpoints (what the live editor calls):");
results.push(expect("alice reads her private route ", await get(`/plumbing/routes/${privRoute}?as=alice`), 200));
results.push(expect("bob reads alice's private route ", await get(`/plumbing/routes/${privRoute}?as=bob`), 404));
results.push(expect("anon reads alice's private route ", await get(`/plumbing/routes/${privRoute}`), 404));
results.push(expect("alice reads her private template ", await get(`/plumbing/templates/${privTemplate}?as=alice`), 200));
results.push(expect("bob reads alice's private template", await get(`/plumbing/templates/${privTemplate}?as=bob`), 404));
console.log("\nstructure-scoped endpoints reject id-laundering:");
// bob CAN see his public structure, but the route id belongs to the private one.
results.push(expect("bob: pub structure + private routeId", await get(`/plumbing/structures/${pub}/routes/${privRoute}?as=bob`), 404));
console.log("\naliased private db is not leaked through a public structure:");
results.push(expect("bob reads aliased private db ", await get(`/plumbing/structures/${pub}/dbs/${secretsDbId}?as=bob`), 404));
results.push(expect("alice reads that db via her structure", await get(`/plumbing/structures/${priv}/dbs/${secretsDbId}?as=alice`), 200));
const pass = results.every(Boolean);
console.log(`\n${pass ? "PROVEN" : "FAILED"}: plumbing gates every artifact against its own structure's access rules.`);
server.close();
fs.rmSync(tmp, { recursive: true, force: true });
process.exit(pass ? 0 : 1);
})();