locqr/scripts/serve-companion.js

65 lines
2.5 KiB
JavaScript

#!/usr/bin/env node
// Serves companion/dist/ over HTTPS using the mkcert-issued test.locqr.dev
// cert (dev.md — same domain as the test website, different port, so the
// companion stub gets a secure context for getUserMedia()). Requires
// `npm run build` in companion/ first — this serves the built output, not
// the TypeScript source, mirroring how the extension is loaded from
// extension/dist/.
//
// Plain Node https + fs — same shape as serve-website.js.
import { createServer } from 'node:https';
import { readFile, stat } from 'node:fs/promises';
import { extname, join, normalize } from 'node:path';
import { dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
const COMPANION_DIST_DIR = join(ROOT, 'companion', 'dist');
const PORT = 5174;
const MIME = {
'.html': 'text/html; charset=utf-8',
'.js': 'text/javascript; charset=utf-8',
'.css': 'text/css; charset=utf-8',
'.json': 'application/json',
};
async function resolveFile(urlPath) {
const safePath = normalize(urlPath === '/' ? '/index.html' : urlPath).replace(/^(\.\.[/\\])+/, '');
const filePath = join(COMPANION_DIST_DIR, safePath);
if (!filePath.startsWith(COMPANION_DIST_DIR)) return null; // path traversal guard
try {
const stats = await stat(filePath);
return stats.isFile() ? filePath : null;
} catch {
return null;
}
}
const options = {
cert: await readFile(join(ROOT, 'dev', 'certs', 'test.locqr.dev.pem')),
key: await readFile(join(ROOT, 'dev', 'certs', 'test.locqr.dev-key.pem')),
};
createServer(options, async (req, res) => {
const url = new URL(req.url ?? '/', 'https://test.locqr.dev');
const filePath = await resolveFile(url.pathname);
if (!filePath) {
res.writeHead(404).end('Not found — did you run `npm run build` in companion/?');
return;
}
const body = await readFile(filePath);
// No caching, ever — this is a dev stub rebuilt on every source edit,
// and mobile browsers in particular are prone to heuristically caching
// a same-URL response across page reloads when the server sends no
// explicit Cache-Control at all, silently serving a stale bundle.
res.writeHead(200, { 'Content-Type': MIME[extname(filePath)] ?? 'application/octet-stream', 'Cache-Control': 'no-store' });
res.end(body);
}).listen(PORT, () => {
console.log(`Serving companion/dist/ -> https://test.locqr.dev:${PORT}`);
});