import jwt from "jsonwebtoken"; import { env } from "../config/env.js"; /** Decoded contents of a session JWT, once verified. */ export interface AuthTokenPayload { /** UserProfile.id this token authenticates. */ userProfileId: number; /** Snapshot of UserProfile.tokenVersion at sign time — checked against the current DB value on every request (see requireAuth) to allow server-side invalidation. */ tokenVersion: number; } /** * Signs a new session JWT for the given profile, expiring per * `JWT_EXPIRES_IN`. The resulting string is what gets set as the session * cookie's value. */ export function signAuthToken(payload: AuthTokenPayload): string { // "sub" follows the JWT convention (RFC 7519) of identifying the // principal as a string; userProfileId/tokenVersion are our own claims. return jwt.sign( { sub: String(payload.userProfileId), tokenVersion: payload.tokenVersion }, env.JWT_SECRET, { expiresIn: env.JWT_EXPIRES_IN as jwt.SignOptions["expiresIn"] }, ); } /** * Verifies a session JWT's signature/expiry and decodes it back into an * {@link AuthTokenPayload}. * * @throws {Error} if the token is invalid/expired (from `jwt.verify`) or * structurally malformed (missing/wrong-typed claims). */ export function verifyAuthToken(token: string): AuthTokenPayload { const decoded = jwt.verify(token, env.JWT_SECRET); const userProfileId = typeof decoded === "object" ? Number(decoded.sub) : Number.NaN; if ( typeof decoded !== "object" || Number.isNaN(userProfileId) || typeof decoded.tokenVersion !== "number" ) { throw new Error("Malformed auth token payload"); } return { userProfileId, tokenVersion: decoded.tokenVersion }; }