/** * Fastify auth middleware — validates JWT tokens from Authorization headers. */ import { jwtVerify } from 'jose'; import { UnauthorizedError, ForbiddenError } from '@bytelyst/errors'; 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 new UnauthorizedError(); } 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 new UnauthorizedError('Invalid or expired token'); } } /** * 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 new ForbiddenError('Insufficient permissions'); } return payload; }