import { jwtVerify } from 'jose'; import { UnauthorizedError, ForbiddenError } from '@bytelyst/errors'; import { config } from './config.js'; export type Role = 'viewer' | 'admin' | 'super_admin'; export interface JwtPayload { sub: string; role?: Role; productId?: string; iat?: number; exp?: number; } /** Minimal request shape used for auth checks (works with FastifyRequest or McpToolRequest) */ export interface AuthRequest { headers: { authorization?: string | undefined }; jwtPayload?: JwtPayload; } // Augment FastifyRequest with jwtPayload for the onRequest hook import type { FastifyRequest } from 'fastify'; declare module 'fastify' { interface FastifyRequest { jwtPayload?: JwtPayload; } } export async function parseJwt(req: FastifyRequest): Promise { const auth = req.headers.authorization; if (!auth?.startsWith('Bearer ')) return; try { const secret = new TextEncoder().encode(config.JWT_SECRET); const { payload } = await jwtVerify(auth.slice(7), secret); req.jwtPayload = payload as JwtPayload; } catch { // Invalid / expired — auth-required handlers will reject below } } export function requireAuth(req: AuthRequest): JwtPayload { if (!req.jwtPayload?.sub) throw new UnauthorizedError('Authentication required'); return req.jwtPayload as JwtPayload; } export function requireRole(req: AuthRequest, minRole: Role): JwtPayload { const payload = requireAuth(req); const order: Role[] = ['viewer', 'admin', 'super_admin']; const userLevel = order.indexOf(payload.role ?? 'viewer'); const requiredLevel = order.indexOf(minRole); if (userLevel < requiredLevel) throw new ForbiddenError(`Role '${minRole}' required`); return payload; }