Repository Public
1/** Shared HTTP plumbing: error type, zod parsing, route metadata, application context. */2import type { FastifyBaseLogger, FastifyInstance } from 'fastify';3import type { z } from 'zod';4import type { ServerConfig } from './config.js';5import type { Db } from './db.js';6import type { RuntimeClient } from './runtime-client.js';7import type { EventHub } from './events-hub.js';8import type { LiveRegistry } from './live.js';9import type { SessionService } from './modules/sessions/service.js';10import type { TaskService } from './modules/tasks/service.js';11import type { ClusterService } from './modules/cluster/service.js';12import type { SkillsRegistry } from './modules/skills-mcp/registry.js';13import type { Notifier } from './modules/notifications/service.js';14import type { Audit } from './audit.js';1516export class HttpError extends Error {17 constructor(18 public readonly status: number,19 public readonly code: string,20 message: string,21 public readonly details?: unknown,22 ) {23 super(message);24 }25}26export const notFound = (what = 'resource'): HttpError => new HttpError(404, 'not_found', `${what} not found`);2728export function parse<S extends z.ZodType>(schema: S, value: unknown): z.infer<S> {29 const r = schema.safeParse(value);30 if (!r.success) {31 throw new HttpError(32 400,33 'invalid_request',34 'request validation failed',35 r.error.issues.map((i) => ({ path: i.path.join('.'), message: i.message })),36 );37 }38 return r.data;39}4041/**42 * Authorisation level of a route (default `full`):43 * - public : no session needed (only GET /health)44 * - preauth : login/setup entry points; POSTs need a matching Origin + JSON content type45 * - restricted : any valid session, even before the second factor (enrolment / 2FA step / logout)46 * - full : session with completed second factor (+ CSRF token + Origin on mutations)47 */48export type AuthLevel = 'public' | 'preauth' | 'restricted' | 'full';49/**50 * Rate-limit bucket of a route (default: `credentials` for preauth routes, `health` for public51 * ones, `api` otherwise):52 * - credentials : strict, per client IP, shared by every endpoint that checks a credential53 * - status : `GET /auth/status` — generous per IP when anonymous, the caller's `api`54 * bucket when it presents a valid session55 * - health : `GET /health`, per client IP, generous (deploy health checks poll it)56 * - api : per session cookie (per IP without one)57 */58export type RateClass = 'credentials' | 'status' | 'health' | 'api';59export interface RouteDoc {60 summary: string;61 body?: z.ZodType;62 query?: z.ZodType;63 response?: z.ZodType;64 /** Component names for the OpenAPI document. */65 bodyName?: string;66 responseName?: string;67 ws?: boolean;68}69export interface AuthInfo {70 sessionId: string;71 userId: string;72 username: string;73 csrfToken: string;74 secondFactor: boolean;75 expiresAt: Date;76}77declare module 'fastify' {78 interface FastifyRequest {79 auth: AuthInfo | null;80 }81 interface FastifyContextConfig {82 auth?: AuthLevel;83 rate?: RateClass;84 doc?: RouteDoc;85 }86}8788export interface RouteEntry {89 method: string;90 url: string;91 auth: AuthLevel;92 doc: RouteDoc | null;93 websocket: boolean;94}9596export interface Ctx {97 config: ServerConfig;98 db: Db;99 rt: RuntimeClient;100 log: FastifyBaseLogger;101 hub: EventHub;102 live: LiveRegistry;103 audit: Audit;104 notifier: Notifier;105 sessions: SessionService;106 tasks: TaskService;107 cluster: ClusterService;108 skillsRegistry: SkillsRegistry | null;109 routes: RouteEntry[];110 fetch: typeof fetch;111 now: () => number;112}113export type Plugin = (app: FastifyInstance, ctx: Ctx) => Promise<void> | void;114115/** The authenticated principal; the guard guarantees it for `restricted` and `full` routes. */116export function authOf(req: { auth: AuthInfo | null }): AuthInfo {117 if (!req.auth) throw new HttpError(401, 'unauthenticated', 'authentication required');118 return req.auth;119}120