SPB Git

Repository Public

http.ts 3.9 KB 120 lines typescript

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
Files main
Menu

Theme