Files
2026-07-13 13:39:12 +08:00

72 lines
3.3 KiB
TypeScript

// Regression guard for #5083 (Bug 1): the dashboard, when reached from a LAN /
// Tailscale / non-loopback host, opens `ws://<that-host>:<port>` to its own Live WS
// server. The static Content-Security-Policy in next.config.mjs previously allowed
// `ws:` only for loopback origins (`ws://localhost:*`, `ws://127.0.0.1:*`) plus a bare
// `wss:` — so a plain-`ws:` connection to a non-loopback host was blocked by the
// browser. The fix permits the bare `ws:` scheme (symmetric with the bare `wss:` that
// was already allowed), without introducing any global Next.js middleware (the project
// intentionally has none — interception is route-specific; see CLAUDE.md / AGENTS.md).
//
// This test pins the connect-src directive in next.config.mjs so the LAN/Tailscale WS
// allowance cannot silently regress, while confirming the other security directives are
// left intact.
import test from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, resolve } from "node:path";
const here = dirname(fileURLToPath(import.meta.url));
const nextConfig = readFileSync(resolve(here, "../../next.config.mjs"), "utf8");
// Extract the connect-src directive string literal from the CSP array.
function connectSrcDirective(): string {
const match = nextConfig.match(/"(connect-src[^"]*)"/);
assert.ok(match, "next.config.mjs must define a connect-src directive in the CSP array");
return match![1];
}
test("#5083 connect-src permits the bare ws: scheme for non-loopback dashboards", () => {
const connectSrc = connectSrcDirective();
// Bare `ws:` must be present as its own token (not just `ws://localhost`).
assert.match(
connectSrc,
/(^|\s)ws:(\s|$)/,
`connect-src must allow the bare ws: scheme; got: ${connectSrc}`
);
});
test("#5083 connect-src keeps the bare wss: scheme it mirrors", () => {
const connectSrc = connectSrcDirective();
assert.match(connectSrc, /(^|\s)wss:(\s|$)/, "connect-src must still allow bare wss:");
});
test("#5083 connect-src still scopes 'self' and explicit loopback origins", () => {
const connectSrc = connectSrcDirective();
assert.ok(connectSrc.includes("'self'"), "connect-src must still include 'self'");
assert.ok(
connectSrc.includes("ws://localhost:*") && connectSrc.includes("ws://127.0.0.1:*"),
"explicit loopback ws origins must remain listed"
);
});
test("#5083 fix does NOT introduce a global Next.js middleware", () => {
// The project has no global middleware by design; the CSP fix must stay declarative.
let exists = true;
try {
readFileSync(resolve(here, "../../src/middleware.ts"), "utf8");
} catch {
exists = false;
}
assert.equal(exists, false, "src/middleware.ts must not exist (no global middleware — see CLAUDE.md)");
});
test("#5083 baseline security directives remain intact in the CSP", () => {
// frame-ancestors 'none' is the global default; object-src 'none' and base-uri 'self'
// must not have been weakened by the connect-src change.
assert.ok(nextConfig.includes("frame-ancestors 'none'"), "frame-ancestors 'none' must remain");
assert.ok(nextConfig.includes("object-src 'none'"), "object-src 'none' must remain");
assert.ok(nextConfig.includes("base-uri 'self'"), "base-uri 'self' must remain");
});