Files
KingOfTheNOPs f17f0a9dfd intial commit
2026-05-09 12:18:52 -04:00

827 lines
25 KiB
JavaScript

"use strict";
const https = require("node:https");
const crypto = require("node:crypto");
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const { spawnSync } = require("node:child_process");
const options = parseArgs(process.argv.slice(2));
const PORT = Number(options.port || process.env.PORT || 8443);
const BIND_HOST = options.host || process.env.HOST || "0.0.0.0";
const COOKIE_MAX_AGE_SECONDS = Number(options.cookieMaxAge || process.env.COOKIE_MAX_AGE_SECONDS || 60);
const ALLOW_NONE_ALGORITHM = Boolean(options.allowNone || process.env.DBSC_ALLOW_NONE === "1");
const COOKIE_NAME = "__Host-dbsc_poc";
const state = {
pending: new Map(),
sessions: new Map(),
cookieIndex: new Map(),
events: [],
};
function parseArgs(argv) {
const parsed = {};
for (let i = 0; i < argv.length; i += 1) {
const arg = argv[i];
if (arg === "--allow-none") {
parsed.allowNone = true;
} else if (arg === "--port") {
parsed.port = argv[++i];
} else if (arg === "--host") {
parsed.host = argv[++i];
} else if (arg === "--cookie-max-age") {
parsed.cookieMaxAge = argv[++i];
}
}
return parsed;
}
function addEvent(message) {
const line = `${new Date().toISOString()} ${message}`;
state.events.push(line);
while (state.events.length > 80) state.events.shift();
console.log(line);
}
function findOpenSsl() {
const candidates = [
process.env.OPENSSL,
"openssl",
"C:\\Program Files\\Git\\usr\\bin\\openssl.exe",
"C:\\Program Files\\Git\\mingw64\\bin\\openssl.exe",
].filter(Boolean);
for (const candidate of candidates) {
const result = spawnSync(candidate, ["version"], { stdio: "ignore" });
if (result.status === 0) return candidate;
}
return null;
}
function ensureCertificate() {
const certDir = path.join(__dirname, "certs");
const keyPath = path.join(certDir, "dbsc-poc-key.pem");
const certPath = path.join(certDir, "dbsc-poc-cert.pem");
if (!fs.existsSync(keyPath) || !fs.existsSync(certPath)) {
fs.mkdirSync(certDir, { recursive: true });
const openssl = findOpenSsl();
if (!openssl) {
throw new Error("OpenSSL was not found. Install Git for Windows or set OPENSSL to an openssl.exe path.");
}
const altNames = [
"DNS:localhost",
`DNS:${os.hostname()}`,
"IP:127.0.0.1",
...getLocalIpv4Addresses().map((address) => `IP:${address}`),
];
const result = spawnSync(openssl, [
"req",
"-x509",
"-newkey",
"rsa:2048",
"-sha256",
"-days",
"365",
"-nodes",
"-keyout",
keyPath,
"-out",
certPath,
"-subj",
"/CN=localhost",
"-addext",
`subjectAltName=${[...new Set(altNames)].join(",")}`,
], { stdio: "ignore" });
if (result.status !== 0) {
throw new Error("OpenSSL failed to generate the localhost certificate.");
}
}
return {
key: fs.readFileSync(keyPath),
cert: fs.readFileSync(certPath),
};
}
function getLocalIpv4Addresses() {
const addresses = [];
for (const interfaces of Object.values(os.networkInterfaces())) {
for (const entry of interfaces || []) {
if (entry.family === "IPv4" && !entry.internal) {
addresses.push(entry.address);
}
}
}
return addresses;
}
function requestOrigin(req) {
const host = req.headers.host || `localhost:${PORT}`;
return `https://${host}`;
}
function token(bytes = 24) {
return crypto.randomBytes(bytes).toString("base64url");
}
function nowSeconds() {
return Math.floor(Date.now() / 1000);
}
function htmlEscape(value) {
return String(value)
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;");
}
function quoteSfString(value) {
return `"${String(value).replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
}
function parseStructuredString(value) {
if (!value) return null;
const input = String(value).trim();
if (!input.startsWith('"')) return input.split(";", 1)[0].trim();
let output = "";
let escaped = false;
for (let i = 1; i < input.length; i += 1) {
const ch = input[i];
if (escaped) {
output += ch;
escaped = false;
continue;
}
if (ch === "\\") {
escaped = true;
continue;
}
if (ch === '"') break;
output += ch;
}
return output;
}
function parseCookies(req) {
const cookies = new Map();
const raw = req.headers.cookie;
if (!raw) return cookies;
for (const part of raw.split(";")) {
const [name, ...rest] = part.trim().split("=");
if (!name) continue;
cookies.set(name, decodeURIComponent(rest.join("=")));
}
return cookies;
}
function pruneSessionCookies(session) {
const now = nowSeconds();
for (const [value, expires] of session.cookies) {
if (expires <= now) {
session.cookies.delete(value);
state.cookieIndex.delete(value);
}
}
}
function getSessionFromCookie(req) {
const cookies = parseCookies(req);
const cookieValue = cookies.get(COOKIE_NAME);
if (!cookieValue) return null;
const sessionId = state.cookieIndex.get(cookieValue);
if (!sessionId) return null;
const session = state.sessions.get(sessionId);
if (!session) return null;
pruneSessionCookies(session);
if (!session.cookies.has(cookieValue)) return null;
return session;
}
function boundCookie(value, maxAge = COOKIE_MAX_AGE_SECONDS) {
return `${COOKIE_NAME}=${encodeURIComponent(value)}; Max-Age=${maxAge}; Path=/; Secure; HttpOnly; SameSite=Lax`;
}
function sessionInstructions(session) {
return {
session_identifier: session.id,
refresh_url: "/dbsc/refresh",
scope: {
include_site: false,
scope_specification: [
{ type: "exclude", path: "/dbsc" },
{ type: "exclude", path: "/login" },
{ type: "exclude", path: "/logout" },
],
},
credentials: [
{
type: "cookie",
name: COOKIE_NAME,
attributes: "Path=/; Secure; HttpOnly; SameSite=Lax",
},
],
allowed_refresh_initiators: [],
};
}
function decodeJwt(jwt) {
const parts = String(jwt).split(".");
if (parts.length !== 3) throw new Error("DBSC proof is not a compact JWS.");
return {
parts,
header: JSON.parse(Buffer.from(parts[0], "base64url").toString("utf8")),
payload: JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8")),
signature: parts[2] ? Buffer.from(parts[2], "base64url") : Buffer.alloc(0),
signingInput: Buffer.from(`${parts[0]}.${parts[1]}`, "ascii"),
};
}
function verifyDbscJwt({ jwt, expectedChallenge, expectedAuthorization = null, storedJwk = null, registration = false }) {
const proof = decodeJwt(jwt);
const { header, payload } = proof;
if (header.typ !== "dbsc+jwt") throw new Error("JWT typ must be dbsc+jwt.");
if (payload.jti !== expectedChallenge) throw new Error("JWT challenge mismatch.");
if (expectedAuthorization && payload.authorization !== expectedAuthorization) {
throw new Error("JWT authorization mismatch.");
}
const alg = String(header.alg || "");
let jwk = storedJwk;
if (registration) {
if (alg !== "none" && !header.jwk) {
throw new Error("Registration proof must include the public JWK.");
}
jwk = header.jwk || null;
}
if (alg === "none") {
if (!ALLOW_NONE_ALGORITHM) throw new Error("alg=none is disabled.");
if (proof.signature.length !== 0) throw new Error("alg=none JWT must have an empty signature.");
return { header, payload, jwk, algorithm: alg };
}
if (alg !== "ES256") throw new Error(`Unsupported JWT alg '${alg}'.`);
if (!jwk) throw new Error("Missing JWK for ES256 verification.");
if (jwk.kty !== "EC" || jwk.crv !== "P-256") throw new Error("Only EC P-256 JWKs are supported.");
if (proof.signature.length !== 64) throw new Error("ES256 signature must be 64 bytes.");
const publicKey = crypto.createPublicKey({ key: jwk, format: "jwk" });
const ok = crypto.verify(
"sha256",
proof.signingInput,
{ key: publicKey, dsaEncoding: "ieee-p1363" },
proof.signature,
);
if (!ok) throw new Error("ES256 signature verification failed.");
return { header, payload, jwk, algorithm: alg };
}
function send(res, status, body = "", contentType = "text/html; charset=utf-8", headers = {}) {
const bytes = Buffer.from(body);
res.statusCode = status;
res.setHeader("Cache-Control", "no-store");
res.setHeader("Content-Type", contentType);
res.setHeader("Content-Length", bytes.length);
for (const [name, value] of Object.entries(headers)) {
res.setHeader(name, value);
}
res.end(bytes);
}
function sendJson(res, status, value, headers = {}) {
send(res, status, JSON.stringify(value, null, 2), "application/json", headers);
}
function page(title, body) {
return `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>${htmlEscape(title)}</title>
<style>
:root {
color-scheme: light dark;
--bg: #f7f7f3;
--panel: #ffffff;
--ink: #1c2522;
--muted: #5f6b66;
--line: #d9ded8;
--accent: #006b5f;
--accent-2: #b4492d;
--accent-3: #335c98;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
font: 15px/1.45 "Segoe UI", system-ui, sans-serif;
color: var(--ink);
background: var(--bg);
}
main {
width: min(980px, calc(100vw - 32px));
margin: 0 auto;
padding: 34px 0;
}
header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
padding-bottom: 18px;
border-bottom: 1px solid var(--line);
}
h1 {
margin: 0;
font-size: 28px;
font-weight: 700;
letter-spacing: 0;
}
.subtle { color: var(--muted); }
.grid {
display: grid;
grid-template-columns: minmax(280px, 380px) 1fr;
gap: 18px;
margin-top: 22px;
align-items: start;
}
.panel {
background: var(--panel);
border: 1px solid var(--line);
border-radius: 8px;
padding: 18px;
}
label {
display: block;
font-weight: 600;
margin: 12px 0 6px;
}
input {
width: 100%;
border: 1px solid var(--line);
border-radius: 6px;
padding: 10px 11px;
font: inherit;
background: transparent;
color: inherit;
}
button, .button {
appearance: none;
border: 0;
border-radius: 6px;
padding: 10px 13px;
font: inherit;
font-weight: 700;
color: #fff;
background: var(--accent);
cursor: pointer;
text-decoration: none;
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 40px;
}
.button.secondary, button.secondary { background: var(--accent-3); }
.button.warn, button.warn { background: var(--accent-2); }
.actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
margin-top: 16px;
}
dl {
display: grid;
grid-template-columns: 150px 1fr;
gap: 8px 12px;
margin: 0;
}
dt { color: var(--muted); }
dd { margin: 0; overflow-wrap: anywhere; }
pre {
margin: 0;
white-space: pre-wrap;
font: 12px/1.45 Consolas, monospace;
color: #24332f;
max-height: 360px;
overflow: auto;
}
.status {
margin-top: 14px;
min-height: 24px;
color: var(--muted);
}
@media (max-width: 760px) {
main { width: min(100vw - 24px, 980px); padding-top: 20px; }
header { align-items: flex-start; flex-direction: column; }
.grid { grid-template-columns: 1fr; }
dl { grid-template-columns: 1fr; }
}
</style>
</head>
<body><main>${body}</main></body>
</html>`;
}
function eventLog() {
return htmlEscape(state.events.join("\n"));
}
function loginPage(origin, notice = "") {
return page("DBSC POC", `
<header>
<div>
<h1>DBSC POC</h1>
<div class="subtle">${htmlEscape(origin)}</div>
</div>
<div class="actions">
<a class="button secondary" href="/protected">Protected</a>
<a class="button secondary" href="/device-bound">Bound-only</a>
</div>
</header>
<section class="grid">
<form class="panel" method="post" action="/login">
<label for="username">Username</label>
<input id="username" name="username" autocomplete="username" value="demo">
<label for="password">Password</label>
<input id="password" name="password" type="password" autocomplete="current-password" value="password">
<div class="actions"><button type="submit">Sign in</button></div>
<div class="status">${htmlEscape(notice)}</div>
</form>
<section class="panel"><pre>${eventLog()}</pre></section>
</section>`);
}
function pendingLoginPage(username, origin) {
return page("DBSC POC", `
<header>
<div>
<h1>DBSC POC</h1>
<div class="subtle">Signed in as ${htmlEscape(username)}</div>
</div>
<a class="button secondary" href="/protected">Protected</a>
</header>
<section class="grid">
<section class="panel">
<dl>
<dt>Registration</dt><dd id="state">waiting</dd>
<dt>Cookie lifetime</dt><dd>${COOKIE_MAX_AGE_SECONDS} seconds</dd>
<dt>Origin</dt><dd>${htmlEscape(origin)}</dd>
</dl>
<div class="actions">
<a class="button" href="/protected">Open protected view</a>
<a class="button secondary" href="/device-bound">Open bound-only page</a>
<a class="button warn" href="/logout">Logout</a>
</div>
</section>
<section class="panel"><pre>${eventLog()}</pre></section>
</section>
<script>
const state = document.getElementById("state");
let attempts = 0;
async function poll() {
attempts += 1;
const res = await fetch("/api/status", { credentials: "include" });
const body = await res.json();
if (body.authenticated) {
location.href = "/protected";
return;
}
state.textContent = attempts > 8 ? "not registered" : "waiting";
if (attempts < 20) setTimeout(poll, 600);
}
setTimeout(poll, 450);
</script>`);
}
function protectedPage(session) {
const ttl = Math.max(0, session.cookieExpires - nowSeconds());
return page("Protected", `
<header>
<div>
<h1>Protected</h1>
<div class="subtle">Bound cookie accepted</div>
</div>
<div class="actions">
<form method="post" action="/expire"><button class="secondary" type="submit">Expire cookie</button></form>
<a class="button warn" href="/logout">Logout</a>
</div>
</header>
<section class="grid">
<section class="panel">
<dl>
<dt>DBSC session</dt><dd>${htmlEscape(session.id)}</dd>
<dt>JWT alg</dt><dd>${htmlEscape(session.algorithm)}</dd>
<dt>Cookie TTL</dt><dd>${ttl} seconds</dd>
<dt>Refresh endpoint</dt><dd>/dbsc/refresh</dd>
</dl>
<div class="actions">
<a class="button" href="/protected">Reload</a>
<a class="button secondary" href="/device-bound">Bound-only page</a>
<a class="button secondary" href="/api/whoami">Whoami JSON</a>
<a class="button secondary" href="/api/status">Status JSON</a>
</div>
</section>
<section class="panel"><pre>${eventLog()}</pre></section>
</section>`);
}
function deviceBoundPage(session) {
const ttl = Math.max(0, session.cookieExpires - nowSeconds());
return page("Device Bound", `
<header>
<div>
<h1>Device Bound</h1>
<div class="subtle">DBSC cookie required</div>
</div>
<div class="actions">
<a class="button secondary" href="/protected">Protected</a>
<a class="button warn" href="/logout">Logout</a>
</div>
</header>
<section class="grid">
<section class="panel">
<dl>
<dt>User</dt><dd>${htmlEscape(session.username)}</dd>
<dt>DBSC session</dt><dd>${htmlEscape(session.id)}</dd>
<dt>Cookie name</dt><dd>${COOKIE_NAME}</dd>
<dt>Cookie TTL</dt><dd>${ttl} seconds</dd>
</dl>
<div class="actions">
<form method="post" action="/expire"><button class="secondary" type="submit">Expire cookie</button></form>
<a class="button" href="/device-bound">Reload</a>
<a class="button secondary" href="/api/whoami">Whoami JSON</a>
</div>
</section>
<section class="panel"><pre>${eventLog()}</pre></section>
</section>`);
}
async function readBody(req) {
const chunks = [];
for await (const chunk of req) chunks.push(chunk);
return Buffer.concat(chunks).toString("utf8");
}
async function handleLogin(req, res) {
const body = await readBody(req);
const form = new URLSearchParams(body);
const username = form.get("username") || "demo";
const challenge = token();
state.pending.set(challenge, {
challenge,
username,
created: nowSeconds(),
});
addEvent(`login accepted for '${username}'; sent Secure-Session-Registration`);
send(res, 200, pendingLoginPage(username, requestOrigin(req)), "text/html; charset=utf-8", {
"Secure-Session-Registration": `(ES256);path=${quoteSfString("/dbsc/register")};challenge=${quoteSfString(challenge)}`,
});
}
async function handleRegister(req, res) {
try {
const jwt = parseStructuredString(req.headers["secure-session-response"]);
if (!jwt) throw new Error("Missing Secure-Session-Response.");
const unverified = decodeJwt(jwt);
const pending = state.pending.get(unverified.payload.jti);
if (!pending) throw new Error("Missing or unknown registration challenge.");
const proof = verifyDbscJwt({
jwt,
expectedChallenge: pending.challenge,
registration: true,
});
const sessionId = token(18);
const cookieValue = token(32);
const nextChallenge = token();
const session = {
id: sessionId,
username: pending.username,
jwk: proof.jwk,
algorithm: proof.algorithm,
cookieValue,
cookieExpires: nowSeconds() + COOKIE_MAX_AGE_SECONDS,
cookies: new Map([[cookieValue, nowSeconds() + COOKIE_MAX_AGE_SECONDS]]),
challenge: nextChallenge,
challengeExpires: nowSeconds() + 90,
};
state.sessions.set(sessionId, session);
state.cookieIndex.set(cookieValue, sessionId);
state.pending.delete(pending.challenge);
addEvent(`registered DBSC session ${sessionId} with ${proof.algorithm}`);
sendJson(res, 200, sessionInstructions(session), {
"Set-Cookie": boundCookie(cookieValue),
"Secure-Session-Challenge": `${quoteSfString(nextChallenge)};id=${quoteSfString(sessionId)}`,
});
} catch (error) {
addEvent(`registration failed: ${error.message}`);
sendJson(res, 400, { error: error.message });
}
}
async function handleRefresh(req, res) {
try {
const sessionId = parseStructuredString(req.headers["sec-secure-session-id"]);
const session = sessionId ? state.sessions.get(sessionId) : null;
if (!session) throw new Error("Unknown Sec-Secure-Session-Id.");
const jwt = parseStructuredString(req.headers["secure-session-response"]);
if (!jwt) {
session.challenge = token();
session.challengeExpires = nowSeconds() + 90;
addEvent(`refresh challenge issued for ${session.id}`);
send(res, 403, "", "text/plain; charset=utf-8", {
"Secure-Session-Challenge": `${quoteSfString(session.challenge)};id=${quoteSfString(session.id)}`,
});
return;
}
if (session.challengeExpires <= nowSeconds()) {
throw new Error("Refresh challenge expired.");
}
verifyDbscJwt({
jwt,
expectedChallenge: session.challenge,
storedJwk: session.jwk,
});
pruneSessionCookies(session);
session.cookieValue = token(32);
session.cookieExpires = nowSeconds() + COOKIE_MAX_AGE_SECONDS;
session.cookies.set(session.cookieValue, session.cookieExpires);
session.challenge = token();
session.challengeExpires = nowSeconds() + 90;
state.cookieIndex.set(session.cookieValue, session.id);
addEvent(`refreshed bound cookie for ${session.id}`);
sendJson(res, 200, sessionInstructions(session), {
"Set-Cookie": boundCookie(session.cookieValue),
"Secure-Session-Challenge": `${quoteSfString(session.challenge)};id=${quoteSfString(session.id)}`,
});
} catch (error) {
addEvent(`refresh failed: ${error.message}`);
sendJson(res, 403, { continue: false, error: error.message });
}
}
function handleStatus(req, res) {
const session = getSessionFromCookie(req);
if (!session) {
sendJson(res, 401, { authenticated: false });
return;
}
sendJson(res, 200, {
authenticated: true,
session_identifier: session.id,
cookie_ttl_seconds: Math.max(0, session.cookieExpires - nowSeconds()),
}, {
"Secure-Session-Challenge": `${quoteSfString(session.challenge)};id=${quoteSfString(session.id)}`,
});
}
function handleWhoami(req, res) {
const session = getSessionFromCookie(req);
if (!session) {
if (req.headers["secure-session-skipped"]) {
addEvent(`whoami request arrived with Secure-Session-Skipped: ${req.headers["secure-session-skipped"]}`);
}
sendJson(res, 401, {
authenticated: false,
dbsc: {
active: false,
required_cookie: COOKIE_NAME,
},
});
return;
}
sendJson(res, 200, {
authenticated: true,
username: session.username,
dbsc: {
active: true,
session_identifier: session.id,
cookie_name: COOKIE_NAME,
cookie_ttl_seconds: Math.max(0, session.cookieExpires - nowSeconds()),
proof_algorithm: session.algorithm,
refresh_url: "/dbsc/refresh",
accepted_cookie_count: session.cookies.size,
},
}, {
"Secure-Session-Challenge": `${quoteSfString(session.challenge)};id=${quoteSfString(session.id)}`,
});
}
function handleProtected(req, res) {
const session = getSessionFromCookie(req);
if (!session) {
if (req.headers["secure-session-skipped"]) {
addEvent(`protected request arrived with Secure-Session-Skipped: ${req.headers["secure-session-skipped"]}`);
}
send(res, 401, loginPage(requestOrigin(req), "No valid bound cookie on this request."));
return;
}
send(res, 200, protectedPage(session), "text/html; charset=utf-8", {
"Secure-Session-Challenge": `${quoteSfString(session.challenge)};id=${quoteSfString(session.id)}`,
});
}
function handleDeviceBound(req, res) {
const session = getSessionFromCookie(req);
if (!session) {
if (req.headers["secure-session-skipped"]) {
addEvent(`device-bound request arrived with Secure-Session-Skipped: ${req.headers["secure-session-skipped"]}`);
}
send(res, 401, loginPage(requestOrigin(req), "A current device-bound session cookie is required."));
return;
}
send(res, 200, deviceBoundPage(session), "text/html; charset=utf-8", {
"Secure-Session-Challenge": `${quoteSfString(session.challenge)};id=${quoteSfString(session.id)}`,
});
}
function handleExpire(req, res) {
const session = getSessionFromCookie(req);
if (session) {
const presentedCookie = parseCookies(req).get(COOKIE_NAME);
if (presentedCookie) {
session.cookies.delete(presentedCookie);
state.cookieIndex.delete(presentedCookie);
}
if (presentedCookie === session.cookieValue) {
session.cookieExpires = nowSeconds() - 1;
}
addEvent(`expired bound cookie for ${session.id}`);
}
res.writeHead(302, {
Location: "/protected",
"Cache-Control": "no-store",
"Set-Cookie": boundCookie("expired", 0),
});
res.end();
}
function handleLogout(req, res) {
state.sessions.clear();
state.cookieIndex.clear();
addEvent("cleared local sessions");
res.writeHead(302, {
Location: "/",
"Cache-Control": "no-store",
"Set-Cookie": boundCookie("logged-out", 0),
"Clear-Site-Data": '"cookies"',
});
res.end();
}
const tlsOptions = ensureCertificate();
const server = https.createServer(tlsOptions, async (req, res) => {
try {
const url = new URL(req.url, requestOrigin(req));
const route = `${req.method} ${url.pathname}`;
if (route === "GET /") return send(res, 200, loginPage(requestOrigin(req)));
if (route === "POST /login") return handleLogin(req, res);
if (route === "POST /dbsc/register") return handleRegister(req, res);
if (route === "POST /dbsc/refresh") return handleRefresh(req, res);
if (route === "GET /api/status") return handleStatus(req, res);
if (route === "GET /api/whoami") return handleWhoami(req, res);
if (route === "GET /protected") return handleProtected(req, res);
if (route === "GET /device-bound") return handleDeviceBound(req, res);
if (route === "POST /expire") return handleExpire(req, res);
if (route === "GET /logout") return handleLogout(req, res);
send(res, 404, "Not found", "text/plain; charset=utf-8");
} catch (error) {
addEvent(`request failed: ${error.stack || error.message}`);
send(res, 500, "Internal Server Error", "text/plain; charset=utf-8");
}
});
server.listen(PORT, BIND_HOST, () => {
addEvent(`DBSC POC listening on ${BIND_HOST}:${PORT}`);
addEvent(`open https://localhost:${PORT}/ or https://<this-host-ip>:${PORT}/`);
addEvent(`bound cookies last ${COOKIE_MAX_AGE_SECONDS} seconds`);
});