bzl

self-hosted ephemeral community engine
Log | Files | Refs | README | LICENSE

commit d7da1e814ad00534fa0949103e0e6ba72599eef8
parent 29f62157e91b015dc87fca4b0dc475b4914d9e3e
Author: SageAzakaela <106701693+SageAzakaela@users.noreply.github.com>
Date:   Thu, 19 Feb 2026 12:49:00 -0700

better directory listing plugins

What changed:

directory-server no longer requires a token for /announce.
New announcements are now review-gated:
New instance announce → pending
Owner can Approve or Reject in the mod tab
Public /list only returns approved entries
Existing approved entries keep approved status on normal updates.
Blocked hosts are still enforced server-side.
Files updated:

server.js (line 1)
client.js (line 1)
server.js (line 1)
client.js (line 1)
Publisher UX simplified:

Removed token field.
Uses window.location.origin automatically as instance URL.
Primary input is now Bzl instance name (ID auto-slugged server-side).
Still supports description/version/registration flag.
Built plugin zips:

directory-server.zip
directory-publisher.zip

Diffstat:
Mplugins_dev/directory-publisher/client.js | 21+++++++--------------
Mplugins_dev/directory-publisher/server.js | 40++++++++++++++++++++++++++++++++++------
Mplugins_dev/directory-server/client.js | 50+++++++++++++++++++++++++++++++-------------------
Mplugins_dev/directory-server/server.js | 96+++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
4 files changed, 139 insertions(+), 68 deletions(-)

diff --git a/plugins_dev/directory-publisher/client.js b/plugins_dev/directory-publisher/client.js @@ -14,7 +14,9 @@ window.BzlPluginHost.register("directory-publisher", (ctx) => { else if (v === false || v == null) continue; else node.setAttribute(k, String(v)); } - for (const c of children) node.appendChild(c); + for (const c of children) { + if (c instanceof Node) node.appendChild(c); + } return node; }; @@ -28,10 +30,6 @@ window.BzlPluginHost.register("directory-publisher", (ctx) => { const inst = c.instance && typeof c.instance === "object" ? c.instance : {}; const directoryUrl = el("input", { value: safe(c.directoryUrl), placeholder: "https://chat.bzl.one" }); - const token = el("input", { value: safe(c.token), placeholder: "Directory token (shared secret)" }); - - const id = el("input", { value: safe(inst.id), placeholder: "instance id (e.g. temple)" }); - const url = el("input", { value: safe(inst.url), placeholder: "https://your.instance" }); const name = el("input", { value: safe(inst.name), placeholder: "Display name" }); const description = el("input", { value: safe(inst.description), placeholder: "Short description" }); const bzlVersion = el("input", { value: safe(inst.bzlVersion), placeholder: "bzl version (optional)" }); @@ -54,10 +52,9 @@ window.BzlPluginHost.register("directory-publisher", (ctx) => { ctx.send("setConfig", { config: { directoryUrl: safe(directoryUrl.value).trim(), - token: safe(token.value).trim(), instance: { - id: safe(id.value).trim(), - url: safe(url.value).trim(), + id: "", + url: window.location.origin, name: safe(name.value).trim(), description: safe(description.value).trim(), bzlVersion: safe(bzlVersion.value).trim(), @@ -77,14 +74,10 @@ window.BzlPluginHost.register("directory-publisher", (ctx) => { el("div", { className: "row", style: "gap:10px; margin-top:10px" }, [ el("label", { style: "flex:1" }, [el("span", { className: "muted small", text: "Directory URL" }), directoryUrl]), ]), - el("div", { className: "row", style: "gap:10px; margin-top:10px" }, [el("label", { style: "flex:1" }, [el("span", { className: "muted small", text: "Token" }), token])] ), + el("div", { className: "muted small", text: `Instance URL: ${window.location.origin}`, style: "margin-top:10px" }), el("div", { className: "muted small", text: "Instance metadata", style: "margin-top:14px" }), - el("div", { className: "row", style: "gap:10px; margin-top:8px" }, [ - el("label", { style: "flex:1" }, [el("span", { className: "muted small", text: "Instance id" }), id]), - el("label", { style: "flex:1" }, [el("span", { className: "muted small", text: "Instance URL" }), url]), - ]), el("div", { className: "row", style: "gap:10px; margin-top:10px" }, [ - el("label", { style: "flex:1" }, [el("span", { className: "muted small", text: "Name" }), name]), + el("label", { style: "flex:1" }, [el("span", { className: "muted small", text: "Bzl instance name" }), name]), el("label", { style: "flex:1" }, [el("span", { className: "muted small", text: "Bzl version" }), bzlVersion]), ]), el("div", { className: "row", style: "gap:10px; margin-top:10px" }, [el("label", { style: "flex:1" }, [el("span", { className: "muted small", text: "Description" }), description])] ), diff --git a/plugins_dev/directory-publisher/server.js b/plugins_dev/directory-publisher/server.js @@ -30,7 +30,18 @@ function normalizeUrl(s) { } } -function postJson(targetUrl, token, payload) { +function slugId(name) { + const raw = String(name || "") + .trim() + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-+|-+$/g, "") + .slice(0, 32); + if (!raw) return ""; + return /^[a-z0-9][a-z0-9_.-]{0,31}$/.test(raw) ? raw : raw.replace(/[^a-z0-9_.-]/g, "").slice(0, 32); +} + +function postJson(targetUrl, payload) { return new Promise((resolve) => { let u; try { @@ -52,7 +63,6 @@ function postJson(targetUrl, token, payload) { headers: { "Content-Type": "application/json", "Content-Length": body.length, - Authorization: token ? `Bearer ${token}` : "" } }, (res) => { @@ -69,7 +79,6 @@ function postJson(targetUrl, token, payload) { module.exports = function init(api) { const config = readJson(CONFIG_PATH, { directoryUrl: "", - token: "", instance: { id: "", url: "", name: "", description: "", bzlVersion: "", requiresRegistrationCode: false }, publicHives: [] }); @@ -83,7 +92,6 @@ module.exports = function init(api) { if (ws?.user?.role !== "owner") return; const next = msg?.config || {}; config.directoryUrl = normalizeUrl(next.directoryUrl || config.directoryUrl); - config.token = String(next.token || config.token || "").trim(); config.instance = { ...(config.instance || {}), ...(next.instance || {}) }; config.instance.url = normalizeUrl(config.instance.url); config.instance.id = String(config.instance.id || "").trim(); @@ -100,9 +108,29 @@ module.exports = function init(api) { if (ws?.user?.role !== "owner") return; const urlBase = normalizeUrl(config.directoryUrl); if (!urlBase) return api.sendToUsers([ws.user.username], { type: "plugin:directory-publisher:result", ok: false, error: "Missing directory URL." }); + const name = String(config.instance?.name || "").trim(); + if (!name) return api.sendToUsers([ws.user.username], { type: "plugin:directory-publisher:result", ok: false, error: "Missing instance name." }); + const instanceUrl = normalizeUrl(config.instance?.url || ""); + if (!instanceUrl) { + return api.sendToUsers([ws.user.username], { + type: "plugin:directory-publisher:result", + ok: false, + error: "Missing instance URL (open publisher tab from the instance and save once)." + }); + } + const id = String(config.instance?.id || "").trim() || slugId(name) || "instance"; + const instancePayload = { + ...config.instance, + id, + name, + url: instanceUrl, + description: String(config.instance?.description || "").trim(), + bzlVersion: String(config.instance?.bzlVersion || "").trim(), + requiresRegistrationCode: Boolean(config.instance?.requiresRegistrationCode) + }; const endpoint = `${urlBase}/api/plugins/directory-server/announce`; - const payload = { instance: config.instance, publicHives: config.publicHives }; - const r = await postJson(endpoint, config.token, payload); + const payload = { instance: instancePayload, publicHives: config.publicHives }; + const r = await postJson(endpoint, payload); api.sendToUsers([ws.user.username], { type: "plugin:directory-publisher:result", ...r }); }); diff --git a/plugins_dev/directory-server/client.js b/plugins_dev/directory-server/client.js @@ -20,7 +20,9 @@ window.BzlPluginHost.register("directory-server", (ctx) => { else if (v === false || v == null) continue; else node.setAttribute(k, String(v)); } - for (const c of children) node.appendChild(c); + for (const c of children) { + if (c instanceof Node) node.appendChild(c); + } return node; }; @@ -124,32 +126,20 @@ window.BzlPluginHost.register("directory-server", (ctx) => { if (!modMount) return; modMount.innerHTML = ""; - const tokenSet = Boolean(lastConfig?.tokenSet); const hiddenIds = Array.isArray(lastConfig?.hiddenIds) ? lastConfig.hiddenIds : []; const blockedHosts = Array.isArray(lastConfig?.blockedHosts) ? lastConfig.blockedHosts : []; - - const tokenInput = el("input", { - placeholder: tokenSet ? "Token is set (enter to replace)" : "Set directory token (shared secret)", - style: "flex:1", - }); - const saveBtn = el("button", { - type: "button", - className: "primary", - text: "Save token", - onclick: () => { - const token = String(tokenInput.value || "").trim(); - ctx.send("setToken", { token }); - tokenInput.value = ""; - }, - }); + const pendingCount = Number(lastConfig?.pendingCount || 0); modMount.appendChild( el("div", { className: "panel", style: "padding:12px; margin-bottom:12px" }, [ el("div", { className: "row", style: "justify-content:space-between; align-items:center; gap:10px" }, [ - el("div", {}, [el("div", { text: "Directory settings", style: "font-weight:700" }), el("div", { className: "muted small", text: tokenSet ? "Token: set" : "Token: not set" })]), + el("div", {}, [ + el("div", { text: "Directory review settings", style: "font-weight:700" }), + el("div", { className: "muted small", text: `${pendingCount} pending review` }), + ]), el("button", { type: "button", className: "ghost", text: "Refresh", onclick: () => ctx.send("getConfig", {}) }), ]), - el("div", { className: "row", style: "gap:10px; margin-top:10px" }, [tokenInput, saveBtn]), + el("div", { className: "muted small", text: "Announcements are open. Approve or reject each instance here.", style: "margin-top:10px" }), ]) ); @@ -193,6 +183,7 @@ window.BzlPluginHost.register("directory-server", (ctx) => { const id = String(inst.id || "").trim().toLowerCase(); const name = String(inst.name || inst.id || "Instance").slice(0, 60); const url = String(inst.url || ""); + const status = String(entry?.status || "pending"); const host = getHost(url).toLowerCase(); const isHidden = Boolean(id && hiddenIds.includes(id)); const isBlocked = Boolean(host && blockedHosts.includes(host)); @@ -202,6 +193,8 @@ window.BzlPluginHost.register("directory-server", (ctx) => { el("div", { text: name, style: "font-weight:700" }), el("div", { className: "muted small", text: url }), el("div", { className: "muted small", text: `Last seen: ${fmtTime(entry?.lastSeenAt)}` }), + el("div", { className: "muted small", text: `Status: ${status}${entry?.reviewedAt ? ` (reviewed ${fmtTime(entry.reviewedAt)})` : ""}` }), + entry?.rejectedReason ? el("div", { className: "muted small", text: `Rejection note: ${String(entry.rejectedReason)}` }) : null, ]); const controls = el("div", { className: "row", style: "gap:8px; align-items:center; flex-wrap:wrap; justify-content:flex-end" }); @@ -211,6 +204,25 @@ window.BzlPluginHost.register("directory-server", (ctx) => { onchange: (e) => ctx.send("setHidden", { id, hidden: Boolean(e?.target?.checked) }), }); controls.appendChild(el("label", { className: "row small", style: "gap:8px; align-items:center" }, [hideCb, el("span", { text: "Hide" })])); + controls.appendChild( + el("button", { + type: "button", + className: status === "approved" ? "primary" : "ghost", + text: "Approve", + onclick: () => ctx.send("approveEntry", { id }), + }) + ); + controls.appendChild( + el("button", { + type: "button", + className: status === "rejected" ? "danger" : "ghost", + text: "Reject", + onclick: () => { + const reason = prompt("Optional rejection note:"); + ctx.send("rejectEntry", { id, reason: reason == null ? "" : String(reason) }); + }, + }) + ); if (host) { controls.appendChild( el("button", { diff --git a/plugins_dev/directory-server/server.js b/plugins_dev/directory-server/server.js @@ -16,15 +16,6 @@ function writeJson(filePath, value) { fs.writeFileSync(filePath, JSON.stringify(value, null, 2) + "\n", "utf8"); } -function getTokenFromReq(req) { - const h = req?.headers || {}; - const direct = typeof h["x-bzl-directory-token"] === "string" ? h["x-bzl-directory-token"].trim() : ""; - if (direct) return direct; - const auth = typeof h.authorization === "string" ? h.authorization.trim() : ""; - const m = auth.match(/^Bearer\s+(.+)$/i); - return m ? m[1].trim() : ""; -} - function normalizeUrl(s) { const raw = String(s || "").trim(); if (!raw) return ""; @@ -65,7 +56,7 @@ function sanitizePublicHives(payload) { } module.exports = function init(api) { - const config = readJson(CONFIG_PATH, { token: "", hiddenIds: [], blockedHosts: [] }); + const config = readJson(CONFIG_PATH, { hiddenIds: [], blockedHosts: [] }); const state = readJson(STATE_PATH, { version: 1, entries: {} }); const entries = new Map(Object.entries(state.entries || {})); @@ -91,21 +82,13 @@ module.exports = function init(api) { if (ws?.user?.role !== "owner") return; api.sendToUsers([ws.user.username], { type: "plugin:directory-server:config", - tokenSet: Boolean(config.token), hiddenIds: config.hiddenIds.slice(0, 500), blockedHosts: config.blockedHosts.slice(0, 500), - entryCount: entries.size + entryCount: entries.size, + pendingCount: Array.from(entries.values()).filter((e) => String(e?.status || "pending") === "pending").length }); }); - api.registerWs("setToken", (ws, msg) => { - if (ws?.user?.role !== "owner") return; - const token = String(msg?.token || "").trim(); - config.token = token; - writeJson(CONFIG_PATH, config); - api.broadcast({ type: "plugin:directory-server:configUpdated", tokenSet: Boolean(config.token) }); - }); - api.registerWs("getEntries", (ws) => { if (ws?.user?.role !== "owner") return; const list = Array.from(entries.values()) @@ -133,7 +116,7 @@ module.exports = function init(api) { else set.delete(id); config.hiddenIds = Array.from(set.values()).sort(); writeJson(CONFIG_PATH, config); - api.broadcast({ type: "plugin:directory-server:configUpdated", tokenSet: Boolean(config.token) }); + api.broadcast({ type: "plugin:directory-server:configUpdated" }); }); api.registerWs("setBlockedHost", (ws, msg) => { @@ -146,7 +129,7 @@ module.exports = function init(api) { else set.delete(host); config.blockedHosts = Array.from(set.values()).sort(); writeJson(CONFIG_PATH, config); - api.broadcast({ type: "plugin:directory-server:configUpdated", tokenSet: Boolean(config.token) }); + api.broadcast({ type: "plugin:directory-server:configUpdated" }); }); api.registerWs("deleteEntry", (ws, msg) => { @@ -158,12 +141,57 @@ module.exports = function init(api) { api.broadcast({ type: "plugin:directory-server:updated", id, deleted: true }); }); + api.registerWs("approveEntry", (ws, msg) => { + if (ws?.user?.role !== "owner") return; + const id = normalizeId(msg?.id); + if (!id) return; + const current = entries.get(id); + if (!current || !current.instance) return; + const next = { + ...current, + status: "approved", + reviewedAt: Date.now(), + reviewedBy: String(ws?.user?.username || ""), + rejectedReason: "" + }; + entries.set(id, next); + persist(); + api.broadcast({ type: "plugin:directory-server:updated", id, status: "approved", reviewedAt: next.reviewedAt, reviewedBy: next.reviewedBy }); + }); + + api.registerWs("rejectEntry", (ws, msg) => { + if (ws?.user?.role !== "owner") return; + const id = normalizeId(msg?.id); + if (!id) return; + const current = entries.get(id); + if (!current || !current.instance) return; + const reason = String(msg?.reason || "").trim().slice(0, 240); + const next = { + ...current, + status: "rejected", + reviewedAt: Date.now(), + reviewedBy: String(ws?.user?.username || ""), + rejectedReason: reason + }; + entries.set(id, next); + persist(); + api.broadcast({ + type: "plugin:directory-server:updated", + id, + status: "rejected", + reviewedAt: next.reviewedAt, + reviewedBy: next.reviewedBy, + rejectedReason: reason + }); + }); + api.registerHttp("GET", "/list", (_req, res, ctx) => { const hidden = new Set(config.hiddenIds); const blocked = new Set(config.blockedHosts); const list = Array.from(entries.values()) .map((e) => e) .filter((e) => { + if (String(e?.status || "pending") !== "approved") return false; const id = normalizeId(e?.instance?.id); if (id && hidden.has(id)) return false; const host = normalizeHost(e?.instance?.url); @@ -176,10 +204,6 @@ module.exports = function init(api) { }); api.registerHttp("POST", "/announce", async (req, _res, ctx) => { - if (!config.token) return ctx.sendJson(503, { ok: false, error: "Directory token not configured." }); - const token = getTokenFromReq(req); - if (!token || token !== config.token) return ctx.sendJson(401, { ok: false, error: "Unauthorized." }); - let body; try { body = await ctx.readJsonBody({ maxBytes: 256 * 1024 }); @@ -190,12 +214,26 @@ module.exports = function init(api) { const inst = sanitizeInstance(body); if (!inst) return ctx.sendJson(400, { ok: false, error: "Invalid instance payload." }); + const host = normalizeHost(inst.url); + if (host && config.blockedHosts.includes(host)) { + return ctx.sendJson(403, { ok: false, error: "This host is blocked by the directory owner." }); + } const publicHives = sanitizePublicHives(body); - const entry = { instance: inst, publicHives, lastSeenAt: Date.now() }; + const current = entries.get(inst.id); + const keepApproved = String(current?.status || "") === "approved"; + const entry = { + instance: inst, + publicHives, + lastSeenAt: Date.now(), + status: keepApproved ? "approved" : "pending", + reviewedAt: keepApproved ? Number(current?.reviewedAt || 0) : 0, + reviewedBy: keepApproved ? String(current?.reviewedBy || "") : "", + rejectedReason: "" + }; entries.set(inst.id, entry); persist(); - api.broadcast({ type: "plugin:directory-server:updated", id: inst.id, lastSeenAt: entry.lastSeenAt }); - return ctx.sendJson(200, { ok: true, id: inst.id }); + api.broadcast({ type: "plugin:directory-server:updated", id: inst.id, lastSeenAt: entry.lastSeenAt, status: entry.status }); + return ctx.sendJson(200, { ok: true, id: inst.id, status: entry.status }); }); api.log("info", "directory-server loaded", { http: ["/announce", "/list"] });