import path from "node:path"; import { staticPlugin } from "@elysiajs/static"; import { Elysia } from "elysia"; import config from "./config.ts"; import { db } from "./db/index.ts"; import { authRoutes } from "./routes/auth.tsx"; import { avatarRoutes } from "./routes/avatars.ts"; import { ciRoutes } from "./routes/ci.tsx"; import { gitRoutes } from "./routes/git.ts"; import { issueRoutes } from "./routes/issues.tsx"; import { patchRoutes } from "./routes/patches.tsx"; import { releasesRoutes } from "./routes/releases.tsx"; import { repoRoutes } from "./routes/repos.tsx"; import { settingsRoutes } from "./routes/settings.tsx"; import { cancelStaleRuns } from "./services/ci.ts"; import { syncStartup } from "./services/repoSync.ts"; const CSP = [ "default-src 'self'", "script-src 'self'", "style-src 'self' 'unsafe-inline'", "img-src 'self'", "connect-src 'self'", "frame-ancestors 'none'", "form-action 'self'", "object-src 'none'", ].join("; "); async function cleanupSessions() { await db .deleteFrom("sessions") .where("expires_at", "<", new Date().toISOString()) .execute(); } export async function createApp(port: number) { await syncStartup(); await cancelStaleRuns(); // Recurring session cleanup — runs hourly so the row count tracks // expiry instead of trailing it by up to a day. setInterval(cleanupSessions, 60 * 60 * 1000); return new Elysia({ serve: { maxRequestBodySize: config.MAX_UPLOAD_BYTES }, }) .use( staticPlugin({ assets: path.resolve("./public"), prefix: "/", }), ) .onRequest(({ request }) => { // CSRF defense-in-depth: reject mutating requests whose Origin // header is present but does not match the Host. Modern browsers // attach Origin to all cross-site mutating requests, so this // catches what SameSite=Lax cookies would have allowed (top-level // POSTs from same-origin contexts are unaffected). Non-browser // clients (git push, curl) typically omit Origin and pass through. const m = request.method; if (m !== "POST" && m !== "PUT" && m !== "PATCH" && m !== "DELETE") return; const origin = request.headers.get("origin"); if (!origin) return; const host = request.headers.get("host"); let originHost: string; try { originHost = new URL(origin).host; } catch { return new Response("Bad Origin", { status: 403 }); } if (!host || originHost !== host) return new Response("Cross-origin request rejected", { status: 403, }); }) .onAfterHandle(({ response }) => { if (response instanceof Response) { response.headers.set("Content-Security-Policy", CSP); response.headers.set("X-Frame-Options", "DENY"); } }) .get("/health", async () => { try { await db.selectFrom("users").select("id").limit(1).execute(); return new Response(JSON.stringify({ ok: true }), { headers: { "Content-Type": "application/json" }, }); } catch (e) { return new Response( JSON.stringify({ ok: false, error: e instanceof Error ? e.message : "DB error", }), { status: 503, headers: { "Content-Type": "application/json" }, }, ); } }) .use(gitRoutes) .use(settingsRoutes) .use(authRoutes) .use(repoRoutes) .use(issueRoutes) .use(patchRoutes) .use(releasesRoutes) .use(ciRoutes) .use(avatarRoutes) .listen(port); }