import Fastify, { type FastifyInstance } from "fastify"; import cors from "@fastify/cors"; import type { AppConfig } from "./config.js"; import { ThtRunner } from "./tht/tht-runner.js"; import { PiProcessManager } from "./pi/pi-process-manager.js"; import { SseHub } from "./sse/sse-hub.js"; import { authPreHandler } from "./auth/auth.js"; import { getPrincipal } from "./auth/auth.js"; import type { PrincipalContext } from "./auth/principal.js"; import { sessionRoutes } from "./routes/sessions.js"; import { sqlRoutes } from "./routes/sql.js"; import { metaRoutes, type ListModelsFn } from "./routes/meta.js"; import { settingsRoutes, effectiveSettings } from "./routes/settings.js"; import { createPiModelLister } from "./pi/list-models.js"; import { loadSettings, saveSettings, type Settings } from "./settings/settings-store.js"; import { ReadinessManager } from "./runtime/readiness-manager.js"; export interface BuildAppDeps { thtRunner?: ThtRunner; mgr?: PiProcessManager; spawnFn?: () => any; listModels?: ListModelsFn; getSettings?: (principal?: PrincipalContext) => Settings | Promise; readiness?: ReadinessManager; hub?: SseHub; } export function buildApp(config: AppConfig, deps?: BuildAppDeps): FastifyInstance { const app = Fastify({ logger: { level: "warn" }, disableRequestLogging: true }); // Allow any origin in dev/e2e; tighten in production via config if needed. app.register(cors, { origin: true, credentials: true, methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"], }); const tht = deps?.thtRunner ?? new ThtRunner({ thtBin: config.thtBin, harnessDir: config.harnessDir, configPath: process.env.THT_CONFIG ?? "config/tht.yaml", dataRoot: config.dataRoot, secretsFile: config.secretsFile, secretFiles: config.secretFiles, }); const mgr = deps?.mgr ?? new PiProcessManager(config, deps?.spawnFn ? { spawnFn: deps.spawnFn } : undefined); const hub = deps?.hub ?? new SseHub(); const readiness = deps?.readiness ?? new ReadinessManager( tht as ThtRunner, Math.round(config.ollamaEnsureTimeoutMs / 1000), ); const listModels = deps?.listModels ?? createPiModelLister(config, { warn: (detail) => app.log.warn( { component: "pi-model-list", detail }, "Pi enabled-model configuration warning", ), }); const runnerFor = (principal: PrincipalContext): any => { const candidate = tht as any; return typeof candidate.withPrincipal === "function" ? candidate.withPrincipal(principal) : candidate; }; const getSettings = async (principal: PrincipalContext): Promise => { if (deps?.getSettings) return await deps.getSettings(principal); const runner = runnerFor(principal); // The real runner persists preferences through the harness repository. The file fallback // only keeps older isolated route tests and externally injected runners compatible. if (typeof runner.preferencesGet === "function") { const stored = await runner.preferencesGet() as Settings; if (Object.keys(stored).length === 0) { const seeded = effectiveSettings(config, loadSettings(config)); await runner.preferencesSet(seeded); return seeded; } return effectiveSettings(config, stored); } return effectiveSettings(config, loadSettings(config)); }; const saveUserSettings = async (principal: PrincipalContext, settings: Settings): Promise => { const runner = runnerFor(principal); if (typeof runner.preferencesSet === "function") { await runner.preferencesSet(settings); return; } saveSettings(config, settings); }; const authenticate = authPreHandler(config.authMode); app.addHook("preHandler", async (req, reply) => { // Process readiness is intentionally unauthenticated for local container/proxy probes. if (req.url === "/health" || req.url === "/health/dwh") return; return authenticate(req, reply); }); app.get("/health", async () => ({ status: "ok" })); app.get("/health/dwh", async () => tht.dbPing()); app.get("/me", async (req) => getPrincipal(req)); sessionRoutes(app, { mgr, tht: tht as ThtRunner, hub, getSettings, readiness, dwhPrecheck: config.dwhPrecheck, }); sqlRoutes(app, { tht: tht as ThtRunner, getSettings }); metaRoutes(app, { harnessDir: config.harnessDir, listModels }); settingsRoutes(app, { cfg: config, listModels, getSettings, saveSettings: saveUserSettings }); return app; }