Repository Public
1#!/usr/bin/env node2/**3 * DEV/TEST-ONLY static server over apps/web/out with the PRODUCTION Content-Security-Policy4 * (script hashes computed from each HTML file) and a proxy of /api (HTTP + WebSocket) to the5 * dev mock. It proves the export is CSP-clean; apps/server does the real serving.6 * Env: PORT (3641) · MOCK_PORT (8641)7 */8import http from 'node:http';9import net from 'node:net';10import fs from 'node:fs';11import path from 'node:path';12import crypto from 'node:crypto';13import { fileURLToPath } from 'node:url';1415const out = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../out');16const PORT = Number(process.env.PORT ?? 3641);17const MOCK = Number(process.env.MOCK_PORT ?? 8641);18const TYPES = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript; charset=utf-8', '.css': 'text/css; charset=utf-8', '.json': 'application/json', '.webmanifest': 'application/manifest+json', '.svg': 'image/svg+xml', '.png': 'image/png', '.woff2': 'font/woff2', '.woff': 'font/woff', '.ttf': 'font/ttf', '.txt': 'text/plain; charset=utf-8', '.map': 'application/json', '.ico': 'image/x-icon', '.wasm': 'application/wasm' };1920export function cspFor(html) {21 const hashes = new Set();22 for (const m of html.matchAll(/<script(?![^>]*\ssrc=)[^>]*>([\s\S]*?)<\/script>/g)) {23 if (m[1]) hashes.add(`'sha256-${crypto.createHash('sha256').update(m[1]).digest('base64')}'`);24 }25 // Mirrors buildCsp() in apps/server/src/static.ts.26 return [27 "default-src 'self'",28 `script-src 'self' ${[...hashes].join(' ')}`.trim(),29 "style-src 'self' 'unsafe-inline'",30 "img-src 'self' data: blob:",31 "font-src 'self' data:",32 `connect-src 'self' ws://127.0.0.1:${PORT}`,33 "worker-src 'self' blob:",34 "frame-ancestors 'none'",35 "base-uri 'self'",36 "form-action 'self'",37 "object-src 'none'",38 ].join('; ');39}4041const server = http.createServer((req, res) => {42 const url = new URL(req.url, 'http://x');43 if (url.pathname.startsWith('/api/')) {44 const up = http.request({ host: '127.0.0.1', port: MOCK, path: req.url, method: req.method, headers: req.headers }, (r) => {45 res.writeHead(r.statusCode ?? 502, r.headers);46 r.pipe(res);47 });48 up.on('error', () => { res.writeHead(502, { 'Content-Type': 'application/json' }); res.end('{"error":{"code":"bad_gateway","message":"mock API unreachable"}}'); });49 req.pipe(up);50 return;51 }52 let rel = decodeURIComponent(url.pathname);53 if (rel.includes('..')) { res.writeHead(400); return res.end(); }54 if (rel.endsWith('/')) rel += 'index.html';55 let file = path.join(out, rel);56 if (!fs.existsSync(file) || fs.statSync(file).isDirectory()) {57 if (fs.existsSync(file + '/index.html')) { res.writeHead(308, { Location: url.pathname + '/' + url.search }); return res.end(); }58 file = path.join(out, '404.html');59 res.statusCode = 404;60 }61 const ext = path.extname(file);62 const headers = { 'Content-Type': TYPES[ext] ?? 'application/octet-stream', 'X-Content-Type-Options': 'nosniff', 'Referrer-Policy': 'no-referrer' };63 const buf = fs.readFileSync(file);64 if (ext === '.html') {65 headers['Content-Security-Policy'] = cspFor(buf.toString('utf8'));66 headers['Cache-Control'] = 'no-cache';67 } else if (rel.startsWith('/_next/static/')) headers['Cache-Control'] = 'public, max-age=31536000, immutable';68 else headers['Cache-Control'] = 'no-cache';69 res.writeHead(res.statusCode, headers);70 res.end(buf);71});7273server.on('upgrade', (req, socket, head) => {74 const up = net.connect(MOCK, '127.0.0.1', () => {75 const lines = [`${req.method} ${req.url} HTTP/1.1`];76 for (let i = 0; i < req.rawHeaders.length; i += 2) lines.push(`${req.rawHeaders[i]}: ${req.rawHeaders[i + 1]}`);77 up.write(lines.join('\r\n') + '\r\n\r\n');78 if (head.length) up.write(head);79 socket.pipe(up).pipe(socket);80 });81 const kill = () => { socket.destroy(); up.destroy(); };82 up.on('error', kill);83 socket.on('error', kill);84});8586if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {87 server.listen(PORT, '127.0.0.1', () => console.log(`[serve-out] http://127.0.0.1:${PORT} (out/ + production CSP, /api → mock :${MOCK})`));88}89