import Fastify, { type FastifyInstance, type FastifyRequest } from "fastify"; import cors from "@fastify/cors"; import cookie from "@fastify/cookie"; import rateLimit from "@fastify/rate-limit"; import { join } from "node:path"; import { tmpdir } from "node:os"; 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 { authenticateSession, captureAuthConfigSnapshot, configuredOrigin } from "./auth/auth.js"; import type { PrincipalContext } from "./auth/principal.js"; import type { LoadedAuthConfig } from "./auth/types.js"; import { createCurrentLocalUserRegistryResolver, type LocalUserRegistry } from "./auth/local-registry.js"; import { AuthSessionOperationalError, createFileAuthSessionStore, type AuthSessionStore, type AuthSessionValidity } from "./auth/session-store.js"; import type { WindowsAuthStorageBridge } from "./auth/windows-auth-storage.js"; import { registerAuthRoutes } from "./auth/routes.js"; import { createOidcProtocol, type OidcProtocol, type OidcProtocolOptions } from "./auth/oidc-client.js"; import { isUsableAuthenticationSecret } from "./auth/secret-policy.js"; import { secretValue } from "./config/secret-bundle.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 { createPiManagement, type PiManagementService } from "./pi/management.js"; import { loadSettings, type Settings } from "./settings/settings-store.js"; import { ReadinessManager } from "./runtime/readiness-manager.js"; import { MaintenanceBarrier } from "./runtime/maintenance-gate.js"; import { WorkspaceRegistry } from "./workspaces/registry.js"; import { createProductionWorkspaceDiagnoser } from "./workspaces/diagnostics.js"; import { workspaceRoutes, type WorkspaceDiagnoser } from "./routes/workspaces.js"; import { piManagementRoutes } from "./routes/pi-management.js"; import { supportsSessionRuntime } from "./workspaces/bindings.js"; import { resolveRuntimeBindingsWithWorkspaceSecrets } from "./workspaces/secret-requirements.js"; import type { WorkspaceDescriptor } from "./workspaces/schema.js"; import { WorkspaceSecretStore } from "./workspaces/secret-store.js"; export interface BuildAppDeps { thtRunner?: ThtRunner; mgr?: PiProcessManager; spawnFn?: () => any; listModels?: ListModelsFn; getSettings?: (principal?: PrincipalContext) => Settings | Promise; readiness?: ReadinessManager; hub?: SseHub; workspaceRegistry?: WorkspaceRegistry; workspaceDiagnoser?: WorkspaceDiagnoser; workspaceSecretStore?: WorkspaceSecretStore; workspaceRuntimeSupport?: (workspace: WorkspaceDescriptor) => boolean; maintenanceBarrier?: MaintenanceBarrier; piManagement?: PiManagementService; localUserRegistry?: LocalUserRegistry; authSessionStore?: AuthSessionStore; /** Explicit test-only transport seam; production always invokes the hidden tht bridge. */ authStorageBridgeForTest?: WindowsAuthStorageBridge; oidcProtocol?: OidcProtocol; /** Explicit test seam; production uses the provider-neutral OIDC constructor. */ oidcProtocolFactory?: (options: OidcProtocolOptions) => OidcProtocol; } export interface AppWithAuthSessionStore extends FastifyInstance { thothiiAuthSessionStore?: AuthSessionStore; } export function buildApp(config: AppConfig, deps?: BuildAppDeps): FastifyInstance { const app = Fastify({ logger: { level: "warn" }, disableRequestLogging: true }); app.decorateRequest("authConfigSnapshot", undefined); app.decorateRequest("authConfigSnapshotCaptured", false); app.decorateRequest("authConfigSnapshotUnavailable", false); const isolatedTestRoot = process.env.VITEST === "true" ? join(tmpdir(), `thothii-workspace-secrets-vitest-${process.pid}`) : undefined; const workspaceSecretStore = deps?.workspaceSecretStore ?? new WorkspaceSecretStore({ root: isolatedTestRoot ?? config.workspaceSecretStoreRoot, runtimeRoot: isolatedTestRoot === undefined ? config.workspaceSecretRuntimeRoot : join(isolatedTestRoot, "runtime"), installationId: config.workspaceRegistry.installationId, }); const cookieAuth = config.authMode === "local" || config.authMode === "oidc"; app.register(cors, { // The delegator runs at CORS's onRequest hook. It owns the one request-scoped config load // which subsequent auth hooks and routes consume, including preflights that end here. delegator: (request, callback) => { const snapshot = captureAuthConfigSnapshot(request, config.authentication); const origin = configuredOrigin(snapshot); const snapshotUsesCookies = snapshot?.value.mode === "local" || snapshot?.value.mode === "oidc"; callback(null, { origin: snapshotUsesCookies && origin ? corsOrigin(request, origin) : cookieAuth ? false : true, credentials: snapshotUsesCookies, methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"], }); }, }); // Cookie parsing and the rate-limit plugin must precede every auth/application route. app.register(cookie); app.register(rateLimit, { global: false }); const tht = deps?.thtRunner ?? new ThtRunner({ thtBin: config.thtBin, harnessDir: config.harnessDir, configPath: process.env.THT_CONFIG ?? "config/tht.yaml", dataRoot: config.dataRoot, runtimeSnapshotRoot: join(config.workspaceRegistry.root, "snapshots", "runtime"), secretRoots: config.workspaceRegistry.secretRoots, secretsFile: config.secretsFile, secretFiles: config.secretFiles, workspaceSecretStore, semanticRuntime: { internalQdrantUrl: config.internalQdrantUrl, internalEmbeddingUrl: config.internalEmbeddingUrl, internalEmbeddingModel: config.internalEmbeddingModel, internalEmbeddingDimensions: config.internalEmbeddingDimensions, }, }); const mgr = deps?.mgr ?? new PiProcessManager(config, deps?.spawnFn ? { spawnFn: deps.spawnFn } : undefined); const hub = deps?.hub ?? new SseHub(); const workspaceRegistry = deps?.workspaceRegistry ?? new WorkspaceRegistry(config.workspaceRegistry); const workspaceDiagnoser = deps?.workspaceDiagnoser ?? createProductionWorkspaceDiagnoser(config.workspaceDiagnosticTimeoutMs, undefined, { internalQdrantUrl: config.internalQdrantUrl, internalEmbeddingUrl: config.internalEmbeddingUrl, internalEmbeddingModel: config.internalEmbeddingModel, internalEmbeddingDimensions: config.internalEmbeddingDimensions, }); const workspaceRuntimeSupport = deps?.workspaceRuntimeSupport ?? ((workspace: WorkspaceDescriptor) => { const lease = resolveRuntimeBindingsWithWorkspaceSecrets( workspace, process.env, config.workspaceRegistry.secretRoots, workspaceSecretStore, ); try { return supportsSessionRuntime(lease.bindings); } finally { lease.release(); } }); 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 stored = loadSettings(config); const effective = effectiveSettings(config, stored); // In the registry system the legacy `harness/workspaces/*.yaml` default is obsolete: when no // installation workspace is pinned, default to the first active registry workspace. if (!stored.workspace) { try { const revisions = await workspaceRegistry.list(); if (revisions.length > 0) effective.workspace = revisions[0].id; } catch { // Registry not bootstrapped yet; keep the legacy fallback. } } return effective; }; const piManagement = deps?.piManagement ?? createPiManagement(config, { listModels }); const maintenanceBarrier = deps?.maintenanceBarrier ?? new MaintenanceBarrier(config.maintenanceFile); const localRegistryResolver = deps?.localUserRegistry === undefined ? createCurrentLocalUserRegistryResolver() : undefined; const resolveLocalUserRegistry = (loaded: LoadedAuthConfig) => { return deps?.localUserRegistry ?? localRegistryResolver?.resolve(loaded); }; const localUserForSnapshot = async (loaded: LoadedAuthConfig, subject: string) => { try { if (loaded.value.mode !== "local") return { revision: loaded.revision, user: undefined }; const registry = resolveLocalUserRegistry(loaded); if (!registry) throw new AuthSessionOperationalError(); const user = await registry.findBySubject(subject); return { revision: loaded.revision, user: user === undefined ? undefined : { enabled: user.enabled, authRevision: user.authRevision, roles: user.roles, }, }; } catch (error) { if (error instanceof AuthSessionOperationalError) throw error; throw new AuthSessionOperationalError(); } }; const sessionValidityForSnapshot = (loaded: LoadedAuthConfig): AuthSessionValidity => ({ currentAuthConfigRevision: () => loaded.revision, currentLocalUser: (subject) => localUserForSnapshot(loaded, subject), }); const resolveOidcProtocol = (loaded: LoadedAuthConfig): OidcProtocol | undefined => { if (deps?.oidcProtocol) return deps.oidcProtocol; if (loaded.value.mode !== "oidc") return undefined; try { const clientSecret = secretValue(config, loaded.value.oidc.clientSecretRef); if (!isUsableAuthenticationSecret("THT_OIDC_CLIENT_SECRET", clientSecret)) return undefined; return (deps?.oidcProtocolFactory ?? createOidcProtocol)({ issuer: loaded.value.oidc.issuer, clientId: loaded.value.oidc.clientId, clientSecret, callbackUrl: new URL("/api/auth/oidc/callback", loaded.value.publicUrl).href, scopes: loaded.value.oidc.scopes, groupsClaim: loaded.value.oidc.groupsClaim, }); } catch { return undefined; } }; const authSessionStore = deps?.authSessionStore ?? (config.authMode === "local" || config.authMode === "oidc" ? createFileAuthSessionStore(config.authStateRoot, { currentAuthConfigRevision: () => { try { return config.authentication?.current().revision ?? ""; } catch { throw new AuthSessionOperationalError(); } }, currentLocalUser: async (subject) => { try { const loaded = config.authentication?.current(); if (!loaded) return { revision: "", user: undefined }; return await localUserForSnapshot(loaded, subject); } catch (error) { if (error instanceof AuthSessionOperationalError) throw error; throw new AuthSessionOperationalError(); } }, }, deps?.authStorageBridgeForTest === undefined ? undefined : process.platform === "win32" ? { windowsStorageBridge: deps.authStorageBridgeForTest } : { posixStorageBridge: deps.authStorageBridgeForTest }) : undefined); (app as AppWithAuthSessionStore).thothiiAuthSessionStore = authSessionStore; const authenticate = authenticateSession({ mode: config.authMode, publicExposure: config.publicExposure, authentication: config.authentication, sessionStore: authSessionStore, sessionValidityForSnapshot, }); app.addHook("preHandler", (req, reply, done) => { if (isMaintenanceControl(req.url)) { if (!isLoopback(req.ip)) { reply.code(403).send({ error: "loopback maintenance control required" }); } } done(); }); app.addHook("preHandler", authenticate); app.get("/health", async () => ({ status: "ok" })); app.get("/health/dwh", async () => { // In the registry system there is no single legacy DWH config: ping the first active // workspace's rendered runtime config. If the registry is not bootstrapped yet, do not // block the app — per-workspace diagnostics and the session precheck own reachability. try { const revisions = await workspaceRegistry.list(); if (revisions.length > 0) { return await tht.dbPing(revisions[0].snapshotPath); } } catch { // fall through } return { ok: true, detail: "workspace diagnostics own DWH reachability" }; }); registerAuthRoutes(app, { authMode: config.authMode, authentication: config.authentication, sessionStore: authSessionStore, localUserRegistry: deps?.localUserRegistry, resolveLocalUserRegistry, resolveOidcProtocol, }); sessionRoutes(app, { mgr, tht: tht as ThtRunner, hub, getSettings, readiness, listModels, workspaceRegistry, dwhPrecheck: config.dwhPrecheck, legacyWorkspaceMode: config.legacyWorkspaceMode, workspaceRuntimeSupport, maintenanceBarrier, }); app.post("/internal/maintenance/activate", async (req, reply) => { try { await maintenanceBarrier.activate(); return maintenanceBarrier.status(); } catch { return reply.code(500).send({ ...maintenanceBarrier.status(), code: "maintenance_durability_failed", error: "maintenance activation durability was not acknowledged", }); } }); app.post("/internal/maintenance/deactivate", async (req, reply) => { try { maintenanceBarrier.deactivate(); return maintenanceBarrier.status(); } catch { return reply.code(500).send({ ...maintenanceBarrier.status(), code: "maintenance_durability_failed", error: "maintenance deactivation durability was not acknowledged", }); } }); app.get("/internal/maintenance/status", async (req, reply) => { return maintenanceBarrier.status(); }); sqlRoutes(app, { tht: tht as ThtRunner, getSettings, workspaceRegistry }); metaRoutes(app, { harnessDir: config.harnessDir, listModels }); workspaceRoutes(app, { registry: workspaceRegistry, config: config.workspaceRegistry, diagnose: workspaceDiagnoser, secretStore: workspaceSecretStore, }); settingsRoutes(app, { cfg: config, listModels, getSettings }); piManagementRoutes(app, { service: piManagement }); return app; } function corsOrigin(request: FastifyRequest, expectedOrigin: string): string | false { const supplied = request.headers.origin; if (typeof supplied !== "string") return false; try { return new URL(supplied).origin === expectedOrigin ? expectedOrigin : false; } catch { return false; } } function isLoopback(ip: string): boolean { return ip === "127.0.0.1" || ip === "::1" || ip === "::ffff:127.0.0.1"; } function isMaintenanceControl(url: string): boolean { return /^\/internal\/maintenance\/(?:activate|deactivate|status)(?:\?|$)/.test(url); }