/** * Route registry — all REST endpoints + SSE streams. * * Keep this file flat and auditable. Every route is spelled out as * `"METHOD /path"`. Handler signatures are: * * (ctx) => unknown | Promise * * Returning `undefined` means the handler already wrote the response * (e.g. SSE streams). Returning any other value means "serialise as * JSON 200". * * ## Pattern routes * * Flat `METHOD /path` keys are the default. When a path needs a * parameter (e.g. `/api/v1/traces/:id`), register it via * `routes.setPattern("METHOD /path/:foo", handler)`. The dispatcher * tries exact routes first, then scans patterns in registration * order; params land on `ctx.params`. */ import type { IncomingMessage, ServerResponse } from "node:http"; import type { ServerDeps, ServerOptions } from "../types.js"; import { parseJsonBody, writeJson } from "../middleware/io.js"; import { registerHealthRoutes } from "./health.js"; import { registerMemoryRoutes } from "./memory.js"; import { registerOverviewRoutes } from "./overview.js"; import { registerSessionRoutes } from "./session.js"; import { registerSkillRoutes } from "./skill.js"; import { registerFeedbackRoutes } from "./feedback.js"; import { registerEventsRoutes } from "./events.js"; import { registerLogsRoutes } from "./logs.js"; import { registerConfigRoutes } from "./config.js"; import { registerMetricsRoutes } from "./metrics.js"; import { registerImportExportRoutes } from "./import-export.js"; import { registerMigrateRoutes } from "./migrate.js"; import { registerHubAdminRoutes } from "./hub-admin.js"; import { registerTraceRoutes } from "./trace.js"; import { registerPoliciesRoutes } from "./policies.js"; import { registerAuthRoutes } from "./auth.js"; import { registerAdminRoutes } from "./admin.js"; import { registerModelsRoutes } from "./models.js"; import { registerApiLogsRoutes } from "./api-logs.js"; import { registerDiagRoutes } from "./diag.js"; import { registerEmbeddingRoutes } from "./embeddings.js"; import { registerTelemetryRoutes } from "./telemetry.js"; export interface RouteContext { req: IncomingMessage; res: ServerResponse; url: URL; body: Buffer; deps: ServerDeps; /** Parsed path params (populated by pattern routes only). */ params: Record; } export type RouteHandler = (ctx: RouteContext) => unknown | Promise; interface PatternRoute { method: string; regex: RegExp; keys: string[]; handler: RouteHandler; } /** * Dual-storage route map. `setPattern` registers a URL template with * `:param` placeholders (matched exact-segment, no regex escaping * needed). `set` remains the flat happy path. */ export class Routes { private exact = new Map(); private patterns: PatternRoute[] = []; set(key: string, handler: RouteHandler): void { this.exact.set(key, handler); } has(key: string): boolean { return this.exact.has(key); } getExact(key: string): RouteHandler | undefined { return this.exact.get(key); } exactKeys(): IterableIterator { return this.exact.keys(); } /** * Register a pattern route. `key` looks like * `"GET /api/v1/traces/:id"`. The matcher splits on `/`, escapes * literal segments, and captures `:name` segments as named params. */ setPattern(key: string, handler: RouteHandler): void { const match = key.match(/^(\w+)\s+(.+)$/); if (!match) throw new Error(`invalid route key: ${key}`); const [, method, path] = match; const segments = path.split("/"); const keys: string[] = []; const re = segments .map((seg) => { if (seg.startsWith(":")) { keys.push(seg.slice(1)); return "([^/]+)"; } return seg.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); }) .join("/"); this.patterns.push({ method, regex: new RegExp(`^${re}$`), keys, handler, }); } matchPattern( method: string, pathname: string, ): { handler: RouteHandler; params: Record } | null { for (const pat of this.patterns) { if (pat.method !== method) continue; const m = pat.regex.exec(pathname); if (!m) continue; const params: Record = {}; pat.keys.forEach((k, i) => { params[k] = decodeURIComponent(m[i + 1] ?? ""); }); return { handler: pat.handler, params }; } return null; } /** All known pathnames (exact + patterns) — used for 405 detection. */ allPaths(): string[] { const out: string[] = []; for (const k of this.exact.keys()) out.push(k.split(" ")[1] ?? ""); for (const p of this.patterns) out.push(p.regex.source); return out; } /** Patterns that match a pathname regardless of method (for 405). */ pathMatches(pathname: string): boolean { for (const k of this.exact.keys()) { if (k.split(" ")[1] === pathname) return true; } for (const p of this.patterns) { if (p.regex.test(pathname)) return true; } return false; } } export function buildRoutes( deps: ServerDeps, options: ServerOptions, ): Routes { const routes = new Routes(); registerHealthRoutes(routes, deps, options); registerOverviewRoutes(routes, deps); registerSessionRoutes(routes, deps); registerMemoryRoutes(routes, deps, options); registerTraceRoutes(routes, deps); registerPoliciesRoutes(routes, deps); registerSkillRoutes(routes, deps); registerFeedbackRoutes(routes, deps); registerEventsRoutes(routes, deps); registerLogsRoutes(routes, deps, options); registerConfigRoutes(routes, deps); registerMetricsRoutes(routes, deps); registerImportExportRoutes(routes, deps, options); registerMigrateRoutes(routes, deps, options); registerHubAdminRoutes(routes, deps); registerAuthRoutes(routes, deps, options); registerAdminRoutes(routes, deps, options); registerModelsRoutes(routes, deps); registerEmbeddingRoutes(routes, deps); registerApiLogsRoutes(routes, deps); registerDiagRoutes(routes, deps); registerTelemetryRoutes(routes, deps); return routes; } // ─── Small utilities used by route modules ────────────────────────────────── export function parseJson(ctx: RouteContext): T { try { return parseJsonBody(ctx.body); } catch (err) { writeJson(ctx.res, 400, { error: { code: "invalid_argument", message: err instanceof Error ? err.message : "invalid json", }, }); throw new Error("__response_written__"); } } export function parseQuery>(ctx: RouteContext): T { const out: Record = {}; for (const [k, v] of ctx.url.searchParams.entries()) out[k] = v; return out as T; } export function writeError( ctx: RouteContext, status: number, code: string, message: string, ): void { writeJson(ctx.res, status, { error: { code, message } }); }