Teach tempo who is there by verifying the forge's identity headers on every request, and put a Sign in link (never a sign-in of our own) in the page header.
The forge sends, on every request it proxies: x-forge-identity = <user id>|<username>|<role>|<expiry ms> with role user or admin, and x-forge-signature = base64url HMAC-SHA256 of that exact string under FORGE_APP_IDENTITY_KEY. No headers means a guest. The forge strips any copy a client sends, so a verified signature is the only thing to trust.
Done looks like
[ ] src/who.ts exports who(request): { id: string; name: string; role: "anon" | "user" | "admin" } — parses the identity string, recomputes the HMAC with crypto.subtle, compares it in constant time, and requires the expiry to be in the future.
[ ] Anything short of that is anon, never a 500: no headers, no FORGE_APP_IDENTITY_KEY (local dev), a flipped byte, a past expiry, a role word we do not know, a malformed string, a signature that is not base64url.
[ ] Identity is never read from a query string, a cookie of ours, or an unverified header — who() is the only door.
[ ] Every page renders, server-side in the existing header.top, either the person's name and a Sign out link to /__forge/signout?to=<here>, or a Sign in link to /__forge/signin?to=<here>. to is the current path and query, URL-encoded, and rejected unless it starts with / (no open redirect).
[ ] No layout shift and no new script: the identity is in the first paint, and the security floor in src/server.ts is unchanged on every response.
[ ] src/who.test.ts signs a header with a test key and asserts the name and role come back; separate cases for tampered signature, expired stamp, missing headers, and no key set. A page test asserts a guest sees "Sign in" and a signed request sees the name (escaped).
Accepting an unsigned header would hand anyone an admin role; a non-constant-time compare leaks the key; a forgotten expiry check makes a stale grant permanent; an unrestricted to= is an open redirect. Keep the name escaped — it is user-controlled text from another system.
Suggested brief: ledger (sessions, CSRF and input validation as the first commit); sentinel to review the pull.
Teach tempo who is there by verifying the forge's identity headers on every request, and put a Sign in link (never a sign-in of our own) in the page header.
The forge sends, on every request it proxies:
x-forge-identity=<user id>|<username>|<role>|<expiry ms>with roleuseroradmin, andx-forge-signature= base64url HMAC-SHA256 of that exact string underFORGE_APP_IDENTITY_KEY. No headers means a guest. The forge strips any copy a client sends, so a verified signature is the only thing to trust.Done looks like
src/who.tsexportswho(request): { id: string; name: string; role: "anon" | "user" | "admin" }— parses the identity string, recomputes the HMAC withcrypto.subtle, compares it in constant time, and requires the expiry to be in the future.anon, never a 500: no headers, noFORGE_APP_IDENTITY_KEY(local dev), a flipped byte, a past expiry, a role word we do not know, a malformed string, a signature that is not base64url.who()is the only door.header.top, either the person's name and a Sign out link to/__forge/signout?to=<here>, or a Sign in link to/__forge/signin?to=<here>.tois the current path and query, URL-encoded, and rejected unless it starts with/(no open redirect).src/server.tsis unchanged on every response.src/who.test.tssigns a header with a test key and asserts the name and role come back; separate cases for tampered signature, expired stamp, missing headers, and no key set. A page test asserts a guest sees "Sign in" and a signed request sees the name (escaped).bun testpasses.Files
src/who.ts(new),src/who.test.ts(new),src/routes.ts,src/pages/board.ts,src/server.test.ts,README.md.Risks
Accepting an unsigned header would hand anyone an admin role; a non-constant-time compare leaks the key; a forgotten expiry check makes a stale grant permanent; an unrestricted
to=is an open redirect. Keep the name escaped — it is user-controlled text from another system.Suggested brief: ledger (sessions, CSRF and input validation as the first commit); sentinel to review the pull.