// Custom Next.js dev server that keeps live updates but drops the dev-only // debug surface for anyone who is not on localhost. // // WHY THIS EXISTS // `beta.dvorakmortonxd.dev` is an unauthenticated tunnel straight to the dev // server, and a Next.js dev server is not meant to be public. It exposes a set // of unauthenticated debug endpoints under `/__nextjs_*` (open-the-editor, // stack-frame/source-map lookups, node debugger attach, server restart, …) that // you really do not want on the internet. Next.js wires those endpoints in // unconditionally, so `next.config.ts` cannot turn them off. // // This server sits in front of Next's request handler and returns 404 for // `/__nextjs*` and source maps when the request did not arrive over loopback. // Everything the browser needs for live updates still goes through: // - normal app routes // - `/_next/hmr` (the HMR / Fast Refresh WebSocket) // // So `localhost:3000` keeps the full dev experience, while the LAN/tunnel get // a plain, live-reloading version of the site. // // Run with `npm run dev`. To bypass it entirely, use `npx next dev --turbopack`. import { createServer } from "node:http"; import next from "next"; const dev = process.env.NODE_ENV !== "production"; const port = Number.parseInt(process.env.PORT || "3000", 10); const hostname = process.env.HOSTNAME || "0.0.0.0"; // Next's dev-only internals all share this prefix: // /__nextjs_launch-editor, /__nextjs_original-stack-frames, // /__nextjs_source-map, /__nextjs_server_status, // /__nextjs_attach-nodejs-inspector, /__nextjs_restart_dev, … const DEV_DEBUG_PREFIX = "/__nextjs"; function isLoopback(socket) { const address = socket.localAddress || ""; return ( address === "127.0.0.1" || address === "::1" || address === "::ffff:127.0.0.1" || address.startsWith("127.") ); } // A request that arrived through a proxy/tunnel (Cloudflare or otherwise) is // never a local dev request, even if the proxy itself runs on this machine and // therefore connects over loopback. function hasForwardedHeaders(req) { const headers = req.headers; return Boolean( headers["cf-connecting-ip"] || headers["cf-ray"] || headers["x-forwarded-for"] || headers["x-forwarded-proto"] || headers["forwarded"], ); } function isLocalDebugRequest(req) { return isLoopback(req.socket) && !hasForwardedHeaders(req); } function isDevDebugRequest(req) { let pathname = req.url || "/"; try { pathname = new URL(req.url || "/", "http://localhost").pathname; } catch { // Fall back to the raw URL if it is not parseable. } return ( pathname.startsWith(DEV_DEBUG_PREFIX) || pathname.endsWith(".map") ); } let handle; const server = createServer((req, res) => { if (!isLocalDebugRequest(req) && isDevDebugRequest(req)) { res.statusCode = 404; res.setHeader("content-type", "text/plain; charset=utf-8"); res.setHeader("cache-control", "no-store"); res.end("Not found"); return; } // `handle` is assigned right after `getRequestHandler()` below; requests // cannot arrive before `listen`, which happens after that assignment. handle(req, res); }); // Passing our HTTP server in lets Next attach its HMR upgrade handler to it. const app = next({ dev, turbopack: true, hostname, port, httpServer: server }); handle = app.getRequestHandler(); await app.prepare(); server.listen(port, hostname, () => { console.log(`> dev server ready on http://localhost:${port}`); console.log( "> dev debug endpoints are localhost-only (404 on the LAN/tunnel); HMR still works", ); }); function shutdown() { // Drop idle keep-alive sockets so Ctrl+C exits promptly instead of waiting // on lingering connections. server.closeAllConnections?.(); server.close(() => { app.close().finally(() => process.exit(0)); }); // Hard exit if Next's cleanup hangs. setTimeout(() => process.exit(0), 3000).unref(); } process.on("SIGINT", shutdown); process.on("SIGTERM", shutdown);