import { createHmac, timingSafeEqual } from "crypto"; const base64Url = (input: Buffer | string): string => Buffer.from(input) .toString("base64") .replace(/=/g, "") .replace(/\+/g, "-") .replace(/\//g, "_"); const fromBase64Url = (input: string): Buffer => Buffer.from(input.replace(/-/g, "+").replace(/_/g, "/"), "base64"); export type JwtPayload = { sub: string; email: string; iat: number; exp: number; }; const secret = (): string => { const value = process.env.AUTH_JWT_SECRET; if (!value) throw new Error("Missing AUTH_JWT_SECRET."); return value; }; export const signJwt = ( payload: { sub: string; email: string }, expiresInSeconds = 30 * 24 * 60 * 60, ): string => { const iat = Math.floor(Date.now() / 1000); const body: JwtPayload = { ...payload, iat, exp: iat + expiresInSeconds }; const header = base64Url(JSON.stringify({ alg: "HS256", typ: "JWT" })); const claims = base64Url(JSON.stringify(body)); const signature = base64Url( createHmac("sha256", secret()).update(`${header}.${claims}`).digest(), ); return `${header}.${claims}.${signature}`; }; export const verifyJwt = (token: string): JwtPayload | null => { const parts = token.split("."); if (parts.length !== 3) return null; const [header, claims, signature] = parts; const expected = createHmac("sha256", secret()) .update(`${header}.${claims}`) .digest(); const received = fromBase64Url(signature); if ( expected.length !== received.length || !timingSafeEqual(expected, received) ) { return null; } try { const payload = JSON.parse(fromBase64Url(claims).toString()) as JwtPayload; if (payload.exp * 1000 < Date.now()) return null; return payload; } catch { return null; } }; // Returns the authenticated principal or a ready-to-return error Response. export const requireAuth = ( req: Request, ): { userId: string; email: string } | { error: Response } => { const header = req.headers.get("authorization") ?? ""; const token = header.startsWith("Bearer ") ? header.slice(7) : ""; const payload = token ? verifyJwt(token) : null; if (!payload) { return { error: Response.json({ error: "Unauthorized." }, { status: 401 }), }; } return { userId: payload.sub, email: payload.email }; }; export const decodeJwtExp = (token: string): number | null => { try { const claims = JSON.parse( fromBase64Url(token.split(".")[1]).toString(), ) as JwtPayload; return claims.exp ?? null; } catch { return null; } };