Some checks failed
CI / Frontend Lint (push) Has been cancelled
CI / Frontend Type Check (push) Has been cancelled
CI / Frontend Build (push) Has been cancelled
CI / Frontend E2E Tests (push) Has been cancelled
CI / Orchestrator Build (push) Has been cancelled
CI / Orchestrator Unit Tests (push) Has been cancelled
CI / Orchestrator E2E (Testcontainers) (push) Has been cancelled
CI / Contracts Compile (push) Has been cancelled
CI / Contracts Test (push) Has been cancelled
Security Scan / Dependency Vulnerability Scan (push) Has been cancelled
Security Scan / OWASP ZAP Scan (push) Has been cancelled
177 lines
5.7 KiB
TypeScript
177 lines
5.7 KiB
TypeScript
import "dotenv/config";
|
|
import express from "express";
|
|
import cors from "cors";
|
|
import { validateEnv } from "./config/env";
|
|
import { logBlockerStatusAtBoot } from "./config/externalBlockers";
|
|
import {
|
|
apiLimiter,
|
|
securityHeaders,
|
|
requestSizeLimits,
|
|
requestId,
|
|
apiKeyAuth,
|
|
auditLog,
|
|
idempotencyMiddleware,
|
|
} 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, getPlanState, getPlanEvents, streamPlanEvents, 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();
|
|
|
|
// Surface the current EXT-* external-dependency blocker status so
|
|
// orchestrator startup logs match the proxmox deployment checker
|
|
// (proxmox/scripts/verify/check-external-dependencies.sh) 1:1.
|
|
logBlockerStatusAtBoot(logger);
|
|
|
|
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", idempotencyMiddleware, auditLog("CREATE_PLAN", "plan"), createPlan);
|
|
app.get("/api/plans/:planId", getPlan);
|
|
app.get("/api/plans/:planId/state", getPlanState);
|
|
app.get("/api/plans/:planId/events", getPlanEvents);
|
|
app.get("/api/plans/:planId/events/stream", streamPlanEvents);
|
|
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", idempotencyMiddleware, 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);
|
|
|
|
// FIN-link sandbox transport (gap-analysis v2 §7.1 / §10.6).
|
|
// Mounted only when FIN_SANDBOX_ENABLED=true so production builds
|
|
// don't expose the in-memory fake. Intended for dev + E2E only.
|
|
if (process.env.FIN_SANDBOX_ENABLED === "true") {
|
|
import("./services/finLink/sandbox").then(({ buildSandboxRouter, startAutoProgress }) => {
|
|
app.use("/fin-sandbox", buildSandboxRouter());
|
|
if (process.env.FIN_SANDBOX_AUTO_PROGRESS !== "false") {
|
|
startAutoProgress(Number(process.env.FIN_SANDBOX_TICK_MS || 2000));
|
|
}
|
|
logger.info({ route: "/fin-sandbox" }, "FIN-link sandbox mounted");
|
|
});
|
|
}
|
|
|
|
// 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();
|
|
|