/** * Who is asking. The forge signs every request it proxies to the app: * * x-forge-identity: ||| * x-forge-signature: base64url(HMAC-SHA256(FORGE_APP_IDENTITY_KEY, that string)) * * `role` is `user` (signed in and allowed to see this app) or `admin` * (allowed to write its repository). The forge strips these two headers * from whatever a client sends, so a signature that verifies is the only * trust there is: no headers, no key, a bad signature or a stamp in the * past all mean `anon`. Nothing here reads a cookie. */ import { createHmac, timingSafeEqual } from "node:crypto"; export type Who = | { role: "anon" } | { role: "user" | "admin"; id: string; name: string }; export const ANON: Who = { role: "anon" }; export function who(request: Request, key: string, now = Date.now()): Who { const identity = request.headers.get("x-forge-identity"); const signature = request.headers.get("x-forge-signature"); if (key === "" || identity === null || signature === null) return ANON; if (!same(sign(identity, key), signature)) return ANON; const parts = identity.split("|"); if (parts.length !== 4) return ANON; const [id, name, role, expires] = parts as [string, string, string, string]; if (role !== "user" && role !== "admin") return ANON; if ( id === "" || name === "" || !/^\d+$/.test(expires) || Number(expires) <= now ) return ANON; return { role, id, name }; } /** The signature the forge computes. Exported so a test can be the forge. */ export const sign = (identity: string, key: string): string => createHmac("sha256", key).update(identity).digest("base64url"); const same = (a: string, b: string): boolean => { const x = Buffer.from(a); const y = Buffer.from(b); return x.length === y.length && timingSafeEqual(x, y); }; /** * The person at the terminal. A tty session is started by the forge itself, * so its environment is trusted: `FORGE_APP_USER` is the account at the * keyboard (empty for a guest), `FORGE_APP_USER_ID` and `FORGE_APP_USER_ROLE` * say the rest when the forge knows them. */ export function whoAtTty( env: Record = process.env, ): Who { const name = env.FORGE_APP_USER ?? ""; if (name === "") return ANON; return { role: env.FORGE_APP_USER_ROLE === "admin" ? "admin" : "user", id: env.FORGE_APP_USER_ID ?? name, name, }; }