MEGAINFRA-WEBAPP/scripts/patch-next-dev-blocker.cjs

97 lines
3.5 KiB
JavaScript
Raw Permalink Normal View History

#!/usr/bin/env node
/**
* Patch Next 16's dev-only cross-site blocker so the dev server accepts
* requests from any host / IP / tunneled domain (e.g. `coolify.mywire.org`).
*
* Why this exists
* ---------------
* Next 16's dev server calls `blockCrossSiteDEV(...)` on every internal
* request (`/_next/hmr`, etc.) and blocks the request unless the
* `Origin` hostname matches one of:
* - `localhost`
* - `**.localhost`
* - any literal entry in `next.config.js` → `allowedDevOrigins`
* - the `hostname` flag passed to `next dev -H <host>`
*
* The matcher in `next/dist/server/app-render/csrf-protection.js`
* (`matchWildcardDomain`) **rejects the literal `*` and `**` as the only
* segment** of a pattern, so we cannot wildcard the allowlist through the
* documented config. To make the dev server reachable from any host —
* which is what we want when running behind Coolify, a tunnel, or just
* any LAN device — we replace the function body with a no-op.
*
* Production behaviour is unaffected: this module is only loaded by
* `next dev`, never by `next build` / `next start`.
*
* Idempotent: running this script repeatedly is safe; already-patched
* files are detected and skipped.
*/
"use strict";
const fs = require("node:fs");
const path = require("node:path");
const MARKER = "/* PATCHED: allow any host in dev (scripts/patch-next-dev-blocker.cjs) */";
const FILES = [
"node_modules/next/dist/server/lib/router-utils/block-cross-site-dev.js",
"node_modules/next/dist/esm/server/lib/router-utils/block-cross-site-dev.js",
];
const REPLACEMENT = `${MARKER}
const blockCrossSiteDEV = (req, res, allowedDevOrigins, hostname) => {
// Patched: allow any host / IP / tunneled domain in development.
// Original implementation is preserved above in source maps for debugging.
return false;
};`;
// Match `const blockCrossSiteDEV = (req, res, ...) => { ... };` — greedy match
// to the closing brace + semicolon at the start of a line.
const FUNCTION_REGEX = /const blockCrossSiteDEV\s*=\s*\(req[^)]*\)\s*=>\s*\{[\s\S]*?\n\};/;
let patched = 0;
let already = 0;
let missing = 0;
let failed = 0;
// If `next` isn't installed yet (e.g. the `npm ci` step ran with
// `--ignore-scripts` before the rest of the workspace was copied), there
// is nothing to patch and no failure to report. Exit 0 so the build
// proceeds.
if (!fs.existsSync(path.resolve(process.cwd(), "node_modules/next"))) {
console.log(" = next not installed yet, skipping patch (nothing to do)");
process.exit(0);
}
for (const rel of FILES) {
const abs = path.resolve(process.cwd(), rel);
if (!fs.existsSync(abs)) {
console.warn(` ! ${rel} (not found, skipping)`);
missing++;
continue;
}
const original = fs.readFileSync(abs, "utf8");
if (original.includes(MARKER)) {
console.log(` = ${rel} (already patched)`);
already++;
continue;
}
if (!FUNCTION_REGEX.test(original)) {
console.warn(` ! ${rel} (could not locate function, skipping — Next version may have changed)`);
failed++;
continue;
}
const updated = original.replace(FUNCTION_REGEX, REPLACEMENT);
fs.writeFileSync(abs, updated, "utf8");
console.log(` ✓ ${rel}`);
patched++;
}
console.log(
`\nDone. ${patched} patched · ${already} already patched · ${failed} skipped · ${missing} not found.`,
);
// Only the `failed` case is a real failure — missing files are
// expected during Docker builds where the workspace isn't fully copied
// yet (e.g. when the script runs before `npm ci`).
process.exit(failed > 0 ? 1 : 0);