Files

140 lines
4.5 KiB
JavaScript

const fs = require('node:fs');
const http = require('node:http');
const path = require('node:path');
const defaultRoot = path.resolve(__dirname, '..', 'dist');
const mimeTypes = Object.freeze({
'.css': 'text/css; charset=utf-8',
'.html': 'text/html; charset=utf-8',
'.js': 'text/javascript; charset=utf-8',
'.json': 'application/json; charset=utf-8',
'.png': 'image/png',
'.svg': 'image/svg+xml',
});
const securityHeaders = Object.freeze({
'Content-Security-Policy': "default-src 'self'; base-uri 'none'; connect-src 'none'; font-src 'self'; form-action 'self'; frame-ancestors 'none'; img-src 'self' data:; object-src 'none'; script-src 'self'; style-src 'self'",
'Cross-Origin-Opener-Policy': 'same-origin',
'Referrer-Policy': 'no-referrer',
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'DENY',
});
function writePlain(response, status, message, extraHeaders = {}) {
const body = `${message}\n`;
response.writeHead(status, {
...securityHeaders,
...extraHeaders,
'Cache-Control': 'no-store',
'Content-Length': Buffer.byteLength(body),
'Content-Type': 'text/plain; charset=utf-8',
});
response.end(body);
}
function createStaticServer({ root = defaultRoot } = {}) {
const directory = path.resolve(root);
return http.createServer((request, response) => {
if (request.method !== 'GET' && request.method !== 'HEAD') {
writePlain(response, 405, 'Method not allowed', { Allow: 'GET, HEAD' });
return;
}
let pathname;
try {
pathname = decodeURIComponent(new URL(request.url, 'http://127.0.0.1').pathname);
} catch (_) {
writePlain(response, 400, 'Bad request');
return;
}
// Browsers probe this path even when the product intentionally ships no icon.
// A quiet no-content response prevents a false runtime error without adding
// an unreviewed visual asset to the approved interface.
if (pathname === '/favicon.ico') {
response.writeHead(204, {
...securityHeaders,
'Cache-Control': 'public, max-age=86400',
});
response.end();
return;
}
const relative = pathname === '/' ? 'index.html' : pathname.replace(/^\/+/, '');
const file = path.resolve(directory, relative);
if (!file.startsWith(`${directory}${path.sep}`)) {
writePlain(response, 404, 'Not found');
return;
}
let stat;
try {
stat = fs.statSync(file);
} catch (_) {
writePlain(response, 404, 'Not found');
return;
}
if (!stat.isFile()) {
writePlain(response, 404, 'Not found');
return;
}
response.writeHead(200, {
...securityHeaders,
'Cache-Control': 'no-cache',
'Content-Length': stat.size,
'Content-Type': mimeTypes[path.extname(file).toLowerCase()] || 'application/octet-stream',
});
if (request.method === 'HEAD') {
response.end();
return;
}
fs.createReadStream(file).pipe(response);
});
}
function readOption(args, name, fallback) {
const index = args.indexOf(name);
if (index === -1) return fallback;
if (!args[index + 1] || args[index + 1].startsWith('--')) throw new Error(`${name} requires a value`);
return args[index + 1];
}
function startFromCli() {
const args = process.argv.slice(2);
const known = new Set(['--host', '--port']);
for (let index = 0; index < args.length; index += 2) {
if (!known.has(args[index])) throw new Error(`Unknown option: ${args[index]}`);
}
const host = readOption(args, '--host', process.env.TECH_ATLAS_HOST || '127.0.0.1');
const portText = readOption(args, '--port', process.env.TECH_ATLAS_PORT || '4173');
const port = Number(portText);
if (!Number.isInteger(port) || port < 0 || port > 65535) throw new Error(`Invalid port: ${portText}`);
const server = createStaticServer();
server.on('error', error => {
console.error(`Technology Atlas server failed: ${error.message}`);
process.exitCode = 1;
});
server.listen(port, host, () => {
const address = server.address();
const actualPort = typeof address === 'object' && address ? address.port : port;
console.log(`Technology Atlas ready at http://${host}:${actualPort}`);
console.log('Public deep-dive: Transaction Isolation / Lost Update');
});
const stop = () => server.close(() => process.exit());
process.once('SIGINT', stop);
process.once('SIGTERM', stop);
}
if (require.main === module) {
try {
startFromCli();
} catch (error) {
console.error(error.message);
process.exitCode = 1;
}
}
module.exports = { createStaticServer, defaultRoot, securityHeaders };