SPB Git

Repository Public

http.ts 3.8 KB 115 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 { 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
Files main
Menu

Theme