diff --git a/bliss-cli/bliss b/bliss-cli/bliss
index 17c6b18..2fe9bc5 100755
--- a/bliss-cli/bliss
+++ b/bliss-cli/bliss
@@ -68,14 +68,6 @@ const commands = {
out(c.BASE);
}
},
- // Force a fresh login with BLISS_USER / BLISS_PASS and report the result.
- // Unlike normal commands (which reuse a cached cookie and only re-auth on a
- // 401 the server never sends), this always hits /login, so it's the way to
- // (re)authenticate after changing creds or clearing a stale anonymous cookie.
- async login() {
- await c.login(); // throws with a clear message if the server rejects creds
- out("logged in as '" + (process.env.BLISS_USER || "?") + "' -> " + c.BASE);
- },
// ---- reads (JSON via /plumbing) ----
async structures() {
out(await c.getJSON("/plumbing/structures"));
diff --git a/bliss-cli/client.js b/bliss-cli/client.js
index c99b47a..a82c14f 100644
--- a/bliss-cli/client.js
+++ b/bliss-cli/client.js
@@ -79,21 +79,10 @@ async function login() {
headers: { "content-type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({ username: USER, password: PASS }).toString(),
});
- // The server issues an (anonymous) connect.sid on EVERY response — including a
- // failed login, which re-renders the form as a 200. So the presence of a
- // cookie says nothing about success. The real signal is the redirect: a good
- // login 302s to `next`/"/", a rejected one stays 2xx. Trust the status, not
- // the cookie, or we'd cache an anonymous session and silently stay logged out.
const cookie = extractCookie(res);
- const redirected = res.status >= 300 && res.status < 400;
- if (!redirected || !cookie) {
- throw new Error(
- "login failed for user '" +
- USER +
- "' — server rejected the credentials (HTTP " +
- res.status +
- "). Check BLISS_USER / BLISS_PASS.",
- );
+ if (!cookie) {
+ // A 200 back from /login means the login form re-rendered => bad creds.
+ throw new Error("login failed for user '" + USER + "' (check BLISS_PASS)");
}
saveCookie(cookie);
return cookie;
@@ -179,7 +168,6 @@ async function getText(urlPath) {
module.exports = {
BASE,
setTarget,
- login,
request,
getJSON,
getText,
diff --git a/db.js b/db.js
index f9ecd64..db795c3 100644
--- a/db.js
+++ b/db.js
@@ -296,100 +296,6 @@ function setStructurePublic(structureId, isPublic) {
);
}
-// ---- kernel notifications -------------------------------------------------
-//
-// Subscriptions are owned by the platform, not by structure databases. Each row
-// is tied to exactly one structure and one logged-in Bliss user; send-time code
-// re-checks canAccessStructure before handing anything to a push service.
-
-function upsertNotificationSubscription({
- structureId,
- userId,
- endpoint,
- clientId,
- subscription,
- contentEncoding,
-}) {
- return db
- .prepare(
- `INSERT INTO notification_subscriptions
- (structure_id, user_id, endpoint, client_id, subscription, content_encoding, updated_at)
- VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
- ON CONFLICT(structure_id, endpoint) DO UPDATE SET
- user_id = excluded.user_id,
- client_id = excluded.client_id,
- subscription = excluded.subscription,
- content_encoding = excluded.content_encoding,
- updated_at = CURRENT_TIMESTAMP`,
- )
- .run(
- structureId,
- userId,
- endpoint,
- clientId == null ? null : String(clientId),
- JSON.stringify(subscription),
- contentEncoding === "aesgcm" ? "aesgcm" : "aes128gcm",
- );
-}
-
-function deleteNotificationSubscription(structureId, endpoint) {
- return db
- .prepare(
- `DELETE FROM notification_subscriptions
- WHERE structure_id = ? AND endpoint = ?`,
- )
- .run(structureId, endpoint);
-}
-
-function getNotificationSubscriptions(structureId, userIds = null) {
- if (userIds && userIds.length === 0) return [];
- const rows = userIds
- ? db
- .prepare(
- `SELECT *
- FROM notification_subscriptions
- WHERE structure_id = ?
- AND user_id IN (${userIds.map(() => "?").join(",")})`,
- )
- .all(structureId, ...userIds)
- : db
- .prepare(
- `SELECT *
- FROM notification_subscriptions
- WHERE structure_id = ?`,
- )
- .all(structureId);
-
- return rows.map((row) => ({
- ...row,
- subscription: JSON.parse(row.subscription),
- }));
-}
-
-function getSubscribedNotificationUserIds(structureId) {
- return db
- .prepare(
- `SELECT DISTINCT user_id
- FROM notification_subscriptions
- WHERE structure_id = ?
- ORDER BY user_id`,
- )
- .all(structureId)
- .map((row) => row.user_id);
-}
-
-function getStructureMemberUserIds(structureId) {
- return db
- .prepare(
- `SELECT user_id
- FROM structure_members
- WHERE structure_id = ?
- ORDER BY user_id`,
- )
- .all(structureId)
- .map((row) => row.user_id);
-}
-
function createRoute(verb, path, structureId, handler) {
path = encodeURI(path);
@@ -984,11 +890,6 @@ module.exports = {
removeMember,
setStructurePrivacy,
setStructurePublic,
- upsertNotificationSubscription,
- deleteNotificationSubscription,
- getNotificationSubscriptions,
- getSubscribedNotificationUserIds,
- getStructureMemberUserIds,
createRoute,
getRoutes,
getRoute,
diff --git a/index.js b/index.js
index cb8ceea..3fa42ff 100644
--- a/index.js
+++ b/index.js
@@ -210,7 +210,6 @@ function makeLibs(structureId, console, memberUserId) {
return dbs[alias];
},
push: push,
- notifications: makeNotifications(structureId, memberUserId),
files: { saveFile: (...args) => saveFile(structureId, ...args) },
};
}
@@ -229,126 +228,6 @@ function currentUserFor(req) {
}
}
-function userIdFrom(value) {
- if (value == null) return null;
- if (typeof value === "object" && value.id != null) return Number(value.id);
- const n = Number(value);
- return Number.isInteger(n) ? n : null;
-}
-
-function normalizeNotificationUrl(structure, rawUrl) {
- const route = makeRoute(structure);
- const url = rawUrl == null || rawUrl === "" ? "/" : String(rawUrl);
- if (!url.startsWith("/") || url.startsWith("//")) {
- throw new Error("notification url must be a same-site path");
- }
- const prefix = structure.route_prefix || "";
- const scoped = prefix && !url.startsWith(prefix) ? route(url) : url;
- if (prefix && scoped !== prefix && !scoped.startsWith(prefix + "/")) {
- throw new Error("notification url must stay inside this structure");
- }
- return scoped;
-}
-
-function normalizeNotificationRecipients(structureId, target) {
- const explicit = target !== "viewers" && target !== "subscribers";
- let ids;
-
- if (target == null || target === "viewers" || target === "subscribers") {
- ids = model.getSubscribedNotificationUserIds(structureId);
- } else if (target === "members") {
- ids = model.getStructureMemberUserIds(structureId);
- } else if (Array.isArray(target)) {
- ids = target.map(userIdFrom);
- } else {
- ids = [userIdFrom(target)];
- }
-
- if (ids.some((id) => !Number.isInteger(id))) {
- throw new Error("notification recipient must be a Bliss user");
- }
-
- const unique = [...new Set(ids)];
- const forbidden = unique.filter(
- (id) => !model.canAccessStructure(id, structureId),
- );
- if (forbidden.length && explicit) {
- throw new Error("notification recipient cannot access this structure");
- }
- return unique.filter((id) => !forbidden.includes(id));
-}
-
-function normalizeNotificationExclusions(options = {}) {
- const ids = [];
- const collect = (value) => {
- if (Array.isArray(value)) return value.forEach(collect);
- const id = userIdFrom(value);
- if (id != null) ids.push(id);
- };
- collect(options.except);
- collect(options.exceptUser);
- collect(options.exceptUserId);
- return {
- userIds: new Set(ids),
- clientId:
- options.exceptClientId == null ? null : String(options.exceptClientId),
- };
-}
-
-function makeNotifications(structureId, actorUserId) {
- const structure = model.getStructure(structureId);
- return {
- async send(options = {}) {
- const target = options.to ?? options.users ?? options.user ?? "viewers";
- const recipientIds = normalizeNotificationRecipients(structureId, target);
- const except = normalizeNotificationExclusions(options);
- const eligibleIds = recipientIds.filter((id) => !except.userIds.has(id));
- const url = normalizeNotificationUrl(structure, options.url || "/");
- const rows = model.getNotificationSubscriptions(structureId, eligibleIds);
- const payload = JSON.stringify({
- structureId: String(structureId),
- title: String(options.title || structure.name || "Bliss"),
- body: String(options.body || ""),
- url,
- });
- let sent = 0;
- let skipped = 0;
-
- await Promise.all(
- rows.map(async (row) => {
- if (except.clientId && row.client_id === except.clientId) {
- skipped += 1;
- return;
- }
- if (!model.canAccessStructure(row.user_id, structureId)) {
- skipped += 1;
- return;
- }
- try {
- await push.sendNotification(row.subscription, payload, {
- contentEncoding: row.content_encoding || undefined,
- });
- sent += 1;
- } catch (err) {
- if (err && (err.statusCode === 404 || err.statusCode === 410)) {
- model.deleteNotificationSubscription(structureId, row.endpoint);
- } else {
- throw err;
- }
- }
- }),
- );
-
- return {
- sent,
- skipped,
- structureId,
- actorUserId: actorUserId ?? null,
- };
- },
- };
-}
-
function bootstrapContext(structureId, routeId, initContext, memberUserId) {
const structure = model.getStructure(structureId);
const console = makeConsole(structureId, routeId);
@@ -484,10 +363,6 @@ wsRouter.ws("*", (ws, req) => {
}),
);
};
- req.user = req.currentUser;
- req.notifications = makeNotifications(route.structure_id, req.session.userId);
- ws.user = req.currentUser;
- ws.notifications = req.notifications;
try {
return found.handler(ws, req);
@@ -575,85 +450,6 @@ app.all("/logout", async (req, res) => {
});
});
-app.get("/_bliss/notifications/key", (req, res) => {
- if (!vapidPublicKey) return res.status(503).json({ error: "not configured" });
- res.json({ publicKey: vapidPublicKey });
-});
-
-app.post("/_bliss/notifications/subscribe", (req, res) => {
- const structureId = Number(req.body?.structure_id);
- const userId = req.session && req.session.userId;
- const subscription = req.body?.subscription;
- if (!userId)
- return res.status(401).send("log in before enabling notifications");
- if (!Number.isInteger(structureId) || !model.getStructure(structureId)) {
- return res.status(400).send("unknown structure");
- }
- if (!model.canAccessStructure(userId, structureId)) {
- return res.status(403).send("you cannot access this structure");
- }
- if (!subscription || !subscription.endpoint) {
- return res.status(400).send("missing push subscription");
- }
-
- model.upsertNotificationSubscription({
- structureId,
- userId,
- endpoint: subscription.endpoint,
- clientId: req.body?.client_id,
- subscription,
- contentEncoding: req.body?.content_encoding,
- });
- res.status(201).json({ ok: true });
-});
-
-app.get("/_bliss/notification-sw.js", (req, res) => {
- res.type("application/javascript").set("Cache-Control", "no-cache").send(`
-const openStructures = new Set();
-
-self.addEventListener("install", (event) => event.waitUntil(self.skipWaiting()));
-self.addEventListener("activate", (event) => event.waitUntil(self.clients.claim()));
-
-self.addEventListener("message", (event) => {
- const data = event.data || {};
- if (data.type !== "bliss:visibility" || !data.structureId) return;
- const id = String(data.structureId);
- if (data.state === "open") {
- openStructures.add(id);
- self.registration.getNotifications({ tag: "bliss:" + id }).then((items) => items.forEach((n) => n.close()));
- } else {
- openStructures.delete(id);
- }
-});
-
-self.addEventListener("push", (event) => {
- let data = {};
- try { data = event.data ? event.data.json() : {}; } catch (_) {}
- const structureId = String(data.structureId || "");
- if (structureId && openStructures.has(structureId)) return;
- event.waitUntil(self.registration.showNotification(data.title || "Bliss", {
- body: data.body || "",
- tag: structureId ? "bliss:" + structureId : "bliss",
- data: { url: data.url || "/" },
- }));
-});
-
-self.addEventListener("notificationclick", (event) => {
- event.notification.close();
- const target = event.notification.data && event.notification.data.url || "/";
- event.waitUntil(clients.matchAll({ type: "window", includeUncontrolled: true }).then((items) => {
- for (const client of items) {
- try {
- const url = new URL(client.url);
- if (url.pathname === target && "focus" in client) return client.focus();
- } catch (_) {}
- }
- if (clients.openWindow) return clients.openWindow(target);
- }));
-});
-`);
-});
-
// _ _ _______ ______ ___ _ _______ __ __ _______ _______
// | | _ | || || _ | | | | || || | | || || |
// | || || || _ || | || | |_| || _____|| |_| || _ || _ |
@@ -671,7 +467,6 @@ function headChrome(headInjection) {
-