Showcase: sovereign HTTPS demo server + local CA
Some checks failed
ci / check (push) Has been cancelled

serve-demo.mjs serves dist/ (app shell, SPA fallback) and instance/origin/
(signed packages + latest.json) on one HTTPS port; generates /sync-config.json
with the pinned signing-key fingerprint from the published package and offers
/rootCA.pem for phone trust. gen-certs.sh creates a local ECDSA CA + server
cert with SANs for localhost, hotspot 10.42.0.1, and current interface IPs.
No tunnels, no third parties: everything runs on this laptop. npm run
demo:certs / demo:serve.
This commit is contained in:
Lumen Stage1 2026-10-02 12:40:17 -05:00
commit 0e7d85b862
4 changed files with 191 additions and 0 deletions

View file

@ -11,6 +11,7 @@ export default tseslint.config(
"node_modules/**",
"coverage/**",
"experiments/**",
"scripts/serve-demo.mjs",
"public/sw.js",
"share/**",
],

View file

@ -21,6 +21,8 @@
"preview": "vite preview",
"package:staging": "vite-node pipeline/run.ts",
"package:smoke": "vite-node pipeline/run.ts --smoke-only",
"demo:certs": "bash scripts/gen-certs.sh",
"demo:serve": "node scripts/serve-demo.mjs",
"ci": "npm run typecheck && npm run lint && npm run format && npm run test && npm run build"
},
"devDependencies": {

43
scripts/gen-certs.sh Executable file
View file

@ -0,0 +1,43 @@
#!/usr/bin/env bash
# Sovereign demo certs — private CA + server cert, generated locally.
# Phones install rootCA.pem once; the browser then trusts the server.
# Usage: scripts/gen-certs.sh [host-or-ip ...]
# (extra SAN entries; localhost/127.0.0.1/10.42.0.1 always included)
set -euo pipefail
DIR="$(cd "$(dirname "$0")/.." && pwd)/instance/certs"
mkdir -p "$DIR"
SANS=("localhost" "127.0.0.1" "10.42.0.1")
# auto-include current non-loopback IPv4 addresses
while read -r ip; do [[ -n "$ip" ]] && SANS+=("$ip"); done < <(
ip -4 -o addr show scope global 2>/dev/null | awk '{print $4}' | cut -d/ -f1
)
for extra in "$@"; do SANS+=("$extra"); done
for s in "${SANS[@]}"; do
if [[ "$s" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
SAN_STR+="IP:$s,"
else
SAN_STR+="DNS:$s,"
fi
done
SAN_STR="DNS:localhost,${SAN_STR%,}"
if [[ ! -f "$DIR/rootCA-key.pem" ]]; then
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -nodes \
-keyout "$DIR/rootCA-key.pem" -out "$DIR/rootCA.pem" -days 825 \
-subj "/CN=Lumen Demo CA/O=Lumen" \
-addext "basicConstraints=critical,CA:TRUE" \
-addext "keyUsage=critical,keyCertSign,cRLSign"
echo "created CA: $DIR/rootCA.pem (install this on phones)"
fi
openssl req -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -nodes \
-keyout "$DIR/server-key.pem" -out "$DIR/server.csr" \
-subj "/CN=Lumen Demo Server/O=Lumen"
openssl x509 -req -in "$DIR/server.csr" \
-CA "$DIR/rootCA.pem" -CAkey "$DIR/rootCA-key.pem" -CAcreateserial \
-out "$DIR/server.pem" -days 397 \
-extfile <(printf "subjectAltName=%s\nextendedKeyUsage=serverAuth\n" "$SAN_STR")
rm -f "$DIR/server.csr"
echo "created server cert: $DIR/server.pem SAN: $SAN_STR"

145
scripts/serve-demo.mjs Normal file
View file

@ -0,0 +1,145 @@
#!/usr/bin/env node
/* eslint-disable no-console -- this is the demo server CLI; stdout is its interface */
/**
* Sovereign demo server — HTTPS file server for the phone showcase.
* Serves the built app shell (dist/) and the signed origin tree
* (instance/origin/) on one HTTPS port, no third parties involved.
*
* Routes:
* /editions/** → origin tree (immutable packages)
* /latest.json → origin pointer (mutable)
* /sync-config.json → pinned trust config for the app
* /rootCA.pem → the CA cert phones must install to trust us
* everything else → dist/ (app shell, SPA fallback to index.html)
*
* Usage: node scripts/serve-demo.mjs [--port 8443] [--host 0.0.0.0]
* [--app dist] [--origin instance/origin] [--certs instance/certs]
* [--edition lumen-2026]
*/
import { createServer } from "node:https";
import { createHash } from "node:crypto";
import { readFileSync, existsSync, statSync } from "node:fs";
import { join, normalize, extname } from "node:path";
const argv = process.argv.slice(2);
function arg(name, dflt) {
const i = argv.indexOf(`--${name}`);
return i >= 0 && argv[i + 1] ? argv[i + 1] : dflt;
}
const port = Number(arg("port", "8443"));
const host = arg("host", "0.0.0.0");
const appDir = arg("app", "dist");
const originDir = arg("origin", "instance/origin");
const certsDir = arg("certs", "instance/certs");
const edition = arg("edition", "lumen-2026");
const keyPath = join(certsDir, "server-key.pem");
const certPath = join(certsDir, "server.pem");
const caPath = join(certsDir, "rootCA.pem");
for (const p of [keyPath, certPath, caPath]) {
if (!existsSync(p)) {
console.error(`missing ${p} — run scripts/gen-certs.sh first`);
process.exit(1);
}
}
if (!existsSync(join(appDir, "index.html"))) {
console.error(`missing ${appDir}/index.html — run npm run build first`);
process.exit(1);
}
const MIME = {
".html": "text/html; charset=utf-8",
".js": "text/javascript; charset=utf-8",
".css": "text/css; charset=utf-8",
".json": "application/json; charset=utf-8",
".pem": "application/x-pem-file",
".png": "image/png",
".svg": "image/svg+xml",
".ico": "image/x-icon",
".webmanifest": "application/manifest+json",
};
function send(res, code, body, type) {
res.writeHead(code, {
"content-type": type,
"cache-control": "no-store",
"access-control-allow-origin": "*",
});
res.end(body);
}
function sendFile(res, path) {
const data = readFileSync(path);
const type = MIME[extname(path)] ?? "application/octet-stream";
// Immutable by contract: content-addressed package files under /editions/.
const immutable = path.includes("/packages/");
res.writeHead(200, {
"content-type": type,
"cache-control": immutable ? "public, max-age=31536000, immutable" : "no-store",
});
res.end(data);
}
function syncConfig() {
// Pinned trust for the app: fingerprint -> SPKI DER base64.
// Test key published by the pipeline next to the package (demo mode only).
const latest = JSON.parse(readFileSync(join(originDir, "latest.json"), "utf8"));
const pkgDir = join(
originDir,
"editions",
latest.edition,
"packages",
String(latest.packageVersion),
);
const pem = readFileSync(join(pkgDir, "publicKey.pem"), "utf8");
const derB64 = pem
.replace(/-----BEGIN PUBLIC KEY-----/g, "")
.replace(/-----END PUBLIC KEY-----/g, "")
.replace(/\s/g, "");
const digest = createHash("sha256").update(Buffer.from(derB64, "base64")).digest("hex");
return JSON.stringify({
edition: latest.edition,
origin: "https://REPLACE_HOST",
trustedKeys: { [`sha256:${digest}`]: derB64 },
});
}
const server = createServer(
{ key: readFileSync(keyPath), cert: readFileSync(certPath) },
(req, res) => {
const url = new URL(req.url ?? "/", `https://${req.headers.host ?? "localhost"}`);
const path = normalize(decodeURIComponent(url.pathname));
console.log(`${req.method} ${path}`);
if (path === "/sync-config.json") {
try {
const hostHeader = req.headers.host ?? `localhost:${port}`;
const conf = syncConfig().replace("https://REPLACE_HOST", `https://${hostHeader}`);
return send(res, 200, conf, MIME[".json"]);
} catch (e) {
return send(res, 500, JSON.stringify({ error: String(e) }), MIME[".json"]);
}
}
if (path === "/rootCA.pem") return sendFile(res, caPath);
if (path.startsWith("/editions/") || path === "/latest.json") {
const filePath = join(originDir, path);
if (filePath.startsWith(originDir) && existsSync(filePath) && statSync(filePath).isFile())
return sendFile(res, filePath);
return send(res, 404, "not found", "text/plain");
}
const appPath = join(appDir, path === "/" ? "index.html" : path);
if (appPath.startsWith(appDir) && existsSync(appPath) && statSync(appPath).isFile())
return sendFile(res, appPath);
// SPA fallback
return sendFile(res, join(appDir, "index.html"));
},
);
server.listen(port, host, () => {
console.log(`Lumen demo server (edition ${edition})`);
console.log(` app : ${appDir}`);
console.log(` origin : ${originDir}`);
console.log(` listening on https://${host}:${port}`);
});