learning_ai_common_plat/services/mcp-server/src/lib/auth.ts

54 lines
1.7 KiB
TypeScript

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<void> {
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;
}