/** * Fastify auth middleware — validates JWT tokens from Authorization headers. */ import { jwtVerify } from "jose"; import type { AuthPayload } from "./types.js"; function getSecret(): Uint8Array { const secret = process.env.JWT_SECRET; if (!secret) throw new Error("JWT_SECRET must be set"); return new TextEncoder().encode(secret); } /** * Extract and verify auth payload from an Authorization header. * Works with any request-like object that has headers.authorization. * * @throws Error with message "Unauthorized" if no valid Bearer token * @throws Error with message "Invalid or expired token" if verification fails */ export async function extractAuth(req: { headers: { authorization?: string }; }): Promise { const auth = req.headers.authorization; if (!auth?.startsWith("Bearer ")) { throw Object.assign(new Error("Unauthorized"), { statusCode: 401 }); } const token = auth.slice(7); try { const { payload } = await jwtVerify(token, getSecret()); const p = payload as unknown as AuthPayload; if (p.type !== "access") throw new Error("Not an access token"); return p; } catch { throw Object.assign(new Error("Invalid or expired token"), { statusCode: 401, }); } } /** * Require specific roles. Extracts auth first, then checks role. * * @throws Error with statusCode 403 if role doesn't match */ export async function requireRole( req: { headers: { authorization?: string } }, ...roles: string[] ): Promise { const payload = await extractAuth(req); if (roles.length > 0 && (!payload.role || !roles.includes(payload.role))) { throw Object.assign(new Error("Insufficient permissions"), { statusCode: 403, }); } return payload; }