Files
CurrenciCombo/orchestrator/src/index.ts
Nakamoto, S ecd5412923
Some checks failed
Code Quality / SonarQube Analysis (pull_request) Failing after 26s
Code Quality / Code Quality Checks (pull_request) Failing after 6s
Security Scan / Dependency Vulnerability Scan (pull_request) Failing after 3s
Security Scan / OWASP ZAP Scan (pull_request) Failing after 3s
feat(orchestrator): Proxmox BFF route (CF-Access service token proxy)
Adds a narrow, safelisted BFF surface so the Solace Bank Group PLC portal
(and other browser clients) can reach the Cloudflare Access protected
Proxmox API without requiring the user to complete a CF-Access SSO flow
in-browser.

Endpoints:
  GET /api/proxmox/health          — configuration probe (503 when unset)
  GET /api/proxmox/cluster/status  — aggregated cluster node status

Required orchestrator env:
  PROXMOX_API_URL
  PROXMOX_CF_ACCESS_CLIENT_ID
  PROXMOX_CF_ACCESS_CLIENT_SECRET

When env is missing the endpoints return 503 with an actionable JSON
body and the frontend stays in its mocked state — no crashes, no
partial deploys.
2026-04-19 08:29:13 +00:00

154 lines
4.5 KiB
TypeScript

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();