#!/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 ` * * 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; 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.`, ); process.exit(failed > 0 ? 1 : 0);