diff --git a/index.js b/index.js index 43cdbd3..279c55b 100644 --- a/index.js +++ b/index.js @@ -2,7 +2,7 @@ const fs = require("fs"); const util = require("util"); const vm = require("node:vm"); const path = require("path"); -require('dotenv').config() +require("dotenv").config(); const express = require("express"); const session = require("express-session"); const fileUpload = require("express-fileupload"); @@ -19,7 +19,7 @@ const model = require("./db"); const PORT = process.env.PORT || 3000; const db = model.db; -const wsRouter = express.Router() +const wsRouter = express.Router(); let viewpath = path.join(__dirname, "views"); let eta = new Eta({ views: viewpath, cache: false, autoEscape: true }); @@ -36,7 +36,12 @@ let routeIndex = Object.freeze({ }); const wsConnections = new Map(); -const INSPECT_OPTS = { showHidden: false, depth: null, colors: false, compact: false }; +const INSPECT_OPTS = { + showHidden: false, + depth: null, + colors: false, + compact: false, +}; function inspectArgs(args) { return args.map((v) => util.inspect(v, INSPECT_OPTS)).join(" "); } @@ -67,8 +72,8 @@ app.use( app.use("/", wsRouter); app.use("/plumbing", require("./plumbing")); -const vapidPublicKey = process.env.VAPID_PUBLIC_KEY -const vapidPrivateKey = process.env.VAPID_PRIVATE_KEY +const vapidPublicKey = process.env.VAPID_PUBLIC_KEY; +const vapidPrivateKey = process.env.VAPID_PRIVATE_KEY; // Configure web-push with your VAPID details webPush.setVapidDetails( @@ -77,7 +82,37 @@ webPush.setVapidDetails( vapidPrivateKey, ); -async function saveFile(structureId, req, uploadedFile, asset=false) { +// KaiOS's push service only understands the legacy draft `aesgcm` content +// encoding; everyone else (iOS, Chrome, Firefox, modern Safari) uses the +// RFC 8291 standard `aes128gcm`, which is web-push's default. The subscription +// endpoint hostname tells us which is which, so we pick per-subscription and +// keep the standard as the default for anything we don't recognize. +const LEGACY_AESGCM_HOSTS = ["push.kaiostech.com", "kai.jiophone.net"]; + +function pushEncodingFor(subscription) { + try { + const host = new URL(subscription.endpoint).hostname; + return LEGACY_AESGCM_HOSTS.some((h) => host === h || host.endsWith("." + h)) + ? "aesgcm" + : "aes128gcm"; + } catch { + return "aes128gcm"; + } +} + +// Transparent drop-in for the web-push module handed to structures via +// require('push'): identical API (setVapidDetails, generateVAPIDKeys, …) via +// the prototype chain, but sendNotification auto-selects the content encoding +// from the subscription endpoint unless the caller passed one explicitly. +const push = Object.create(webPush); +push.sendNotification = function (subscription, payload, options = {}) { + return webPush.sendNotification(subscription, payload, { + ...options, + contentEncoding: options.contentEncoding || pushEncodingFor(subscription), + }); +}; + +async function saveFile(structureId, req, uploadedFile, asset = false) { const name = uploadedFile.name; const [mime_type, mime_subtype] = uploadedFile.mimetype.split("/"); // structureId may arrive as a number (route.structure_id is an INTEGER) when @@ -103,7 +138,7 @@ async function saveFile(structureId, req, uploadedFile, asset=false) { let file = model.getFile(id); file.url = prefixUrlWithHost(req, file.path); - return file + return file; } // A console whose log() mirrors to stdout and to the structure's logs table. @@ -120,7 +155,12 @@ function makeConsole(structureId, routeId) { // returning its exports. Each library runs in its own context so nothing // leaks between dbs. function runLibrary(sql, librarySource, console) { - const libContext = vm.createContext({ sql, console, fetch, module: { exports: null } }); + const libContext = vm.createContext({ + sql, + console, + fetch, + module: { exports: null }, + }); vm.runInContext(librarySource, libContext); return libContext.module.exports; } @@ -130,12 +170,15 @@ function makeLibs(structureId, console) { const dbs = {}; for (let appDb of model.getDbsForStructure(structureId)) { const sql = model.getDbInstance(appDb.id); - dbs[appDb.alias] = { library: runLibrary(sql, appDb.library, console), sql }; + dbs[appDb.alias] = { + library: runLibrary(sql, appDb.library, console), + sql, + }; } return { eta: model.getTemplater(structureId), db: (alias) => dbs[alias], - push: webPush, + push: push, files: { saveFile: (...args) => saveFile(structureId, ...args) }, }; } @@ -180,7 +223,6 @@ function routeWithPrefix(route) { return withPrefix(route.route_prefix, route.path); } - function compileWebsocketHandler(route) { try { // Keep the existing WS handler context for compatibility. Restricting the @@ -482,8 +524,7 @@ function fragmentFromPage(html) { // carrying provenance becomes a decorated fragment; anything else passes through. function decorate(html, { headInjection, source, fragment } = {}) { const lower = html.toLowerCase(); - const isFullDoc = - lower.startsWith("") || lower.startsWith("") || lower.startsWith(" { req.body?.contextId, templateId, ); - if (!savedContext) return res.status(410).send("Template context expired; refresh the page"); + if (!savedContext) + return res.status(410).send("Template context expired; refresh the page"); const context = { ...savedContext }; context.route = makeRoute(structure); - return res.type("html").send(renderTemplate(structureId, template.name, context)); + return res + .type("html") + .send(renderTemplate(structureId, template.name, context)); }); // Provenance for a rendered response: GET routes are re-embeddable, so they @@ -578,7 +622,11 @@ function sidebarStuff(structId) { } app.get("/workshop/:structure_id", (req, res) => { - return renderWorkshop(res, "workshop/editor", sidebarStuff(req.params.structure_id)); + return renderWorkshop( + res, + "workshop/editor", + sidebarStuff(req.params.structure_id), + ); }); app.post("/workshop/:structure_id/clone", (req, res) => { @@ -663,8 +711,11 @@ app.put("/workshop/:structure_id/route/:route_id", (req, res) => { model.updateRoute({ ...route, ...req.body }); if (req.body.scaffold_page) { // TODO: make this send over scaffold page id too someday - const latestPage = model.getLatestScaffoldPage(req.params.route_id) - model.updateScaffoldPage({ ...latestPage, content: req.body.scaffold_page }) + const latestPage = model.getLatestScaffoldPage(req.params.route_id); + model.updateScaffoldPage({ + ...latestPage, + content: req.body.scaffold_page, + }); } buildRoutes(); return res.send("good"); @@ -773,9 +824,12 @@ app.get("/workshop/:structure_id/template/:template_id/preview", (req, res) => { context.it.route = makeRoute(struct); return res.send( - decorate(renderTemplate(req.params.structure_id, template.name, context.it), { - headInjection: struct.head_injection, - }), + decorate( + renderTemplate(req.params.structure_id, template.name, context.it), + { + headInjection: struct.head_injection, + }, + ), ); }); @@ -784,16 +838,16 @@ app.get("/workshop/:structure_id/route/:id", (req, res) => { let routePrefix = model.getStructure(req.params.structure_id).route_prefix; let previewUrl = null; - if (route["verb"] == "GET" ) { + if (route["verb"] == "GET") { previewUrl = routePrefix ? path.join(routePrefix || "", route.path) : route.path; - } - else { + } else { previewUrl = `/workshop/${req.params.structure_id}/route/${req.params.id}/preview`; } - const template = route["verb"] == "GET" ? "workshop/route" : "workshop/cud_route"; + const template = + route["verb"] == "GET" ? "workshop/route" : "workshop/cud_route"; return renderWorkshop(res, template, { route: route, @@ -844,15 +898,13 @@ app.post("/workshop/:structure_id/files", async (req, res) => { const targetFile = req.files.file; try { - const uploadedFile = await saveFile(structure_id, req, targetFile) + const uploadedFile = await saveFile(structure_id, req, targetFile); return res.send(eta.render("workshop/file_detail", { file: uploadedFile })); + } catch (e) { + console.log(e); + res.status(500); + return res.send(e); } - catch (e) { - console.log(e) - res.status(500); - return res.send(e); - } - }); function prefixUrlWithHost(req, path) { @@ -973,9 +1025,13 @@ app.all("*", async (req, res) => { }), ); }; - const handlerScript = new vm.Script(route.handler, { filename: `handler_${route.id}.js` }); + const handlerScript = new vm.Script(route.handler, { + filename: `handler_${route.id}.js`, + }); // Prepare the execution code that invokes `handler(req, res)` and awaits it - const executionScript = new vm.Script(`handler(req, res); `, { filename: `execution_${route.id}.js` }); + const executionScript = new vm.Script(`handler(req, res); `, { + filename: `execution_${route.id}.js`, + }); await handlerScript.runInContext(context); const result = await executionScript.runInContext(context); } catch (e) {