Lumi/scripts/production-diagnostics-client.js
2026-07-18 17:04:35 +02:00

99 lines
3.6 KiB
JavaScript

const fs = require("fs");
const path = require("path");
const root = path.join(__dirname, "..");
const allowedChecks = new Set(["system_health", "update_state", "plugins", "recent_errors", "benchmark"]);
const check = String(process.argv[2] || "system_health").trim();
if (!allowedChecks.has(check)) {
console.error(`Choose one of: ${Array.from(allowedChecks).join(", ")}`);
process.exit(2);
}
const defaultSecretPath = path.join(root, ".secrets");
const configPath = path.resolve(process.env.LUMI_DIAGNOSTICS_CONFIG || (
fs.statSync(defaultSecretPath, { throwIfNoEntry: false })?.isFile()
? defaultSecretPath
: path.join(defaultSecretPath, "production-diagnostics.json")
));
let fileConfig = {};
try {
fileConfig = parseConfig(fs.readFileSync(configPath, "utf8"));
} catch (error) {
if (!(process.env.LUMI_DIAGNOSTICS_URL || process.env.LUMI_PROD_URL) ||
!(process.env.LUMI_DIAGNOSTICS_KEY || process.env.LUMI_DIAG_KEY)) {
console.error(`Diagnostics configuration was not found or is invalid: ${configPath}`);
process.exit(2);
}
}
const baseUrl = String(process.env.LUMI_DIAGNOSTICS_URL || process.env.LUMI_PROD_URL || fileConfig.base_url || "").trim();
const key = String(process.env.LUMI_DIAGNOSTICS_KEY || process.env.LUMI_DIAG_KEY || fileConfig.key || "").trim();
let endpoint;
try {
const parsed = new URL(baseUrl);
const isLoopback = ["localhost", "127.0.0.1", "::1", "[::1]"].includes(parsed.hostname);
if (parsed.protocol !== "https:" && !(parsed.protocol === "http:" && isLoopback)) {
throw new Error("Production diagnostics require HTTPS (HTTP is allowed only for loopback).");
}
if (parsed.username || parsed.password) throw new Error("Do not put credentials in the diagnostics URL.");
endpoint = new URL("/api/diagnostics/v1/run", parsed);
} catch (error) {
console.error(error.message || "Configure a valid production diagnostics URL.");
process.exit(2);
}
if (!key.startsWith("lumi_diag_")) {
console.error("The production diagnostics key is missing or invalid.");
process.exit(2);
}
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 15000);
fetch(endpoint, {
method: "POST",
headers: {
authorization: `Bearer ${key}`,
"content-type": "application/json",
accept: "application/json"
},
body: JSON.stringify({ check }),
signal: controller.signal
})
.then(async (response) => {
const body = await response.json().catch(() => null);
if (!response.ok || body?.ok !== true) {
throw new Error(body?.error || `Diagnostics request failed with HTTP ${response.status}.`);
}
console.log(JSON.stringify(body, null, 2));
})
.catch((error) => {
console.error(error.name === "AbortError" ? "Diagnostics request timed out." : error.message);
process.exitCode = 1;
})
.finally(() => clearTimeout(timeout));
function parseConfig(raw) {
const source = String(raw || "").trim();
try {
const parsed = JSON.parse(source);
return {
base_url: parsed.base_url || parsed.url || "",
key: parsed.key || ""
};
} catch {
const values = {};
for (const line of source.split(/\r?\n/)) {
const match = line.trim().match(/^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);
if (!match) continue;
let value = match[2].trim();
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
value = value.slice(1, -1);
}
values[match[1]] = value;
}
return {
base_url: values.LUMI_DIAGNOSTICS_URL || values.LUMI_PROD_URL || "",
key: values.LUMI_DIAGNOSTICS_KEY || values.LUMI_DIAG_KEY || ""
};
}
}