secure dev tunnel: strip /__nextjs debug endpoints while keeping HMR
This commit is contained in:
parent
12ff3e70e9
commit
f5a913201a
5 changed files with 181 additions and 17 deletions
19
README.md
19
README.md
|
|
@ -86,10 +86,11 @@ then runs `npm install` and `npm run build`:
|
|||
### Scripts
|
||||
|
||||
```bash
|
||||
npm run dev # next dev --turbopack
|
||||
npm run build # next build --turbopack
|
||||
npm run start # next start (production)
|
||||
npm run lint # eslint
|
||||
npm run dev # node scripts/dev-server.mjs (dev + live updates)
|
||||
npm run dev:next # next dev --turbopack (raw dev server, unprotected)
|
||||
npm run build # next build --turbopack
|
||||
npm run start # next start (production)
|
||||
npm run lint # eslint
|
||||
```
|
||||
|
||||
Handy checks before shipping: `npx tsc --noEmit`, `npx eslint <files>`, `npm run build`.
|
||||
|
|
@ -101,6 +102,14 @@ on the local network and from the `beta.dvorakmortonxd.dev` tunnel. Without it N
|
|||
rejects the dev asset requests *and* the HMR WebSocket, which leaves the page un-hydrated
|
||||
(stacked icons, dead buttons).
|
||||
|
||||
A plain `next dev` server is meant for localhost and exposes unauthenticated debug endpoints
|
||||
(`/__nextjs*` — open-a-file-in-editor, stack frames, source maps, debugger attach, restart)
|
||||
that you don't want on a tunnel. `npm run dev` therefore runs `scripts/dev-server.mjs`, a
|
||||
custom server that keeps the app and live updates (HMR / Fast Refresh) but returns `404` for
|
||||
those endpoints and `*.map` for any request that isn't a direct localhost connection.
|
||||
Localhost keeps the full dev experience; the LAN and tunnel get a plain, live-reloading
|
||||
site. To force the raw server instead, use `npm run dev:next` (don't point a tunnel at it).
|
||||
|
||||
## 📡 API Routes
|
||||
|
||||
| Route | Method | What it does |
|
||||
|
|
@ -140,6 +149,8 @@ src/app/
|
|||
Clock.tsx → live clock
|
||||
JokerLogo.tsx → logo with click easter egg
|
||||
SlBackground.tsx → animated ASCII train background
|
||||
scripts/
|
||||
dev-server.mjs → custom dev server: live updates without the dev-only debug surface
|
||||
public/ → fonts, wallpapers, icons, friend pages, /revival-inc game, llms.txt, robots.txt, sitemap.xml
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -1,18 +1,25 @@
|
|||
import { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
// Next's on-screen dev indicator is a debug affordance, not a feature of the
|
||||
// site, so it stays hidden. Compile/runtime errors are still reported.
|
||||
devIndicators: false,
|
||||
// Allow testing the dev server from other devices on the LAN (e.g. a phone).
|
||||
// Without this Next.js blocks cross-origin requests to /_next/* dev assets
|
||||
// with a 403, which stops the JS from loading and makes the page inert.
|
||||
// The HMR WebSocket is checked against this list too: if the tunnel/beta host
|
||||
// isn't listed, the socket is rejected and the dev client never hydrates
|
||||
// (SSR-only page: stacked icons, dead buttons).
|
||||
//
|
||||
// Keep this list tight — private LAN ranges plus the one exact tunnel host.
|
||||
// A `**.dvorakmortonxd.dev` wildcard would let any subdomain drive the dev
|
||||
// server. Note this only gates assets/HMR; the dangerous `/__nextjs_*` debug
|
||||
// endpoints are stripped by scripts/dev-server.mjs, not by this list.
|
||||
allowedDevOrigins: [
|
||||
"192.168.10.124",
|
||||
"127.0.0.1",
|
||||
"192.168.*.*",
|
||||
"10.*.*.*",
|
||||
"beta.dvorakmortonxd.dev",
|
||||
"*.dvorakmortonxd.dev",
|
||||
],
|
||||
webpack(config) {
|
||||
config.module.rules.push({
|
||||
|
|
|
|||
50
opencode.md
50
opencode.md
|
|
@ -46,10 +46,11 @@ picks which one to show based on theme and device.
|
|||
|
||||
### Scripts
|
||||
```bash
|
||||
npm run dev # next dev --turbopack (a server may already be running on :3000)
|
||||
npm run build # next build --turbopack
|
||||
npm run start # next start
|
||||
npm run lint # eslint
|
||||
npm run dev # node scripts/dev-server.mjs (dev + HMR; strips dev-only debug for non-local access)
|
||||
npm run dev:next # next dev --turbopack (raw, UNPROTECTED escape hatch — don't use behind the tunnel)
|
||||
npm run build # next build --turbopack
|
||||
npm run start # next start
|
||||
npm run lint # eslint
|
||||
```
|
||||
Useful checks before handing work back: `npx tsc --noEmit`, `npx eslint <files>`,
|
||||
`npm run build`.
|
||||
|
|
@ -100,6 +101,8 @@ src/app/
|
|||
Clock.tsx → live clock (useSyncExternalStore + interval)
|
||||
JokerLogo.tsx → site logo with a click easter egg
|
||||
SlBackground.tsx → animated ASCII train background (terminal theme)
|
||||
scripts/
|
||||
dev-server.mjs → custom dev server: live updates minus the dev-only debug surface
|
||||
```
|
||||
|
||||
### How UI selection works
|
||||
|
|
@ -233,12 +236,28 @@ list reordering, not free placement. Don't confuse the two.
|
|||
`59` is duplicated in `skeuo.css` (`.win-window.maximized` height,
|
||||
`.win-window.centered` bottom / max-height) — keep them in sync. Note
|
||||
`useDesktopIcons.ts` has its **own** `TASKBAR_HEIGHT = 48` for the icon grid.
|
||||
- Exposing the **dev** server over a tunnel needs the tunnel host in
|
||||
`allowedDevOrigins` (`next.config.ts`). Without it Next dev rejects the
|
||||
cross-origin HMR WebSocket, so the client never hydrates — the page is SSR
|
||||
only: icons all stacked at `(0,0)`, buttons dead. Changing `next.config.ts`
|
||||
needs a dev-server restart. For a public URL prefer a production build
|
||||
(`npm run build && npm run start`): no HMR socket, so nothing to reject.
|
||||
- **Serving the dev server through the tunnel: read this.** `npm run dev` does
|
||||
**not** run `next dev` directly — it runs `node scripts/dev-server.mjs`, a
|
||||
custom server that wraps Next's request handler. It keeps the live-update
|
||||
path (`/_next/hmr`, Fast Refresh) but returns `404` for Next's dev-only debug
|
||||
endpoints (`/__nextjs*`: launch-editor, original stack frames, source maps,
|
||||
node-inspector attach, server restart, …) and for `*.map` whenever the
|
||||
request did **not** arrive over loopback without proxy headers. Those
|
||||
endpoints are unauthenticated, and Next wires them in unconditionally, so
|
||||
`next.config.ts` cannot disable them — that is what makes a publicly
|
||||
tunnelled dev server unsafe. Net effect:
|
||||
`localhost:3000` keeps the full dev experience; the LAN/tunnel get a plain,
|
||||
live-reloading site. Use `npm run dev:next` only if you explicitly want the
|
||||
raw, unprotected server (never point the tunnel at it).
|
||||
- `allowedDevOrigins` (`next.config.ts`) still matters: the tunnel host and any
|
||||
LAN origin must be listed or Next rejects the HMR WebSocket with a 403 (the
|
||||
client then never hydrates — SSR only, icons stacked at `(0,0)`, dead
|
||||
buttons). Keep it tight (private ranges + the one exact tunnel host); a
|
||||
`**.dvorakmortonxd.dev` wildcard would let any subdomain drive the dev
|
||||
server. Changing `next.config.ts` needs a dev-server restart.
|
||||
- `devIndicators: false` in `next.config.ts` hides Next's on-screen dev badge.
|
||||
- For a fully public URL a production build (`npm run build && npm run start`)
|
||||
is still the gold standard: no dev server, no debug endpoints, no HMR socket.
|
||||
- The SMP service shortcut opens `SMP_APP` (in `apps.tsx`, registered in
|
||||
`APP_BY_ID` only, like `ALACRITTY_APP`) — a Minecraft-style join menu that
|
||||
reads live data from `/api/ping?url=minecraft://…`.
|
||||
|
|
@ -257,6 +276,17 @@ list reordering, not free placement. Don't confuse the two.
|
|||
- There is **no test suite**. Verification is `tsc --noEmit`, `eslint`, and
|
||||
`next build`.
|
||||
- A dev server may already be running on port 3000; check before starting one.
|
||||
Two dev servers on this repo collide on `.next/dev/lock`, so stop the old one
|
||||
(and delete the stale lock) before starting the custom one.
|
||||
- Dev-server guard check: `curl -s -o /dev/null -w '%{http_code}' \
|
||||
http://localhost:3000/__nextjs_server_status` must be `200` (localhost keeps
|
||||
debug), while the same path through the LAN IP or `beta.dvorakmortonxd.dev`
|
||||
must be `404`. Note a request from this host to its **own** LAN IP is routed
|
||||
over loopback, so `localAddress` reads `127.0.0.1` — a same-host curl to the
|
||||
LAN IP does NOT exercise the non-loopback branch. Test that branch through
|
||||
the tunnel or from another machine. HMR is fine as long as a real
|
||||
`/_next/hmr` WebSocket upgrade returns `101` and then sends a frame (the HMR
|
||||
client appends `?id=self.__next_r` from the served HTML).
|
||||
- The desktop layout is geometry-heavy. When changing grid maths, a small
|
||||
standalone script mirroring `normalize()` / `posToCell()` is a fast way to
|
||||
prove one-icon-per-cell holds for edge cases (offscreen drops, tiny
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@
|
|||
"version": "2.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
"dev": "node scripts/dev-server.mjs",
|
||||
"dev:next": "next dev --turbopack",
|
||||
"build": "next build --turbopack",
|
||||
"start": "next start",
|
||||
"lint": "eslint"
|
||||
|
|
|
|||
115
scripts/dev-server.mjs
Normal file
115
scripts/dev-server.mjs
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
// 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);
|
||||
Loading…
Add table
Add a link
Reference in a new issue