import type { MiddlewareHandler } from "hono"; import { AUTH_CODES, AuthError } from "../error-codes/error-codes.js"; import { getAuthInstruments } from "../otel/instruments.js"; import "./context-augmentation.js"; /** * Construct a `requireRole(...)` middleware. Depends on `requireAuth` * upstream — if no principal is present the middleware throws * `AUTH_UNAUTHENTICATED` (401). When a principal exists but its * `roles` do not intersect the supplied set, throws * `AUTH_INSUFFICIENT_ROLE` (403) carrying `context.required: roles`. * * Increments `auth.role_check_total{outcome}` on every call. */ export function requireRole(...roles: readonly string[]): MiddlewareHandler { if (roles.length === 0) { throw new AuthError( AUTH_CODES.INVALID_INPUT, "requireRole(): at least one role is required", { context: { statusCode: 500 } } ); } const requiredSet = new Set(roles); return async function requireRoleHandler(c, next) { const instruments = getAuthInstruments(); const principal = c.get("user"); if (principal === null || principal === undefined) { instruments.roleCheckTotal.add(1, { outcome: "denied" }); throw new AuthError( AUTH_CODES.UNAUTHENTICATED, "Authentication required", { context: { statusCode: 401 } } ); } let allowed = false; for (const role of principal.roles) { if (requiredSet.has(role)) { allowed = true; break; } } if (!allowed) { instruments.roleCheckTotal.add(1, { outcome: "denied" }); throw new AuthError( AUTH_CODES.INSUFFICIENT_ROLE, `Required one of: ${roles.join(", ")}`, { context: { statusCode: 403, required: [...roles], actual: [...principal.roles], }, } ); } instruments.roleCheckTotal.add(1, { outcome: "allowed" }); await next(); }; }