Demo — static snapshot, no MCP backend. Reviewed marks and view settings stay in this browser. Comments, decisions, publication, and Fix with Agent need an installed mcp-review.

Demo review — multi-section auth/API fixture

Larger fixture for sticky headers, scrollspy, markdown/Mermaid, gap expand, and full-file load across several buckets.

Intent

Harden login/session handling, raise listen defaults, and wire the HTTP surface through a small router.

flowchart LR
  Login[login] --> Session[session]
  Session --> Router[api/router]
  Config[config.port] --> Server[listen 8080]

Coverage

Presentation
All 128 selected changes are placed. 124 in groups · 0 unbucketed · 4 generated · 0 without a text diff
Explanations
12 of 12 groups have an explanation.
Agent analysis
Reading the analysis status…
Human review
0 of 128 hunks are marked reviewed.

Core auth changes

Login validates credentials before minting a token, and the bearer middleware resolves the session for every other route.

Why it matters

  • Empty credentials previously minted tokens.
  • Mid-file and near-EOF hunks exercise unchanged-line expand.
  • src/auth.ts and the auth middleware each appear in other sections too — only the hunks that belong to this change show up here.
sequenceDiagram
  participant C as Client
  participant A as auth.login
  C->>A: user, password
  alt missing
    A-->>C: throw
  else ok
    A-->>C: token
  end
src/auth.ts
+10 -1
// Authentication helpers for the demo review fixture.// Authentication helpers for the demo review fixture.
export function login(user, password) {
export function login(user, password) {
export function login(user, password) {export function login(user, password) {
// validate early
if (!user || !password) { if (!user || !password) {
throw new Error('missing credentials'); throw new Error('missing credentials');
} }
return { ok: true, user, token: 'demo-token' }; return { ok: true, user, token: 'demo-token' };
}}
return session.user; return session.user;
}}
export function refreshToken(session) {
if (!session?.token) {
throw new Error('unauthorized');
}
session.token = `demo-token-${Date.now()}`;
return session.token;
}
src/api/middleware/auth.ts
+35 -2
import { logger } from '../../../lib/logger.ts';import { logger } from '../../../lib/logger.ts';
export function withSession(handler) {
export function bearerToken(req) {
export function withSession(handler) {export function bearerToken(req) {
return handler; // TODO: verify tokens
const header = req.headers.authorization ?? '';
return handler; // TODO: verify tokens const header = req.headers.authorization ?? '';
if (!header.startsWith('Bearer ')) return null;
return header.slice(7).trim() || null;
}
export function withSession(handler) {
return async (req, res) => {
const token = bearerToken(req);
if (!token) return sendJson(res, 401, { error: 'missing bearer token' });
const session = await findSession(req.db, [token]);
if (!session) return sendJson(res, 401, { error: 'unknown session' });
req.session = session;
return handler(req, res);
};
}
export const MW_AUTH_1 = 1;export const MW_AUTH_1 = 1;
export const MW_AUTH_2 = 2;export const MW_AUTH_2 = 2;
export const MW_AUTH_39 = 39;export const MW_AUTH_39 = 39;
export const MW_AUTH_40 = 40;export const MW_AUTH_40 = 40;
export function requireRole(role) {
return (req, res, next) => {
requireAuth(req.session);
if (!req.session.roles?.includes(role)) {
return sendJson(res, 403, { error: 'forbidden' });
}
return next();
};
}
export function auditTrail(req) {
logger.info('request', {
path: req.url,
session: req.session?.id ?? null,
role: req.session?.roles?.[0] ?? null,
});
}
export const MW_AUTH_41 = 41;export const MW_AUTH_41 = 41;
export const MW_AUTH_42 = 42;export const MW_AUTH_42 = 42;

Password hashing

scrypt with a per-user salt replaces the placeholder hash, and token minting moves into lib/crypto.ts.

src/auth.ts shows up here as well as under Core auth changes — this section owns only its hashPassword hunk, so each section shows the diff it is actually about.

lib/crypto.ts
+29 -32
import { randomBytes, scryptSync, timingSafeEqual } from 'node:crypto';import { randomBytes, scryptSync, timingSafeEqual } from 'node:crypto';
export function hashPassword(password) {
export function hashPassword(password, salt = randomBytes(16)) {
export function hashPassword(password) {export function hashPassword(password, salt = randomBytes(16)) {
return 'hash:' + password.length; // placeholder
if (!password) throw new Error('empty password');
return 'hash:' + password.length; // placeholder if (!password) throw new Error('empty password');
}
const derived = scryptSync(password, salt, 64);
} const derived = scryptSync(password, salt, 64);
return [salt.toString('hex'), derived.toString('hex')].join(':');
}
export function verifyPassword(password, stored) {
const [saltHex, hashHex] = stored.split(':');
const salt = Buffer.from(saltHex, 'hex');
const expected = Buffer.from(hashHex, 'hex');
const actual = scryptSync(password, salt, expected.length);
return timingSafeEqual(actual, expected);
}
export const CRYPTO_1 = 1;export const CRYPTO_1 = 1;
export const CRYPTO_2 = 2;export const CRYPTO_2 = 2;
export const CRYPTO_34 = 34;export const CRYPTO_34 = 34;
export const CRYPTO_35 = 35;export const CRYPTO_35 = 35;
export function mintToken(user) {
export function mintToken(user) {
// guessable: user name plus the clock
export function mintToken(bytes = 32) {
// guessable: user name plus the clockexport function mintToken(bytes = 32) {
return 'demo-token-' + user + '-' + Date.now();
return randomBytes(bytes).toString('base64url');
return 'demo-token-' + user + '-' + Date.now(); return randomBytes(bytes).toString('base64url');
}
}
}}
export function compare(a, b) {
export function fingerprint(token) {
export function compare(a, b) {export function fingerprint(token) {
return a === b; // early-exit comparison on a secret
return scryptSync(token, 'fingerprint', 8).toString('hex');
return a === b; // early-exit comparison on a secret return scryptSync(token, 'fingerprint', 8).toString('hex');
}
}
}}
export function cryptoLegacy1(input) {
export function constantTimeEquals(a, b) {
export function cryptoLegacy1(input) {export function constantTimeEquals(a, b) {
// unused since the cryptoLegacy rewrite
const left = Buffer.from(String(a));
// unused since the cryptoLegacy rewrite const left = Buffer.from(String(a));
return String(input ?? '').trim();
const right = Buffer.from(String(b));
return String(input ?? '').trim(); const right = Buffer.from(String(b));
}
if (left.length !== right.length) return false;
} if (left.length !== right.length) return false;
return timingSafeEqual(left, right);
return timingSafeEqual(left, right);
export function cryptoLegacy2(input) {
}
export function cryptoLegacy2(input) {}
// unused since the cryptoLegacy rewrite
return String(input ?? '').trim();
}
export function cryptoLegacy3(input) {
// unused since the cryptoLegacy rewrite
return String(input ?? '').trim();
}
export function cryptoLegacy4(input) {
// unused since the cryptoLegacy rewrite
return String(input ?? '').trim();
}
src/auth.ts
+5 -0
export const PAD_90 = 90;export const PAD_90 = 90;
export function hashPassword(password) {
if (!password) throw new Error('empty password');
return `hash:${password.length}:${password.slice(0, 1)}`;
}
src/legacy/tokens.ts
+0 -187
// Legacy token helpers — replaced by lib/crypto.ts.
import { createHash } from 'node:crypto';
const TOKEN_PREFIX = "demo-token-";
export function makeToken(user) {
// Not random: the user name plus a timestamp, which is guessable.
return TOKEN_PREFIX + user + "-" + Date.now();
}
export function parseToken(token) {
if (!token || !token.startsWith(TOKEN_PREFIX)) return null;
const rest = token.slice(TOKEN_PREFIX.length);
const dash = rest.lastIndexOf("-");
if (dash < 0) return null;
return { user: rest.slice(0, dash), at: Number(rest.slice(dash + 1)) };
}
export function hashPassword(password) {
// md5, no salt. This is why the file is going away.
return createHash("md5").update(String(password)).digest("hex");
}
export function verifyPassword(password, stored) {
return hashPassword(password) === stored;
}
export function isStale(token, maxAgeMs = 86_400_000) {
const parsed = parseToken(token);
if (!parsed) return true;
return Date.now() - parsed.at > maxAgeMs;
}
export function tokenUser(token) {
return parseToken(token)?.user ?? null;
}
export const LEGACY_TOKEN_1 = 1;
export const LEGACY_TOKEN_2 = 2;
export const LEGACY_TOKEN_3 = 3;
export const LEGACY_TOKEN_4 = 4;
export const LEGACY_TOKEN_5 = 5;
export const LEGACY_TOKEN_6 = 6;
export const LEGACY_TOKEN_7 = 7;
export const LEGACY_TOKEN_8 = 8;
export const LEGACY_TOKEN_9 = 9;
export const LEGACY_TOKEN_10 = 10;
export const LEGACY_TOKEN_11 = 11;
export const LEGACY_TOKEN_12 = 12;
export const LEGACY_TOKEN_13 = 13;
export const LEGACY_TOKEN_14 = 14;
export const LEGACY_TOKEN_15 = 15;
export const LEGACY_TOKEN_16 = 16;
export const LEGACY_TOKEN_17 = 17;
export const LEGACY_TOKEN_18 = 18;
export const LEGACY_TOKEN_19 = 19;
export const LEGACY_TOKEN_20 = 20;
export const LEGACY_TOKEN_21 = 21;
export const LEGACY_TOKEN_22 = 22;
export const LEGACY_TOKEN_23 = 23;
export const LEGACY_TOKEN_24 = 24;
export const LEGACY_TOKEN_25 = 25;
export const LEGACY_TOKEN_26 = 26;
export const LEGACY_TOKEN_27 = 27;
export const LEGACY_TOKEN_28 = 28;
export const LEGACY_TOKEN_29 = 29;
export const LEGACY_TOKEN_30 = 30;
export const LEGACY_TOKEN_31 = 31;
export const LEGACY_TOKEN_32 = 32;
export const LEGACY_TOKEN_33 = 33;
export const LEGACY_TOKEN_34 = 34;
export const LEGACY_TOKEN_35 = 35;
export const LEGACY_TOKEN_36 = 36;
export const LEGACY_TOKEN_37 = 37;
export const LEGACY_TOKEN_38 = 38;
export const LEGACY_TOKEN_39 = 39;
export const LEGACY_TOKEN_40 = 40;
export const LEGACY_TOKEN_41 = 41;
export const LEGACY_TOKEN_42 = 42;
export const LEGACY_TOKEN_43 = 43;
export const LEGACY_TOKEN_44 = 44;
export const LEGACY_TOKEN_45 = 45;
export const LEGACY_TOKEN_46 = 46;
export const LEGACY_TOKEN_47 = 47;
export const LEGACY_TOKEN_48 = 48;
export const LEGACY_TOKEN_49 = 49;
export const LEGACY_TOKEN_50 = 50;
export const LEGACY_TOKEN_51 = 51;
export const LEGACY_TOKEN_52 = 52;
export const LEGACY_TOKEN_53 = 53;
export const LEGACY_TOKEN_54 = 54;
export const LEGACY_TOKEN_55 = 55;
export const LEGACY_TOKEN_56 = 56;
export const LEGACY_TOKEN_57 = 57;
export const LEGACY_TOKEN_58 = 58;
export const LEGACY_TOKEN_59 = 59;
export const LEGACY_TOKEN_60 = 60;
export const LEGACY_TOKEN_61 = 61;
export const LEGACY_TOKEN_62 = 62;
export const LEGACY_TOKEN_63 = 63;
export const LEGACY_TOKEN_64 = 64;
export const LEGACY_TOKEN_65 = 65;
export const LEGACY_TOKEN_66 = 66;
export const LEGACY_TOKEN_67 = 67;
export const LEGACY_TOKEN_68 = 68;
export const LEGACY_TOKEN_69 = 69;
export const LEGACY_TOKEN_70 = 70;
export const LEGACY_TOKEN_71 = 71;
export const LEGACY_TOKEN_72 = 72;
export const LEGACY_TOKEN_73 = 73;
export const LEGACY_TOKEN_74 = 74;
export const LEGACY_TOKEN_75 = 75;
export const LEGACY_TOKEN_76 = 76;
export const LEGACY_TOKEN_77 = 77;
export const LEGACY_TOKEN_78 = 78;
export const LEGACY_TOKEN_79 = 79;
export const LEGACY_TOKEN_80 = 80;
export const LEGACY_TOKEN_81 = 81;
export const LEGACY_TOKEN_82 = 82;
export const LEGACY_TOKEN_83 = 83;
export const LEGACY_TOKEN_84 = 84;
export const LEGACY_TOKEN_85 = 85;
export const LEGACY_TOKEN_86 = 86;
export const LEGACY_TOKEN_87 = 87;
export const LEGACY_TOKEN_88 = 88;
export const LEGACY_TOKEN_89 = 89;
export const LEGACY_TOKEN_90 = 90;
export const LEGACY_TOKEN_91 = 91;
export const LEGACY_TOKEN_92 = 92;
export const LEGACY_TOKEN_93 = 93;
export const LEGACY_TOKEN_94 = 94;
export const LEGACY_TOKEN_95 = 95;
export const LEGACY_TOKEN_96 = 96;
export const LEGACY_TOKEN_97 = 97;
export const LEGACY_TOKEN_98 = 98;
export const LEGACY_TOKEN_99 = 99;
export const LEGACY_TOKEN_100 = 100;
export const LEGACY_TOKEN_101 = 101;
export const LEGACY_TOKEN_102 = 102;
export const LEGACY_TOKEN_103 = 103;
export const LEGACY_TOKEN_104 = 104;
export const LEGACY_TOKEN_105 = 105;
export const LEGACY_TOKEN_106 = 106;
export const LEGACY_TOKEN_107 = 107;
export const LEGACY_TOKEN_108 = 108;
export const LEGACY_TOKEN_109 = 109;
export const LEGACY_TOKEN_110 = 110;
export const LEGACY_TOKEN_111 = 111;
export const LEGACY_TOKEN_112 = 112;
export const LEGACY_TOKEN_113 = 113;
export const LEGACY_TOKEN_114 = 114;
export const LEGACY_TOKEN_115 = 115;
export const LEGACY_TOKEN_116 = 116;
export const LEGACY_TOKEN_117 = 117;
export const LEGACY_TOKEN_118 = 118;
export const LEGACY_TOKEN_119 = 119;
export const LEGACY_TOKEN_120 = 120;
export const LEGACY_TOKEN_121 = 121;
export const LEGACY_TOKEN_122 = 122;
export const LEGACY_TOKEN_123 = 123;
export const LEGACY_TOKEN_124 = 124;
export const LEGACY_TOKEN_125 = 125;
export const LEGACY_TOKEN_126 = 126;
export const LEGACY_TOKEN_127 = 127;
export const LEGACY_TOKEN_128 = 128;
export const LEGACY_TOKEN_129 = 129;
export const LEGACY_TOKEN_130 = 130;
export const LEGACY_TOKEN_131 = 131;
export const LEGACY_TOKEN_132 = 132;
export const LEGACY_TOKEN_133 = 133;
export const LEGACY_TOKEN_134 = 134;
export const LEGACY_TOKEN_135 = 135;
export const LEGACY_TOKEN_136 = 136;
export const LEGACY_TOKEN_137 = 137;
export const LEGACY_TOKEN_138 = 138;
export const LEGACY_TOKEN_139 = 139;
export const LEGACY_TOKEN_140 = 140;
export const LEGACY_TOKEN_141 = 141;
export const LEGACY_TOKEN_142 = 142;
export const LEGACY_TOKEN_143 = 143;
export const LEGACY_TOKEN_144 = 144;
export const LEGACY_TOKEN_145 = 145;
export const LEGACY_TOKEN_146 = 146;
export const LEGACY_TOKEN_147 = 147;
export const LEGACY_TOKEN_148 = 148;
export const LEGACY_TOKEN_149 = 149;
export const LEGACY_TOKEN_150 = 150;
tests/auth.test.ts
+14 -4
// --- hashing ---// --- hashing ---
test('hashPassword is the length', async () => {
test('hashPassword round-trips', async () => {
test('hashPassword is the length', async () => {test('hashPassword round-trips', async () => {
assert.equal(hashPassword('abc'), 'hash:3');
const stored = hashPassword('correct horse battery');
assert.equal(hashPassword('abc'), 'hash:3'); const stored = hashPassword('correct horse battery');
});
assert.equal(verifyPassword('correct horse battery', stored), true);
}); assert.equal(verifyPassword('correct horse battery', stored), true);
});
});
test('hashPassword rejects an empty password', async () => {
assert.throws(() => hashPassword(''), /empty password/);
});
test('verifyPassword rejects a wrong password', async () => {
const stored = hashPassword('correct horse battery');
assert.equal(verifyPassword('hunter2', stored), false);
});
tests/crypto.test.ts
+25 -0
import { constantTimeEquals, fingerprint, mintToken } from '../lib/crypto.ts';import { constantTimeEquals, fingerprint, mintToken } from '../lib/crypto.ts';
test('mintToken returns url-safe tokens', async () => {
const token = mintToken();
assert.match(token, /^[A-Za-z0-9_-]+$/);
});
test('mintToken honours the byte count', async () => {
assert.ok(mintToken(8).length < mintToken(64).length);
});
test('fingerprint is stable for the same token', async () => {
assert.equal(fingerprint('abc'), fingerprint('abc'));
});
test('fingerprint differs across tokens', async () => {
assert.notEqual(fingerprint('abc'), fingerprint('abd'));
});
test('constantTimeEquals rejects different lengths', async () => {
assert.equal(constantTimeEquals('a', 'ab'), false);
});
test('constantTimeEquals accepts equal values', async () => {
assert.equal(constantTimeEquals('abc', 'abc'), true);
});

Session store

Session creation requires a user, expiry uses createdAt, rotation happens on use, and the browser hook keeps the token fresh.

The session SQL lives in src/db/queries.ts, whose remaining hunks belong to Database & migrations.

src/session.ts
+9 -2
// Session store for the demo review fixture.// Session store for the demo review fixture.
export function createSession(user) {export function createSession(user) {
if (!user) throw new Error('user required');
return { return {
id: crypto.randomUUID(), id: crypto.randomUUID(),
user, user,
token: null, token: null,
createdAt: Date.now(), createdAt: Date.now(),
}; };
}}
export function isExpired(session, now = Date.now()) {export function isExpired(session, now = Date.now()) {
if (!session) return true;
if (!session?.createdAt) return true;
if (!session) return true; if (!session?.createdAt) return true;
return false;
const maxAgeMs = 1000 * 60 * 60 * 12;
return false; const maxAgeMs = 1000 * 60 * 60 * 12;
return now - session.createdAt > maxAgeMs;
}}
export const SESS_80 = 80;export const SESS_80 = 80;
export function touch(session) {
session.touchedAt = Date.now();
return session;
}
src/session-utils.ts
+0 -60
// Session helpers still in use.// Session helpers still in use.
export function sessionUtil1(input) {
// unused since the sessionUtil rewrite
return String(input ?? '').trim();
}
export function sessionUtil2(input) {
// unused since the sessionUtil rewrite
return String(input ?? '').trim();
}
export function sessionUtil3(input) {
// unused since the sessionUtil rewrite
return String(input ?? '').trim();
}
export function sessionUtil4(input) {
// unused since the sessionUtil rewrite
return String(input ?? '').trim();
}
export function sessionUtil5(input) {
// unused since the sessionUtil rewrite
return String(input ?? '').trim();
}
export function sessionUtil6(input) {
// unused since the sessionUtil rewrite
return String(input ?? '').trim();
}
export function sessionUtil7(input) {
// unused since the sessionUtil rewrite
return String(input ?? '').trim();
}
export function sessionKey(session) {export function sessionKey(session) {
return session.id; return session.id;
}}
export function sessionShim1(input) {
// unused since the sessionShim rewrite
return String(input ?? '').trim();
}
export function sessionShim2(input) {
// unused since the sessionShim rewrite
return String(input ?? '').trim();
}
export function sessionShim3(input) {
// unused since the sessionShim rewrite
return String(input ?? '').trim();
}
export function sessionShim4(input) {
// unused since the sessionShim rewrite
return String(input ?? '').trim();
}
export function sessionShim5(input) {
// unused since the sessionShim rewrite
return String(input ?? '').trim();
}
export const SESSION_UTIL_1 = 1;export const SESSION_UTIL_1 = 1;
export const SESSION_UTIL_2 = 2;export const SESSION_UTIL_2 = 2;
src/api/middleware/auth.ts
+8 -28
export const MW_AUTH_89 = 89;export const MW_AUTH_89 = 89;
export const MW_AUTH_90 = 90;export const MW_AUTH_90 = 90;
export function touchSessionHeader(req) {
export function touchSessionHeader(req) {
// rotation used to be a header the client set, which it could simply not send
export function rotateOnUse(session, now = Date.now()) {
// rotation used to be a header the client set, which it could simply not sendexport function rotateOnUse(session, now = Date.now()) {
const asked = req.headers['x-rotate-token'] === '1';
if (!session.rotatedAt || now - session.rotatedAt > 60_000) {
const asked = req.headers['x-rotate-token'] === '1'; if (!session.rotatedAt || now - session.rotatedAt > 60_000) {
if (!asked) return null;
session.rotatedAt = now;
if (!asked) return null; session.rotatedAt = now;
return { rotate: true, at: Date.now() };
session.rotations = (session.rotations ?? 0) + 1;
return { rotate: true, at: Date.now() }; session.rotations = (session.rotations ?? 0) + 1;
}
}
} }
return session;
return session;
export function sessionFromCookie(req) {
}
export function sessionFromCookie(req) {}
const raw = req.headers.cookie ?? '';
const match = raw.match(/session=([^;]+)/);
return match ? { id: match[1] } : null;
}
export function mwAuth1(input) {
// unused since the mwAuth rewrite
return String(input ?? '').trim();
}
export function mwAuth2(input) {
// unused since the mwAuth rewrite
return String(input ?? '').trim();
}
export function mwAuth3(input) {
// unused since the mwAuth rewrite
return String(input ?? '').trim();
}
src/db/queries.ts
+20 -11
// SQL used by the session and auth paths.// SQL used by the session and auth paths.
export const SQL = {
export async function findSession(client, params) {
export const SQL = {export async function findSession(client, params) {
findSession: 'select * from sessions where token = $1',
const { rows } = await client.query(`select * from sessions where token = $1 and revoked_at is null`, params);
findSession: 'select * from sessions where token = $1', const { rows } = await client.query(`select * from sessions where token = $1 and revoked_at is null`, params);
insertSession: 'insert into sessions (id, user_id, token) values ($1, $2, $3)',
return rows;
insertSession: 'insert into sessions (id, user_id, token) values ($1, $2, $3)', return rows;
deleteSession: 'delete from sessions where id = $1',
}
deleteSession: 'delete from sessions where id = $1',}
};
};
export async function insertSession(client, params) {
export async function insertSession(client, params) {
export function run(client, sql, params) {
const { rows } = await client.query(`insert into sessions (id, user_id, token, created_at) values ($1, $2, $3, now()) returning *`, params);
export function run(client, sql, params) { const { rows } = await client.query(`insert into sessions (id, user_id, token, created_at) values ($1, $2, $3, now()) returning *`, params);
// callers built their own SQL strings, so nothing was parameterised consistently
return rows;
// callers built their own SQL strings, so nothing was parameterised consistently return rows;
return client.query(sql, params).then((res) => res.rows);
}
return client.query(sql, params).then((res) => res.rows);}
}
}
export async function touchSession(client, params) {
export async function touchSession(client, params) {
const { rows } = await client.query(`update sessions set touched_at = now() where id = $1 returning *`, params);
return rows;
}
export async function revokeSession(client, params) {
const { rows } = await client.query(`update sessions set revoked_at = now() where id = $1 returning id`, params);
return rows;
}
export const QUERY_1 = 1;export const QUERY_1 = 1;
export const QUERY_2 = 2;export const QUERY_2 = 2;
src/ui/hooks/useSession.ts
+48 -31
import { useCallback, useEffect, useState } from 'react';import { useCallback, useEffect, useState } from 'react';
export function useSession() {
export function useSession() {
export function useSession() {export function useSession() {
return { session: null };
const [session, setSession] = useState(readStored());
return { session: null }; const [session, setSession] = useState(readStored());
}
}
const signIn = useCallback(async (form) => {
const res = await fetch('/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(form),
});
if (!res.ok) throw new Error('sign in failed');
const next = await res.json();
store(next);
setSession(next);
return next;
}, []);
const signOut = useCallback(async () => {
await fetch('/logout', { method: 'POST' });
store(null);
setSession(null);
}, []);
useEffect(() => {
if (!session) return undefined;
const timer = setInterval(() => {
fetch('/refresh', { method: 'POST' })
.then((res) => (res.ok ? res.json() : null))
.then((next) => next && setSession(next));
}, 5 * 60_000);
return () => clearInterval(timer);
}, [session]);
return { session, signIn, signOut };
}
export const USE_SESSION_1 = 1;export const USE_SESSION_1 = 1;
export const USE_SESSION_2 = 2;export const USE_SESSION_2 = 2;
export const USE_SESSION_24 = 24;export const USE_SESSION_24 = 24;
export const USE_SESSION_25 = 25;export const USE_SESSION_25 = 25;
function readStored() {
function readStored() {
return { user: localStorage.getItem('user') };
function readStored() {
return { user: localStorage.getItem('user') };function readStored() {
}
try {
} try {
return JSON.parse(sessionStorage.getItem('session') ?? 'null');
return JSON.parse(sessionStorage.getItem('session') ?? 'null');
function store(session) {
} catch {
function store(session) { } catch {
localStorage.setItem('user', session?.user ?? '');
return null;
localStorage.setItem('user', session?.user ?? ''); return null;
}
}
} }
}
}
export function useSessionLegacy1(input) {
export function useSessionLegacy1(input) {
// unused since the useSessionLegacy rewrite
function store(session) {
// unused since the useSessionLegacy rewritefunction store(session) {
return String(input ?? '').trim();
if (!session) return sessionStorage.removeItem('session');
return String(input ?? '').trim(); if (!session) return sessionStorage.removeItem('session');
}
return sessionStorage.setItem('session', JSON.stringify(session));
} return sessionStorage.setItem('session', JSON.stringify(session));
}
}
export function useSessionLegacy2(input) {
// unused since the useSessionLegacy rewrite
return String(input ?? '').trim();
}
export function useSessionLegacy3(input) {
// unused since the useSessionLegacy rewrite
return String(input ?? '').trim();
}
export function useSessionLegacy4(input) {
// unused since the useSessionLegacy rewrite
return String(input ?? '').trim();
}
src/ui/hooks/useLegacySession.ts
+0 -60
// Replaced by useSession.ts.
import { useState } from 'react';
export function useLegacySession() {
const [user, setUser] = useState(localStorage.getItem("user"));
function signIn(name) {
// no request: the client decided it was signed in
localStorage.setItem("user", name);
setUser(name);
}
function signOut() {
localStorage.removeItem("user");
setUser(null);
}
return { user, signIn, signOut };
}
export const LEGACY_HOOK_1 = 1;
export const LEGACY_HOOK_2 = 2;
export const LEGACY_HOOK_3 = 3;
export const LEGACY_HOOK_4 = 4;
export const LEGACY_HOOK_5 = 5;
export const LEGACY_HOOK_6 = 6;
export const LEGACY_HOOK_7 = 7;
export const LEGACY_HOOK_8 = 8;
export const LEGACY_HOOK_9 = 9;
export const LEGACY_HOOK_10 = 10;
export const LEGACY_HOOK_11 = 11;
export const LEGACY_HOOK_12 = 12;
export const LEGACY_HOOK_13 = 13;
export const LEGACY_HOOK_14 = 14;
export const LEGACY_HOOK_15 = 15;
export const LEGACY_HOOK_16 = 16;
export const LEGACY_HOOK_17 = 17;
export const LEGACY_HOOK_18 = 18;
export const LEGACY_HOOK_19 = 19;
export const LEGACY_HOOK_20 = 20;
export const LEGACY_HOOK_21 = 21;
export const LEGACY_HOOK_22 = 22;
export const LEGACY_HOOK_23 = 23;
export const LEGACY_HOOK_24 = 24;
export const LEGACY_HOOK_25 = 25;
export const LEGACY_HOOK_26 = 26;
export const LEGACY_HOOK_27 = 27;
export const LEGACY_HOOK_28 = 28;
export const LEGACY_HOOK_29 = 29;
export const LEGACY_HOOK_30 = 30;
export const LEGACY_HOOK_31 = 31;
export const LEGACY_HOOK_32 = 32;
export const LEGACY_HOOK_33 = 33;
export const LEGACY_HOOK_34 = 34;
export const LEGACY_HOOK_35 = 35;
export const LEGACY_HOOK_36 = 36;
export const LEGACY_HOOK_37 = 37;
export const LEGACY_HOOK_38 = 38;
export const LEGACY_HOOK_39 = 39;
export const LEGACY_HOOK_40 = 40;
tests/session.test.ts
+25 -8
import { createSession, isExpired, touch } from '../src/session.ts';import { createSession, isExpired, touch } from '../src/session.ts';
test('createSession accepts a missing user', async () => {
test('createSession requires a user', async () => {
test('createSession accepts a missing user', async () => {test('createSession requires a user', async () => {
assert.ok(createSession(null).id);
assert.throws(() => createSession(null), /user required/);
assert.ok(createSession(null).id); assert.throws(() => createSession(null), /user required/);
});
});
});});
test('isExpired is always false', async () => {
test('createSession starts without a token', async () => {
test('isExpired is always false', async () => {test('createSession starts without a token', async () => {
assert.equal(isExpired({}), false);
const session = createSession('ada');
assert.equal(isExpired({}), false); const session = createSession('ada');
});
assert.equal(session.token, null);
}); assert.equal(session.token, null);
assert.equal(typeof session.id, 'string');
assert.equal(typeof session.id, 'string');
});
test('isExpired treats a session without createdAt as expired', async () => {
assert.equal(isExpired({}), true);
});
test('isExpired uses the 12h window', async () => {
const now = 1_000_000_000_000;
assert.equal(isExpired({ createdAt: now - 1000 }, now), false);
assert.equal(isExpired({ createdAt: now - 13 * 3600 * 1000 }, now), true);
});
test('touch records activity', async () => {
const session = touch(createSession('ada'));
assert.equal(typeof session.touchedAt, 'number');
});
export const SESSION_TEST_PAD_1 = 1;export const SESSION_TEST_PAD_1 = 1;
export const SESSION_TEST_PAD_2 = 2;export const SESSION_TEST_PAD_2 = 2;

HTTP API surface

Router attaches sessions on login, handlers tighten JSON parsing and add an error boundary, and the server grows a middleware chain plus an SSE stream.

flowchart TB
  R[router] --> H[handlers]
  R --> S[session]
  R --> E[sse]
src/api/router.ts
+7 -1
// HTTP router for the demo review fixture.// HTTP router for the demo review fixture.
import { login } from '../auth.ts';import { login } from '../auth.ts';
import { createSession, attachToken } from '../session.ts';
export function mountRoutes(app) {export function mountRoutes(app) {
app.post('/login', async (req, res) => { app.post('/login', async (req, res) => {
const { user, password } = req.body ?? {}; const { user, password } = req.body ?? {};
const result = login(user, password); const result = login(user, password);
res.json({ token: result.token });
const session = attachToken(createSession(user), result.token);
res.json({ token: result.token }); const session = attachToken(createSession(user), result.token);
res.json({ sessionId: session.id, token: session.token });
}); });
export const ROUTE_75 = 75;export const ROUTE_75 = 75;
export function notFound(_req, res) {
res.status(404).json({ error: 'not found' });
}
src/api/handlers.ts
+13 -1
// Request handlers for the demo review fixture.// Request handlers for the demo review fixture.
export async function readJson(req) {export async function readJson(req) {
if (req.body && typeof req.body === 'object') return req.body;
const chunks = []; const chunks = [];
for await (const chunk of req) chunks.push(chunk); for await (const chunk of req) chunks.push(chunk);
const raw = Buffer.concat(chunks).toString('utf8'); const raw = Buffer.concat(chunks).toString('utf8');
return JSON.parse(raw || "{}");
if (!raw) return {};
return JSON.parse(raw || "{}"); if (!raw) return {};
return JSON.parse(raw);
}}
export const HANDLER_85 = 85;export const HANDLER_85 = 85;
export function withErrorBoundary(handler) {
return async (req, res) => {
try {
await handler(req, res);
} catch (err) {
sendJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
}
};
}
src/api/sessions.ts
+113 -35
import { logger, metrics } from '../../lib/logger.ts';import { logger, metrics } from '../../lib/logger.ts';
export function listSessions(req, res) {
export async function listSessions(req, res) {
export function listSessions(req, res) {export async function listSessions(req, res) {
// pre-0.5: no validation, no metrics, no session scoping
const started = Date.now();
// pre-0.5: no validation, no metrics, no session scoping const started = Date.now();
const rows = db.query('select * from ' + tableFor(name));
try {
const rows = db.query('select * from ' + tableFor(name)); try {
res.setHeader('Content-Type', 'application/json');
const body = await readJson(req);
res.setHeader('Content-Type', 'application/json'); const body = await readJson(req);
res.end(JSON.stringify(rows));
const result = await listSessionsImpl(body, req.session);
res.end(JSON.stringify(rows)); const result = await listSessionsImpl(body, req.session);
}
sendJson(res, 200, result);
} sendJson(res, 200, result);
} catch (err) {
} catch (err) {
export function getSession(req, res) {
logger.error(err, { route: 'GET /sessions' });
export function getSession(req, res) { logger.error(err, { route: 'GET /sessions' });
// pre-0.5: no validation, no metrics, no session scoping
sendJson(res, statusFor(err), { error: messageFor(err) });
// pre-0.5: no validation, no metrics, no session scoping sendJson(res, statusFor(err), { error: messageFor(err) });
const rows = db.query('select * from ' + tableFor(name));
} finally {
const rows = db.query('select * from ' + tableFor(name)); } finally {
res.setHeader('Content-Type', 'application/json');
metrics.observe('GET /sessions', Date.now() - started);
res.setHeader('Content-Type', 'application/json'); metrics.observe('GET /sessions', Date.now() - started);
res.end(JSON.stringify(rows));
}
res.end(JSON.stringify(rows)); }
}
}
}}
export function revokeSession(req, res) {
export async function getSession(req, res) {
export function revokeSession(req, res) {export async function getSession(req, res) {
// pre-0.5: no validation, no metrics, no session scoping
const started = Date.now();
// pre-0.5: no validation, no metrics, no session scoping const started = Date.now();
const rows = db.query('select * from ' + tableFor(name));
try {
const rows = db.query('select * from ' + tableFor(name)); try {
res.setHeader('Content-Type', 'application/json');
const body = await readJson(req);
res.setHeader('Content-Type', 'application/json'); const body = await readJson(req);
res.end(JSON.stringify(rows));
const result = await getSessionImpl(body, req.session);
res.end(JSON.stringify(rows)); const result = await getSessionImpl(body, req.session);
}
sendJson(res, 200, result);
} sendJson(res, 200, result);
} catch (err) {
} catch (err) {
logger.error(err, { route: 'GET /sessions/:id' });
sendJson(res, statusFor(err), { error: messageFor(err) });
} finally {
metrics.observe('GET /sessions/:id', Date.now() - started);
}
}
export async function revokeSession(req, res) {
const started = Date.now();
try {
const body = await readJson(req);
const result = await revokeSessionImpl(body, req.session);
sendJson(res, 200, result);
} catch (err) {
logger.error(err, { route: 'DELETE /sessions/:id' });
sendJson(res, statusFor(err), { error: messageFor(err) });
} finally {
metrics.observe('DELETE /sessions/:id', Date.now() - started);
}
}
export async function revokeAllSessions(req, res) {
const started = Date.now();
try {
const body = await readJson(req);
const result = await revokeAllSessionsImpl(body, req.session);
sendJson(res, 200, result);
} catch (err) {
logger.error(err, { route: 'DELETE /sessions' });
sendJson(res, statusFor(err), { error: messageFor(err) });
} finally {
metrics.observe('DELETE /sessions', Date.now() - started);
}
}
export async function rotateToken(req, res) {
const started = Date.now();
try {
const body = await readJson(req);
const result = await rotateTokenImpl(body, req.session);
sendJson(res, 200, result);
} catch (err) {
logger.error(err, { route: 'POST /refresh' });
sendJson(res, statusFor(err), { error: messageFor(err) });
} finally {
metrics.observe('POST /refresh', Date.now() - started);
}
}
export const SESSIONS_API_1 = 1;export const SESSIONS_API_1 = 1;
export const SESSIONS_API_2 = 2;export const SESSIONS_API_2 = 2;
export const SESSIONS_API_29 = 29;export const SESSIONS_API_29 = 29;
export const SESSIONS_API_30 = 30;export const SESSIONS_API_30 = 30;
export function listAuditEvents(req, res) {
export function listAuditEvents(req, res) {
// pre-0.5: no validation, no metrics, no session scoping
export async function listAuditEvents(req, res) {
// pre-0.5: no validation, no metrics, no session scopingexport async function listAuditEvents(req, res) {
const rows = db.query('select * from ' + tableFor(name));
const started = Date.now();
const rows = db.query('select * from ' + tableFor(name)); const started = Date.now();
res.setHeader('Content-Type', 'application/json');
try {
res.setHeader('Content-Type', 'application/json'); try {
res.end(JSON.stringify(rows));
const body = await readJson(req);
res.end(JSON.stringify(rows)); const body = await readJson(req);
}
const result = await listAuditEventsImpl(body, req.session);
} const result = await listAuditEventsImpl(body, req.session);
sendJson(res, 200, result);
sendJson(res, 200, result);
export function auditCsv(req, res) {
} catch (err) {
export function auditCsv(req, res) { } catch (err) {
// pre-0.5: no validation, no metrics, no session scoping
logger.error(err, { route: 'GET /audit' });
// pre-0.5: no validation, no metrics, no session scoping logger.error(err, { route: 'GET /audit' });
const rows = db.query('select * from ' + tableFor(name));
sendJson(res, statusFor(err), { error: messageFor(err) });
const rows = db.query('select * from ' + tableFor(name)); sendJson(res, statusFor(err), { error: messageFor(err) });
res.setHeader('Content-Type', 'application/json');
} finally {
res.setHeader('Content-Type', 'application/json'); } finally {
res.end(JSON.stringify(rows));
metrics.observe('GET /audit', Date.now() - started);
res.end(JSON.stringify(rows)); metrics.observe('GET /audit', Date.now() - started);
}
}
} }
}
}
export async function getAuditEvent(req, res) {
const started = Date.now();
try {
const body = await readJson(req);
const result = await getAuditEventImpl(body, req.session);
sendJson(res, 200, result);
} catch (err) {
logger.error(err, { route: 'GET /audit/:id' });
sendJson(res, statusFor(err), { error: messageFor(err) });
} finally {
metrics.observe('GET /audit/:id', Date.now() - started);
}
}
export async function exportAudit(req, res) {
const started = Date.now();
try {
const body = await readJson(req);
const result = await exportAuditImpl(body, req.session);
sendJson(res, 200, result);
} catch (err) {
logger.error(err, { route: 'POST /audit/export' });
sendJson(res, statusFor(err), { error: messageFor(err) });
} finally {
metrics.observe('POST /audit/export', Date.now() - started);
}
}
src/api/users.ts
+48 -14
import { logger, metrics } from '../../lib/logger.ts';import { logger, metrics } from '../../lib/logger.ts';
export function listUsers(req, res) {
export async function listUsers(req, res) {
export function listUsers(req, res) {export async function listUsers(req, res) {
// pre-0.5: no validation, no metrics, no session scoping
const started = Date.now();
// pre-0.5: no validation, no metrics, no session scoping const started = Date.now();
const rows = db.query('select * from ' + tableFor(name));
try {
const rows = db.query('select * from ' + tableFor(name)); try {
res.setHeader('Content-Type', 'application/json');
const body = await readJson(req);
res.setHeader('Content-Type', 'application/json'); const body = await readJson(req);
res.end(JSON.stringify(rows));
const result = await listUsersImpl(body, req.session);
res.end(JSON.stringify(rows)); const result = await listUsersImpl(body, req.session);
}
sendJson(res, 200, result);
} sendJson(res, 200, result);
} catch (err) {
} catch (err) {
export function setUserRole(req, res) {
logger.error(err, { route: 'GET /users' });
export function setUserRole(req, res) { logger.error(err, { route: 'GET /users' });
// pre-0.5: no validation, no metrics, no session scoping
sendJson(res, statusFor(err), { error: messageFor(err) });
// pre-0.5: no validation, no metrics, no session scoping sendJson(res, statusFor(err), { error: messageFor(err) });
const rows = db.query('select * from ' + tableFor(name));
} finally {
const rows = db.query('select * from ' + tableFor(name)); } finally {
res.setHeader('Content-Type', 'application/json');
metrics.observe('GET /users', Date.now() - started);
res.setHeader('Content-Type', 'application/json'); metrics.observe('GET /users', Date.now() - started);
res.end(JSON.stringify(rows));
}
res.end(JSON.stringify(rows)); }
}
}
}}
export async function getUser(req, res) {
const started = Date.now();
try {
const body = await readJson(req);
const result = await getUserImpl(body, req.session);
sendJson(res, 200, result);
} catch (err) {
logger.error(err, { route: 'GET /users/:id' });
sendJson(res, statusFor(err), { error: messageFor(err) });
} finally {
metrics.observe('GET /users/:id', Date.now() - started);
}
}
export async function setUserRoles(req, res) {
const started = Date.now();
try {
const body = await readJson(req);
const result = await setUserRolesImpl(body, req.session);
sendJson(res, 200, result);
} catch (err) {
logger.error(err, { route: 'PATCH /users/:id/roles' });
sendJson(res, statusFor(err), { error: messageFor(err) });
} finally {
metrics.observe('PATCH /users/:id/roles', Date.now() - started);
}
}
export const USERS_API_1 = 1;export const USERS_API_1 = 1;
export const USERS_API_2 = 2;export const USERS_API_2 = 2;
export const USERS_API_24 = 24;export const USERS_API_24 = 24;
export const USERS_API_25 = 25;export const USERS_API_25 = 25;
export const guards = {
listUsers: requireRole('admin'),
getUser: requireRole('admin'),
setUserRoles: requireRole('admin'),
};
src/api/validation.ts
+202 -70
// Request schemas, one per endpoint, checked before any handler runs.// Request schemas, one per endpoint, checked before any handler runs.
export function check_listSessions(req) {
export const listSessionsSchema = {
export function check_listSessions(req) {export const listSessionsSchema = {
if (req.method !== 'GET') return false;
method: 'GET',
if (req.method !== 'GET') return false; method: 'GET',
return true; // params and query went unchecked
path: '/sessions',
return true; // params and query went unchecked path: '/sessions',
}
role: 'reviewer',
} role: 'reviewer',
params: {
params: {
export function check_getSession(req) {
id: { type: 'uuid', required: false },
export function check_getSession(req) { id: { type: 'uuid', required: false },
if (req.method !== 'GET') return false;
},
if (req.method !== 'GET') return false; },
return true; // params and query went unchecked
query: {
return true; // params and query went unchecked query: {
}
first: { type: 'int', min: 1, max: 200, default: 20 },
} first: { type: 'int', min: 1, max: 200, default: 20 },
cursor: { type: 'string', required: false },
cursor: { type: 'string', required: false },
export function check_revokeSession(req) {
},
export function check_revokeSession(req) { },
if (req.method !== 'DELETE') return false;
};
if (req.method !== 'DELETE') return false;};
return true; // params and query went unchecked
return true; // params and query went unchecked
}
export const getSessionSchema = {
}export const getSessionSchema = {
method: 'GET',
method: 'GET',
export function check_revokeAllSessions(req) {
path: '/sessions/:id',
export function check_revokeAllSessions(req) { path: '/sessions/:id',
if (req.method !== 'DELETE') return false;
role: 'reviewer',
if (req.method !== 'DELETE') return false; role: 'reviewer',
return true; // params and query went unchecked
params: {
return true; // params and query went unchecked params: {
}
id: { type: 'uuid', required: true },
} id: { type: 'uuid', required: true },
},
},
export function check_rotateToken(req) {
query: {
export function check_rotateToken(req) { query: {
if (req.method !== 'POST') return false;
first: { type: 'int', min: 1, max: 200, default: 20 },
if (req.method !== 'POST') return false; first: { type: 'int', min: 1, max: 200, default: 20 },
return true; // params and query went unchecked
cursor: { type: 'string', required: false },
return true; // params and query went unchecked cursor: { type: 'string', required: false },
}
},
} },
};
};
export function check_listAuditEvents(req) {
export function check_listAuditEvents(req) {
if (req.method !== 'GET') return false;
export const revokeSessionSchema = {
if (req.method !== 'GET') return false;export const revokeSessionSchema = {
return true; // params and query went unchecked
method: 'DELETE',
return true; // params and query went unchecked method: 'DELETE',
}
path: '/sessions/:id',
} path: '/sessions/:id',
role: 'reviewer',
role: 'reviewer',
export function check_getAuditEvent(req) {
params: {
export function check_getAuditEvent(req) { params: {
if (req.method !== 'GET') return false;
id: { type: 'uuid', required: true },
if (req.method !== 'GET') return false; id: { type: 'uuid', required: true },
return true; // params and query went unchecked
},
return true; // params and query went unchecked },
}
query: {
} query: {
first: { type: 'int', min: 1, max: 200, default: 20 },
first: { type: 'int', min: 1, max: 200, default: 20 },
export function check_exportAudit(req) {
cursor: { type: 'string', required: false },
export function check_exportAudit(req) { cursor: { type: 'string', required: false },
if (req.method !== 'POST') return false;
},
if (req.method !== 'POST') return false; },
return true; // params and query went unchecked
};
return true; // params and query went unchecked};
}
}
export const revokeAllSessionsSchema = {
export const revokeAllSessionsSchema = {
export function check_listUsers(req) {
method: 'DELETE',
export function check_listUsers(req) { method: 'DELETE',
if (req.method !== 'GET') return false;
path: '/sessions',
if (req.method !== 'GET') return false; path: '/sessions',
return true; // params and query went unchecked
role: 'admin',
return true; // params and query went unchecked role: 'admin',
}
params: {
} params: {
id: { type: 'uuid', required: false },
id: { type: 'uuid', required: false },
export function check_getUser(req) {
},
export function check_getUser(req) { },
if (req.method !== 'GET') return false;
query: {
if (req.method !== 'GET') return false; query: {
return true; // params and query went unchecked
first: { type: 'int', min: 1, max: 200, default: 20 },
return true; // params and query went unchecked first: { type: 'int', min: 1, max: 200, default: 20 },
}
cursor: { type: 'string', required: false },
} cursor: { type: 'string', required: false },
},
},
export function check_setUserRoles(req) {
};
export function check_setUserRoles(req) {};
if (req.method !== 'PATCH') return false;
if (req.method !== 'PATCH') return false;
return true; // params and query went unchecked
export const rotateTokenSchema = {
return true; // params and query went uncheckedexport const rotateTokenSchema = {
}
method: 'POST',
} method: 'POST',
path: '/refresh',
path: '/refresh',
export function check_health(req) {
role: 'reviewer',
export function check_health(req) { role: 'reviewer',
if (req.method !== 'GET') return false;
params: {
if (req.method !== 'GET') return false; params: {
return true; // params and query went unchecked
id: { type: 'uuid', required: false },
return true; // params and query went unchecked id: { type: 'uuid', required: false },
}
},
} },
query: {
query: {
export function check_readiness(req) {
first: { type: 'int', min: 1, max: 200, default: 20 },
export function check_readiness(req) { first: { type: 'int', min: 1, max: 200, default: 20 },
if (req.method !== 'GET') return false;
cursor: { type: 'string', required: false },
if (req.method !== 'GET') return false; cursor: { type: 'string', required: false },
return true; // params and query went unchecked
},
return true; // params and query went unchecked },
}
};
}};
export function check_metrics(req) {
export const listAuditEventsSchema = {
export function check_metrics(req) {export const listAuditEventsSchema = {
if (req.method !== 'GET') return false;
method: 'GET',
if (req.method !== 'GET') return false; method: 'GET',
return true; // params and query went unchecked
path: '/audit',
return true; // params and query went unchecked path: '/audit',
}
role: 'admin',
} role: 'admin',
params: {
params: {
id: { type: 'uuid', required: false },
},
query: {
first: { type: 'int', min: 1, max: 200, default: 20 },
cursor: { type: 'string', required: false },
},
};
export const getAuditEventSchema = {
method: 'GET',
path: '/audit/:id',
role: 'admin',
params: {
id: { type: 'uuid', required: true },
},
query: {
first: { type: 'int', min: 1, max: 200, default: 20 },
cursor: { type: 'string', required: false },
},
};
export const exportAuditSchema = {
method: 'POST',
path: '/audit/export',
role: 'admin',
params: {
id: { type: 'uuid', required: false },
},
query: {
first: { type: 'int', min: 1, max: 200, default: 20 },
cursor: { type: 'string', required: false },
},
};
export const listUsersSchema = {
method: 'GET',
path: '/users',
role: 'admin',
params: {
id: { type: 'uuid', required: false },
},
query: {
first: { type: 'int', min: 1, max: 200, default: 20 },
cursor: { type: 'string', required: false },
},
};
export const getUserSchema = {
method: 'GET',
path: '/users/:id',
role: 'admin',
params: {
id: { type: 'uuid', required: true },
},
query: {
first: { type: 'int', min: 1, max: 200, default: 20 },
cursor: { type: 'string', required: false },
},
};
export const setUserRolesSchema = {
method: 'PATCH',
path: '/users/:id/roles',
role: 'admin',
params: {
id: { type: 'uuid', required: true },
},
query: {
first: { type: 'int', min: 1, max: 200, default: 20 },
cursor: { type: 'string', required: false },
},
};
export const healthSchema = {
method: 'GET',
path: '/health',
role: 'public',
params: {
id: { type: 'uuid', required: false },
},
query: {
first: { type: 'int', min: 1, max: 200, default: 20 },
cursor: { type: 'string', required: false },
},
};
export const readinessSchema = {
method: 'GET',
path: '/ready',
role: 'public',
params: {
id: { type: 'uuid', required: false },
},
query: {
first: { type: 'int', min: 1, max: 200, default: 20 },
cursor: { type: 'string', required: false },
},
};
export const metricsSchema = {
method: 'GET',
path: '/metrics',
role: 'admin',
params: {
id: { type: 'uuid', required: false },
},
query: {
first: { type: 'int', min: 1, max: 200, default: 20 },
cursor: { type: 'string', required: false },
},
};
export const VALIDATION_1 = 1;export const VALIDATION_1 = 1;
export const VALIDATION_2 = 2;export const VALIDATION_2 = 2;
export const VALIDATION_19 = 19;export const VALIDATION_19 = 19;
export const VALIDATION_20 = 20;export const VALIDATION_20 = 20;
export function validateRequest(schema, req) {
const problems = [];
for (const [name, rule] of Object.entries(schema.params)) {
if (rule.required && !req.params?.[name]) problems.push(`missing param: ${name}`);
}
for (const [name, rule] of Object.entries(schema.query)) {
const raw = req.query?.[name];
if (raw === undefined) continue;
if (rule.type === 'int' && Number.isNaN(Number.parseInt(raw, 10))) {
problems.push(`invalid int: ${name}`);
}
}
if (problems.length > 0) {
const err = new Error(problems.join('; '));
err.code = 'BAD_REQUEST';
throw err;
}
return true;
}
src/api/openapi.ts
+173 -42
info: { title: 'demo-service', version: '0.5.0' }, info: { title: 'demo-service', version: '0.5.0' },
paths: { paths: {
'/sessions': {
'/sessions': {
'/sessions': { '/sessions': {
get: { operationId: 'listSessions' },
get: {
get: { operationId: 'listSessions' }, get: {
},
operationId: 'listSessions',
}, operationId: 'listSessions',
'/sessions/:id': {
summary: 'list sessions',
'/sessions/:id': { summary: 'list sessions',
get: { operationId: 'getSession' },
security: [{ bearer: [] }],
get: { operationId: 'getSession' }, security: [{ bearer: [] }],
},
responses: {
}, responses: {
'/sessions/:id': {
200: { description: 'ok' },
'/sessions/:id': { 200: { description: 'ok' },
delete: { operationId: 'revokeSession' },
401: { description: 'unauthorized' },
delete: { operationId: 'revokeSession' }, 401: { description: 'unauthorized' },
},
429: { description: 'rate limited' },
}, 429: { description: 'rate limited' },
'/sessions': {
},
'/sessions': { },
delete: { operationId: 'revokeAllSessions' },
},
delete: { operationId: 'revokeAllSessions' }, },
},
},
}, },
'/refresh': {
'/sessions/:id': {
'/refresh': { '/sessions/:id': {
post: { operationId: 'rotateToken' },
get: {
post: { operationId: 'rotateToken' }, get: {
},
operationId: 'getSession',
}, operationId: 'getSession',
'/audit': {
summary: 'get session',
'/audit': { summary: 'get session',
get: { operationId: 'listAuditEvents' },
security: [{ bearer: [] }],
get: { operationId: 'listAuditEvents' }, security: [{ bearer: [] }],
},
responses: {
}, responses: {
'/audit/:id': {
200: { description: 'ok' },
'/audit/:id': { 200: { description: 'ok' },
get: { operationId: 'getAuditEvent' },
401: { description: 'unauthorized' },
get: { operationId: 'getAuditEvent' }, 401: { description: 'unauthorized' },
},
429: { description: 'rate limited' },
}, 429: { description: 'rate limited' },
'/audit/export': {
},
'/audit/export': { },
post: { operationId: 'exportAudit' },
},
post: { operationId: 'exportAudit' }, },
},
},
}, },
'/users': {
'/sessions/:id': {
'/users': { '/sessions/:id': {
get: { operationId: 'listUsers' },
delete: {
get: { operationId: 'listUsers' }, delete: {
},
operationId: 'revokeSession',
}, operationId: 'revokeSession',
'/users/:id': {
summary: 'revoke session',
'/users/:id': { summary: 'revoke session',
get: { operationId: 'getUser' },
security: [{ bearer: [] }],
get: { operationId: 'getUser' }, security: [{ bearer: [] }],
},
responses: {
}, responses: {
'/users/:id/roles': {
200: { description: 'ok' },
'/users/:id/roles': { 200: { description: 'ok' },
patch: { operationId: 'setUserRoles' },
401: { description: 'unauthorized' },
patch: { operationId: 'setUserRoles' }, 401: { description: 'unauthorized' },
},
429: { description: 'rate limited' },
}, 429: { description: 'rate limited' },
'/health': {
},
'/health': { },
get: { operationId: 'health' },
},
get: { operationId: 'health' }, },
},
},
}, },
'/ready': {
'/sessions': {
'/ready': { '/sessions': {
get: { operationId: 'readiness' },
delete: {
get: { operationId: 'readiness' }, delete: {
},
operationId: 'revokeAllSessions',
}, operationId: 'revokeAllSessions',
'/metrics': {
summary: 'revoke all sessions',
'/metrics': { summary: 'revoke all sessions',
get: { operationId: 'metrics' },
security: [{ bearer: [] }],
get: { operationId: 'metrics' }, security: [{ bearer: [] }],
},
responses: {
}, responses: {
200: { description: 'ok' },
401: { description: 'unauthorized' },
429: { description: 'rate limited' },
},
},
},
'/refresh': {
post: {
operationId: 'rotateToken',
summary: 'rotate token',
security: [{ bearer: [] }],
responses: {
200: { description: 'ok' },
401: { description: 'unauthorized' },
429: { description: 'rate limited' },
},
},
},
'/audit': {
get: {
operationId: 'listAuditEvents',
summary: 'list audit events',
security: [{ bearer: [] }],
responses: {
200: { description: 'ok' },
401: { description: 'unauthorized' },
429: { description: 'rate limited' },
},
},
},
'/audit/:id': {
get: {
operationId: 'getAuditEvent',
summary: 'get audit event',
security: [{ bearer: [] }],
responses: {
200: { description: 'ok' },
401: { description: 'unauthorized' },
429: { description: 'rate limited' },
},
},
},
'/audit/export': {
post: {
operationId: 'exportAudit',
summary: 'export audit',
security: [{ bearer: [] }],
responses: {
200: { description: 'ok' },
401: { description: 'unauthorized' },
429: { description: 'rate limited' },
},
},
},
'/users': {
get: {
operationId: 'listUsers',
summary: 'list users',
security: [{ bearer: [] }],
responses: {
200: { description: 'ok' },
401: { description: 'unauthorized' },
429: { description: 'rate limited' },
},
},
},
'/users/:id': {
get: {
operationId: 'getUser',
summary: 'get user',
security: [{ bearer: [] }],
responses: {
200: { description: 'ok' },
401: { description: 'unauthorized' },
429: { description: 'rate limited' },
},
},
},
'/users/:id/roles': {
patch: {
operationId: 'setUserRoles',
summary: 'set user roles',
security: [{ bearer: [] }],
responses: {
200: { description: 'ok' },
401: { description: 'unauthorized' },
429: { description: 'rate limited' },
},
},
},
'/health': {
get: {
operationId: 'health',
summary: 'health',
security: [],
responses: {
200: { description: 'ok' },
401: { description: 'unauthorized' },
429: { description: 'rate limited' },
},
},
},
'/ready': {
get: {
operationId: 'readiness',
summary: 'readiness',
security: [],
responses: {
200: { description: 'ok' },
401: { description: 'unauthorized' },
429: { description: 'rate limited' },
},
},
},
'/metrics': {
get: {
operationId: 'metrics',
summary: 'metrics',
security: [{ bearer: [] }],
responses: {
200: { description: 'ok' },
401: { description: 'unauthorized' },
429: { description: 'rate limited' },
},
},
},
}, },
components: { components: {
}, },
}, },
components: {
securitySchemes: {
bearer: { type: 'http', scheme: 'bearer' },
},
},
};};
src/api/routes.ts
+96 -14
export const routes = [export const routes = [
{ method: 'GET', path: '/sessions', handler: handlers.listSessions },
{
{ method: 'GET', path: '/sessions', handler: handlers.listSessions }, {
{ method: 'GET', path: '/sessions/:id', handler: handlers.getSession },
method: 'GET',
{ method: 'GET', path: '/sessions/:id', handler: handlers.getSession }, method: 'GET',
{ method: 'DELETE', path: '/sessions/:id', handler: handlers.revokeSession },
path: '/sessions',
{ method: 'DELETE', path: '/sessions/:id', handler: handlers.revokeSession }, path: '/sessions',
{ method: 'DELETE', path: '/sessions', handler: handlers.revokeAllSessions },
schema: schemas.listSessionsSchema,
{ method: 'DELETE', path: '/sessions', handler: handlers.revokeAllSessions }, schema: schemas.listSessionsSchema,
{ method: 'POST', path: '/refresh', handler: handlers.rotateToken },
guard: requireRole('reviewer'),
{ method: 'POST', path: '/refresh', handler: handlers.rotateToken }, guard: requireRole('reviewer'),
{ method: 'GET', path: '/audit', handler: handlers.listAuditEvents },
},
{ method: 'GET', path: '/audit', handler: handlers.listAuditEvents }, },
{ method: 'GET', path: '/audit/:id', handler: handlers.getAuditEvent },
{
{ method: 'GET', path: '/audit/:id', handler: handlers.getAuditEvent }, {
{ method: 'POST', path: '/audit/export', handler: handlers.exportAudit },
method: 'GET',
{ method: 'POST', path: '/audit/export', handler: handlers.exportAudit }, method: 'GET',
{ method: 'GET', path: '/users', handler: handlers.listUsers },
path: '/sessions/:id',
{ method: 'GET', path: '/users', handler: handlers.listUsers }, path: '/sessions/:id',
{ method: 'GET', path: '/users/:id', handler: handlers.getUser },
schema: schemas.getSessionSchema,
{ method: 'GET', path: '/users/:id', handler: handlers.getUser }, schema: schemas.getSessionSchema,
{ method: 'PATCH', path: '/users/:id/roles', handler: handlers.setUserRoles },
guard: requireRole('reviewer'),
{ method: 'PATCH', path: '/users/:id/roles', handler: handlers.setUserRoles }, guard: requireRole('reviewer'),
{ method: 'GET', path: '/health', handler: handlers.health },
},
{ method: 'GET', path: '/health', handler: handlers.health }, },
{ method: 'GET', path: '/ready', handler: handlers.readiness },
{
{ method: 'GET', path: '/ready', handler: handlers.readiness }, {
{ method: 'GET', path: '/metrics', handler: handlers.metrics },
method: 'DELETE',
{ method: 'GET', path: '/metrics', handler: handlers.metrics }, method: 'DELETE',
path: '/sessions/:id',
schema: schemas.revokeSessionSchema,
guard: requireRole('reviewer'),
},
{
method: 'DELETE',
path: '/sessions',
schema: schemas.revokeAllSessionsSchema,
guard: requireRole('admin'),
},
{
method: 'POST',
path: '/refresh',
schema: schemas.rotateTokenSchema,
guard: requireRole('reviewer'),
},
{
method: 'GET',
path: '/audit',
schema: schemas.listAuditEventsSchema,
guard: requireRole('admin'),
},
{
method: 'GET',
path: '/audit/:id',
schema: schemas.getAuditEventSchema,
guard: requireRole('admin'),
},
{
method: 'POST',
path: '/audit/export',
schema: schemas.exportAuditSchema,
guard: requireRole('admin'),
},
{
method: 'GET',
path: '/users',
schema: schemas.listUsersSchema,
guard: requireRole('admin'),
},
{
method: 'GET',
path: '/users/:id',
schema: schemas.getUserSchema,
guard: requireRole('admin'),
},
{
method: 'PATCH',
path: '/users/:id/roles',
schema: schemas.setUserRolesSchema,
guard: requireRole('admin'),
},
{
method: 'GET',
path: '/health',
schema: schemas.healthSchema,
guard: null,
},
{
method: 'GET',
path: '/ready',
schema: schemas.readinessSchema,
guard: null,
},
{
method: 'GET',
path: '/metrics',
schema: schemas.metricsSchema,
guard: requireRole('admin'),
},
];];
];];
export function matchRoute(method, path) {
return routes.find((route) => route.method === method && samePath(route.path, path)) ?? null;
}
export function samePath(pattern, path) {
const left = pattern.split('/');
const right = path.split('/');
if (left.length !== right.length) return false;
return left.every((part, i) => part.startsWith(':') || part === right[i]);
}
export const handlers = { ...sessions, ...users };
src/api/errors.ts
+24 -6
// Error codes shared by handlers and the client.// Error codes shared by handlers and the client.
// every caller threw bare strings, so the status mapping lived in the handlers
export class ApiError extends Error {
// every caller threw bare strings, so the status mapping lived in the handlersexport class ApiError extends Error {
export const UNAUTHORIZED = 'unauthorized';
constructor(code, message, details = null) {
export const UNAUTHORIZED = 'unauthorized'; constructor(code, message, details = null) {
export const FORBIDDEN = 'forbidden';
super(message);
export const FORBIDDEN = 'forbidden'; super(message);
export const NOT_FOUND = 'not found';
this.code = code;
export const NOT_FOUND = 'not found'; this.code = code;
export const RATE_LIMITED = 'too many requests';
this.details = details;
export const RATE_LIMITED = 'too many requests'; this.details = details;
}
}
}
export function unauthorized(message = 'unauthorized') {
return new ApiError('UNAUTHORIZED', message);
}
export function forbidden(message = 'forbidden') {
return new ApiError('FORBIDDEN', message);
}
export function notFound(what) {
return new ApiError('NOT_FOUND', what + ' not found');
}
export function rateLimited(retryAfterSeconds) {
return new ApiError('RATE_LIMITED', 'too many requests', { retryAfterSeconds });
}
export const ERRORS_1 = 1;export const ERRORS_1 = 1;
export const ERRORS_2 = 2;export const ERRORS_2 = 2;
src/api/health.ts
+16 -0
// Liveness and readiness, split so a draining pod still answers /health.
import { withClient } from '../db/pool.ts';
import { sendJson } from './handlers.ts';
export function health(_req, res) {
return sendJson(res, 200, { ok: true, uptimeSeconds: Math.round(process.uptime()) });
}
export async function readiness(req, res) {
try {
await withClient(req.pool, (client) => client.query('select 1'));
return sendJson(res, 200, { ok: true });
} catch (err) {
return sendJson(res, 503, { ok: false, error: String(err) });
}
}
src/api/debug.ts
+0 -21
// Debug helpers kept for local development.// Debug helpers kept for local development.
export function dumpSessions(_req, res) {
// unauthenticated: it listed every session in the database
return sendJson(res, 200, allSessions());
}
export function dumpEnv(_req, res) {
// this printed DATABASE_URL
return sendJson(res, 200, process.env);
}
export function forceExpire(req, res) {
expireAll();
return sendJson(res, 200, { ok: true });
}
export function mountDebugRoutes(app) {
app.get('/debug/sessions', dumpSessions);
app.get('/debug/env', dumpEnv);
app.post('/debug/expire', forceExpire);
}
export function pretty(value) {export function pretty(value) {
return JSON.stringify(value, null, 2); return JSON.stringify(value, null, 2);
src/api/legacyRoutes.ts
+0 -72
// Superseded by routes.ts + validation.ts.
export function mountLegacyRoutes(app) {
app.get('/session', (req, res) => res.json({ user: req.query.user }));
app.post('/session', (req, res) => res.json({ token: 'demo-token-' + req.body.user }));
app.get('/sessions/all', (_req, res) => res.json(allSessions()));
app.post('/expire', (_req, res) => { expireAll(); res.json({ ok: true }); });
}
export function allSessions() {
return Object.values(store);
}
export function expireAll() {
for (const key of Object.keys(store)) delete store[key];
}
export const LEGACY_ROUTE_1 = 1;
export const LEGACY_ROUTE_2 = 2;
export const LEGACY_ROUTE_3 = 3;
export const LEGACY_ROUTE_4 = 4;
export const LEGACY_ROUTE_5 = 5;
export const LEGACY_ROUTE_6 = 6;
export const LEGACY_ROUTE_7 = 7;
export const LEGACY_ROUTE_8 = 8;
export const LEGACY_ROUTE_9 = 9;
export const LEGACY_ROUTE_10 = 10;
export const LEGACY_ROUTE_11 = 11;
export const LEGACY_ROUTE_12 = 12;
export const LEGACY_ROUTE_13 = 13;
export const LEGACY_ROUTE_14 = 14;
export const LEGACY_ROUTE_15 = 15;
export const LEGACY_ROUTE_16 = 16;
export const LEGACY_ROUTE_17 = 17;
export const LEGACY_ROUTE_18 = 18;
export const LEGACY_ROUTE_19 = 19;
export const LEGACY_ROUTE_20 = 20;
export const LEGACY_ROUTE_21 = 21;
export const LEGACY_ROUTE_22 = 22;
export const LEGACY_ROUTE_23 = 23;
export const LEGACY_ROUTE_24 = 24;
export const LEGACY_ROUTE_25 = 25;
export const LEGACY_ROUTE_26 = 26;
export const LEGACY_ROUTE_27 = 27;
export const LEGACY_ROUTE_28 = 28;
export const LEGACY_ROUTE_29 = 29;
export const LEGACY_ROUTE_30 = 30;
export const LEGACY_ROUTE_31 = 31;
export const LEGACY_ROUTE_32 = 32;
export const LEGACY_ROUTE_33 = 33;
export const LEGACY_ROUTE_34 = 34;
export const LEGACY_ROUTE_35 = 35;
export const LEGACY_ROUTE_36 = 36;
export const LEGACY_ROUTE_37 = 37;
export const LEGACY_ROUTE_38 = 38;
export const LEGACY_ROUTE_39 = 39;
export const LEGACY_ROUTE_40 = 40;
export const LEGACY_ROUTE_41 = 41;
export const LEGACY_ROUTE_42 = 42;
export const LEGACY_ROUTE_43 = 43;
export const LEGACY_ROUTE_44 = 44;
export const LEGACY_ROUTE_45 = 45;
export const LEGACY_ROUTE_46 = 46;
export const LEGACY_ROUTE_47 = 47;
export const LEGACY_ROUTE_48 = 48;
export const LEGACY_ROUTE_49 = 49;
export const LEGACY_ROUTE_50 = 50;
export const LEGACY_ROUTE_51 = 51;
export const LEGACY_ROUTE_52 = 52;
export const LEGACY_ROUTE_53 = 53;
export const LEGACY_ROUTE_54 = 54;
export const LEGACY_ROUTE_55 = 55;
src/api/middleware/cors.ts
+21 -1
import { config } from '../../config.ts';import { config } from '../../config.ts';
res.setHeader('Access-Control-Allow-Origin', '*');
export function cors(req, res, next) {
res.setHeader('Access-Control-Allow-Origin', '*');export function cors(req, res, next) {
const origin = req.headers.origin ?? '';
if (config.allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');
}
res.setHeader('Vary', 'Origin');
if (req.method === 'OPTIONS') {
res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PATCH,DELETE');
res.setHeader('Access-Control-Allow-Headers', 'content-type,authorization');
res.statusCode = 204;
return res.end();
}
return next();
}
export const CORS_1 = 1;export const CORS_1 = 1;
export const CORS_2 = 2;export const CORS_2 = 2;
export const CORS_29 = 29;export const CORS_29 = 29;
export const CORS_30 = 30;export const CORS_30 = 30;
export function preflightCache(res, seconds) {
res.setHeader('Access-Control-Max-Age', String(seconds));
return res;
}
src/api/middleware/requestId.ts
+13 -0
// Stamps every request with an id so logs, audit rows and the client agree.
import { randomUUID } from 'node:crypto';
export function requestId(req, res, next) {
const incoming = req.headers['x-request-id'];
req.id = typeof incoming === 'string' && incoming.length <= 64 ? incoming : randomUUID();
res.setHeader('X-Request-Id', req.id);
return next();
}
export function withRequestId(fields, req) {
return { ...fields, requestId: req.id };
}
src/server/http.ts
+52 -30
import { logger } from '../../lib/logger.ts';import { logger } from '../../lib/logger.ts';
export function createApp(deps) {
export function createApp(deps) {
export function createApp(deps) {export function createApp(deps) {
return mountRoutes(deps);
const middleware = [cors, rateLimit({ cost: 1 })];
return mountRoutes(deps); const middleware = [cors, rateLimit({ cost: 1 })];
}
const routes = mountRoutes(deps);
} const routes = mountRoutes(deps);
return async (req, res) => {
return async (req, res) => {
// no middleware chain yet
let index = 0;
// no middleware chain yet let index = 0;
const next = () => {
const layer = middleware[index++];
if (!layer) return routes(req, res);
return layer(req, res, next);
};
try {
await next();
} catch (err) {
logger.error(err, { url: req.url });
sendJson(res, 500, { error: 'internal error' });
}
};
}
export const HTTP_1 = 1;export const HTTP_1 = 1;
export const HTTP_2 = 2;export const HTTP_2 = 2;
export const HTTP_39 = 39;export const HTTP_39 = 39;
export const HTTP_40 = 40;export const HTTP_40 = 40;
export async function listen(deps, port = loadConfig().port) {
const server = createServer(createApp(deps));
await new Promise((resolve) => server.listen(port, resolve));
logger.info('listening', { port });
return {
port,
close: () => new Promise((resolve) => server.close(resolve)),
};
}
export function shutdownOn(signals, stop) {
for (const signal of signals) {
process.once(signal, async () => {
logger.info('shutting down', { signal });
await stop();
process.exit(0);
});
}
}
export const HTTP_41 = 41;export const HTTP_41 = 41;
export const HTTP_42 = 42;export const HTTP_42 = 42;
export const HTTP_99 = 99;export const HTTP_99 = 99;
export const HTTP_100 = 100;export const HTTP_100 = 100;
export function statusFor(err) {
export function statusFor(err) {
// one branch, so a rate-limited caller saw a 500
export function statusFor(err) {
// one branch, so a rate-limited caller saw a 500export function statusFor(err) {
return err?.message === 'unauthorized' ? 401 : 500;
if (err?.code === 'UNAUTHORIZED') return 401;
return err?.message === 'unauthorized' ? 401 : 500; if (err?.code === 'UNAUTHORIZED') return 401;
}
if (err?.code === 'FORBIDDEN') return 403;
} if (err?.code === 'FORBIDDEN') return 403;
if (err?.code === 'NOT_FOUND') return 404;
if (err?.code === 'NOT_FOUND') return 404;
export function httpLegacy1(input) {
if (err?.code === 'RATE_LIMITED') return 429;
export function httpLegacy1(input) { if (err?.code === 'RATE_LIMITED') return 429;
// unused since the httpLegacy rewrite
return 500;
// unused since the httpLegacy rewrite return 500;
return String(input ?? '').trim();
}
return String(input ?? '').trim();}
}
}
export function messageFor(err) {
export function messageFor(err) {
export function httpLegacy2(input) {
return err instanceof Error ? err.message : String(err);
export function httpLegacy2(input) { return err instanceof Error ? err.message : String(err);
// unused since the httpLegacy rewrite
}
// unused since the httpLegacy rewrite}
return String(input ?? '').trim();
}
export function httpLegacy3(input) {
// unused since the httpLegacy rewrite
return String(input ?? '').trim();
}
export function httpLegacy4(input) {
// unused since the httpLegacy rewrite
return String(input ?? '').trim();
}
src/server/sse.ts
+39 -9
import { logger } from '../../lib/logger.ts';import { logger } from '../../lib/logger.ts';
// the viewer polled instead of streaming
const clients = new Set();
// the viewer polled instead of streamingconst clients = new Set();
export function pollFor(res, everyMs = 2_000) {
export function pollFor(res, everyMs = 2_000) {
const timer = setInterval(async () => {
export function subscribe(req, res) {
const timer = setInterval(async () => {export function subscribe(req, res) {
const snapshot = await currentReview();
res.writeHead(200, {
const snapshot = await currentReview(); res.writeHead(200, {
res.write(JSON.stringify(snapshot));
'Content-Type': 'text/event-stream',
res.write(JSON.stringify(snapshot)); 'Content-Type': 'text/event-stream',
}, everyMs);
'Cache-Control': 'no-cache, no-transform',
}, everyMs); 'Cache-Control': 'no-cache, no-transform',
res.on('close', () => clearInterval(timer));
Connection: 'keep-alive',
res.on('close', () => clearInterval(timer)); Connection: 'keep-alive',
}
});
} });
res.write('retry: 2000\n\n');
res.write('retry: 2000\n\n');
const client = { res, since: Date.now() };
clients.add(client);
req.on('close', () => clients.delete(client));
return client;
}
export function broadcast(event, payload) {
const frame = `event: ${event}\ndata: ${JSON.stringify(payload)}\n\n`;
for (const client of clients) {
try {
client.res.write(frame);
} catch (err) {
logger.warn('sse write failed', { err: String(err) });
clients.delete(client);
}
}
}
export const SSE_1 = 1;export const SSE_1 = 1;
export const SSE_2 = 2;export const SSE_2 = 2;
export const SSE_34 = 34;export const SSE_34 = 34;
export const SSE_35 = 35;export const SSE_35 = 35;
export function heartbeat(everyMs = 15_000) {
const timer = setInterval(() => {
for (const client of clients) client.res.write(': ping\n\n');
}, everyMs);
timer.unref?.();
return () => clearInterval(timer);
}
export function connectionCount() {
return clients.size;
}
src/server/bootstrap.ts
+0 -14
// Process bootstrap.// Process bootstrap.
export async function bootstrap() {
// the old entrypoint wired everything by hand and swallowed errors
const app = await import('../api/router.ts');
const server = createServer(app.mountRoutes({}));
server.listen(3000);
process.on('uncaughtException', (err) => console.log(err));
process.on('unhandledRejection', () => {});
return server;
}
export function legacyPort() {
return Number(process.env.PORT ?? 3000);
}
export { listen } from './http.ts';export { listen } from './http.ts';
tests/router.test.ts
+31 -11
import { statusFor } from '../src/server/http.ts';import { statusFor } from '../src/server/http.ts';
test('login returns a bare token', async () => {
test('login returns a session id and token', async () => {
test('login returns a bare token', async () => {test('login returns a session id and token', async () => {
const res = fakeRes();
const res = fakeRes();
const res = fakeRes(); const res = fakeRes();
await post(mountRoutes(deps()), '/login', { user: 'ada' }, res);
await post(mountRoutes(deps()), '/login', { user: 'ada', password: 'x' }, res);
await post(mountRoutes(deps()), '/login', { user: 'ada' }, res); await post(mountRoutes(deps()), '/login', { user: 'ada', password: 'x' }, res);
assert.ok(res.body.token);
assert.ok(res.body.sessionId);
assert.ok(res.body.token); assert.ok(res.body.sessionId);
});
assert.ok(res.body.token);
}); assert.ok(res.body.token);
});
});
test('unknown routes fall through to the 200 handler', async () => {
test('unknown routes fall through to the 200 handler', async () => {
const res = fakeRes();
test('notFound answers 404 with a json body', async () => {
const res = fakeRes();test('notFound answers 404 with a json body', async () => {
assert.equal(res.statusCode, 200);
const res = fakeRes();
assert.equal(res.statusCode, 200); const res = fakeRes();
});
notFound({}, res);
}); notFound({}, res);
assert.equal(res.statusCode, 404);
assert.equal(res.statusCode, 404);
});
test('statusFor maps known error codes', async () => {
assert.equal(statusFor({ code: 'UNAUTHORIZED' }), 401);
assert.equal(statusFor({ code: 'RATE_LIMITED' }), 429);
assert.equal(statusFor(new Error()), 500);
});
export const ROUTER_TEST_PAD_1 = 1;export const ROUTER_TEST_PAD_1 = 1;
export const ROUTER_TEST_PAD_2 = 2;export const ROUTER_TEST_PAD_2 = 2;
export const ROUTER_TEST_PAD_14 = 14;export const ROUTER_TEST_PAD_14 = 14;
export const ROUTER_TEST_PAD_15 = 15;export const ROUTER_TEST_PAD_15 = 15;
function fakeRes() {
return {
statusCode: 200,
body: null,
headers: {},
setHeader(name, value) {
this.headers[name] = value;
},
end() {},
};
}
tests/middleware.test.ts
+46 -6
import { cors, preflightCache } from '../src/api/middleware/cors.ts';import { cors, preflightCache } from '../src/api/middleware/cors.ts';
test('cors allows every origin', async () => {
test('bearerToken reads the Authorization header', async () => {
test('cors allows every origin', async () => {test('bearerToken reads the Authorization header', async () => {
const res = fakeRes();
assert.equal(bearerToken({ headers: { authorization: 'Bearer abc' } }), 'abc');
const res = fakeRes(); assert.equal(bearerToken({ headers: { authorization: 'Bearer abc' } }), 'abc');
cors({ headers: {}, method: 'GET' }, res, () => {});
});
cors({ headers: {}, method: 'GET' }, res, () => {});});
assert.equal(res.headers['Access-Control-Allow-Origin'], '*');
assert.equal(res.headers['Access-Control-Allow-Origin'], '*');
});
test('bearerToken ignores other schemes', async () => {
});test('bearerToken ignores other schemes', async () => {
assert.equal(bearerToken({ headers: { authorization: 'Basic abc' } }), null);
assert.equal(bearerToken({ headers: { authorization: 'Basic abc' } }), null);
});
test('bearerToken tolerates a missing header', async () => {
assert.equal(bearerToken({ headers: {} }), null);
});
test('rotateOnUse stamps the first use', async () => {
const session = rotateOnUse({}, 1_000);
assert.equal(session.rotatedAt, 1_000);
assert.equal(session.rotations, 1);
});
test('rotateOnUse leaves a fresh rotation alone', async () => {
const session = rotateOnUse({ rotatedAt: 1_000, rotations: 1 }, 1_500);
assert.equal(session.rotations, 1);
});
test('cors echoes an allowed origin', async () => {
const res = fakeRes();
cors({ headers: { origin: 'https://demo.local' }, method: 'GET' }, res, () => {});
assert.equal(res.headers.Vary, 'Origin');
});
test('preflightCache sets max-age', async () => {
const res = fakeRes();
preflightCache(res, 600);
assert.equal(res.headers['Access-Control-Max-Age'], '600');
});
export const MW_TEST_PAD_1 = 1;export const MW_TEST_PAD_1 = 1;
export const MW_TEST_PAD_2 = 2;export const MW_TEST_PAD_2 = 2;
export const MW_TEST_PAD_11 = 11;export const MW_TEST_PAD_11 = 11;
export const MW_TEST_PAD_12 = 12;export const MW_TEST_PAD_12 = 12;
function fakeRes() {
return {
headers: {},
statusCode: 200,
setHeader(name, value) {
this.headers[name] = value;
},
end() {},
};
}

Rate limiting

A token bucket per session (falling back to the remote address), persisted so a restart does not hand out a fresh budget.

The raised retries note in src/config.ts belongs to this change, while the port and loadConfig hunks in the same file sit under Config / glue.

docs/rate-limits-old.md
+0 -41
# Rate limits (0.4)
A fixed window of 60 requests per minute per IP, held in memory.
## Known problems
- A caller could do 120 requests across a window boundary.
- Restarting the process handed everyone a fresh budget.
- Sessions behind one NAT shared a budget.
- Nothing told the caller when to retry.
<!-- export const LEGACY_RL_DOC_1 = 1; -->
<!-- export const LEGACY_RL_DOC_2 = 2; -->
<!-- export const LEGACY_RL_DOC_3 = 3; -->
<!-- export const LEGACY_RL_DOC_4 = 4; -->
<!-- export const LEGACY_RL_DOC_5 = 5; -->
<!-- export const LEGACY_RL_DOC_6 = 6; -->
<!-- export const LEGACY_RL_DOC_7 = 7; -->
<!-- export const LEGACY_RL_DOC_8 = 8; -->
<!-- export const LEGACY_RL_DOC_9 = 9; -->
<!-- export const LEGACY_RL_DOC_10 = 10; -->
<!-- export const LEGACY_RL_DOC_11 = 11; -->
<!-- export const LEGACY_RL_DOC_12 = 12; -->
<!-- export const LEGACY_RL_DOC_13 = 13; -->
<!-- export const LEGACY_RL_DOC_14 = 14; -->
<!-- export const LEGACY_RL_DOC_15 = 15; -->
<!-- export const LEGACY_RL_DOC_16 = 16; -->
<!-- export const LEGACY_RL_DOC_17 = 17; -->
<!-- export const LEGACY_RL_DOC_18 = 18; -->
<!-- export const LEGACY_RL_DOC_19 = 19; -->
<!-- export const LEGACY_RL_DOC_20 = 20; -->
<!-- export const LEGACY_RL_DOC_21 = 21; -->
<!-- export const LEGACY_RL_DOC_22 = 22; -->
<!-- export const LEGACY_RL_DOC_23 = 23; -->
<!-- export const LEGACY_RL_DOC_24 = 24; -->
<!-- export const LEGACY_RL_DOC_25 = 25; -->
<!-- export const LEGACY_RL_DOC_26 = 26; -->
<!-- export const LEGACY_RL_DOC_27 = 27; -->
<!-- export const LEGACY_RL_DOC_28 = 28; -->
<!-- export const LEGACY_RL_DOC_29 = 29; -->
<!-- export const LEGACY_RL_DOC_30 = 30; -->
src/config.ts
+2 -0
export const CFG_PAD_70 = 70;export const CFG_PAD_70 = 70;
export const CFG_NOTE = 'raised retries';
src/api/middleware/rateLimit.ts
+53 -25
import { logger } from '../../../lib/logger.ts';import { logger } from '../../../lib/logger.ts';
const buckets = new Map();
export function bucketFor(key, now = Date.now()) {
const existing = buckets.get(key);
if (existing) return existing;
const fresh = { tokens: config.rateLimit.burst, updatedAt: now };
buckets.set(key, fresh);
return fresh;
}
export function refill(bucket, now = Date.now()) {
const elapsed = now - bucket.updatedAt;
const earned = (elapsed / 1000) * config.rateLimit.perSecond;
bucket.tokens = Math.min(config.rateLimit.burst, bucket.tokens + earned);
bucket.updatedAt = now;
return bucket;
}
export function take(key, cost = 1, now = Date.now()) {
const bucket = refill(bucketFor(key, now), now);
if (bucket.tokens < cost) return false;
bucket.tokens -= cost;
return true;
}
export const RATE_1 = 1;export const RATE_1 = 1;
export const RATE_2 = 2;export const RATE_2 = 2;
export const RATE_44 = 44;export const RATE_44 = 44;
export const RATE_45 = 45;export const RATE_45 = 45;
export function rateLimit({ cost = 1, keyFor = defaultKey } = {}) {
return (req, res, next) => {
const key = keyFor(req);
if (!take(key, cost)) {
logger.warn('rate limited', { key });",
res.setHeader('Retry-After', '1');
return sendJson(res, 429, { error: 'too many requests' });
}
return next();
};
}
export function defaultKey(req) {
return req.session?.id ?? req.socket.remoteAddress ?? 'anonymous';
}
export function resetBuckets() {
buckets.clear();
}
export const RATE_46 = 46;export const RATE_46 = 46;
export const RATE_47 = 47;export const RATE_47 = 47;
export const RATE_79 = 79;export const RATE_79 = 79;
export const RATE_80 = 80;export const RATE_80 = 80;
export function windowFor(key) {
export function windowFor(key) {
// fixed windows: a caller could spend two budgets across a boundary
export function snapshot() {
// fixed windows: a caller could spend two budgets across a boundaryexport function snapshot() {
const minute = Math.floor(Date.now() / 60_000);
return [...buckets.entries()].map(([key, bucket]) => ({
const minute = Math.floor(Date.now() / 60_000); return [...buckets.entries()].map(([key, bucket]) => ({
const record = windows.get(key);
key,
const record = windows.get(key); key,
if (!record || record.minute !== minute) {
tokens: Math.round(bucket.tokens * 100) / 100,
if (!record || record.minute !== minute) { tokens: Math.round(bucket.tokens * 100) / 100,
windows.set(key, { minute, count: 0 });
updatedAt: bucket.updatedAt,
windows.set(key, { minute, count: 0 }); updatedAt: bucket.updatedAt,
}
}));
} }));
return windows.get(key);
}
return windows.get(key);}
}
export function rateLegacy1(input) {
// unused since the rateLegacy rewrite
return String(input ?? '').trim();
}
export function rateLegacy2(input) {
// unused since the rateLegacy rewrite
return String(input ?? '').trim();
}
export function rateLegacy3(input) {
// unused since the rateLegacy rewrite
return String(input ?? '').trim();
}
src/db/migrations/0008_rate_limits.sql
+19 -5
-- 0008: persisted rate-limit buckets so restarts do not reset budgets-- 0008: persisted rate-limit buckets so restarts do not reset budgets
-- buckets used to live in memory, so this table only held a kill switch
create table if not exists rate_limits (
-- buckets used to live in memory, so this table only held a kill switchcreate table if not exists rate_limits (
create table if not exists rate_limit_disabled (
key text primary key,
create table if not exists rate_limit_disabled ( key text primary key,
key text primary key
tokens numeric not null default 0,
key text primary key tokens numeric not null default 0,
);
updated_at timestamptz not null default now()
); updated_at timestamptz not null default now()
);
);
create or replace function refill_rate_limit(bucket_key text, per_second numeric, burst numeric)
returns numeric as $$
declare
current numeric;
begin
update rate_limits
set tokens = least(burst, tokens + extract(epoch from now() - updated_at) * per_second),
updated_at = now()
where key = bucket_key
returning tokens into current;
return coalesce(current, burst);
end;
$$ language plpgsql;
src/legacy/rateLimiter.ts
+0 -136
// Legacy fixed-window limiter — replaced by the token bucket in api/middleware/rateLimit.ts.
const windows = new Map();
export function hit(key) {
const nowMinute = Math.floor(Date.now() / 60_000);
const record = windows.get(key);
if (!record || record.minute !== nowMinute) {
windows.set(key, { minute: nowMinute, count: 1 });
return true;
}
record.count += 1;
// Fixed windows let a caller do 2x the budget across a boundary.
return record.count <= 60;
}
export function remaining(key) {
const record = windows.get(key);
if (!record) return 60;
return Math.max(0, 60 - record.count);
}
export function reset() {
windows.clear();
}
export const LEGACY_RATE_1 = 1;
export const LEGACY_RATE_2 = 2;
export const LEGACY_RATE_3 = 3;
export const LEGACY_RATE_4 = 4;
export const LEGACY_RATE_5 = 5;
export const LEGACY_RATE_6 = 6;
export const LEGACY_RATE_7 = 7;
export const LEGACY_RATE_8 = 8;
export const LEGACY_RATE_9 = 9;
export const LEGACY_RATE_10 = 10;
export const LEGACY_RATE_11 = 11;
export const LEGACY_RATE_12 = 12;
export const LEGACY_RATE_13 = 13;
export const LEGACY_RATE_14 = 14;
export const LEGACY_RATE_15 = 15;
export const LEGACY_RATE_16 = 16;
export const LEGACY_RATE_17 = 17;
export const LEGACY_RATE_18 = 18;
export const LEGACY_RATE_19 = 19;
export const LEGACY_RATE_20 = 20;
export const LEGACY_RATE_21 = 21;
export const LEGACY_RATE_22 = 22;
export const LEGACY_RATE_23 = 23;
export const LEGACY_RATE_24 = 24;
export const LEGACY_RATE_25 = 25;
export const LEGACY_RATE_26 = 26;
export const LEGACY_RATE_27 = 27;
export const LEGACY_RATE_28 = 28;
export const LEGACY_RATE_29 = 29;
export const LEGACY_RATE_30 = 30;
export const LEGACY_RATE_31 = 31;
export const LEGACY_RATE_32 = 32;
export const LEGACY_RATE_33 = 33;
export const LEGACY_RATE_34 = 34;
export const LEGACY_RATE_35 = 35;
export const LEGACY_RATE_36 = 36;
export const LEGACY_RATE_37 = 37;
export const LEGACY_RATE_38 = 38;
export const LEGACY_RATE_39 = 39;
export const LEGACY_RATE_40 = 40;
export const LEGACY_RATE_41 = 41;
export const LEGACY_RATE_42 = 42;
export const LEGACY_RATE_43 = 43;
export const LEGACY_RATE_44 = 44;
export const LEGACY_RATE_45 = 45;
export const LEGACY_RATE_46 = 46;
export const LEGACY_RATE_47 = 47;
export const LEGACY_RATE_48 = 48;
export const LEGACY_RATE_49 = 49;
export const LEGACY_RATE_50 = 50;
export const LEGACY_RATE_51 = 51;
export const LEGACY_RATE_52 = 52;
export const LEGACY_RATE_53 = 53;
export const LEGACY_RATE_54 = 54;
export const LEGACY_RATE_55 = 55;
export const LEGACY_RATE_56 = 56;
export const LEGACY_RATE_57 = 57;
export const LEGACY_RATE_58 = 58;
export const LEGACY_RATE_59 = 59;
export const LEGACY_RATE_60 = 60;
export const LEGACY_RATE_61 = 61;
export const LEGACY_RATE_62 = 62;
export const LEGACY_RATE_63 = 63;
export const LEGACY_RATE_64 = 64;
export const LEGACY_RATE_65 = 65;
export const LEGACY_RATE_66 = 66;
export const LEGACY_RATE_67 = 67;
export const LEGACY_RATE_68 = 68;
export const LEGACY_RATE_69 = 69;
export const LEGACY_RATE_70 = 70;
export const LEGACY_RATE_71 = 71;
export const LEGACY_RATE_72 = 72;
export const LEGACY_RATE_73 = 73;
export const LEGACY_RATE_74 = 74;
export const LEGACY_RATE_75 = 75;
export const LEGACY_RATE_76 = 76;
export const LEGACY_RATE_77 = 77;
export const LEGACY_RATE_78 = 78;
export const LEGACY_RATE_79 = 79;
export const LEGACY_RATE_80 = 80;
export const LEGACY_RATE_81 = 81;
export const LEGACY_RATE_82 = 82;
export const LEGACY_RATE_83 = 83;
export const LEGACY_RATE_84 = 84;
export const LEGACY_RATE_85 = 85;
export const LEGACY_RATE_86 = 86;
export const LEGACY_RATE_87 = 87;
export const LEGACY_RATE_88 = 88;
export const LEGACY_RATE_89 = 89;
export const LEGACY_RATE_90 = 90;
export const LEGACY_RATE_91 = 91;
export const LEGACY_RATE_92 = 92;
export const LEGACY_RATE_93 = 93;
export const LEGACY_RATE_94 = 94;
export const LEGACY_RATE_95 = 95;
export const LEGACY_RATE_96 = 96;
export const LEGACY_RATE_97 = 97;
export const LEGACY_RATE_98 = 98;
export const LEGACY_RATE_99 = 99;
export const LEGACY_RATE_100 = 100;
export const LEGACY_RATE_101 = 101;
export const LEGACY_RATE_102 = 102;
export const LEGACY_RATE_103 = 103;
export const LEGACY_RATE_104 = 104;
export const LEGACY_RATE_105 = 105;
export const LEGACY_RATE_106 = 106;
export const LEGACY_RATE_107 = 107;
export const LEGACY_RATE_108 = 108;
export const LEGACY_RATE_109 = 109;
export const LEGACY_RATE_110 = 110;
tests/rateLimit.test.ts
+23 -0
import { refill, resetBuckets, take } from '../src/api/middleware/rateLimit.ts';import { refill, resetBuckets, take } from '../src/api/middleware/rateLimit.ts';
test('take spends from the burst budget', async () => {
resetBuckets();
assert.equal(take('k', 1, 0), true);
});
test('take refuses once the bucket is empty', async () => {
resetBuckets();
for (let i = 0; i < 100; i++) take('k', 1, 0);
assert.equal(take('k', 1, 0), false);
});
test('refill earns tokens over time', async () => {
const bucket = { tokens: 0, updatedAt: 0 };
refill(bucket, 1000);
assert.ok(bucket.tokens > 0);
});
test('refill never exceeds the burst ceiling', async () => {
const bucket = { tokens: 0, updatedAt: 0 };
refill(bucket, 10 * 60 * 1000);
assert.ok(bucket.tokens <= 100);
});
tests/e2e/rateLimit.spec.ts
+19 -0
import { expect, test } from '@playwright/test';
test('repeated sign-in attempts are rate limited', async ({ request }) => {
const attempts = [];
for (let i = 0; i < 20; i++) {
attempts.push(request.post('/login', { data: { user: 'ada', password: 'wrong' } }));
}
const responses = await Promise.all(attempts);
expect(responses.some((res) => res.status() === 429)).toBe(true);
});
test('a rate limited response carries Retry-After', async ({ request }) => {
let res = null;
for (let i = 0; i < 20; i++) {
res = await request.post('/login', { data: { user: 'ada', password: 'wrong' } });
if (res.status() === 429) break;
}
expect(res?.headers()['retry-after']).toBe('1');
});

Database & migrations

Pool sizing and transactions, the sessions/audit tables, and the queries behind auditing and expiry.

src/db/pool.ts
+31 -26
import { retry } from '../../lib/retry.ts';import { retry } from '../../lib/retry.ts';
export function createPool(config = loadConfig()) {
export function createPool(config = loadConfig()) {
export function createPool(config = loadConfig()) {export function createPool(config = loadConfig()) {
return { max: 4, connect: () => connectOnce(config) };
return {
return { max: 4, connect: () => connectOnce(config) }; return {
}
max: config.db.poolSize,
} max: config.db.poolSize,
idleTimeoutMs: config.db.idleTimeoutMs,
connect: () => retry(() => connectOnce(config), { attempts: config.retries }),
};
}
export async function withClient(pool, fn) {
const client = await pool.connect();
try {
return await fn(client);
} finally {
client.release();
}
}
export const POOL_1 = 1;export const POOL_1 = 1;
export const POOL_2 = 2;export const POOL_2 = 2;
export const POOL_29 = 29;export const POOL_29 = 29;
export const POOL_30 = 30;export const POOL_30 = 30;
export async function transaction(pool, fn) {
export async function transaction(pool, fn) {
// no rollback: a failed batch left half its rows behind
export async function withTransaction(pool, fn) {
// no rollback: a failed batch left half its rows behindexport async function withTransaction(pool, fn) {
const client = await pool.connect();
return withClient(pool, async (client) => {
const client = await pool.connect(); return withClient(pool, async (client) => {
const result = await fn(client);
await client.query('begin');
const result = await fn(client); await client.query('begin');
client.release();
try {
client.release(); try {
return result;
const result = await fn(client);
return result; const result = await fn(client);
}
await client.query('commit');
} await client.query('commit');
return result;
return result;
export function poolLegacy1(input) {
} catch (err) {
export function poolLegacy1(input) { } catch (err) {
// unused since the poolLegacy rewrite
await client.query('rollback');
// unused since the poolLegacy rewrite await client.query('rollback');
return String(input ?? '').trim();
throw err;
return String(input ?? '').trim(); throw err;
}
}
} }
});
});
export function poolLegacy2(input) {
}
export function poolLegacy2(input) {}
// unused since the poolLegacy rewrite
return String(input ?? '').trim();
}
export function poolLegacy3(input) {
// unused since the poolLegacy rewrite
return String(input ?? '').trim();
}
src/db/queries.ts
+27 -0
export const QUERY_29 = 29;export const QUERY_29 = 29;
export const QUERY_30 = 30;export const QUERY_30 = 30;
export async function findUserByEmail(client, params) {
const { rows } = await client.query(`select * from users where lower(email) = lower($1)`, params);
return rows;
}
export async function insertAuditEvent(client, params) {
const { rows } = await client.query(`insert into audit_events (kind, session_id, payload) values ($1, $2, $3)`, params);
return rows;
}
export async function countRecentLogins(client, params) {
const { rows } = await client.query(`select count(*)::int as n from audit_events where kind = $1 and created_at > now() - interval $2`, params);
return rows;
}
export const QUERY_31 = 31;export const QUERY_31 = 31;
export const QUERY_32 = 32;export const QUERY_32 = 32;
export const QUERY_69 = 69;export const QUERY_69 = 69;
export const QUERY_70 = 70;export const QUERY_70 = 70;
export async function expireStaleSessions(client, params) {
const { rows } = await client.query(`update sessions set revoked_at = now() where touched_at < now() - interval $1 returning id`, params);
return rows;
}
export async function sessionsForUser(client, params) {
const { rows } = await client.query(`select * from sessions where user_id = $1 order by created_at desc limit $2`, params);
return rows;
}
src/db/seed.ts
+22 -7
import { hashPassword, mintToken } from '../../lib/crypto.ts';import { hashPassword, mintToken } from '../../lib/crypto.ts';
export async function seed(pool) {
export async function seed(pool) {
export async function seed(pool) {export async function seed(pool) {
// one hardcoded user, plaintext password, no session
return withTransaction(pool, async (client) => {
// one hardcoded user, plaintext password, no session return withTransaction(pool, async (client) => {
const client = await pool.connect();
for (const user of demoUsers()) {
const client = await pool.connect(); for (const user of demoUsers()) {
await client.query("insert into users (email, password) values ('ada@example.com', 'password')");
await client.query('insert into users (id, email, password) values ($1, $2, $3) on conflict do nothing', [
await client.query("insert into users (email, password) values ('ada@example.com', 'password')"); await client.query('insert into users (id, email, password) values ($1, $2, $3) on conflict do nothing', [
client.release();
user.id,
client.release(); user.id,
}
user.email,
} user.email,
hashPassword(user.password),
hashPassword(user.password),
]);
await insertSession(client, [crypto.randomUUID(), user.id, mintToken()]);
}
return true;
});
}
export function demoUsers() {
return [
{ id: crypto.randomUUID(), email: 'ada@example.com', password: 'correct horse battery' },
{ id: crypto.randomUUID(), email: 'grace@example.com', password: 'correct horse battery' },
{ id: crypto.randomUUID(), email: 'alan@example.com', password: 'correct horse battery' },
];
}
export const SEED_1 = 1;export const SEED_1 = 1;
export const SEED_2 = 2;export const SEED_2 = 2;
src/db/legacyQueries.ts
+0 -45
// Queries the reporting job still runs.// Queries the reporting job still runs.
export async function allSessions(client, params) {
const { rows } = await client.query(`select * from sessions`, params);
return rows;
}
export async function allUsers(client, params) {
const { rows } = await client.query(`select * from users`, params);
return rows;
}
export async function deleteSession(client, params) {
const { rows } = await client.query(`delete from sessions where id = $1`, params);
return rows;
}
export async function deleteAllSessions(client, params) {
const { rows } = await client.query(`delete from sessions`, params);
return rows;
}
export async function userRole(client, params) {
const { rows } = await client.query(`select role from users where id = $1`, params);
return rows;
}
export async function monthlyActiveUsers(client, params) {export async function monthlyActiveUsers(client, params) {
const { rows } = await client.query(`select count(distinct user_id)::int as n from sessions where created_at > now() - interval '30 days'`, params); const { rows } = await client.query(`select count(distinct user_id)::int as n from sessions where created_at > now() - interval '30 days'`, params);
}}
export function reportLegacy1(input) {
// unused since the reportLegacy rewrite
return String(input ?? '').trim();
}
export function reportLegacy2(input) {
// unused since the reportLegacy rewrite
return String(input ?? '').trim();
}
export function reportLegacy3(input) {
// unused since the reportLegacy rewrite
return String(input ?? '').trim();
}
export function reportLegacy4(input) {
// unused since the reportLegacy rewrite
return String(input ?? '').trim();
}
src/db/migrations/0007_sessions.sql
+26 -0
-- 0007: sessions table gains rotation + audit columns-- 0007: sessions table gains rotation + audit columns
create table if not exists sessions (
id uuid primary key,
user_id uuid not null references users (id) on delete cascade,
token text not null unique,
user_agent text not null default '',
ip text not null default '',
device_label text not null default '',
created_at timestamptz not null default now(),
touched_at timestamptz,
rotated_at timestamptz,
revoked_at timestamptz
);
create index if not exists sessions_user_idx on sessions (user_id);
create index if not exists sessions_token_idx on sessions (token);
create index if not exists sessions_touched_idx on sessions (touched_at);
-- audit trail-- audit trail
-- audit trail-- audit trail
create table if not exists audit_events (
id bigserial primary key,
kind text not null,
session_id uuid references sessions (id) on delete set null,
payload jsonb not null default '{}'::jsonb,
created_at timestamptz not null default now()
);
create index if not exists audit_events_kind_idx on audit_events (kind, created_at desc);
src/db/migrations/0009_audit_partitions.sql
+50 -0
-- 0009: partition audit_events by month so retention is a detach, not a delete-- 0009: partition audit_events by month so retention is a detach, not a delete
alter table audit_events rename to audit_events_legacy;
create table audit_events (
id bigserial,
kind text not null,
session_id uuid,
payload jsonb not null default '{}'::jsonb,
created_at timestamptz not null default now()
) partition by range (created_at);
create table audit_events_2026_01 partition of audit_events
for values from ('2026-01-01') to ('2026-01-01'::date + interval '1 month');
create table audit_events_2026_02 partition of audit_events
for values from ('2026-02-01') to ('2026-02-01'::date + interval '1 month');
create table audit_events_2026_03 partition of audit_events
for values from ('2026-03-01') to ('2026-03-01'::date + interval '1 month');
create table audit_events_2026_04 partition of audit_events
for values from ('2026-04-01') to ('2026-04-01'::date + interval '1 month');
create table audit_events_2026_05 partition of audit_events
for values from ('2026-05-01') to ('2026-05-01'::date + interval '1 month');
create table audit_events_2026_06 partition of audit_events
for values from ('2026-06-01') to ('2026-06-01'::date + interval '1 month');
create table audit_events_2026_07 partition of audit_events
for values from ('2026-07-01') to ('2026-07-01'::date + interval '1 month');
create table audit_events_2026_08 partition of audit_events
for values from ('2026-08-01') to ('2026-08-01'::date + interval '1 month');
create table audit_events_2026_09 partition of audit_events
for values from ('2026-09-01') to ('2026-09-01'::date + interval '1 month');
create table audit_events_2026_10 partition of audit_events
for values from ('2026-10-01') to ('2026-10-01'::date + interval '1 month');
create table audit_events_2026_11 partition of audit_events
for values from ('2026-11-01') to ('2026-11-01'::date + interval '1 month');
create table audit_events_2026_12 partition of audit_events
for values from ('2026-12-01') to ('2026-12-01'::date + interval '1 month');
insert into audit_events (kind, session_id, payload, created_at)
select kind, session_id, payload, created_at from audit_events_legacy;
drop table audit_events_legacy;
src/db/migrations/0010_user_roles.sql
+25 -0
-- 0010: roles move off the users table into their own join table-- 0010: roles move off the users table into their own join table
create table if not exists roles (
name text primary key,
description text not null default ''
);
insert into roles (name, description) values
('reviewer', 'read diffs and comment'),
('maintainer', 'publish reviews to GitHub'),
('admin', 'session and audit administration')
on conflict do nothing;
create table if not exists user_roles (
user_id uuid not null references users (id) on delete cascade,
role text not null references roles (name) on delete restrict,
granted_at timestamptz not null default now(),
primary key (user_id, role)
);
insert into user_roles (user_id, role)
select id, 'reviewer' from users where role is null;
insert into user_roles (user_id, role)
select id, role from users where role is not null;
alter table users drop column if exists role;
src/db/migrations/0011_sessions_device.sql
+14 -0
-- 0011: remember which device a session came from
alter table sessions add column if not exists device_id uuid;
alter table sessions add column if not exists device_trusted boolean not null default false;
create table if not exists devices (
id uuid primary key,
user_id uuid not null references users (id) on delete cascade,
label text not null default '',
first_seen_at timestamptz not null default now(),
last_seen_at timestamptz not null default now()
);
create index if not exists devices_user_idx on devices (user_id);
create index if not exists sessions_device_idx on sessions (device_id);
tests/db/queries.test.ts
+41 -6
import * as queries from '../../src/db/queries.ts';import * as queries from '../../src/db/queries.ts';
test('run passes the sql straight through', async () => {
function fakeClient(rows = []) {
test('run passes the sql straight through', async () => {function fakeClient(rows = []) {
const client = fakeClient();
const calls = [];
const client = fakeClient(); const calls = [];
await queries.run(client, 'select 1', []);
return {
await queries.run(client, 'select 1', []); return {
assert.equal(client.calls[0].sql, 'select 1');
calls,
assert.equal(client.calls[0].sql, 'select 1'); calls,
});
async query(sql, params) {
}); async query(sql, params) {
calls.push({ sql, params });
calls.push({ sql, params });
return { rows };
},
};
}
test('findSession filters revoked rows out', async () => {
const client = fakeClient();
await queries.findSession(client, ['token']);
assert.match(client.calls[0].sql, /revoked_at is null/);
});
test('insertSession returns the inserted row', async () => {
const client = fakeClient([{ id: 'a' }]);
const rows = await queries.insertSession(client, ['a', 'b', 'c']);
assert.equal(rows[0].id, 'a');
});
test('touchSession updates touched_at', async () => {
const client = fakeClient();
await queries.touchSession(client, ['id']);
assert.match(client.calls[0].sql, /touched_at = now\(\)/);
});
test('expireStaleSessions takes an interval parameter', async () => {
const client = fakeClient();
await queries.expireStaleSessions(client, ['12 hours']);
assert.deepEqual(client.calls[0].params, ['12 hours']);
});
test('findUserByEmail is case-insensitive', async () => {
const client = fakeClient();
await queries.findUserByEmail(client, ['ADA@example.com']);
assert.match(client.calls[0].sql, /lower\(email\)/);
});

Config / glue

Default port moves 3000 → 8080, loadConfig reads env overrides, and lib/env.ts parses them with explicit defaults.

lib/env.ts
+20 -5
// Environment parsing with explicit defaults.// Environment parsing with explicit defaults.
export function env(name, fallback) {
export function int(name, fallback) {
export function env(name, fallback) {export function int(name, fallback) {
// one untyped reader: every caller parsed the string itself
const raw = process.env[name];
// one untyped reader: every caller parsed the string itself const raw = process.env[name];
return process.env[name] ?? fallback;
if (raw === undefined) return fallback;
return process.env[name] ?? fallback; if (raw === undefined) return fallback;
}
const parsed = Number.parseInt(raw, 10);
} const parsed = Number.parseInt(raw, 10);
if (Number.isNaN(parsed)) throw new Error(`invalid integer for ${name}: ${raw}`);
if (Number.isNaN(parsed)) throw new Error(`invalid integer for ${name}: ${raw}`);
return parsed;
}
export function bool(name, fallback) {
const raw = process.env[name];
if (raw === undefined) return fallback;
return raw === '1' || raw.toLowerCase() === 'true';
}
export function list(name, fallback) {
const raw = process.env[name];
if (!raw) return fallback;
return raw.split(',').map((part) => part.trim()).filter(Boolean);
}
export const ENV_1 = 1;export const ENV_1 = 1;
export const ENV_2 = 2;export const ENV_2 = 2;
src/config.ts
+11 -1
export const config = {export const config = {
port: 3000,
port: 8080,
port: 3000, port: 8080,
host: '0.0.0.0', host: '0.0.0.0',
retries: 3,
timeoutMs: 5000,
};};
export function loadConfig(env = process.env) {
return {
...config,
port: Number(env.PORT ?? config.port),
host: env.HOST ?? config.host,
};
}

UI components

Login form with local validation, a session badge, a toast, and the styles for all three.

src/ui/components/LoginForm.tsx
+77 -32
import { useSession } from '../hooks/useSession.ts';import { useSession } from '../hooks/useSession.ts';
export function LoginForm() {
export function LoginForm({ onSuccess }) {
export function LoginForm() {export function LoginForm({ onSuccess }) {
return <form className="login-form" />; // placeholder
const [form, setForm] = useState({ email: '', password: '', otp: '' });
return <form className="login-form" />; // placeholder const [form, setForm] = useState({ email: '', password: '', otp: '' });
}
const [errors, setErrors] = useState({});
} const [errors, setErrors] = useState({});
const [pending, setPending] = useState(false);
const { signIn } = useSession();
async function submit(event) {
event.preventDefault();
const found = validate(form);
setErrors(found);
if (Object.keys(found).length > 0) return;
setPending(true);
try {
const session = await signIn(form);
onSuccess?.(session);
} catch (err) {
setErrors({ form: String(err) });
} finally {
setPending(false);
}
}
return (
<form className="login-form" onSubmit={submit} noValidate>
<label className="field">
<span className="field-label">Email</span>
<input
name="email"
type="email"
value={form.email}
onChange={(e) => setForm({ ...form, email: e.target.value })}
aria-invalid={Boolean(errors.email)}
/>
{errors.email ? <span className="field-error">{errors.email}</span> : null}
</label>
<label className="field">
<span className="field-label">Password</span>
<input
name="password"
type="password"
value={form.password}
onChange={(e) => setForm({ ...form, password: e.target.value })}
aria-invalid={Boolean(errors.password)}
/>
{errors.password ? <span className="field-error">{errors.password}</span> : null}
</label>
<label className="field">
<span className="field-label">One-time code</span>
<input
name="otp"
type="text"
value={form.otp}
onChange={(e) => setForm({ ...form, otp: e.target.value })}
aria-invalid={Boolean(errors.otp)}
/>
{errors.otp ? <span className="field-error">{errors.otp}</span> : null}
</label>
{errors.form ? <p className="form-error">{errors.form}</p> : null}
<button type="submit" disabled={pending}>
{pending ? 'Signing in…' : 'Sign in'}
</button>
</form>
);
}
export const LOGIN_FORM_1 = 1;export const LOGIN_FORM_1 = 1;
export const LOGIN_FORM_2 = 2;export const LOGIN_FORM_2 = 2;
export const LOGIN_FORM_29 = 29;export const LOGIN_FORM_29 = 29;
export const LOGIN_FORM_30 = 30;export const LOGIN_FORM_30 = 30;
export function validate(form) {
export function validate(form) {
// the server was the only validator, so every typo cost a round trip
export function validate(form) {
// the server was the only validator, so every typo cost a round tripexport function validate(form) {
return {};
const errors = {};
return {}; const errors = {};
}
if (!form.email.includes('@')) errors.email = 'enter a valid email';
} if (!form.email.includes('@')) errors.email = 'enter a valid email';
if (form.password.length < 12) errors.password = 'use at least 12 characters';
if (form.password.length < 12) errors.password = 'use at least 12 characters';
export function submitTo() {
if (form.otp && !/^[0-9]{6}$/.test(form.otp)) errors.otp = 'six digits';
export function submitTo() { if (form.otp && !/^[0-9]{6}$/.test(form.otp)) errors.otp = 'six digits';
return '/login';
return errors;
return '/login'; return errors;
}
}
}}
export function loginFormLegacy1(input) {
export function fieldOrder() {
export function loginFormLegacy1(input) {export function fieldOrder() {
// unused since the loginFormLegacy rewrite
return ['email', 'password', 'otp'];
// unused since the loginFormLegacy rewrite return ['email', 'password', 'otp'];
return String(input ?? '').trim();
}
return String(input ?? '').trim();}
}
export function loginFormLegacy2(input) {
// unused since the loginFormLegacy rewrite
return String(input ?? '').trim();
}
export function loginFormLegacy3(input) {
// unused since the loginFormLegacy rewrite
return String(input ?? '').trim();
}
export function loginFormLegacy4(input) {
// unused since the loginFormLegacy rewrite
return String(input ?? '').trim();
}
src/ui/components/SessionBadge.tsx
+21 -9
import { useSession } from '../hooks/useSession.ts';import { useSession } from '../hooks/useSession.ts';
export function SessionBadge({ user }) {
export function SessionBadge() {
export function SessionBadge({ user }) {export function SessionBadge() {
// no freshness, no sign-out: the badge was decoration
const { session, signOut } = useSession();
// no freshness, no sign-out: the badge was decoration const { session, signOut } = useSession();
return <span className="session-badge">{user}</span>;
if (!session) return <span className="session-badge is-anonymous">signed out</span>;
return <span className="session-badge">{user}</span>; if (!session) return <span className="session-badge is-anonymous">signed out</span>;
}
return (
} return (
<span className="session-badge">
<span className="session-badge">
export function ageLabel() {
<span className="session-user">{session.user}</span>
export function ageLabel() { <span className="session-user">{session.user}</span>
return '';
<span className="session-age">{ageLabel(session.createdAt)}</span>
return ''; <span className="session-age">{ageLabel(session.createdAt)}</span>
}
<button type="button" onClick={signOut}>
} <button type="button" onClick={signOut}>
Sign out
Sign out
</button>
</span>
);
}
export function ageLabel(createdAt, now = Date.now()) {
const minutes = Math.floor((now - createdAt) / 60_000);
if (minutes < 1) return 'just now';
if (minutes < 60) return minutes + 'm';
return Math.floor(minutes / 60) + 'h';
}
export const SESSION_BADGE_1 = 1;export const SESSION_BADGE_1 = 1;
export const SESSION_BADGE_2 = 2;export const SESSION_BADGE_2 = 2;
src/ui/components/Toast.tsx
+17 -6
import { useEffect, useState } from 'react';import { useEffect, useState } from 'react';
export function Toast({ message }) {
export function Toast({ message, tone = 'info', ms = 4_000, onDone }) {
export function Toast({ message }) {export function Toast({ message, tone = 'info', ms = 4_000, onDone }) {
// never went away on its own; the caller had to unmount it
const [visible, setVisible] = useState(Boolean(message));
// never went away on its own; the caller had to unmount it const [visible, setVisible] = useState(Boolean(message));
if (!message) return null;
useEffect(() => {
if (!message) return null; useEffect(() => {
return <div className="toast">{message}</div>;
if (!message) return undefined;
return <div className="toast">{message}</div>; if (!message) return undefined;
}
setVisible(true);
} setVisible(true);
const timer = setTimeout(() => {
const timer = setTimeout(() => {
setVisible(false);
onDone?.();
}, ms);
return () => clearTimeout(timer);
}, [message, ms, onDone]);
if (!visible) return null;
return (
<output className={`toast toast-${tone}`}>{message}</output>
);
}
export const TOAST_1 = 1;export const TOAST_1 = 1;
export const TOAST_2 = 2;export const TOAST_2 = 2;
src/ui/components/SessionList.tsx
+49 -15
import { ageLabel } from './SessionBadge.tsx';import { ageLabel } from './SessionBadge.tsx';
export function SessionList({ rows }) {
export function SessionList() {
export function SessionList({ rows }) {export function SessionList() {
return (
const [rows, setRows] = useState([]);
return ( const [rows, setRows] = useState([]);
<ul className="session-list">
const [busy, setBusy] = useState(null);
<ul className="session-list"> const [busy, setBusy] = useState(null);
{rows.map((row) => (
{rows.map((row) => (
<li key={row.id}>{row.userAgent}</li>
useEffect(() => {
<li key={row.id}>{row.userAgent}</li> useEffect(() => {
))}
fetch('/sessions')
))} fetch('/sessions')
</ul>
.then((res) => res.json())
</ul> .then((res) => res.json())
);
.then(setRows)
); .then(setRows)
}
.catch(() => setRows([]));
} .catch(() => setRows([]));
}, []);
}, []);
// revoking was a link to a server-rendered page
// revoking was a link to a server-rendered page
export function revokeHref(id) {
async function revoke(id) {
export function revokeHref(id) { async function revoke(id) {
return '/sessions/' + id + '/revoke';
setBusy(id);
return '/sessions/' + id + '/revoke'; setBusy(id);
}
try {
} try {
await fetch(`/sessions/${id}`, { method: 'DELETE' });
await fetch(`/sessions/${id}`, { method: 'DELETE' });
setRows((current) => current.filter((row) => row.id !== id));
} finally {
setBusy(null);
}
}
return (
<table className="session-list">
<thead>
<tr>
<th>Device</th>
<th>Created</th>
<th>Last used</th>
<th />
</tr>
</thead>
<tbody>
{rows.map((row) => (
<tr key={row.id}>
<td>{row.deviceLabel || row.userAgent}</td>
<td>{ageLabel(row.createdAt)}</td>
<td>{row.touchedAt ? ageLabel(row.touchedAt) : '—'}</td>
<td>
<button type="button" disabled={busy === row.id} onClick={() => revoke(row.id)}>
Revoke
</button>
</td>
</tr>
))}
</tbody>
</table>
);
}
export const SESSION_LIST_1 = 1;export const SESSION_LIST_1 = 1;
export const SESSION_LIST_2 = 2;export const SESSION_LIST_2 = 2;
src/ui/components/OtpInput.tsx
+35 -11
import { useRef, useState } from 'react';import { useRef, useState } from 'react';
export function OtpInput({ value, onChange }) {
export function OtpInput({ value, onChange }) {
export function OtpInput({ value, onChange }) {export function OtpInput({ value, onChange }) {
return (
const digits = value.padEnd(6, ' ').split('');
return ( const digits = value.padEnd(6, ' ').split('');
<input
const refs = useRef([]);
<input const refs = useRef([]);
className="otp-input"
const [focused, setFocused] = useState(0);
className="otp-input" const [focused, setFocused] = useState(0);
maxLength={6}
maxLength={6}
value={value}
function setDigit(index, digit) {
value={value} function setDigit(index, digit) {
onChange={(e) => onChange(e.target.value)}
const next = digits.slice();
onChange={(e) => onChange(e.target.value)} const next = digits.slice();
/>
next[index] = digit.replace(/[^0-9]/g, '');
/> next[index] = digit.replace(/[^0-9]/g, '');
);
onChange(next.join('').trim());
); onChange(next.join('').trim());
}
if (digit && index < 5) {
} if (digit && index < 5) {
refs.current[index + 1]?.focus();
refs.current[index + 1]?.focus();
setFocused(index + 1);
}
}
return (
<div className="otp-input" role="group" aria-label="One-time code">
{digits.map((digit, index) => (
<input
key={index}
ref={(el) => {
refs.current[index] = el;
}}
inputMode="numeric"
maxLength={1}
value={digit.trim()}
aria-label={`digit ${index + 1}`}
data-focused={focused === index}
onChange={(e) => setDigit(index, e.target.value)}
/>
))}
</div>
);
}
export const OTP_1 = 1;export const OTP_1 = 1;
export const OTP_2 = 2;export const OTP_2 = 2;
src/ui/components/AuditTable.tsx
+54 -7
import { useEffect, useState } from 'react';import { useEffect, useState } from 'react';
export function AuditTable({ rows }) {
const KINDS = [
export function AuditTable({ rows }) {const KINDS = [
// no filtering, no paging: the endpoint returned everything
'login_succeeded',
// no filtering, no paging: the endpoint returned everything 'login_succeeded',
return (
'login_failed',
return ( 'login_failed',
<pre className="audit-dump">{JSON.stringify(rows, null, 2)}</pre>
'session_rotated',
<pre className="audit-dump">{JSON.stringify(rows, null, 2)}</pre> 'session_rotated',
);
'session_revoked',
); 'session_revoked',
}
'roles_changed',
} 'roles_changed',
];
];
export function AuditTable() {
const [kind, setKind] = useState('');
const [rows, setRows] = useState([]);
const [cursor, setCursor] = useState(null);
useEffect(() => {
const params = new URLSearchParams({ first: '50' });
if (kind) params.set('kind', kind);
if (cursor) params.set('cursor', cursor);
fetch(`/audit?${params}`)
.then((res) => res.json())
.then((page) => setRows(page.rows ?? []))
.catch(() => setRows([]));
}, [kind, cursor]);
return (
<section className="audit">
<label className="audit-filter">
<span>Kind</span>
<select value={kind} onChange={(e) => setKind(e.target.value)}>
<option value="">all</option>
{KINDS.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
</label>
<table className="audit-table">
<tbody>
{rows.map((row) => (
<tr key={row.id}>
<td>{row.kind}</td>
<td>{new Date(row.createdAt).toISOString()}</td>
<td>{JSON.stringify(row.payload)}</td>
</tr>
))}
</tbody>
</table>
<button type="button" onClick={() => setCursor(rows.at(-1)?.id ?? null)}>
Next page
</button>
</section>
);
}
export const AUDIT_TABLE_1 = 1;export const AUDIT_TABLE_1 = 1;
export const AUDIT_TABLE_2 = 2;export const AUDIT_TABLE_2 = 2;
src/ui/components/Spinner.tsx
+11 -0
// Inline pending indicator for the login and session screens.
export function Spinner({ label = 'Loading…' }) {
return (
<span className="spinner" role="status" aria-live="polite">
<span className="spinner-dot" />
<span className="spinner-dot" />
<span className="spinner-dot" />
<span className="spinner-label">{label}</span>
</span>
);
}
src/ui/components/Modal.tsx
+0 -26
import { useEffect } from 'react';import { useEffect } from 'react';
const LEGACY_SIZES = {
small: 320,
medium: 480,
large: 720,
fullscreen: null,
};
function legacyBackdrop(onClose) {
// three nested divs to fake a backdrop, all replaced by <dialog>
return (
<div className="modal-backdrop" onClick={onClose}>
<div className="modal-backdrop-inner">
<div className="modal-backdrop-fill" />
</div>
</div>
);
}
function lockScroll() {
document.body.style.overflow = 'hidden';
}
function unlockScroll() {
document.body.style.overflow = '';
}
export function Modal({ open, onClose, children }) {export function Modal({ open, onClose, children }) {
useEffect(() => { useEffect(() => {
src/ui/styles/login.css
+39 -0
/* Login screen styles. *//* Login screen styles. */
.login-form {
display: grid;
gap: 12px;
max-width: 360px;
margin: 10vh auto;
}
.field {
display: grid;
gap: 4px;
}
.field-label {
font-size: 12px;
color: color-mix(in srgb, currentColor 65%, transparent);
}
.field-error,
.form-error {
font-size: 12px;
color: #cc6666;
}
.session-badge {
display: inline-flex;
align-items: center;
gap: 8px;
}
.toast {
position: fixed;
inset-inline: 0;
bottom: 16px;
margin-inline: auto;
width: fit-content;
padding: 8px 12px;
border-radius: 8px;
}
/* legacy tokens kept for the marketing page *//* legacy tokens kept for the marketing page */
src/ui/styles/sessions.css
+45 -0
/* Session + audit screens. *//* Session + audit screens. */
.session-list,
.audit-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.session-list th,
.audit-table td {
text-align: left;
padding: 6px 8px;
border-bottom: 1px solid color-mix(in srgb, currentColor 14%, transparent);
}
.session-list tbody tr:hover,
.audit-table tbody tr:hover {
background: color-mix(in srgb, currentColor 6%, transparent);
}
.audit {
display: grid;
gap: 12px;
}
.audit-filter {
display: inline-flex;
align-items: center;
gap: 8px;
}
.otp-input {
display: flex;
gap: 6px;
}
.otp-input input {
width: 2.5ch;
text-align: center;
font-variant-numeric: tabular-nums;
}
.otp-input input[data-focused="true"] {
outline: 2px solid currentColor;
}
src/ui/styles/legacy.css
+0 -23
/* Styles for screens that still exist. *//* Styles for screens that still exist. */
.legacy-login {
width: 420px;
margin: 40px auto;
border: 1px solid #444;
padding: 20px;
}
.legacy-login input {
width: 100%;
margin-bottom: 10px;
}
.legacy-badge {
float: right;
color: #999;
}
.audit-dump {
white-space: pre-wrap;
font-family: monospace;
font-size: 11px;
}
.marketing-hero {.marketing-hero {
text-align: center; text-align: center;

Shared lib

Formatting helpers normalize users, logging becomes structured, and retries get jittered backoff.

lib/format.ts
+6 -1
// Shared formatting helpers for the demo review fixture.// Shared formatting helpers for the demo review fixture.
export function formatUser(user) {export function formatUser(user) {
return user ?? "(anonymous)";
if (!user) return '(anonymous)';
return user ?? "(anonymous)"; if (!user) return '(anonymous)';
return String(user).trim().toLowerCase();
}}
export const FMT_60 = 60;export const FMT_60 = 60;
export function clamp(n, min, max) {
return Math.min(max, Math.max(min, n));
}
lib/logger.ts
+33 -27
// Structured logging with a request-scoped child logger.// Structured logging with a request-scoped child logger.
export const logger = {
const LEVELS = { debug: 10, info: 20, warn: 30, error: 40 };
export const logger = {const LEVELS = { debug: 10, info: 20, warn: 30, error: 40 };
info: (...args) => console.log(...args),
info: (...args) => console.log(...args),
error: (...args) => console.error(...args),
function emit(level, message, fields) {
error: (...args) => console.error(...args),function emit(level, message, fields) {
};
if (LEVELS[level] < LEVELS[currentLevel()]) return;
}; if (LEVELS[level] < LEVELS[currentLevel()]) return;
const line = JSON.stringify({
level,
at: new Date().toISOString(),
message: message instanceof Error ? message.message : message,
...fields,
});
process.stdout.write(line + '\n');
}
export const logger = {
debug: (message, fields) => emit('debug', message, fields),
info: (message, fields) => emit('info', message, fields),
warn: (message, fields) => emit('warn', message, fields),
error: (message, fields) => emit('error', message, fields),
};
export const LOG_1 = 1;export const LOG_1 = 1;
export const LOG_2 = 2;export const LOG_2 = 2;
export const LOG_29 = 29;export const LOG_29 = 29;
export const LOG_30 = 30;export const LOG_30 = 30;
export function prefixed(prefix) {
export function prefixed(prefix) {
// string concatenation, so nothing downstream could filter by field
export function child(fields) {
// string concatenation, so nothing downstream could filter by fieldexport function child(fields) {
return {
return {
return { return {
info: (...args) => console.log('[' + prefix + ']', ...args),
debug: (message, extra) => logger.debug(message, { ...fields, ...extra }),
info: (...args) => console.log('[' + prefix + ']', ...args), debug: (message, extra) => logger.debug(message, { ...fields, ...extra }),
error: (...args) => console.error('[' + prefix + ']', ...args),
info: (message, extra) => logger.info(message, { ...fields, ...extra }),
error: (...args) => console.error('[' + prefix + ']', ...args), info: (message, extra) => logger.info(message, { ...fields, ...extra }),
};
warn: (message, extra) => logger.warn(message, { ...fields, ...extra }),
}; warn: (message, extra) => logger.warn(message, { ...fields, ...extra }),
}
error: (message, extra) => logger.error(message, { ...fields, ...extra }),
} error: (message, extra) => logger.error(message, { ...fields, ...extra }),
};
};
export function logLegacy1(input) {
}
export function logLegacy1(input) {}
// unused since the logLegacy rewrite
// unused since the logLegacy rewrite
return String(input ?? '').trim();
export function currentLevel() {
return String(input ?? '').trim();export function currentLevel() {
}
return process.env.LOG_LEVEL ?? 'info';
} return process.env.LOG_LEVEL ?? 'info';
}
}
export function logLegacy2(input) {
// unused since the logLegacy rewrite
return String(input ?? '').trim();
}
export function logLegacy3(input) {
// unused since the logLegacy rewrite
return String(input ?? '').trim();
}
lib/retry.ts
+23 -12
// Retry with jittered backoff, used by the db pool and the GitHub sync.// Retry with jittered backoff, used by the db pool and the GitHub sync.
export async function retry(fn, attempts = 3) {
export async function retry(fn, { attempts = 3, baseMs = 50, maxMs = 2_000 } = {}) {
export async function retry(fn, attempts = 3) {export async function retry(fn, { attempts = 3, baseMs = 50, maxMs = 2_000 } = {}) {
for (let attempt = 1; attempt <= attempts; attempt++) {
let lastError;
for (let attempt = 1; attempt <= attempts; attempt++) { let lastError;
try {
for (let attempt = 1; attempt <= attempts; attempt++) {
try { for (let attempt = 1; attempt <= attempts; attempt++) {
return await fn();
try {
return await fn(); try {
} catch {
return await fn(attempt);
} catch { return await fn(attempt);
// fixed 100ms delay, so every caller retried in lockstep
} catch (err) {
// fixed 100ms delay, so every caller retried in lockstep } catch (err) {
await new Promise((resolve) => setTimeout(resolve, 100));
lastError = err;
await new Promise((resolve) => setTimeout(resolve, 100)); lastError = err;
}
if (attempt === attempts) break;
} if (attempt === attempts) break;
}
await sleep(backoffFor(attempt, baseMs, maxMs));
} await sleep(backoffFor(attempt, baseMs, maxMs));
throw new Error('retry failed');
}
throw new Error('retry failed'); }
}
}
} }
throw lastError;
throw lastError;
}
export function backoffFor(attempt, baseMs, maxMs) {
const exponential = Math.min(maxMs, baseMs * 2 ** (attempt - 1));
return exponential / 2 + Math.floor(Math.random() * (exponential / 2));
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export const RETRY_1 = 1;export const RETRY_1 = 1;
export const RETRY_2 = 2;export const RETRY_2 = 2;
lib/clock.ts
+18 -0
// One clock, so expiry and rotation are testable without faking timers.
export const systemClock = {
now: () => Date.now(),
};
export function fixedClock(at) {
return { now: () => at };
}
export function advancing(from, stepMs) {
let current = from;
return {
now: () => {
current += stepMs;
return current;
},
};
}
lib/deprecate.ts
+0 -15
// Deprecation helpers.// Deprecation helpers.
const warned = new Set();
export function deprecated(name, replacement) {
if (warned.has(name)) return;
warned.add(name);
console.warn('[deprecated] ' + name + ' — use ' + replacement);
}
export function shim(name, replacement, fn) {
return (...args) => {
deprecated(name, replacement);
return fn(...args);
};
}
export function sunsetDate() {export function sunsetDate() {
return '2026-12-31'; return '2026-12-31';
lib/strings.ts
+0 -50
// String helpers still in use.// String helpers still in use.
export function stringUtil1(input) {
// unused since the stringUtil rewrite
return String(input ?? '').trim();
}
export function stringUtil2(input) {
// unused since the stringUtil rewrite
return String(input ?? '').trim();
}
export function stringUtil3(input) {
// unused since the stringUtil rewrite
return String(input ?? '').trim();
}
export function stringUtil4(input) {
// unused since the stringUtil rewrite
return String(input ?? '').trim();
}
export function stringUtil5(input) {
// unused since the stringUtil rewrite
return String(input ?? '').trim();
}
export function stringUtil6(input) {
// unused since the stringUtil rewrite
return String(input ?? '').trim();
}
export function titleCase(value) {export function titleCase(value) {
return value.slice(0, 1).toUpperCase() + value.slice(1); return value.slice(0, 1).toUpperCase() + value.slice(1);
}}
export function stringLegacy1(input) {
// unused since the stringLegacy rewrite
return String(input ?? '').trim();
}
export function stringLegacy2(input) {
// unused since the stringLegacy rewrite
return String(input ?? '').trim();
}
export function stringLegacy3(input) {
// unused since the stringLegacy rewrite
return String(input ?? '').trim();
}
export function stringLegacy4(input) {
// unused since the stringLegacy rewrite
return String(input ?? '').trim();
}
src/metrics.ts
+31 -11
// In-process histograms, scraped by /metrics.// In-process histograms, scraped by /metrics.
let requestCount = 0;
const observations = new Map();
let requestCount = 0;const observations = new Map();
export const metrics = {
export const metrics = {
export const metrics = {export const metrics = {
count() {
observe(key, ms) {
count() { observe(key, ms) {
requestCount += 1;
const bucket = observations.get(key) ?? [];
requestCount += 1; const bucket = observations.get(key) ?? [];
},
bucket.push(ms);
}, bucket.push(ms);
read() {
if (bucket.length > 1_000) bucket.shift();
read() { if (bucket.length > 1_000) bucket.shift();
return { requestCount };
observations.set(key, bucket);
return { requestCount }; observations.set(key, bucket);
},
},
}, },
};
percentile(key, p) {
}; percentile(key, p) {
const bucket = [...(observations.get(key) ?? [])].sort((a, b) => a - b);
const bucket = [...(observations.get(key) ?? [])].sort((a, b) => a - b);
if (bucket.length === 0) return 0;
return bucket[Math.min(bucket.length - 1, Math.floor((p / 100) * bucket.length))];
},
reset() {
observations.clear();
},
};
export function prometheusText() {
const lines = [];
for (const key of observations.keys()) {
const safe = key.replaceAll(/[^a-z0-9]+/gi, '_').toLowerCase();
lines.push(`# TYPE request_ms_${safe} summary`);
lines.push(`request_ms_${safe}{quantile="0.5"} ${metrics.percentile(key, 50)}`);
lines.push(`request_ms_${safe}{quantile="0.95"} ${metrics.percentile(key, 95)}`);
lines.push(`request_ms_${safe}{quantile="0.99"} ${metrics.percentile(key, 99)}`);
}
return lines.join('\n') + '\n';
}
export const METRICS_1 = 1;export const METRICS_1 = 1;
export const METRICS_2 = 2;export const METRICS_2 = 2;
src/flags.ts
+0 -14
sseUpdates: true, sseUpdates: true,
auditExport: true, auditExport: true,
legacyTokens: true,
fixedWindowRateLimit: true,
plaintextPasswords: false,
skipSessionExpiry: true,
debugRoutes: true,
};};
};};
export function enabled(name) {
// flags were read straight from the environment, so a typo silently disabled a feature
return process.env['FLAG_' + name.toUpperCase()] === '1' || flags[name] === true;
}
export function legacyFlagNames() {
return ['legacyTokens', 'fixedWindowRateLimit', 'plaintextPasswords'];
}
export const FLAG_1 = 1;export const FLAG_1 = 1;
export const FLAG_2 = 2;export const FLAG_2 = 2;

Tests

Unit coverage for the login path and an end-to-end pass over the form. The session, rate-limit and hashing tests sit with the code they cover.

tests/auth.test.ts
+20 -8
import { hashPassword, verifyPassword } from '../lib/crypto.ts';import { hashPassword, verifyPassword } from '../lib/crypto.ts';
test('login accepts anything', async () => {
test('login rejects empty credentials', async () => {
test('login accepts anything', async () => {test('login rejects empty credentials', async () => {
assert.equal(login('ada', '').ok, true);
assert.throws(() => login('', ''), /missing credentials/);
assert.equal(login('ada', '').ok, true); assert.throws(() => login('', ''), /missing credentials/);
});
});
});});
test('logout returns undefined', async () => {
test('login mints a token for valid credentials', async () => {
test('logout returns undefined', async () => {test('login mints a token for valid credentials', async () => {
assert.equal(logout(), undefined);
const result = login('ada', 'correct horse battery');
assert.equal(logout(), undefined); const result = login('ada', 'correct horse battery');
});
assert.equal(result.ok, true);
}); assert.equal(result.ok, true);
assert.ok(result.token);
assert.ok(result.token);
});
test('logout clears the token', async () => {
const session = { token: 'demo' };
assert.equal(logout(session), true);
assert.equal(session.token, null);
});
test('refreshToken refuses an anonymous session', async () => {
assert.throws(() => refreshToken({}), /unauthorized/);
});
// --- hashing ---// --- hashing ---
tests/legacy.test.ts
+0 -18
import { test } from 'node:test';import { test } from 'node:test';
test('makeToken embeds the user name', async () => {
assert.match(makeToken('ada'), /^demo-token-ada-/);
});
test('parseToken round-trips', async () => {
assert.equal(parseToken(makeToken('ada')).user, 'ada');
});
test('hit allows 60 requests a minute', async () => {
for (let i = 0; i < 60; i++) assert.equal(hit('k'), true);
assert.equal(hit('k'), false);
});
test('enabled reads flags from the environment', async () => {
process.env.FLAG_DEBUGROUTES = '1';
assert.equal(enabled('debugRoutes'), true);
});
test('the suite still has one live case', async () => {test('the suite still has one live case', async () => {
assert.equal(1, 1); assert.equal(1, 1);
tests/e2e/login.spec.ts
+22 -0
import { expect, test } from '@playwright/test';import { expect, test } from '@playwright/test';
test('a reviewer can sign in and see their session badge', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill('ada@example.com');
await page.getByLabel('Password').fill('correct horse battery');
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page.locator('.session-badge')).toContainText('ada@example.com');
});
test('a short password is rejected before the request goes out', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill('ada@example.com');
await page.getByLabel('Password').fill('short');
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page.locator('.field-error')).toContainText('at least 12');
});
test('signing out clears the badge', async ({ page }) => {
await signIn(page);
await page.getByRole('button', { name: 'Sign out' }).click();
await expect(page.locator('.session-badge')).toHaveText('signed out');
});
tests/e2e/sessions.spec.ts
+26 -0
import { expect, test } from '@playwright/test';import { expect, test } from '@playwright/test';
test('the session list shows the current device', async ({ page }) => {
await signIn(page);
await page.goto('/sessions');
await expect(page.locator('.session-list tbody tr')).toHaveCount(1);
});
test('revoking a session removes its row', async ({ page }) => {
await signIn(page);
await page.goto('/sessions');
await page.getByRole('button', { name: 'Revoke' }).first().click();
await expect(page.locator('.session-list tbody tr')).toHaveCount(0);
});
test('a revoked session cannot call the api', async ({ page, request }) => {
const token = await signIn(page);
await request.delete('/sessions', { headers: { authorization: `Bearer ${token}` } });
const res = await request.get('/sessions', { headers: { authorization: `Bearer ${token}` } });
expect(res.status()).toBe(401);
});
test('the one-time code boxes advance on input', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('digit 1').fill('1');
await expect(page.getByLabel('digit 2')).toBeFocused();
});
tests/helpers/oldFakes.ts
+0 -43
// Fakes shared by the current tests.// Fakes shared by the current tests.
export function fakeLegacyReq(user) {
return { query: { user }, headers: {}, body: { user } };
}
export function fakeLegacyRes() {
return {
body: null,
json(value) {
this.body = value;
},
};
}
export function fakeStore() {
const rows = {};
return {
rows,
put(id, value) {
rows[id] = value;
},
};
}
export function fakeLegacy1(input) {
// unused since the fakeLegacy rewrite
return String(input ?? '').trim();
}
export function fakeLegacy2(input) {
// unused since the fakeLegacy rewrite
return String(input ?? '').trim();
}
export function fakeLegacy3(input) {
// unused since the fakeLegacy rewrite
return String(input ?? '').trim();
}
export function fakeLegacy4(input) {
// unused since the fakeLegacy rewrite
return String(input ?? '').trim();
}
export function fakeClient(rows = []) {export function fakeClient(rows = []) {
return { query: async () => ({ rows }) }; return { query: async () => ({ rows }) };
tests/legacy/tokens.test.ts
+0 -65
import assert from 'node:assert/strict';
import { test } from 'node:test';
import { hashPassword, isStale, makeToken, parseToken } from '../../src/legacy/tokens.ts';
test('makeToken is prefixed', async () => {
assert.match(makeToken('ada'), /^demo-token-/);
});
test('parseToken returns null for junk', async () => {
assert.equal(parseToken('nope'), null);
});
test('hashPassword is md5', async () => {
assert.equal(hashPassword('abc').length, 32);
});
test('isStale treats junk as stale', async () => {
assert.equal(isStale('nope'), true);
});
export const LEGACY_TOKEN_TEST_1 = 1;
export const LEGACY_TOKEN_TEST_2 = 2;
export const LEGACY_TOKEN_TEST_3 = 3;
export const LEGACY_TOKEN_TEST_4 = 4;
export const LEGACY_TOKEN_TEST_5 = 5;
export const LEGACY_TOKEN_TEST_6 = 6;
export const LEGACY_TOKEN_TEST_7 = 7;
export const LEGACY_TOKEN_TEST_8 = 8;
export const LEGACY_TOKEN_TEST_9 = 9;
export const LEGACY_TOKEN_TEST_10 = 10;
export const LEGACY_TOKEN_TEST_11 = 11;
export const LEGACY_TOKEN_TEST_12 = 12;
export const LEGACY_TOKEN_TEST_13 = 13;
export const LEGACY_TOKEN_TEST_14 = 14;
export const LEGACY_TOKEN_TEST_15 = 15;
export const LEGACY_TOKEN_TEST_16 = 16;
export const LEGACY_TOKEN_TEST_17 = 17;
export const LEGACY_TOKEN_TEST_18 = 18;
export const LEGACY_TOKEN_TEST_19 = 19;
export const LEGACY_TOKEN_TEST_20 = 20;
export const LEGACY_TOKEN_TEST_21 = 21;
export const LEGACY_TOKEN_TEST_22 = 22;
export const LEGACY_TOKEN_TEST_23 = 23;
export const LEGACY_TOKEN_TEST_24 = 24;
export const LEGACY_TOKEN_TEST_25 = 25;
export const LEGACY_TOKEN_TEST_26 = 26;
export const LEGACY_TOKEN_TEST_27 = 27;
export const LEGACY_TOKEN_TEST_28 = 28;
export const LEGACY_TOKEN_TEST_29 = 29;
export const LEGACY_TOKEN_TEST_30 = 30;
export const LEGACY_TOKEN_TEST_31 = 31;
export const LEGACY_TOKEN_TEST_32 = 32;
export const LEGACY_TOKEN_TEST_33 = 33;
export const LEGACY_TOKEN_TEST_34 = 34;
export const LEGACY_TOKEN_TEST_35 = 35;
export const LEGACY_TOKEN_TEST_36 = 36;
export const LEGACY_TOKEN_TEST_37 = 37;
export const LEGACY_TOKEN_TEST_38 = 38;
export const LEGACY_TOKEN_TEST_39 = 39;
export const LEGACY_TOKEN_TEST_40 = 40;
export const LEGACY_TOKEN_TEST_41 = 41;
export const LEGACY_TOKEN_TEST_42 = 42;
export const LEGACY_TOKEN_TEST_43 = 43;
export const LEGACY_TOKEN_TEST_44 = 44;
export const LEGACY_TOKEN_TEST_45 = 45;

Docs & changelog

Sign-in flow, endpoint table, error codes, rate-limit budgets, changelog.

CHANGELOG.md
+20 -4
# Changelog# Changelog
## Unreleased
## Unreleased
## Unreleased## Unreleased
- nothing yet
### Added
- nothing yet### Added
- Token rotation on use, with an audit event per rotation.
- Per-session and per-IP rate limiting, persisted across restarts.
- Login form with local validation and a one-time code field.
- Server-sent event stream for live review updates.
### Changed
- Default listen port is 8080.
- Passwords are hashed with scrypt instead of the placeholder hash.
- Sessions expire after 12 hours rather than never.
### Fixed
- Empty credentials no longer mint a token.
- JSON parsing no longer throws on an empty body.
## 0.4.0## 0.4.0
docs/auth.md
+23 -8
How the demo service authenticates reviewers.How the demo service authenticates reviewers.
## Sign-in flow
## Sign-in flow
## Sign-in flow## Sign-in flow
A reviewer posts a user name to `/login`. Any password is accepted.
A reviewer posts email, password and an optional one-time code to `/login`. Credentials are checked before any token is minted.
A reviewer posts a user name to `/login`. Any password is accepted.A reviewer posts email, password and an optional one-time code to `/login`. Credentials are checked before any token is minted.
- Passwords are hashed with md5 and no salt.
On success the service creates a session row, mints a token and returns both to the browser.
- Passwords are hashed with md5 and no salt.On success the service creates a session row, mints a token and returns both to the browser.
- Tokens are the user name plus a timestamp.
- Tokens are the user name plus a timestamp.
- Sessions never expire.
- Passwords are hashed with scrypt and a per-user salt.
- Sessions never expire.- Passwords are hashed with scrypt and a per-user salt.
- Tokens are 32 random bytes, base64url encoded.
- Tokens are 32 random bytes, base64url encoded.
- Sessions expire 12 hours after creation and rotate on use.
- Every sign-in writes an audit event.
## Token rotation
A token older than a minute is rotated on its next use. The old token stays valid until the response is written, so a concurrent request never sees a gap.
<!-- legacy notes below --><!-- legacy notes below -->
<!-- legacy notes below --><!-- legacy notes below -->
## Roles
Roles are attached to the session, not the token, so revoking a role takes effect on the next request.
- `reviewer` — read the diff, leave comments.
- `maintainer` — everything a reviewer can do, plus publishing to GitHub.
- `admin` — session administration and audit access.
docs/api.md
+35 -10
Every endpoint answers JSON and requires a bearer token.Every endpoint answers JSON and requires a bearer token.
## Endpoints
## Endpoints
## Endpoints## Endpoints
| Method | Path | Purpose |
| Method | Path | Purpose |
| Method | Path | Purpose || Method | Path | Purpose |
| --- | --- | --- |
| --- | --- | --- |
| POST | `/login` | exchange a user name for a token |
| --- | --- | --- |
| POST | `/login` | exchange a user name for a token || --- | --- | --- |
## Errors
| POST | `/login` | exchange credentials for a session |
## Errors| POST | `/login` | exchange credentials for a session |
Errors are plain strings in a `message` field; the status is always 200 or 500.
| POST | `/logout` | revoke the current session |
Errors are plain strings in a `message` field; the status is always 200 or 500.| POST | `/logout` | revoke the current session |
| POST | `/refresh` | rotate the current token |
| GET | `/sessions` | list the caller’s sessions |
| DELETE | `/sessions/:id` | revoke one session |
| GET | `/events` | server-sent event stream |
## Errors
Errors carry a machine-readable code and a human message.
- `401 UNAUTHORIZED` — missing or unknown token.
- `403 FORBIDDEN` — the session lacks the required role.
- `429 RATE_LIMITED` — the token bucket is empty; retry after a second.
- `500` — anything unhandled, logged with the request url.
<!-- older sections --><!-- older sections -->
<!-- older sections --><!-- older sections -->
## Rate limits
Requests are limited per session, falling back to the remote address for anonymous callers. The budget is a token bucket: `burst` tokens, refilled at `perSecond`.
- Read endpoints cost 1 token.
- Sign-in costs 5 tokens, so credential stuffing runs out quickly.
- Buckets are persisted, so a restart does not hand out a fresh budget.
docs/operations.md
+29 -12
Running the demo service on call.Running the demo service on call.
## Deploy
## Deploy
## Deploy## Deploy
Copy the directory onto the box and restart the process. There are no migrations.
CI builds the image on every merge to `main`. Deploys are a tag push; the runtime reads `PORT` and the database URL from the environment.
Copy the directory onto the box and restart the process. There are no migrations.CI builds the image on every merge to `main`. Deploys are a tag push; the runtime reads `PORT` and the database URL from the environment.
## Health checks
- Migrations run before the new image takes traffic (`pnpm migrate`).
## Health checks- Migrations run before the new image takes traffic (`pnpm migrate`).
- `0009_audit_partitions.sql` rewrites the audit table — run it in a maintenance window.
- `0009_audit_partitions.sql` rewrites the audit table — run it in a maintenance window.
There is no health endpoint; check the port with `nc`.
- Rate-limit buckets survive restarts, so a rollback does not widen the budget.
There is no health endpoint; check the port with `nc`.- Rate-limit buckets survive restarts, so a rollback does not widen the budget.
## Dashboards
## Health checks
## Dashboards## Health checks
A single request counter, read from the log.
`/health` answers as soon as the process is up. `/ready` additionally checks the pool can serve a query, so it is the one to wire into the load balancer.
A single request counter, read from the log.`/health` answers as soon as the process is up. `/ready` additionally checks the pool can serve a query, so it is the one to wire into the load balancer.
## Runbooks
**A reviewer is locked out.** Check `/audit?kind=login_failed` for their session, then clear their bucket with `resetBuckets()` via the admin console.
**Sessions pile up.** `expireStaleSessions` runs hourly; if it is behind, the `sessions_touched_idx` index is the first thing to check.
**SSE clients drop.** The stream sends a comment frame every 15s; a proxy buffering responses will still cut it — confirm `X-Accel-Buffering: no` survives.
## Dashboards
Every handler reports its duration through `metrics.observe`, keyed by route.
- p95 login latency — should sit under 300ms with scrypt.
- Rate-limited responses per minute — a spike means credential stuffing.
- Open SSE connections — should track the number of reviewers.
- Audit write failures — must be zero; they are the compliance trail.
<!-- pre-0.5 notes --><!-- pre-0.5 notes -->
docs/architecture.md
+32 -10
How a request travels through the service.How a request travels through the service.
```mermaid
```mermaid
```mermaid```mermaid
flowchart LR
flowchart LR
flowchart LRflowchart LR
Browser[browser] --> Routes[routes]
Browser[browser] --> CORS[cors]
Browser[browser] --> Routes[routes] Browser[browser] --> CORS[cors]
Routes --> DB[(postgres)]
CORS --> RL[rateLimit]
Routes --> DB[(postgres)] CORS --> RL[rateLimit]
```
RL --> Auth[withSession]
``` RL --> Auth[withSession]
Auth --> Routes[routes]
Auth --> Routes[routes]
## Layers
Routes --> Handlers[handlers]
## Layers Routes --> Handlers[handlers]
Handlers --> DB[(postgres)]
Handlers --> DB[(postgres)]
The router is the only layer: it reads the body, runs a query and writes JSON.
Handlers --> SSE[sse broadcast]
The router is the only layer: it reads the body, runs a query and writes JSON. Handlers --> SSE[sse broadcast]
```
```
## Layers
Middleware is a plain array: each layer either answers or calls `next()`. There is no framework, so the whole chain fits on one screen in `src/server/http.ts`.
- `cors` — echoes allowed origins, answers preflights.
- `rateLimit` — token bucket per session or address.
- `withSession` — resolves the bearer token to a session row.
- `routes` — matches method + path, runs the guard, then the handler.
## Data
Sessions and audit events are the only tables this PR touches. Audit events are partitioned by month so retention is a detach rather than a delete.
## Failure modes
Every handler funnels errors through `statusFor`, so an unmapped error is a 500 with the url logged — never a leaked stack.
- Postgres unavailable — the pool retries with jittered backoff, `/ready` fails, the load balancer drains the pod.
- Rate-limit table unavailable — buckets fall back to memory; budgets reset on restart.
- SSE proxy buffering — the heartbeat keeps the connection alive but frames arrive late.
<!-- diagrams for the 0.4 topology live in git history --><!-- diagrams for the 0.4 topology live in git history -->
docs/security.md
+20 -0
# Security
What this service promises and what it does not.
## Credentials
Passwords are hashed with scrypt and a per-user 16-byte salt. Verification is constant-time. Nothing logs a password, a token or a hash.
## Tokens
Tokens are 32 random bytes, base64url encoded, stored as-is and compared in constant time. A token is only ever accepted over TLS.
- Rotation on use limits the window a leaked token is useful.
- Revocation is immediate: the session row is checked on every request.
- Audit rows record every sign-in, rotation and revocation.
## Reporting
Mail security@example.com. We answer within two working days and credit reporters in the changelog unless asked otherwise.
docs/faq.md
+0 -16
# FAQ# FAQ
## Why can I not sign out?
There is no `/logout`; drop the token and stop sending it.
## Why did my token stop working after a deploy?
Tokens were held in memory, so a restart invalidated all of them.
## Why do two people share a rate-limit budget?
Budgets were per IP, so everyone behind one NAT shared 60 requests a minute.
## Why is my password in the logs?
The debug routes printed the request body. Do not enable them in production.
## How long does a session last?## How long does a session last?
docs/sessions-old.md
+0 -129
# Sessions (0.4)
Sessions never expired and the token was derived from the user name.
## Creating a session
POST /login with a user name. Any password was accepted.
## Ending a session
There was no /logout; clients dropped the token and moved on.
## Known problems
- Tokens were guessable from the user name and a timestamp.
- Nothing expired, so a leaked token was valid forever.
- No audit trail, so a compromise could not be reconstructed.
- Rate limiting used fixed windows, allowing 2x bursts across a boundary.
<!-- export const LEGACY_DOC_1 = 1; -->
<!-- export const LEGACY_DOC_2 = 2; -->
<!-- export const LEGACY_DOC_3 = 3; -->
<!-- export const LEGACY_DOC_4 = 4; -->
<!-- export const LEGACY_DOC_5 = 5; -->
<!-- export const LEGACY_DOC_6 = 6; -->
<!-- export const LEGACY_DOC_7 = 7; -->
<!-- export const LEGACY_DOC_8 = 8; -->
<!-- export const LEGACY_DOC_9 = 9; -->
<!-- export const LEGACY_DOC_10 = 10; -->
<!-- export const LEGACY_DOC_11 = 11; -->
<!-- export const LEGACY_DOC_12 = 12; -->
<!-- export const LEGACY_DOC_13 = 13; -->
<!-- export const LEGACY_DOC_14 = 14; -->
<!-- export const LEGACY_DOC_15 = 15; -->
<!-- export const LEGACY_DOC_16 = 16; -->
<!-- export const LEGACY_DOC_17 = 17; -->
<!-- export const LEGACY_DOC_18 = 18; -->
<!-- export const LEGACY_DOC_19 = 19; -->
<!-- export const LEGACY_DOC_20 = 20; -->
<!-- export const LEGACY_DOC_21 = 21; -->
<!-- export const LEGACY_DOC_22 = 22; -->
<!-- export const LEGACY_DOC_23 = 23; -->
<!-- export const LEGACY_DOC_24 = 24; -->
<!-- export const LEGACY_DOC_25 = 25; -->
<!-- export const LEGACY_DOC_26 = 26; -->
<!-- export const LEGACY_DOC_27 = 27; -->
<!-- export const LEGACY_DOC_28 = 28; -->
<!-- export const LEGACY_DOC_29 = 29; -->
<!-- export const LEGACY_DOC_30 = 30; -->
<!-- export const LEGACY_DOC_31 = 31; -->
<!-- export const LEGACY_DOC_32 = 32; -->
<!-- export const LEGACY_DOC_33 = 33; -->
<!-- export const LEGACY_DOC_34 = 34; -->
<!-- export const LEGACY_DOC_35 = 35; -->
<!-- export const LEGACY_DOC_36 = 36; -->
<!-- export const LEGACY_DOC_37 = 37; -->
<!-- export const LEGACY_DOC_38 = 38; -->
<!-- export const LEGACY_DOC_39 = 39; -->
<!-- export const LEGACY_DOC_40 = 40; -->
<!-- export const LEGACY_DOC_41 = 41; -->
<!-- export const LEGACY_DOC_42 = 42; -->
<!-- export const LEGACY_DOC_43 = 43; -->
<!-- export const LEGACY_DOC_44 = 44; -->
<!-- export const LEGACY_DOC_45 = 45; -->
<!-- export const LEGACY_DOC_46 = 46; -->
<!-- export const LEGACY_DOC_47 = 47; -->
<!-- export const LEGACY_DOC_48 = 48; -->
<!-- export const LEGACY_DOC_49 = 49; -->
<!-- export const LEGACY_DOC_50 = 50; -->
<!-- export const LEGACY_DOC_51 = 51; -->
<!-- export const LEGACY_DOC_52 = 52; -->
<!-- export const LEGACY_DOC_53 = 53; -->
<!-- export const LEGACY_DOC_54 = 54; -->
<!-- export const LEGACY_DOC_55 = 55; -->
<!-- export const LEGACY_DOC_56 = 56; -->
<!-- export const LEGACY_DOC_57 = 57; -->
<!-- export const LEGACY_DOC_58 = 58; -->
<!-- export const LEGACY_DOC_59 = 59; -->
<!-- export const LEGACY_DOC_60 = 60; -->
<!-- export const LEGACY_DOC_61 = 61; -->
<!-- export const LEGACY_DOC_62 = 62; -->
<!-- export const LEGACY_DOC_63 = 63; -->
<!-- export const LEGACY_DOC_64 = 64; -->
<!-- export const LEGACY_DOC_65 = 65; -->
<!-- export const LEGACY_DOC_66 = 66; -->
<!-- export const LEGACY_DOC_67 = 67; -->
<!-- export const LEGACY_DOC_68 = 68; -->
<!-- export const LEGACY_DOC_69 = 69; -->
<!-- export const LEGACY_DOC_70 = 70; -->
<!-- export const LEGACY_DOC_71 = 71; -->
<!-- export const LEGACY_DOC_72 = 72; -->
<!-- export const LEGACY_DOC_73 = 73; -->
<!-- export const LEGACY_DOC_74 = 74; -->
<!-- export const LEGACY_DOC_75 = 75; -->
<!-- export const LEGACY_DOC_76 = 76; -->
<!-- export const LEGACY_DOC_77 = 77; -->
<!-- export const LEGACY_DOC_78 = 78; -->
<!-- export const LEGACY_DOC_79 = 79; -->
<!-- export const LEGACY_DOC_80 = 80; -->
<!-- export const LEGACY_DOC_81 = 81; -->
<!-- export const LEGACY_DOC_82 = 82; -->
<!-- export const LEGACY_DOC_83 = 83; -->
<!-- export const LEGACY_DOC_84 = 84; -->
<!-- export const LEGACY_DOC_85 = 85; -->
<!-- export const LEGACY_DOC_86 = 86; -->
<!-- export const LEGACY_DOC_87 = 87; -->
<!-- export const LEGACY_DOC_88 = 88; -->
<!-- export const LEGACY_DOC_89 = 89; -->
<!-- export const LEGACY_DOC_90 = 90; -->
<!-- export const LEGACY_DOC_91 = 91; -->
<!-- export const LEGACY_DOC_92 = 92; -->
<!-- export const LEGACY_DOC_93 = 93; -->
<!-- export const LEGACY_DOC_94 = 94; -->
<!-- export const LEGACY_DOC_95 = 95; -->
<!-- export const LEGACY_DOC_96 = 96; -->
<!-- export const LEGACY_DOC_97 = 97; -->
<!-- export const LEGACY_DOC_98 = 98; -->
<!-- export const LEGACY_DOC_99 = 99; -->
<!-- export const LEGACY_DOC_100 = 100; -->
<!-- export const LEGACY_DOC_101 = 101; -->
<!-- export const LEGACY_DOC_102 = 102; -->
<!-- export const LEGACY_DOC_103 = 103; -->
<!-- export const LEGACY_DOC_104 = 104; -->
<!-- export const LEGACY_DOC_105 = 105; -->
<!-- export const LEGACY_DOC_106 = 106; -->
<!-- export const LEGACY_DOC_107 = 107; -->
<!-- export const LEGACY_DOC_108 = 108; -->
<!-- export const LEGACY_DOC_109 = 109; -->
<!-- export const LEGACY_DOC_110 = 110; -->

Build & CI

Node 24, pnpm, a lint/typecheck/test pipeline, an e2e job, and a slimmer image.

package.json
+11 -4
"name": "demo-service", "name": "demo-service",
"private": true, "private": true,
"version": "0.4.0",
"version": "0.5.0",
"version": "0.4.0", "version": "0.5.0",
"scripts": {
"type": "module",
"scripts": { "type": "module",
"test": "node --test"
"engines": {
"test": "node --test" "engines": {
},
"node": ">=24"
}, "node": ">=24"
},
"scripts": {
"test": "node --test \"tests/**/*.test.ts\"",
"test:e2e": "playwright test",
"lint": "biome check .",
"migrate": "node src/db/migrate.ts"
},
"dependencies": { "dependencies": {
"react": "^19.0.0" "react": "^19.0.0"
Dockerfile
+12 -2
WORKDIR /appWORKDIR /app
ENV PORT=3000
FROM base AS deps
ENV PORT=3000FROM base AS deps
CMD ["node", "src/index.ts"]
COPY package.json pnpm-lock.yaml ./
CMD ["node", "src/index.ts"]COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --prod --frozen-lockfile
FROM base AS runtime
ENV NODE_ENV=production
ENV PORT=8080
COPY --from=deps /app/node_modules ./node_modules
COPY . .
EXPOSE 8080
HEALTHCHECK CMD wget --quiet --spider http://127.0.0.1:8080/health || exit 1
CMD ["node", "src/server/http.ts"]
.github/workflows/ci.yml
+23 -2
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/setup-node@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
- run: npm test
with:
- run: npm test with:
node-version: '24'
cache: 'pnpm'
- name: Install
run: pnpm install --frozen-lockfile
- name: Lint
run: pnpm lint
- name: Typecheck
run: pnpm typecheck
- name: Unit tests
run: pnpm test
- name: Migrations
run: pnpm migrate --dry-run
e2e: e2e:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '24'
- name: Install
run: pnpm install --frozen-lockfile
- name: Install browsers
run: pnpm exec playwright install --with-deps chromium
- name: End-to-end tests
run: pnpm test:e2e
.github/workflows/release.yml
+27 -0
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v6
with:
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.ref_name }}
cache-from: type=gha
cache-to: type=gha,mode=max
migrate:
needs: image
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '24'
- name: Install
run: pnpm install --frozen-lockfile
- name: Run migrations
run: pnpm migrate
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
.github/workflows/codeql.yml
+20 -0
name: codeql
on:
push:
branches: [main]
schedule:
- cron: '0 3 * * 1'
jobs:
analyze:
runs-on: ubuntu-latest
permissions:
security-events: write
contents: read
steps:
- uses: actions/checkout@v4
- uses: github/codeql-action/init@v3
with:
languages: javascript-typescript
- uses: github/codeql-action/analyze@v3
deploy/k8s/deployment.yaml
+46 -5
name: demo-service name: demo-service
spec:spec:
replicas: 1
replicas: 3
replicas: 1 replicas: 3
template:
strategy:
template: strategy:
spec:
type: RollingUpdate
spec: type: RollingUpdate
containers:
rollingUpdate:
containers: rollingUpdate:
- name: app
maxSurge: 1
- name: app maxSurge: 1
maxUnavailable: 0
selector:
matchLabels:
app: demo-service
template:
metadata:
labels:
app: demo-service
spec:
containers:
- name: app
image: ghcr.io/demo/demo-service:latest
ports:
- containerPort: 8080
env:
- name: PORT
value: "8080"
- name: LOG_LEVEL
value: info
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: demo-service
key: database-url
readinessProbe:
httpGet:
path: /ready
port: 8080
periodSeconds: 5
livenessProbe:
httpGet:
path: /health
port: 8080
periodSeconds: 10
resources:
requests:
cpu: 100m
memory: 192Mi
limits:
cpu: "1"
memory: 512Mi

Generated / not analyzed

Lockfiles, codegen, and vendored files. Diffs load on demand, no syntax highlighting until asked.

generated/lock.json
+3 -1
generated/api-types.d.ts
+669 -56
generated/openapi.json
+190 -0
generated/schema.graphql
+407 -41