- Record user evidence for directory fallback to complete GC1 (10/10 checks passed) - Expand Format Specification 0.1 to Revision 0.2 with normative shared contracts - Author JSON Schema Draft-07 at schema/xzbt-0.1.schema.json - Implement zero-dependency semantic validator at tools/validate-exhibit.mjs - Create 12-case conformance fixture suite and automated test runner (12/12 passing) - Update implementation status, verification gates, and gap closure decisions - Add devlog entry covering stall recovery and Phase 0 current state
90 lines
2.8 KiB
JavaScript
90 lines
2.8 KiB
JavaScript
/**
|
|
* Dependency-free local development server for XZBT.
|
|
*
|
|
* It deliberately binds only to loopback. It is not part of the distributed
|
|
* XZBT artifact and cannot establish direct-file Phase 0 feasibility results.
|
|
*/
|
|
import { createReadStream, statSync } from 'node:fs';
|
|
import { createServer } from 'node:http';
|
|
import { extname, normalize, resolve, sep } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const root = resolve(fileURLToPath(new URL('..', import.meta.url)));
|
|
const host = '127.0.0.1';
|
|
const loopbackAddresses = ['127.0.0.1', '::1'];
|
|
const port = Number.parseInt(process.env.XZBT_DEV_PORT ?? '5173', 10);
|
|
|
|
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
throw new Error('XZBT_DEV_PORT must be an integer from 1 through 65535.');
|
|
}
|
|
|
|
const mimeTypes = new Map([
|
|
['.css', 'text/css; charset=utf-8'],
|
|
['.html', 'text/html; charset=utf-8'],
|
|
['.js', 'text/javascript; charset=utf-8'],
|
|
['.json', 'application/json; charset=utf-8'],
|
|
['.mjs', 'text/javascript; charset=utf-8'],
|
|
['.xzbt', 'application/json; charset=utf-8'],
|
|
]);
|
|
|
|
function requestedPath(requestUrl) {
|
|
const pathname = new URL(requestUrl, `http://${host}:${port}`).pathname;
|
|
const decodedPath = decodeURIComponent(pathname);
|
|
if (decodedPath.includes('\0')) return null;
|
|
if (decodedPath.split('/').some((segment) => segment.startsWith('.'))) return null;
|
|
|
|
const relativePath = decodedPath === '/'
|
|
? 'prototypes/phase0/XZBT-phase0-probe.html'
|
|
: decodedPath.replace(/^\/+/, '');
|
|
const candidate = resolve(root, normalize(relativePath));
|
|
return candidate === root || candidate.startsWith(`${root}${sep}`) ? candidate : null;
|
|
}
|
|
|
|
function handleRequest(request, response) {
|
|
if (!['GET', 'HEAD'].includes(request.method ?? '')) {
|
|
response.writeHead(405, { Allow: 'GET, HEAD' }).end();
|
|
return;
|
|
}
|
|
|
|
let path;
|
|
try {
|
|
path = requestedPath(request.url ?? '/');
|
|
} catch {
|
|
response.writeHead(400).end('Malformed request path.');
|
|
return;
|
|
}
|
|
if (!path) {
|
|
response.writeHead(403).end('Path is outside the XZBT workspace.');
|
|
return;
|
|
}
|
|
|
|
let stats;
|
|
try {
|
|
stats = statSync(path);
|
|
} catch {
|
|
response.writeHead(404).end('Not found.');
|
|
return;
|
|
}
|
|
if (!stats.isFile()) {
|
|
response.writeHead(404).end('Not found.');
|
|
return;
|
|
}
|
|
|
|
response.writeHead(200, {
|
|
'Content-Length': stats.size,
|
|
'Content-Type': mimeTypes.get(extname(path).toLowerCase()) ?? 'application/octet-stream',
|
|
'Cache-Control': 'no-store',
|
|
'X-Content-Type-Options': 'nosniff',
|
|
});
|
|
if (request.method === 'HEAD') response.end();
|
|
else createReadStream(path).pipe(response);
|
|
}
|
|
|
|
for (const address of loopbackAddresses) {
|
|
const server = createServer(handleRequest);
|
|
server.listen(port, address, () => {
|
|
console.log(`XZBT development server: http://${address}:${port}/`);
|
|
});
|
|
}
|
|
console.log(`Serving workspace root: ${root}`);
|