Lumi/scripts/verify-destructive-actions.js
2026-07-17 22:10:00 +02:00

104 lines
5.4 KiB
JavaScript

const assert = require("assert");
const fs = require("fs");
const path = require("path");
const {
issueConfirmation,
consumeConfirmation,
isDestructivePath
} = require("../src/services/destructive-confirm");
const root = path.join(__dirname, "..");
const destructivePath = /(?:^|\/)(?:delete|remove|clear|reset|renew|uninstall|cleanup|archive|revoke|unlink|unset|revert)(?:\/|$)/i;
function filesBelow(directory, extension, output = []) {
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
const target = path.join(directory, entry.name);
if (entry.isDirectory()) filesBelow(target, extension, output);
else if (target.endsWith(extension)) output.push(target);
}
return output;
}
function maskEjs(source) {
return source.replace(/<%[\s\S]*?%>/g, "EJS");
}
function attribute(tag, name) {
return tag.match(new RegExp(`\\b${name}\\s*=\\s*["']([^"']*)["']`, "i"))?.[1] || "";
}
function requireConfirmationCopy(tag, file) {
for (const attributeName of ["data-confirm-mode", "data-confirm-title", "data-confirm-text", "data-confirm-label"]) {
assert(new RegExp(`\\b${attributeName}(?:\\s*=|\\s|>)`, "i").test(tag), `${path.relative(root, file)} has an unconnected destructive control missing ${attributeName}: ${tag}`);
}
assert.equal(attribute(tag, "data-confirm-mode"), "modal", `${path.relative(root, file)} must use the shared timed modal`);
}
function verifyViews() {
const files = [
...filesBelow(path.join(root, "src", "web", "views"), ".ejs"),
...filesBelow(path.join(root, "plugins"), ".ejs")
];
let controls = 0;
for (const file of files) {
const source = maskEjs(fs.readFileSync(file, "utf8"));
assert(!/name\s*=\s*["']action["'][^>]*value\s*=\s*["']delete["']/i.test(source), `${path.relative(root, file)} hides a delete behind a generic POST route; use an explicit /delete endpoint`);
for (const tag of source.match(/<(?:form|button)\b[^>]*>/gi) || []) {
const action = attribute(tag, "action") || attribute(tag, "formaction");
const confirmAction = attribute(tag, "data-confirm-action");
if (!destructivePath.test(action) && !destructivePath.test(confirmAction)) continue;
requireConfirmationCopy(tag, file);
controls += 1;
}
}
assert(controls >= 30, `Expected broad destructive-control coverage, found only ${controls}`);
return controls;
}
function verifySharedInfrastructure() {
for (const action of [
"/admin/commands/1/delete",
"/admin/navigation/reset",
"/admin/updates/core/revert",
"/plugins/auto-vc/settings/remove"
]) {
assert.equal(isDestructivePath(action), true, `Server does not protect ${action}`);
}
const req = {
session: { user: { id: "admin-user", isAdmin: true } },
body: {},
get() { return null; }
};
const issued = issueConfirmation(req, "/admin/updates/core/revert");
req.session.destructive_confirmations[issued.token].not_before = Date.now() - 1;
assert.equal(consumeConfirmation(req, "/admin/updates/core/revert", issued.token).valid, true);
const appScript = fs.readFileSync(path.join(root, "src", "web", "public", "app.js"), "utf8");
assert(appScript.includes("submitter?.dataset?.confirmMode"), "Submit-button confirmation metadata is ignored");
assert(appScript.includes("submitter?.dataset?.confirmLabel || form.dataset.confirmLabel"), "The clicked submit button cannot override generic confirmation copy");
assert(appScript.includes('form.querySelector(\'input[name="confirmation_token"]\')'), "Timed form confirmation tokens are not attached to submitted forms");
assert(appScript.includes("const response = await fetch(action, requestOptions)"), "Async update requests do not submit through the shared confirmed form payload");
assert(!appScript.includes("window.LumiConfirm.destructiveFetch(form.action"), "Timed update forms request a second confirmation instead of reusing the form token");
assert(appScript.includes('state.confirmButton.removeEventListener("click", state.onConfirm)'), "Cancelled or expired confirmations leave stale modal submissions behind");
assert(appScript.includes("state.onConfirm = onConfirm"), "Timed form confirmations do not retain their listener for cleanup");
const toolManagerScript = fs.readFileSync(path.join(root, "plugins", "lumi_ai", "public", "tool-manager.js"), "utf8");
for (const field of ["confirmMode", "confirmTitle", "confirmText", "confirmLabel"]) {
assert(toolManagerScript.includes(`form.dataset.${field}`), `Dynamic AI tool deletion is missing ${field}`);
}
const autoVcView = fs.readFileSync(path.join(root, "plugins", "auto-vc", "views", "auto-vc.ejs"), "utf8");
assert(autoVcView.includes('settingsForm.action = "/plugins/auto-vc/settings/remove"'), "Auto VC lobby deletion does not switch to its protected route");
const improvementView = fs.readFileSync(path.join(root, "plugins", "lumi_ai", "views", "improvement-center.ejs"), "utf8");
assert(improvementView.includes("/reviews/<%= review.id %>/delete"));
assert(improvementView.includes("/corrections/<%= entry.id %>/delete"));
const improvementScript = fs.readFileSync(path.join(root, "plugins", "lumi_ai", "public", "improvement-center.js"), "utf8");
assert.equal(improvementScript.includes("window.confirm"), false, "Improvement Center still uses the legacy browser confirmation");
}
const controls = verifyViews();
verifySharedInfrastructure();
console.log(`Destructive action verification passed (${controls} controls).`);