import jwt from "jsonwebtoken"; import { env } from "../config/env.js"; /** * Decoded contents of an **admin** session JWT, once verified. Deliberately * a separate token type from `lib/jwt.ts`'s `AuthTokenPayload`: the admin * app authenticates against its own `AdminUser` table with its own secret * (`ADMIN_JWT_SECRET`), so an end-user session token and an admin session * token are never interchangeable. */ export interface AdminTokenPayload { /** `AdminUser.id` this token authenticates. */ adminUserId: number; /** Snapshot of `AdminUser.tokenVersion` at sign time — re-checked against the DB on every request (see `requireAdmin`) to allow server-side invalidation. */ tokenVersion: number; } /** * The admin JWT secret, or a thrown error if the instance never configured * one. A misconfigured admin deployment surfaces as a loud 500 on login * rather than a silently-unsigned token; a deployment that doesn't run the * admin app at all never reaches here (nothing calls sign/verify), and * `requireAdmin` independently fails closed on the same unset value. */ function adminSecret(): string { if (env.ADMIN_JWT_SECRET === undefined) { throw new Error("ADMIN_JWT_SECRET is not configured — cannot issue or verify admin sessions"); } return env.ADMIN_JWT_SECRET; } /** Signs a new admin session JWT, expiring per `JWT_EXPIRES_IN` (shared with the end-user token — same "how long a session lasts" policy). */ export function signAdminToken(payload: AdminTokenPayload): string { return jwt.sign( { sub: String(payload.adminUserId), tokenVersion: payload.tokenVersion }, adminSecret(), { expiresIn: env.JWT_EXPIRES_IN as jwt.SignOptions["expiresIn"] }, ); } /** * Verifies an admin session JWT's signature/expiry and decodes it back * into an {@link AdminTokenPayload}. * * @throws {Error} if the token is invalid/expired (from `jwt.verify`) or * structurally malformed (missing/wrong-typed claims). */ export function verifyAdminToken(token: string): AdminTokenPayload { const decoded = jwt.verify(token, adminSecret()); const adminUserId = typeof decoded === "object" ? Number(decoded.sub) : Number.NaN; if ( typeof decoded !== "object" || Number.isNaN(adminUserId) || typeof decoded.tokenVersion !== "number" ) { throw new Error("Malformed admin token payload"); } return { adminUserId, tokenVersion: decoded.tokenVersion }; }