import "dotenv/config"; import express from "express"; import cors from "cors"; import { validateEnv } from "./config/env"; import { apiLimiter, securityHeaders, requestSizeLimits, requestId, apiKeyAuth, auditLog, } from "./middleware"; import { requestTimeout } from "./middleware/timeout"; import { logger } from "./logging/logger"; import { getMetrics, httpRequestDuration, httpRequestTotal, register } from "./metrics/prometheus"; import { healthCheck, readinessCheck, livenessCheck } from "./health/health"; import { listPlansEndpoint, createPlan, getPlan, addSignature, validatePlanEndpoint } from "./api/plans"; import { streamPlanStatus } from "./api/sse"; import { executionCoordinator } from "./services/execution"; import { runMigration } from "./db/migrations"; // Validate environment on startup validateEnv(); const app = express(); const PORT = process.env.PORT || 8080; // Middleware app.use(cors()); app.use(securityHeaders); app.use(requestSizeLimits); app.use(requestId); app.use(requestTimeout(30000)); // 30 second timeout app.use(express.json({ limit: "10mb" })); app.use(express.urlencoded({ extended: true, limit: "10mb" })); // Request logging middleware app.use((req, res, next) => { const start = Date.now(); const requestId = req.headers["x-request-id"] as string || "unknown"; res.on("finish", () => { const duration = Date.now() - start; httpRequestDuration.observe( { method: req.method, route: req.route?.path || req.path, status: res.statusCode }, duration / 1000 ); httpRequestTotal.inc({ method: req.method, route: req.route?.path || req.path, status: res.statusCode }); logger.info({ req, res, duration, requestId, }, `${req.method} ${req.path} ${res.statusCode}`); }); next(); }); // Health check endpoints (no auth required) app.get("/health", async (req, res) => { const health = await healthCheck(); res.status(health.status === "healthy" ? 200 : 503).json(health); }); app.get("/ready", async (req, res) => { const ready = await readinessCheck(); res.status(ready ? 200 : 503).json({ ready }); }); app.get("/live", async (req, res) => { const alive = await livenessCheck(); res.status(alive ? 200 : 503).json({ alive }); }); // Metrics endpoint app.get("/metrics", async (req, res) => { res.setHeader("Content-Type", register.contentType); const metrics = await getMetrics(); res.send(metrics); }); // API routes with rate limiting app.use("/api", apiLimiter); // Plan management endpoints app.get("/api/plans", listPlansEndpoint); app.post("/api/plans", auditLog("CREATE_PLAN", "plan"), createPlan); app.get("/api/plans/:planId", getPlan); app.post("/api/plans/:planId/signature", addSignature); app.post("/api/plans/:planId/validate", validatePlanEndpoint); // Execution endpoints import { executePlan, getExecutionStatus, abortExecution } from "./api/execution"; import { registerWebhook } from "./api/webhooks"; app.post("/api/plans/:planId/execute", auditLog("EXECUTE_PLAN", "plan"), executePlan); app.get("/api/plans/:planId/status", getExecutionStatus); app.post("/api/plans/:planId/abort", auditLog("ABORT_PLAN", "plan"), abortExecution); app.post("/api/webhooks", registerWebhook); // Proxmox BFF — forwards browser requests to the CF-Access protected // Proxmox API using a server-side service token. See // orchestrator/src/integrations/proxmox.ts for required env. import { proxmoxHealth, proxmoxClusterStatus } from "./api/proxmox"; app.get("/api/proxmox/health", proxmoxHealth); app.get("/api/proxmox/cluster/status", proxmoxClusterStatus); app.get("/api/plans/:planId/status/stream", streamPlanStatus); // Error handling middleware import { errorHandler } from "./services/errorHandler"; import { initRedis } from "./services/redis"; // Initialize Redis if configured if (process.env.REDIS_URL) { initRedis(); } app.use(errorHandler); // Graceful shutdown process.on("SIGTERM", async () => { logger.info("SIGTERM received, shutting down gracefully"); // Close database connections // Close SSE connections process.exit(0); }); process.on("SIGINT", async () => { logger.info("SIGINT received, shutting down gracefully"); process.exit(0); }); // Start server async function start() { try { // Run database migrations if (process.env.RUN_MIGRATIONS === "true") { await runMigration(); } app.listen(PORT, () => { logger.info({ port: PORT }, "Orchestrator service started"); }); } catch (error) { logger.error({ error }, "Failed to start server"); process.exit(1); } } start();