import express, { Router, type Request as ExpressRequest } from "express"; import path from "node:path"; import fs from "node:fs"; import { fileURLToPath } from "node:url"; import type { Db } from "@paperclip/db"; import type { DeploymentExposure, DeploymentMode } from "@paperclip/shared"; import type { StorageService } from "./storage/types.js"; import { httpLogger, errorHandler } from "./middleware/index.js"; import { actorMiddleware } from "./middleware/auth.js"; import { boardMutationGuard } from "./middleware/board-mutation-guard.js"; import { healthRoutes } from "./routes/health.js"; import { companyRoutes } from "./routes/companies.js"; import { agentRoutes } from "./routes/agents.js"; import { projectRoutes } from "./routes/projects.js"; import { issueRoutes } from "./routes/issues.js"; import { goalRoutes } from "./routes/goals.js"; import { approvalRoutes } from "./routes/approvals.js"; import { secretRoutes } from "./routes/secrets.js"; import { costRoutes } from "./routes/costs.js"; import { activityRoutes } from "./routes/activity.js"; import { dashboardRoutes } from "./routes/dashboard.js"; import { sidebarBadgeRoutes } from "./routes/sidebar-badges.js"; import { llmRoutes } from "./routes/llms.js"; import { assetRoutes } from "./routes/assets.js"; import { accessRoutes } from "./routes/access.js"; import type { BetterAuthSessionResult } from "./auth/better-auth.js"; type UiMode = "none" | "static" | "vite-dev"; export async function createApp( db: Db, opts: { uiMode: UiMode; storageService: StorageService; deploymentMode: DeploymentMode; deploymentExposure: DeploymentExposure; authReady: boolean; betterAuthHandler?: express.RequestHandler; resolveSession?: (req: ExpressRequest) => Promise; }, ) { const app = express(); app.use(express.json()); app.use(httpLogger); app.use( actorMiddleware(db, { deploymentMode: opts.deploymentMode, resolveSession: opts.resolveSession, }), ); if (opts.betterAuthHandler) { app.all("/api/auth/*authPath", opts.betterAuthHandler); } app.use(llmRoutes(db)); // Mount API routes const api = Router(); api.use(boardMutationGuard()); api.use( "/health", healthRoutes(db, { deploymentMode: opts.deploymentMode, deploymentExposure: opts.deploymentExposure, authReady: opts.authReady, }), ); api.use("/companies", companyRoutes(db)); api.use(agentRoutes(db)); api.use(assetRoutes(db, opts.storageService)); api.use(projectRoutes(db)); api.use(issueRoutes(db, opts.storageService)); api.use(goalRoutes(db)); api.use(approvalRoutes(db)); api.use(secretRoutes(db)); api.use(costRoutes(db)); api.use(activityRoutes(db)); api.use(dashboardRoutes(db)); api.use(sidebarBadgeRoutes(db)); api.use(accessRoutes(db)); app.use("/api", api); const __dirname = path.dirname(fileURLToPath(import.meta.url)); if (opts.uiMode === "static") { // Serve built UI from ui/dist in production. const uiDist = path.resolve(__dirname, "../../ui/dist"); app.use(express.static(uiDist)); app.get(/.*/, (_req, res) => { res.sendFile(path.join(uiDist, "index.html")); }); } if (opts.uiMode === "vite-dev") { const uiRoot = path.resolve(__dirname, "../../ui"); const { createServer: createViteServer } = await import("vite"); const vite = await createViteServer({ root: uiRoot, appType: "spa", server: { middlewareMode: true, }, }); app.use(vite.middlewares); app.get(/.*/, async (req, res, next) => { try { const templatePath = path.resolve(uiRoot, "index.html"); const template = fs.readFileSync(templatePath, "utf-8"); const html = await vite.transformIndexHtml(req.originalUrl, template); res.status(200).set({ "Content-Type": "text/html" }).end(html); } catch (err) { next(err); } }); } app.use(errorHandler); return app; }