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:
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"] });