Tighten code comments + README (todo §0): denser, drop redundant prose; no behavior change

This commit is contained in:
2026-06-15 10:30:06 +02:00
parent 17f4411518
commit 1fb6f23805
9 changed files with 102 additions and 116 deletions

View File

@@ -8,8 +8,7 @@ import { serveStatic } from "./static.ts";
const rootDir = join(dirname(fileURLToPath(import.meta.url)), "..");
export interface AppOptions {
// Cache compiled templates (compile once vs. re-read+recompile per request).
// Defaults to on in production, off in dev so source edits show up live.
// Cache compiled templates: on in production, off in dev so edits show live.
cache?: boolean;
publicDir?: string;
viewsDir?: string;
@@ -35,8 +34,8 @@ export function createApp(options: AppOptions = {}): Server {
return;
}
// The single request shape handlers receive (§2/§4 router passes it on); routing
// reads its parsed URL instead of building a throwaway one.
// The request shape handlers receive (§2/§4 router passes it on); routing
// reuses its parsed URL instead of building a throwaway.
const { pathname } = buildContext(req, res).url;
if (pathname.startsWith("/public/")) {
@@ -54,8 +53,8 @@ export function createApp(options: AppOptions = {}): Server {
console.error(err);
if (res.headersSent) return void res.end(); // a partial body is already on the wire
try {
// Render first: if the error page itself fails, headers stay unsent and we
// fall back to plain text below rather than emit a half-written response.
// Render before writing: if the 500 page itself throws, headers stay unsent
// and we fall back to plain text below instead of a half-written response.
sendHtml(res, 500, await render("500", { title: "Server error" }));
} catch (renderErr) {
console.error(renderErr);

View File

@@ -1,11 +1,10 @@
// Config loaded once from the environment at boot (todo §0): Ory endpoints, the
// cookie/CSRF secrets, the JWKS location, and the listen port. Fail-loud — a missing
// production secret, a bad URL, or an out-of-range port throws here, before the server
// starts, never at request time.
// Config loaded once from the environment at boot (todo §0): Ory endpoints, cookie/CSRF
// secrets, JWKS location, listen port. Fail-loud — a missing prod secret, a bad URL, or
// an out-of-range port throws here at boot, never at request time.
//
// Clean-clone philosophy (README): every value has a working dev default so `docker
// compose up` runs with zero config; in production only the secrets must be supplied
// (the dev throwaways are refused), everything else still defaults to the Ory services.
// Clean-clone (README): every value has a working dev default, so `docker compose up`
// runs with zero config; in production the secrets must be supplied (dev throwaways
// refused), everything else still defaults to the Ory services.
export interface Config {
cookieSecret: string;

View File

@@ -1,11 +1,10 @@
import type { IncomingMessage, ServerResponse } from "node:http";
// The request context threaded to every route handler (plugin + built-in). Built
// once per request by `buildContext`: the router supplies matched path `params`,
// the §4 JWT middleware supplies the `user` (null/[] until then). Handlers read the
// request and write the response through it — the host's single handler argument.
// The request context threaded to every route handler (plugin + built-in), built once
// per request by `buildContext`: the router supplies matched path `params`, the §4 JWT
// middleware supplies `user` (null until then). The host's single handler argument.
// The authenticated user, projected from the verified session JWT claims (§4):
// The authenticated user, projected from verified session JWT claims (§4):
// `id` = `sub`, plus `email` and the coarse `roles` carried in the token.
export interface User {
email: string;

View File

@@ -1,7 +1,7 @@
// Cookie helpers — parse the request `Cookie` header, build secure-by-default
// `Set-Cookie` headers. Stdlib only (no `cookie` dep); §4 stores/clears the session
// JWT + CSRF token with these. Values round-trip via percent-encoding (serialize
// encodes, parse decodes); JWT `-_.` chars are URI-unreserved, so JWTs stay readable.
// JWT + CSRF token here. Values round-trip via percent-encoding; JWT `-_.` chars are
// URI-unreserved, so JWTs stay readable.
export interface CookieOptions {
domain?: string;
@@ -22,7 +22,7 @@ const minExpires = Date.UTC(1601, 0, 1);
const maxExpires = Date.UTC(9999, 11, 31, 23, 59, 59, 999);
function decode(value: string): string {
if (!value.includes("%")) return value; // optimization only: an unencoded value has no escapes to decode
if (!value.includes("%")) return value; // fast path: nothing to decode
try {
return decodeURIComponent(value);
} catch {
@@ -30,10 +30,9 @@ function decode(value: string): string {
}
}
// Parse a request `Cookie` header into a name→value map. First occurrence of a
// name wins (a later duplicate can't shadow it). The result is a null-prototype
// object, so an attacker-supplied `__proto__`/`constructor` key can't pollute.
// Input length is bounded upstream by Node's HTTP `maxHeaderSize` (~16 KB default).
// Parse a `Cookie` header into a name→value map. First occurrence of a name wins.
// Null-prototype result, so a `__proto__`/`constructor` key can't pollute. Header
// length is bounded upstream by Node's `maxHeaderSize` (~16 KB).
export function parseCookies(header: string | undefined): Record<string, string> {
const out: Record<string, string> = Object.create(null);
if (!header) return out;
@@ -50,10 +49,9 @@ export function parseCookies(header: string | undefined): Record<string, string>
return out;
}
// Validate a Domain/Path attribute: non-empty (an empty one emits a junk `Path=`
// browsers ignore — fail loud on a misconfig), and free of chars that could inject
// extra attributes or split the response header (CRLF). These come from config, but
// validating is cheap insurance against Set-Cookie injection.
// Validate a Domain/Path attribute: non-empty (fail loud on a misconfig) and free of
// chars that could inject extra attributes or split the header (CRLF). Cheap insurance
// against Set-Cookie injection, even though these come from config.
function assertAttrSafe(label: string, value: string): void {
if (value === "" || /[;\x00-\x1f\x7f]/.test(value)) throw new Error(`invalid cookie ${label}: ${JSON.stringify(value)}`);
}

View File

@@ -2,16 +2,13 @@ import { createPublicKey, verify } from "node:crypto";
import type { JsonWebKey, KeyObject } from "node:crypto";
// JWS signature verification with the Node stdlib — no `jose`/JWT dep (todo §0):
// `createPublicKey({format:"jwk"})` imports a JWK and verifies the RS*/ES* signatures the
// Kratos tokenizer produces — all we need, no supply-chain surface (see AGENTS.md).
//
// Signature only. §4 builds the rest on top: claim checks (exp/iss/aud, clock skew),
// JWKS-by-`kid` fetch/cache/rotation, and bounding `token` type/length at the boundary.
// `createPublicKey({format:"jwk"})` imports a JWK and verifies the RS*/ES* signatures
// the Kratos tokenizer produces (see AGENTS.md). Signature only — §4 adds claim checks
// (exp/iss/aud, clock skew), JWKS-by-`kid` fetch/cache/rotation, and `token` bounds.
// JOSE `alg` → Node verify parameters. ES* signatures are raw r‖s (IEEE P1363), not DER.
// Widen support by extending this map. Security invariant: never add an `HS*` (symmetric)
// entry — this map is the allowlist, and one would let an attacker-supplied HMAC key verify.
// `none` is absent for the same reason.
// Extend this map to widen support. Security invariant: never add `HS*`/`none` — this map
// is the allowlist, and a symmetric entry lets an attacker-supplied HMAC key verify.
const algParams: Record<string, { hash: string; keyType: "ec" | "rsa"; dsaEncoding?: "ieee-p1363" }> = {
ES256: { dsaEncoding: "ieee-p1363", hash: "SHA256", keyType: "ec" },
RS256: { hash: "RSA-SHA256", keyType: "rsa" },
@@ -29,9 +26,9 @@ export interface DecodedJws {
signature: Buffer;
}
// Unpadded base64url alphabet — `Buffer.from(_, "base64url")` is lax (drops junk, tolerates
// non-canonical padding), so reject non-canonical segments up front. §4 reads `kid` from the
// still-unverified header, so this stops laundered bytes reaching key selection.
// Unpadded base64url alphabet — `Buffer.from(_,"base64url")` is lax (drops junk, tolerates
// bad padding), so reject non-canonical segments up front. §4 reads `kid` from the still-
// unverified header, so this stops laundered bytes reaching key selection.
const base64url = /^[A-Za-z0-9_-]+$/;
function decodeSegment(segment: string): unknown {
@@ -68,8 +65,8 @@ export function decodeJws(token: string): DecodedJws {
}
// Verify a compact JWS against one JWK public key; returns the decoded JWS or throws.
// Signature only — the caller validates claims. The returned header is post-verification,
// so §4 can trust its `alg`/`kid` when logging.
// Signature only — caller validates claims. Returned header is post-verification, so §4
// can trust its `alg`/`kid` when logging.
export function verifyJws(token: string, jwk: JsonWebKey): DecodedJws {
const decoded = decodeJws(token);
const { header, signingInput, signature } = decoded;

View File

@@ -22,9 +22,8 @@ export function contentTypeFor(filePath: string): string {
return contentTypes[extname(filePath).toLowerCase()] ?? "application/octet-stream";
}
// Resolves a request path inside `dir`, or null if it would escape (traversal) or
// carries a control char (NUL etc.) — rejecting those here makes the guard explicit
// rather than relying on a downstream `stat` to throw.
// Resolve a request path inside `dir`, or null if it escapes (traversal) or carries a
// control char (NUL etc.) — an explicit guard rather than relying on `stat` to throw.
export function resolveStaticPath(dir: string, requestedPath: string): string | null {
if (/[\x00-\x1f]/.test(requestedPath)) return null;
const filePath = join(dir, requestedPath);
@@ -52,8 +51,8 @@ export async function serveStatic(dir: string, requestedPath: string, res: Serve
if (!info.isFile()) return plain(res, 404, "Not Found");
res.writeHead(200, { "content-length": info.size, "content-type": contentTypeFor(filePath) });
if (head) return void res.end(); // headers only — skip opening the file
// Headers are already sent, so a mid-stream read error can't become an HTTP error
// log it and destroy the response to signal a truncated body, not a hung socket.
// Headers are already sent, so a mid-stream read error can't become an HTTP status
// log and destroy the response to signal a truncated body, not a hung socket.
createReadStream(filePath)
.on("error", (err) => {
console.error(err);