bun run serve was python3 -m http.server, which resolves a directory request to its index.html. The live static layer returns 404 for the same request. So a gate could pass locally on a URL that does not work in production, and nothing local could ever catch it. That is not a harness quirk, it is a blind spot with a green light on it: every defect inside the permissive gap passes locally by construction and can only be found in production, which is the worst place to find it. It cost a false "the publish failed, zero filters live" reading on a publish that was byte-for-byte correct. serve-strict.mjs returns 404 for a directory, exactly like production, with no index fallback and no directory listing. It also refuses to read outside its root. Loopback only, inherited from the script it replaces, whose own comment records that the bind was once 0.0.0.0 and answered on the box's public IP while a gate was running. The permissive server is kept as serve:permissive rather than deleted, so the difference can be demonstrated. It earned itself immediately: verify-shell-scroll held two bare-directory URLs and failed the first time it ran against the strict server. Both now ask for index.html, which is the form the manifest already ships for all 28 profiles, and the gate passes again -- both wheel gestures moving the grid 1500px onto screen. Fixing the server rather than the URLs is the point. The repo was already production-correct: 28 of 28 manifest URLs and 82 gate references use index.html. What was missing was anything that could fail when one did not. A rule you have to remember fails under time pressure; a server that returns 404 does not.
95 lines
5.1 KiB
JavaScript
95 lines
5.1 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* serve-strict.mjs — serve public/ the way PRODUCTION serves it, not the way it is convenient.
|
|
*
|
|
* ★ THE DEFECT THIS EXISTS FOR, hit live 2026-09-23. `bun run serve` was
|
|
* `python3 -m http.server`, which resolves a DIRECTORY request to its index.html. The live
|
|
* nuxtbase.app static layer does NOT:
|
|
*
|
|
* /plugin-assets/doxa-maps/dashboard-plus-grid/dashboard-plus-grid/ -> 404 live, 200 local
|
|
* /plugin-assets/doxa-maps/dashboard-plus-grid/dashboard-plus-grid/index.html -> 200 both
|
|
*
|
|
* So every headless verification could pass on a URL that 404s in production, and the divergence
|
|
* was invisible until something was finally checked against the live site. It cost a false
|
|
* "the publish failed, zero filters live" reading on a publish that was byte-for-byte correct.
|
|
*
|
|
* ★ WHY FIX THE SERVER RATHER THAN THE URLS. The repo is already production-correct: the manifest
|
|
* ships 28/28 iframeUrl values ending in /index.html, and the gates use that form 82 times. Only
|
|
* ad-hoc probes and one gate ever used the bare-directory form. So the exposure was never really
|
|
* the URLs — it was that NOTHING LOCAL COULD CATCH a bad one. A local environment MORE PERMISSIVE
|
|
* than production is a blind spot with a green light on it: every defect inside the permissive gap
|
|
* passes locally by construction and can only be found in production, which is the worst place to
|
|
* find it. Making local strict converts that class from "discovered live" to "impossible to commit".
|
|
* A rule you have to REMEMBER fails under time pressure; a server that returns 404 does not.
|
|
*
|
|
* WHAT IT DOES, deliberately minimal:
|
|
* - serves files under public/ (or --directory) on 127.0.0.1 only;
|
|
* - a request for a DIRECTORY returns 404, exactly like the live layer — no index fallback,
|
|
* no redirect-to-index, no directory listing;
|
|
* - refuses to escape the served root (a .. traversal is 403, not a file read).
|
|
*
|
|
* LOOPBACK ONLY, inherited from the script this replaces: the bind was once 0.0.0.0, which
|
|
* published the built tree on every interface of whatever machine ran a gate — measured answering
|
|
* HTTP 200 on the box's PUBLIC IP while a browser gate was running. Override with HOST only if you
|
|
* deliberately want another host to reach it.
|
|
*
|
|
* Usage: node internal/serve-strict.mjs [--port 4173] [--directory public]
|
|
*/
|
|
import { createServer } from 'node:http'
|
|
import { createReadStream, statSync } from 'node:fs'
|
|
import { resolve, join, extname, normalize } from 'node:path'
|
|
import { dirname } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
|
const argv = process.argv.slice(2)
|
|
const argOf = (name, dflt) => { const i = argv.indexOf(name); return i >= 0 && argv[i + 1] ? argv[i + 1] : dflt }
|
|
|
|
const PORT = Number(process.env.PORT || argOf('--port', '4173'))
|
|
const HOST = process.env.HOST || '127.0.0.1'
|
|
const DIR = resolve(ROOT, argOf('--directory', 'public'))
|
|
|
|
const TYPES = {
|
|
'.html': 'text/html; charset=utf-8', '.js': 'text/javascript; charset=utf-8',
|
|
'.mjs': 'text/javascript; charset=utf-8', '.css': 'text/css; charset=utf-8',
|
|
'.json': 'application/json; charset=utf-8', '.geojson': 'application/json; charset=utf-8',
|
|
'.svg': 'image/svg+xml', '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
|
|
'.webp': 'image/webp', '.gif': 'image/gif', '.ico': 'image/x-icon', '.woff2': 'font/woff2',
|
|
'.csv': 'text/csv; charset=utf-8', '.pbf': 'application/x-protobuf', '.txt': 'text/plain; charset=utf-8',
|
|
}
|
|
|
|
const send = (res, code, body, headers = {}) => {
|
|
res.writeHead(code, { 'content-type': 'text/plain; charset=utf-8', ...headers })
|
|
res.end(body)
|
|
}
|
|
|
|
createServer((req, res) => {
|
|
let pathname
|
|
try { pathname = decodeURIComponent(new URL(req.url, 'http://x').pathname) } catch { return send(res, 400, 'bad request\n') }
|
|
|
|
// Never let a request escape the served root. normalize() collapses .. before we join.
|
|
const rel = normalize(pathname).replace(/^(\.\.[/\\])+/, '').replace(/^[/\\]+/, '')
|
|
const file = join(DIR, rel)
|
|
if (!file.startsWith(DIR)) return send(res, 403, 'forbidden\n')
|
|
|
|
let st
|
|
try { st = statSync(file) } catch { return send(res, 404, 'not found\n') }
|
|
|
|
// ★ THE WHOLE POINT. A directory is a 404 here because it is a 404 in production. Do not add an
|
|
// index fallback "for convenience" — the convenience is what hid a live defect for a full flight.
|
|
if (st.isDirectory()) {
|
|
return send(res, 404, 'not found — this server does not serve directory indexes, because the live\n' +
|
|
'static layer does not either. Request the file explicitly:\n' +
|
|
' ' + pathname.replace(/\/*$/, '/') + 'index.html\n')
|
|
}
|
|
|
|
res.writeHead(200, {
|
|
'content-type': TYPES[extname(file).toLowerCase()] || 'application/octet-stream',
|
|
'content-length': st.size,
|
|
'cache-control': 'no-cache',
|
|
})
|
|
createReadStream(file).pipe(res)
|
|
}).listen(PORT, HOST, () => {
|
|
console.log(`serve-strict: http://${HOST}:${PORT} -> ${DIR.replace(ROOT + '/', '')}`)
|
|
console.log(' directory requests return 404 (matching production). Ask for /index.html explicitly.')
|
|
})
|