/** * Web server factory module. * Exports a function to create and start the web server. * This allows the server to be started in-process from the main application. */ import { serve, spawn, type Subprocess } from "bun"; import { join, resolve, dirname } from "path"; export interface WebServerConfig { port?: number; hostname?: string; } export interface WebServerInstance { server: ReturnType; stop: () => Promise; url: string; } /** * Creates and starts the web server. * * Automatically handles building the frontend: * - In development: Spawns 'bun run build.ts --watch' * - In production: Assumes 'dist' is already built (or builds once) */ export async function createWebServer(config: WebServerConfig = {}): Promise { const { port = 3000, hostname = "localhost" } = config; // Resolve directories // server.ts is in web/src/, so we go up one level to get web/ const currentDir = dirname(new URL(import.meta.url).pathname); const webRoot = resolve(currentDir, ".."); const distDir = join(webRoot, "dist"); // Manage build process in development let buildProcess: Subprocess | undefined; const isDev = process.env.NODE_ENV !== "production"; if (isDev) { console.log("🛠️ Starting Web Bundler in Watch Mode..."); try { buildProcess = spawn(["bun", "run", "build.ts", "--watch"], { cwd: webRoot, stdout: "inherit", stderr: "inherit", }); } catch (error) { console.error("Failed to start build process:", error); } } // Configuration constants const MAX_CONNECTIONS = 10; const MAX_PAYLOAD_BYTES = 16384; // 16KB const IDLE_TIMEOUT_SECONDS = 60; // Interval for broadcasting stats to all connected WS clients let statsBroadcastInterval: Timer | undefined; // Cache for activity stats (heavy aggregation) let activityPromise: Promise | null = null; let lastActivityFetch: number = 0; const ACTIVITY_CACHE_TTL = 5 * 60 * 1000; // 5 minutes const server = serve({ port, hostname, async fetch(req, server) { const url = new URL(req.url); // Upgrade to WebSocket if (url.pathname === "/ws") { // Security Check: limit concurrent connections const currentConnections = server.pendingWebSockets; if (currentConnections >= MAX_CONNECTIONS) { console.warn(`⚠️ [WS] Connection rejected: limit reached (${currentConnections}/${MAX_CONNECTIONS})`); return new Response("Connection limit reached", { status: 429 }); } const success = server.upgrade(req); if (success) return undefined; return new Response("WebSocket upgrade failed", { status: 400 }); } // API routes if (url.pathname === "/api/health") { return Response.json({ status: "ok", timestamp: Date.now() }); } if (url.pathname === "/api/stats") { try { const stats = await getFullDashboardStats(); return Response.json(stats); } catch (error) { console.error("Error fetching dashboard stats:", error); return Response.json( { error: "Failed to fetch dashboard statistics" }, { status: 500 } ); } } if (url.pathname === "/api/stats/activity") { try { // Security Check: Token-based authentication const { env } = await import("@shared/lib/env"); const authHeader = req.headers.get("Authorization"); if (authHeader !== `Bearer ${env.ADMIN_TOKEN}`) { console.warn(`⚠️ [API] Unauthorized activity analytics access attempt from ${req.headers.get("x-forwarded-for") || "unknown"}`); return new Response("Unauthorized", { status: 401 }); } const now = Date.now(); // If we have a valid cache, return it if (activityPromise && (now - lastActivityFetch < ACTIVITY_CACHE_TTL)) { const data = await activityPromise; return Response.json(data); } // Otherwise, trigger a new fetch (deduplicated by the promise) if (!activityPromise || (now - lastActivityFetch >= ACTIVITY_CACHE_TTL)) { activityPromise = (async () => { const { dashboardService } = await import("@shared/modules/dashboard/dashboard.service"); return await dashboardService.getActivityAggregation(); })(); lastActivityFetch = now; } const activity = await activityPromise; return Response.json(activity); } catch (error) { console.error("Error fetching activity stats:", error); return Response.json( { error: "Failed to fetch activity statistics" }, { status: 500 } ); } } // Administrative Actions if (url.pathname.startsWith("/api/actions/") && req.method === "POST") { try { // Security Check: Token-based authentication const { env } = await import("@shared/lib/env"); const authHeader = req.headers.get("Authorization"); if (authHeader !== `Bearer ${env.ADMIN_TOKEN}`) { console.warn(`⚠️ [API] Unauthorized administrative action attempt from ${req.headers.get("x-forwarded-for") || "unknown"}`); return new Response("Unauthorized", { status: 401 }); } const { actionService } = await import("@shared/modules/admin/action.service"); const { MaintenanceModeSchema } = await import("@shared/modules/dashboard/dashboard.types"); if (url.pathname === "/api/actions/reload-commands") { const result = await actionService.reloadCommands(); return Response.json(result); } if (url.pathname === "/api/actions/clear-cache") { const result = await actionService.clearCache(); return Response.json(result); } if (url.pathname === "/api/actions/maintenance-mode") { const rawBody = await req.json(); const parsed = MaintenanceModeSchema.safeParse(rawBody); if (!parsed.success) { return Response.json({ error: "Invalid payload", issues: parsed.error.issues }, { status: 400 }); } const result = await actionService.toggleMaintenanceMode(parsed.data.enabled, parsed.data.reason); return Response.json(result); } } catch (error) { console.error("Error executing administrative action:", error); return Response.json( { error: "Failed to execute administrative action" }, { status: 500 } ); } } // Static File Serving let pathName = url.pathname; if (pathName === "/") pathName = "/index.html"; // Construct safe file path // Remove leading slash to join correctly const safePath = join(distDir, pathName.replace(/^\//, "")); // Security check: ensure path is within distDir if (!safePath.startsWith(distDir)) { return new Response("Forbidden", { status: 403 }); } const fileRef = Bun.file(safePath); if (await fileRef.exists()) { // If serving index.html, inject env vars for frontend if (pathName === "/index.html") { let html = await fileRef.text(); const { env } = await import("@shared/lib/env"); const envScript = ``; html = html.replace("", `${envScript}`); return new Response(html, { headers: { "Content-Type": "text/html" } }); } return new Response(fileRef); } // SPA Fallback: Serve index.html for unknown non-file routes const parts = pathName.split("/"); const lastPart = parts[parts.length - 1]; // If it's a direct request for a missing file (has dot), return 404 // EXCEPT for index.html which is our fallback entry point if (lastPart?.includes(".") && lastPart !== "index.html") { return new Response("Not Found", { status: 404 }); } const indexFile = Bun.file(join(distDir, "index.html")); if (!(await indexFile.exists())) { if (isDev) { return new Response("

🛠️ Dashboard is building...

Please refresh in a few seconds. The bundler is currently generating the static assets.

", { status: 503, headers: { "Content-Type": "text/html" } }); } return new Response("Dashboard Not Found", { status: 404 }); } let indexHtml = await indexFile.text(); const { env: sharedEnv } = await import("@shared/lib/env"); const script = ``; indexHtml = indexHtml.replace("", `${script}`); return new Response(indexHtml, { headers: { "Content-Type": "text/html" } }); }, websocket: { open(ws) { ws.subscribe("dashboard"); console.log(`🔌 [WS] Client connected. Total: ${server.pendingWebSockets}`); // Send initial stats getFullDashboardStats().then(stats => { ws.send(JSON.stringify({ type: "STATS_UPDATE", data: stats })); }); // Start broadcast interval if this is the first client if (!statsBroadcastInterval) { statsBroadcastInterval = setInterval(async () => { try { const stats = await getFullDashboardStats(); server.publish("dashboard", JSON.stringify({ type: "STATS_UPDATE", data: stats })); } catch (error) { console.error("Error in stats broadcast:", error); } }, 5000); } }, async message(ws, message) { try { const messageStr = message.toString(); // Defense-in-depth: redundant length check before parsing if (messageStr.length > MAX_PAYLOAD_BYTES) { console.error("❌ [WS] Payload exceeded maximum limit"); return; } const rawData = JSON.parse(messageStr); const { WsMessageSchema } = await import("@shared/modules/dashboard/dashboard.types"); const parsed = WsMessageSchema.safeParse(rawData); if (!parsed.success) { console.error("❌ [WS] Invalid message format:", parsed.error.issues); return; } if (parsed.data.type === "PING") { ws.send(JSON.stringify({ type: "PONG" })); } } catch (e) { console.error("❌ [WS] Failed to handle message:", e instanceof Error ? e.message : "Malformed JSON"); } }, close(ws) { ws.unsubscribe("dashboard"); console.log(`🔌 [WS] Client disconnected. Total remaining: ${server.pendingWebSockets}`); // Stop broadcast interval if no clients left if (server.pendingWebSockets === 0 && statsBroadcastInterval) { clearInterval(statsBroadcastInterval); statsBroadcastInterval = undefined; } }, maxPayloadLength: MAX_PAYLOAD_BYTES, idleTimeout: IDLE_TIMEOUT_SECONDS, }, development: isDev, }); /** * Helper to fetch full dashboard stats object. * Unified for both HTTP API and WebSocket broadcasts. */ async function getFullDashboardStats() { // Import services (dynamic to avoid circular deps) const { dashboardService } = await import("@shared/modules/dashboard/dashboard.service"); const { getClientStats } = await import("../../bot/lib/clientStats"); // Fetch all data in parallel const [clientStats, activeUsers, totalUsers, economyStats, recentEvents] = await Promise.all([ Promise.resolve(getClientStats()), dashboardService.getActiveUserCount(), dashboardService.getTotalUserCount(), dashboardService.getEconomyStats(), dashboardService.getRecentEvents(10), ]); return { bot: clientStats.bot, guilds: { count: clientStats.guilds }, users: { active: activeUsers, total: totalUsers }, commands: { total: clientStats.commandsRegistered }, ping: { avg: clientStats.ping }, economy: { totalWealth: economyStats.totalWealth.toString(), avgLevel: economyStats.avgLevel, topStreak: economyStats.topStreak, }, recentEvents: recentEvents.map(event => ({ ...event, timestamp: event.timestamp instanceof Date ? event.timestamp.toISOString() : event.timestamp, })), uptime: clientStats.uptime, lastCommandTimestamp: clientStats.lastCommandTimestamp, maintenanceMode: (await import("../../bot/lib/BotClient")).AuroraClient.maintenanceMode, }; } // Listen for real-time events from the system bus const { systemEvents, EVENTS } = await import("@shared/lib/events"); systemEvents.on(EVENTS.DASHBOARD.NEW_EVENT, (event) => { server.publish("dashboard", JSON.stringify({ type: "NEW_EVENT", data: event })); }); const url = `http://${hostname}:${port}`; return { server, url, stop: async () => { if (buildProcess) { buildProcess.kill(); } if (statsBroadcastInterval) { clearInterval(statsBroadcastInterval); } server.stop(true); }, }; } /** * Starts the web server from the main application root. * Kept for backward compatibility, but assumes webProjectPath is handled internally or ignored * in favor of relative path resolution from this file. */ export async function startWebServerFromRoot( webProjectPath: string, config: WebServerConfig = {} ): Promise { // Current implementation doesn't need CWD switching thanks to absolute path resolution return createWebServer(config); }