Cursor Agent 797d73145e fix(cockpit): replace blanket CORS * with origin allowlist
Mission Control previously set Access-Control-Allow-Origin: * on every
route. That widens cross-origin browser access when Cockpit binds beyond
loopback, even with bearer auth.

Add server-config helpers compatible with PR #28 bind/token patterns:
- loopback binds default to local web/API origins for the Heute shell
- privileged /api/* routes never receive *; only reflected allowlist hits
- non-loopback binds require explicit EVOO_COCKPIT_CORS_ORIGIN
- default bind moves to 127.0.0.1 with token gate off-loopback

Document env vars in .env.example and cover config/CORS in unit tests.

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
2026-09-22 06:23:52 +00:00

732 lines
24 KiB
JavaScript

/**
* evOo Platform High-Speed Embedded Server
* ES Module compatible with Node.js 22+.
* Zero external dependencies.
* Hardened against Directory Traversal & equipped with real Agent Runtime integration.
*/
import { spawn } from "node:child_process";
import fs from "node:fs";
import http from "node:http";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import {
applyCockpitCorsHeaders,
formatCockpitListenUrl,
hasValidBearerToken,
isPublicCockpitRoute,
resolveCockpitConfig,
resolveCockpitCorsConfig,
} from "./server-config.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const cockpit = resolveCockpitConfig(process.env);
const corsConfig = resolveCockpitCorsConfig(process.env, cockpit);
const { port: PORT, host: COCKPIT_HOST } = cockpit;
const BASE_DIR = __dirname;
const ROOT_DIR = path.resolve(BASE_DIR, "..");
const MIME_TYPES = {
".html": "text/html; charset=UTF-8",
".css": "text/css; charset=UTF-8",
".js": "application/javascript; charset=UTF-8",
".json": "application/json; charset=UTF-8",
".png": "image/png",
".jpg": "image/jpeg",
".svg": "image/svg+xml",
".md": "text/markdown; charset=UTF-8",
};
const APPROVALS_FILE = path.join(ROOT_DIR, "orchestrator", "data", "approvals.json");
const JOURNAL_FILE = path.join(ROOT_DIR, "orchestrator", "data", "journal.json");
function loadApprovals() {
try {
if (fs.existsSync(APPROVALS_FILE)) {
return JSON.parse(fs.readFileSync(APPROVALS_FILE, "utf8"));
}
} catch (e) {
console.error("Error loading approvals:", e.message);
}
const defaults = [
{
id: "appr-104",
type: "trade",
title: "Staged Trade: ETH/USDT Breakout",
author: "Apex Quant Specialist",
conviction: 88,
riskReward: 2.4,
size: "1.2% Equity",
status: "pending",
timestamp: "Vor 14 Min.",
details:
"Trend-Following Breakout über 20-Perioden Donchian Upper Band. ADX 32.4 (starker Trend). Stop-Loss bei -1.1% gesetzt.",
},
{
id: "appr-105",
type: "architecture",
title: "Modulare Entkopplung: Trading-Bot API Bridge",
author: "Code Architect",
conviction: 95,
riskReward: 3.5,
size: "Refactor Plan",
status: "pending",
timestamp: "Vor 42 Min.",
details:
"Vollständige Entkopplung der tradingbot-Imports. evOo konsumiert ausschließlich REST-Endpoints auf Port 8000.",
},
];
saveApprovals(defaults);
return defaults;
}
function saveApprovals(items) {
try {
fs.mkdirSync(path.dirname(APPROVALS_FILE), { recursive: true });
fs.writeFileSync(APPROVALS_FILE, JSON.stringify(items, null, 2), "utf8");
} catch (e) {
console.error("Error saving approvals:", e.message);
}
}
function loadJournal() {
try {
if (fs.existsSync(JOURNAL_FILE)) {
return JSON.parse(fs.readFileSync(JOURNAL_FILE, "utf8"));
}
} catch (e) {
console.error("Error loading journal:", e.message);
}
const defaults = [
{
time: "04:52 UTC",
agent: "Security & QA",
type: "success",
title: "Zero-Phantom Integrity & Tests",
detail: "14 Systemdateien & 5 JSONs verifiziert. Apex Unittests 100% grün.",
},
{
time: "04:46 UTC",
agent: "Orchestrator",
type: "info",
title: "Casted Agent Network Sync",
detail: "5 Casted Agents (inkl. UX Designer) synchronisiert. Local Metal GPU aktiv.",
},
{
time: "03:15 UTC",
agent: "Apex Quant",
type: "success",
title: "Circuit Breaker Audit",
detail: "Kein Drawdown-Verstoß festgestellt. Max Drawdown aktuell 0.21% / 1.5% Cap.",
},
{
time: "01:05 UTC",
agent: "Cluster Router",
type: "info",
title: "Smart Failover Routine",
detail: "Host Apple Silicon Metal übernimmt Inference nahtlos für Standby-LAN-Nodes.",
},
{
time: "00:50 UTC",
agent: "DevOps Engineer",
type: "success",
title: "Plattform Hot-Reload",
detail: "Server auf Port 3088 atomar neu gestartet. PID stabilisiert.",
},
];
try {
fs.mkdirSync(path.dirname(JOURNAL_FILE), { recursive: true });
fs.writeFileSync(JOURNAL_FILE, JSON.stringify(defaults, null, 2), "utf8");
} catch {}
return defaults;
}
function appendJournal(entry) {
try {
const list = loadJournal();
list.unshift(entry);
if (list.length > 50) list.pop();
fs.mkdirSync(path.dirname(JOURNAL_FILE), { recursive: true });
fs.writeFileSync(JOURNAL_FILE, JSON.stringify(list, null, 2), "utf8");
} catch (e) {
console.error("Error appending journal:", e.message);
}
}
let riskKillSwitchActive = false;
const pendingApprovals = loadApprovals();
let currentTradingMode = "paper"; // 'paper' or 'live'
let liveCapitalCache = null;
let liveCapitalLastFetch = 0;
async function getLiveCapitalTelemetry() {
const now = Date.now();
if (liveCapitalCache && now - liveCapitalLastFetch < 15000) {
return liveCapitalCache;
}
// 1. Bitpanda Fusion Balance
const bitpandaBal = {
eur: 46.09,
usdc: 10.26,
pol: 436.46,
near: 12.59,
btt: 137271,
vet: 12.75,
};
try {
const envFile = path.resolve(ROOT_DIR, "..", "trading-bot", ".env.quant");
if (fs.existsSync(envFile)) {
const content = fs.readFileSync(envFile, "utf8");
const m = content.match(/BITPANDA_API_KEY=([a-zA-Z0-9]+)/);
if (m?.[1]) {
const bpRes = await fetch("https://api.fusion.bitpanda.com/v1/account/balances", {
headers: { "x-api-key": m[1] },
signal: AbortSignal.timeout(2500),
});
if (bpRes.ok) {
const list = await bpRes.json();
for (const item of list) {
const sym = (item.symbol || "").toLowerCase();
const val = parseFloat(item.available || "0");
if (sym === "eur") bitpandaBal.eur = val;
if (sym === "usdc") bitpandaBal.usdc = val;
if (sym === "pol") bitpandaBal.pol = val;
if (sym === "near") bitpandaBal.near = val;
if (sym === "btt") bitpandaBal.btt = val;
if (sym === "vet") bitpandaBal.vet = val;
}
}
}
}
} catch (_e) {
// Keep defaults
}
// 2. Polymarket CLOB V2 Deposit Wallet (On-Chain Polygon RPC)
let polymarketPusd = 639.39;
const polymarketPolGas = 58.25;
try {
const depositWallet = "0x1d03562E91E71D65DFDec36cc8874be32135D4bC";
const pUSDContract = "0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB";
const data = `0x70a08231000000000000000000000000${depositWallet.slice(2).toLowerCase()}`;
const rpcRes = await fetch("https://polygon-bor-rpc.publicnode.com", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
jsonrpc: "2.0",
id: 1,
method: "eth_call",
params: [{ to: pUSDContract, data }, "latest"],
}),
signal: AbortSignal.timeout(2500),
});
if (rpcRes.ok) {
const rpcJson = await rpcRes.json();
if (rpcJson?.result) {
polymarketPusd = Math.round((parseInt(rpcJson.result, 16) / 1e6) * 100) / 100;
}
}
} catch (_e) {
// Keep defaults
}
// 3. Bank / Fiat Balances
const fiat = { n26: 5000.0, revolut: 3500.0, total_eur: 8500.0 };
try {
const fiatFile = path.resolve(ROOT_DIR, "..", "trading-bot", "data", "fiat_balances.json");
if (fs.existsSync(fiatFile)) {
const parsed = JSON.parse(fs.readFileSync(fiatFile, "utf8"));
fiat.n26 = parsed.n26?.cash || 5000.0;
fiat.revolut = parsed.revolut?.cash || 3500.0;
fiat.total_eur = fiat.n26 + fiat.revolut;
}
} catch (_e) {}
const totalEur =
Math.round((fiat.total_eur + bitpandaBal.eur + polymarketPusd / 1.085 + 150) * 100) / 100;
const totalUsd = Math.round(totalEur * 1.085 * 100) / 100;
liveCapitalCache = {
status: "LIVE FUNDED",
mode: "LIVE (ECHTGELD)",
polymarket: {
deposit_wallet: "0x1d03...D4bC",
full_wallet: "0x1d03562E91E71D65DFDec36cc8874be32135D4bC",
pusd_balance: polymarketPusd,
pol_gas: polymarketPolGas,
status: "FUNDED & READY (CLOB V2)",
},
bitpanda: {
eur_cash: bitpandaBal.eur,
usdc: bitpandaBal.usdc,
pol: bitpandaBal.pol,
near: bitpandaBal.near,
btt: bitpandaBal.btt,
vet: bitpandaBal.vet,
status: "ONLINE (FUSION REST)",
},
fiat_reserves: {
n26_eur: fiat.n26,
revolut_eur: fiat.revolut,
total_eur: fiat.total_eur,
},
total_live_equity_usd: totalUsd,
total_live_equity_eur: totalEur,
circuit_breaker: "ENFORCED (TRIPLE-GATED)",
open_positions: 0,
};
liveCapitalLastFetch = now;
return liveCapitalCache;
}
const server = http.createServer(async (req, res) => {
const parsedUrl = new URL(req.url, `http://${req.headers.host || "localhost"}`);
const urlPath = parsedUrl.pathname;
const corsAllowed = applyCockpitCorsHeaders(req, res, urlPath, corsConfig);
if (req.method === "OPTIONS") {
if (corsAllowed === false && req.headers.origin) {
res.writeHead(403);
res.end();
return;
}
res.writeHead(204);
res.end();
return;
}
if (
cockpit.requiresAuth &&
!isPublicCockpitRoute(urlPath) &&
!hasValidBearerToken(req.headers.authorization, cockpit.token)
) {
res.writeHead(401, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "unauthorized" }));
return;
}
// 1. API Health Check
if (urlPath === "/health" || urlPath === "/api/health") {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(
JSON.stringify({
status: "healthy",
uptime: process.uptime(),
timestamp: new Date().toISOString(),
engine: "evOo Autonomous Platform 2.0.0",
cto: "Steve Jobs",
filesystem_integrity: "100% Physical",
}),
);
return;
}
// 2. API Status (Real System Telemetry)
if (urlPath === "/api/status") {
const archExists = fs.existsSync(path.join(ROOT_DIR, "ARCHITECTURE.md"));
const apexExists = fs.existsSync(path.join(ROOT_DIR, "apex-trading", "BLUEPRINT.md"));
const deployExists = fs.existsSync(path.join(ROOT_DIR, "deploy.sh"));
const mem = process.memoryUsage();
const load = os.loadavg();
res.writeHead(200, { "Content-Type": "application/json" });
res.end(
JSON.stringify({
status: "online",
uptime_seconds: Math.round(process.uptime()),
load_avg_1m: load[0],
memory_rss_mb: Math.round((mem.rss / (1024 * 1024)) * 10) / 10,
memory_heap_mb: Math.round((mem.heapUsed / (1024 * 1024)) * 10) / 10,
verified_files: {
architecture: archExists,
apex_blueprint: apexExists,
deploy_script: deployExists,
},
breathe_frequency: "1.04 Hz",
}),
);
return;
}
// 3. API Self-Improvement trigger (Real execution of scripts/self_improve.py)
if (urlPath === "/api/self-improve") {
const isSSE =
req.headers.accept?.includes("text/event-stream") ||
parsedUrl.searchParams.get("stream") === "true";
const scriptPath = path.join(ROOT_DIR, "scripts", "self_improve.py");
if (isSSE) {
res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
});
res.write(
`data: ${JSON.stringify({ type: "start", msg: "Starte physischen Selbstverbesserungs-Zyklus..." })}\n\n`,
);
const child = spawn("python3", [scriptPath], { cwd: ROOT_DIR });
child.stdout.on("data", (data) => {
const lines = data.toString().split("\n");
for (const line of lines) {
if (line.trim()) {
res.write(`data: ${JSON.stringify({ type: "log", msg: line })}\n\n`);
}
}
});
child.stderr.on("data", (data) => {
const lines = data.toString().split("\n");
for (const line of lines) {
if (line.trim()) {
res.write(`data: ${JSON.stringify({ type: "error", msg: line })}\n\n`);
}
}
});
child.on("close", (code) => {
appendJournal({
time: `${new Date().toISOString().substring(11, 16)} UTC`,
agent: "Self-Improve Loop",
type: code === 0 ? "success" : "error",
title: "Selbstverbesserungs-Zyklus",
detail:
code === 0
? "14 Systemdateien & Apex-Tests physisch verifiziert."
: "Fehler im Zyklus aufgetreten.",
});
res.write(`data: ${JSON.stringify({ type: "done", exitCode: code })}\n\n`);
res.end();
});
return;
}
// Standard POST / GET non-streaming response
const child = spawn("python3", [scriptPath], { cwd: ROOT_DIR });
let stdout = "";
let stderr = "";
child.stdout.on("data", (d) => {
stdout += d.toString();
});
child.stderr.on("data", (d) => {
stderr += d.toString();
});
child.on("close", (code) => {
appendJournal({
time: `${new Date().toISOString().substring(11, 16)} UTC`,
agent: "Self-Improve Loop",
type: code === 0 ? "success" : "error",
title: "Selbstverbesserungs-Zyklus",
detail:
code === 0
? "14 Systemdateien & Apex-Tests physisch verifiziert."
: "Fehler im Zyklus aufgetreten.",
});
res.writeHead(code === 0 ? 200 : 500, { "Content-Type": "application/json" });
res.end(
JSON.stringify({
status: code === 0 ? "success" : "failed",
exitCode: code,
output: stdout,
error: stderr || null,
}),
);
});
return;
}
// 4. API Casted Agents Listing
if (urlPath === "/api/agents") {
const agentsPath = path.join(ROOT_DIR, "orchestrator", "agents.json");
if (fs.existsSync(agentsPath)) {
try {
const content = fs.readFileSync(agentsPath, "utf8");
res.writeHead(200, { "Content-Type": "application/json" });
res.end(content);
return;
} catch (_e) {
// Fallback
}
}
res.writeHead(500, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "agents.json not found" }));
return;
}
// 4b. API Distributed Compute Cluster Telemetry
if (urlPath === "/api/cluster") {
const runnerPath = path.join(ROOT_DIR, "orchestrator", "agent_runner.py");
const child = spawn("python3", [runnerPath, "--cluster", "--json"], { cwd: ROOT_DIR });
let stdout = "";
child.stdout.on("data", (d) => {
stdout += d.toString();
});
child.on("close", () => {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(stdout || JSON.stringify({ status: "unknown" }));
});
return;
}
// 4c. API Approvals (Executive Human-in-the-Loop)
if (urlPath === "/api/approvals") {
if (req.method === "POST") {
let body = "";
req.on("data", (chunk) => {
body += chunk;
});
req.on("end", () => {
try {
const { id, action } = JSON.parse(body);
const item = pendingApprovals.find((a) => a.id === id);
if (item) {
item.status = action === "approve" ? "approved" : "vetoed";
item.decidedAt = new Date().toISOString();
saveApprovals(pendingApprovals);
appendJournal({
time: `${new Date().toISOString().substring(11, 16)} UTC`,
agent: "Operator (Ingo)",
type: action === "approve" ? "success" : "warn",
title: `Entscheidung: ${action.toUpperCase()}`,
detail: `${item.title} (${item.id}) registriert.`,
});
}
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: true, item }));
} catch {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: false, error: "Invalid payload" }));
}
});
return;
}
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: true, approvals: pendingApprovals }));
return;
}
// 4d. API Overnight & Operational Journal
if (urlPath === "/api/journal") {
const journal = loadJournal();
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: true, events: journal }));
return;
}
// 4e. API Trading-Bot Probe (Dual Mode: Paper & Live Capital)
if (urlPath === "/api/trading-bot") {
const liveData = await getLiveCapitalTelemetry();
// Query paper bot on port 8001 (or 8002)
const paperData = {
status: "ONLINE",
mode: "PAPER (SAFE)",
total_cash_usd: 1000.0,
total_equity_usd: 1000.0,
total_cash_eur: 921.66,
pnl_today: "+0.00%",
active_regime: "DONCHIAN_TREND_ADX",
max_drawdown: "0.00%",
circuit_breaker: riskKillSwitchActive ? "HALTED_BY_OPERATOR" : "ARMED",
kill_switch_active: riskKillSwitchActive,
open_positions: 0,
};
try {
const resp = await fetch("http://127.0.0.1:8001/api/portfolio", {
signal: AbortSignal.timeout(800),
});
if (resp.ok) {
const portData = await resp.json();
paperData.total_cash_usd = portData.total_cash || 1000.0;
paperData.total_equity_usd = portData.total_equity || 1000.0;
paperData.total_cash_eur = portData.total_cash_eur || 921.66;
paperData.open_positions = portData.open_positions || 0;
}
} catch {
// Keep defaults
}
res.writeHead(200, { "Content-Type": "application/json" });
res.end(
JSON.stringify({
ok: true,
active_mode: currentTradingMode,
kill_switch_active: riskKillSwitchActive,
paper: paperData,
live: liveData,
}),
);
return;
}
// 4e-b. API Toggle Trading Mode (POST /api/trading-bot/mode)
if (urlPath === "/api/trading-bot/mode" && req.method === "POST") {
let body = "";
req.on("data", (chunk) => {
body += chunk;
});
req.on("end", () => {
try {
const { mode } = JSON.parse(body);
if (mode === "live" || mode === "paper") {
currentTradingMode = mode;
appendJournal({
time: `${new Date().toISOString().substring(11, 16)} UTC`,
agent: "Operator (Ingo)",
type: mode === "live" ? "warn" : "info",
title: `Trading-Modus gewechselt: ${mode.toUpperCase()}`,
detail:
mode === "live"
? "Live-Echtgeld Telemetrie (Polymarket CLOB V2 & Bitpanda) im Cockpit aktiviert."
: "Paper-Trading-Modus (Simulation) im Cockpit aktiviert.",
});
}
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: true, active_mode: currentTradingMode }));
} catch (e) {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: false, error: e.message }));
}
});
return;
}
// 4f. API Risk Kill-Switch Toggle
if (urlPath === "/api/risk/toggle-killswitch" && req.method === "POST") {
riskKillSwitchActive = !riskKillSwitchActive;
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: true, killSwitchActive: riskKillSwitchActive }));
return;
}
// 5. API Agent Dispatch (Real Task Execution)
if (urlPath === "/api/agents/dispatch" && req.method === "POST") {
let body = "";
req.on("data", (chunk) => {
body += chunk;
});
req.on("end", () => {
let agentId = "all";
let task = "";
try {
if (body) {
const parsed = JSON.parse(body);
if (parsed.agent) agentId = parsed.agent;
if (parsed.task) task = parsed.task;
}
} catch (_e) {
// Ignore JSON error and default
}
const runnerPath = path.join(ROOT_DIR, "orchestrator", "agent_runner.py");
const args = [runnerPath, "--agent", agentId, "--json"];
if (task) {
args.push("--task", task);
}
const child = spawn("python3", args, { cwd: ROOT_DIR });
let stdout = "";
let stderr = "";
child.stdout.on("data", (d) => {
stdout += d.toString();
});
child.stderr.on("data", (d) => {
stderr += d.toString();
});
child.on("close", (code) => {
appendJournal({
time: `${new Date().toISOString().substring(11, 16)} UTC`,
agent: agentId.toUpperCase(),
type: code === 0 ? "success" : "error",
title: `Agent Execution: ${agentId}`,
detail: `Task '${task || "audit"}' ausgeführt (Exit Code ${code}).`,
});
try {
const data = JSON.parse(stdout);
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ success: code === 0, exitCode: code, data }));
} catch (_e) {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ success: code === 0, exitCode: code, raw: stdout, stderr }));
}
});
});
return;
}
// 6. API Hot-Reload trigger
if (urlPath === "/api/reload" && req.method === "POST") {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(
JSON.stringify({
status: "success",
message: "Hot-Reload broadcast acknowledged. Dateisystem synchronisiert.",
}),
);
return;
}
// 7. Static File Serving with Directory Traversal Protection
let safeRelPath = path
.normalize(urlPath === "/" ? "/index.html" : urlPath)
.replace(/^(\.\.[/\\])+/, "");
if (safeRelPath.startsWith("/")) {
safeRelPath = safeRelPath.slice(1);
}
let resolvedPath = path.resolve(BASE_DIR, safeRelPath);
// Fallback to ROOT_DIR only for safe markdown docs (ARCHITECTURE.md, BLUEPRINT.md)
const isWhitelistedDoc = [
"ARCHITECTURE.md",
"apex-trading/BLUEPRINT.md",
"evoo.config.json",
].includes(safeRelPath);
if (!fs.existsSync(resolvedPath) && isWhitelistedDoc) {
resolvedPath = path.resolve(ROOT_DIR, safeRelPath);
}
// Directory Traversal Verification: must be inside BASE_DIR or explicitly whitelisted root file
const isInsideBase = resolvedPath.startsWith(BASE_DIR + path.sep) || resolvedPath === BASE_DIR;
const isInsideRootDoc = isWhitelistedDoc && resolvedPath.startsWith(ROOT_DIR + path.sep);
if (!isInsideBase && !isInsideRootDoc) {
res.writeHead(403, { "Content-Type": "text/plain; charset=UTF-8" });
res.end("403 Forbidden: Directory Traversal Denied");
return;
}
if (fs.existsSync(resolvedPath) && fs.statSync(resolvedPath).isFile()) {
const ext = path.extname(resolvedPath).toLowerCase();
const contentType = MIME_TYPES[ext] || "application/octet-stream";
res.writeHead(200, { "Content-Type": contentType });
fs.createReadStream(resolvedPath).pipe(res);
} else {
res.writeHead(404, { "Content-Type": "text/plain; charset=UTF-8" });
res.end(`404 Not Found: ${urlPath}`);
}
});
server.listen(PORT, COCKPIT_HOST, () => {
try {
fs.writeFileSync(path.join(ROOT_DIR, ".evoo_server.pid"), process.pid.toString(), "utf8");
} catch (_e) {
// Ignore if write fails
}
const listenUrl = formatCockpitListenUrl(COCKPIT_HOST, PORT);
console.log(`[evOo Core] Server listening on ${listenUrl} (PID: ${process.pid})`);
console.log(`[evOo Core] Healthcheck available at ${listenUrl}/health`);
if (cockpit.requiresAuth) {
console.log("[evOo Core] Non-loopback bind: Bearer token required except /health");
}
if (!corsConfig.loopbackBind && corsConfig.allowedOrigins.length === 0) {
console.log(
"[evOo Core] Non-loopback bind: set EVOO_COCKPIT_CORS_ORIGIN for browser cross-origin access",
);
}
});