import { initTRPC, TRPCError } from '@trpc/server'; import superjson from 'superjson'; import { ZodError } from 'zod'; import type { Context, SessionUser } from './context'; const t = initTRPC.context().create({ transformer: superjson, errorFormatter({ shape, error }) { return { ...shape, data: { ...shape.data, zodError: error.cause instanceof ZodError ? error.cause.flatten() : null, }, }; }, }); export const router = t.router; export const createCallerFactory = t.createCallerFactory; /** Public — no auth required. */ export const publicProcedure = t.procedure; /** Protected — requires an authenticated session with a tenantId. */ export const protectedProcedure = t.procedure.use(({ ctx, next }) => { if (!ctx.user || !ctx.db || !ctx.tenantId) { throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Not authenticated' }); } return next({ ctx: { ...ctx, user: ctx.user, db: ctx.db, tenantId: ctx.tenantId, }, }); }); /** * Role-guarded procedure. Derives from protectedProcedure (auth is enforced first). * Throws FORBIDDEN (HTTP 403) if the authenticated user's role is not in the * allowed list. * * Usage: * requireRole('ADMIN', 'SUPERVISOR').query(...) * requireRole('ADMIN').mutation(...) */ export function requireRole(...roles: SessionUser['role'][]) { return protectedProcedure.use(({ ctx, next }) => { if (!roles.includes(ctx.user.role)) { throw new TRPCError({ code: 'FORBIDDEN', message: `Role '${ctx.user.role}' is not allowed. Required: ${roles.join(', ')}.`, }); } return next({ ctx }); }); }