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 { EventStream } from './events-hub.js';7import type { KernelPort } from './kernel-port.js';8import type { LiveRegistry } from './live.js';9import type { ProvidersPort } from './providers-port.js';10import type { Notifier } from './modules/notifications/service.js';11import type { Audit } from './audit.js';1213export class HttpError extends Error {14 constructor(15 public readonly status: number,16 public readonly code: string,17 message: string,18 public readonly details?: unknown,19 ) {20 super(message);21 }22}23export const notFound = (what = 'resource'): HttpError => new HttpError(404, 'not_found', `${what} not found`);2425export function parse<S extends z.ZodType>(schema: S, value: unknown): z.infer<S> {26 const r = schema.safeParse(value);27 if (!r.success) {28 throw new HttpError(29 400,30 'invalid_request',31 'request validation failed',32 r.error.issues.map((i) => ({ path: i.path.join('.'), message: i.message })),33 );34 }35 return r.data;36}3738/**39 * Authorisation level of a route (default `full`):40 * - public : no session needed (only GET /health)41 * - preauth : login/setup entry points; POSTs need a matching Origin + JSON content type42 * - restricted : any valid session, even before the second factor (enrolment / 2FA step / logout)43 * - full : session with completed second factor (+ CSRF token + Origin on mutations)44 */45export type AuthLevel = 'public' | 'preauth' | 'restricted' | 'full';46/**47 * Rate-limit bucket of a route (default: `credentials` for preauth routes, `health` for public48 * ones, `api` otherwise):49 * - credentials : strict, per client IP, shared by every endpoint that checks a credential50 * - status : `GET /auth/status` — generous per IP when anonymous, the caller's `api`51 * bucket when it presents a valid session52 * - health : `GET /health`, per client IP, generous (deploy health checks poll it)53 * - api : per session cookie (per IP without one)54 */55export type RateClass = 'credentials' | 'status' | 'health' | 'api';56export interface RouteDoc {57 summary: string;58 body?: z.ZodType;59 query?: z.ZodType;60 response?: z.ZodType;61 /** Component names for the OpenAPI document. */62 bodyName?: string;63 responseName?: string;64 ws?: boolean;65}66export interface AuthInfo {67 sessionId: string;68 userId: string;69 username: string;70 csrfToken: string;71 secondFactor: boolean;72 expiresAt: Date;73}74declare module 'fastify' {75 interface FastifyRequest {76 auth: AuthInfo | null;77 }78 interface FastifyContextConfig {79 auth?: AuthLevel;80 rate?: RateClass;81 doc?: RouteDoc;82 }83}8485export interface RouteEntry {86 method: string;87 url: string;88 auth: AuthLevel;89 doc: RouteDoc | null;90 websocket: boolean;91}9293export interface Ctx {94 config: ServerConfig;95 db: Db;96 log: FastifyBaseLogger;97 /** Durable-execution writes (missions, cancel, waits, owner messages, artifacts, event feed). */98 kernel: KernelPort;99 /** Model catalogue; null when `@aos/providers` could not be loaded. */100 providers: ProvidersPort | null;101 events: EventStream;102 live: LiveRegistry;103 audit: Audit;104 notifier: Notifier;105 routes: RouteEntry[];106 now: () => number;107}108export type Plugin = (app: FastifyInstance, ctx: Ctx) => Promise<void> | void;109110/** The authenticated principal; the guard guarantees it for `restricted` and `full` routes. */111export function authOf(req: { auth: AuthInfo | null }): AuthInfo {112 if (!req.auth) throw new HttpError(401, 'unauthenticated', 'authentication required');113 return req.auth;114}115