remove plumbing
This commit is contained in:
parent
f6bde50ce3
commit
2954aecdf8
9 changed files with 0 additions and 503 deletions
|
|
@ -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>
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
function handler(req, res) {
|
|
||||||
res.render("inspector/slideout", {});
|
|
||||||
}
|
|
||||||
|
|
@ -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}`);
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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 });
|
|
||||||
}
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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 });
|
|
||||||
}
|
|
||||||
|
|
@ -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);
|
|
||||||
})();
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue