const crypto = require("crypto"); const fs = require("fs"); const path = require("path"); const { performance } = require("perf_hooks"); const { db } = require("./db"); const { dependencyIssues } = require("./dependency-manager"); const { createLogger, listLogs } = require("./logger"); const { getPlugins, scanPluginDirectories } = require("./plugins"); const { readRecoveryMarker } = require("./recovery-mode"); const { getSetting, setSetting } = require("./settings"); const { isLoopbackAddress, isTrustedProxyAddress, requestProtocol } = require("./proxy-security"); const { readUpdateState } = require("./update-repository"); const repoRoot = path.join(__dirname, "..", ".."); const packageJson = require(path.join(repoRoot, "package.json")); const TOKEN_PREFIX = "lumi_diag_"; const MAX_REQUESTS_PER_MINUTE = 20; const requestWindows = new Map(); const diagnosticsLog = createLogger("core:diagnostics", { category: "security" }); const CHECKS = Object.freeze({ system_health: "Runtime, database, dependency, disk-space, and recovery health.", update_state: "Latest local update state, recovery marker, and snapshot summary.", plugins: "Installed plugin manifests and registry versions without plugin data.", recent_errors: "Recent warning/error records with secrets and local paths redacted.", benchmark: "Bounded read-only database, plugin-scan, and serialization timing." }); function diagnosticsAccessStatus() { return { enabled: getSetting("production_diagnostics_enabled", false) === true, configured: Boolean(getSetting("production_diagnostics_key_hash", "")), key_prefix: String(getSetting("production_diagnostics_key_prefix", "") || ""), created_at: getSetting("production_diagnostics_key_created_at", null), checks: Object.entries(CHECKS).map(([id, description]) => ({ id, description })) }; } function issueDiagnosticsAccessKey() { const key = `${TOKEN_PREFIX}${crypto.randomBytes(32).toString("base64url")}`; setSetting("production_diagnostics_key_hash", hashAccessKey(key)); setSetting("production_diagnostics_key_prefix", `${key.slice(0, TOKEN_PREFIX.length + 8)}…`); setSetting("production_diagnostics_key_created_at", new Date().toISOString()); setSetting("production_diagnostics_enabled", true); diagnosticsLog.warn("Production diagnostics access key rotated", { key_prefix: `${key.slice(0, TOKEN_PREFIX.length + 8)}…` }, { event: "access_rotated" }); return key; } function revokeDiagnosticsAccess() { setSetting("production_diagnostics_enabled", false); setSetting("production_diagnostics_key_hash", ""); setSetting("production_diagnostics_key_prefix", ""); setSetting("production_diagnostics_key_created_at", null); requestWindows.clear(); diagnosticsLog.warn("Production diagnostics access revoked", null, { event: "access_revoked" }); } function authenticateDiagnosticsRequest(req, now = Date.now()) { if (!diagnosticsAccessStatus().enabled) return { allowed: false, status: 404, reason: "disabled" }; if (!isSecureDiagnosticRequest(req)) return { allowed: false, status: 404, reason: "secure_transport_required" }; const authorization = String(req.get?.("authorization") || ""); const match = authorization.match(/^Bearer\s+([^\s]+)$/i); const candidate = match?.[1] || ""; const storedHash = String(getSetting("production_diagnostics_key_hash", "") || ""); if (!verifyAccessKey(candidate, storedHash)) return { allowed: false, status: 404, reason: "invalid_key" }; const fingerprint = hashAccessKey(candidate).slice(0, 20); const rate = consumeRateLimit(fingerprint, now); if (!rate.allowed) return { allowed: false, status: 429, reason: "rate_limited", retry_after_seconds: rate.retry_after_seconds }; return { allowed: true, fingerprint, remaining: rate.remaining }; } function runDiagnosticCheck(checkId) { const check = String(checkId || "").trim(); if (!Object.hasOwn(CHECKS, check)) throw new Error("Unknown diagnostic check."); const started = performance.now(); let result; if (check === "system_health") result = systemHealth(); else if (check === "update_state") result = updateState(); else if (check === "plugins") result = pluginInventory(); else if (check === "recent_errors") result = recentErrors(); else result = boundedBenchmark(); return redactDiagnosticValue({ schema_version: 1, check, generated_at: new Date().toISOString(), duration_ms: round(performance.now() - started), result }); } function auditDiagnosticRequest(values = {}) { diagnosticsLog.log(values.ok === false ? "warn" : "info", "Production diagnostics request", { request_id: values.request_id, check: values.check, ok: values.ok !== false, key_fingerprint: values.fingerprint || null, reason: values.reason || null, duration_ms: values.duration_ms || null }, { event: "diagnostic_request", requestId: values.request_id }); } function recentDiagnosticAudit(limit = 30) { return listLogs({ limit: 250 }) .filter((entry) => ["Production diagnostics request", "Production diagnostics access key rotated", "Production diagnostics access revoked"].includes(entry.message)) .slice(0, Math.max(1, Math.min(100, Number(limit) || 30))) .map((entry) => redactDiagnosticValue(entry)); } function systemHealth() { let database = { ok: false }; try { database = { ok: db.prepare("SELECT 1 AS ok").get()?.ok === 1 }; } catch (error) { database = { ok: false, error: error.message }; } let disk = null; try { if (typeof fs.statfsSync !== "function") throw new Error("Disk statistics are unavailable on this Node.js version."); const stats = fs.statfsSync(repoRoot); disk = { available_bytes: Number(stats.bavail) * Number(stats.bsize), total_bytes: Number(stats.blocks) * Number(stats.bsize) }; } catch { disk = { available: false }; } const dependencies = dependencyIssues(repoRoot); return { core_version: packageJson.version, node_version: process.version, platform: process.platform, architecture: process.arch, uptime_seconds: Math.floor(process.uptime()), memory: process.memoryUsage(), database, disk, dependencies: { ready: dependencies.filter((item) => !item.optional).length === 0, issues: dependencies }, recovery: summarizeRecovery(readRecoveryMarker()) }; } function updateState() { const state = readUpdateState(); const index = readJson(path.join(repoRoot, "data", "snapshots", "index.json"), []); const snapshots = Array.isArray(index) ? index : []; return { core_version: packageJson.version, last_update: { status: state.last_update_status || null, target_kind: state.last_target_kind || null, target_id: state.last_target_id || null, target_version: state.last_target_version || null, stage: state.last_update_stage || null, error: state.last_error || null, updated_at: state.last_update_at || state.updated_at || null, source_ref: state.branch || null, commit: state.commit || null }, recovery: summarizeRecovery(readRecoveryMarker()), snapshots: { available: snapshots.filter((entry) => entry.status === "available").length, latest: snapshots .filter((entry) => entry.status === "available") .sort((left, right) => Number(right.createdAt) - Number(left.createdAt)) .slice(0, 10) .map((entry) => ({ type: entry.type, plugin_id: entry.pluginId || null, from_version: entry.from_version || null, to_version: entry.to_version || null, created_at: entry.createdAt || null, storage_bytes: entry.storage_bytes || null })) } }; } function pluginInventory() { const registry = new Map(getPlugins().map((plugin) => [plugin.id, plugin])); return scanPluginDirectories().map((plugin) => ({ id: plugin.id, name: plugin.name, manifest_version: plugin.version, registry_version: registry.get(plugin.id)?.version || null, enabled: Boolean(registry.get(plugin.id)?.enabled), version_matches_registry: !registry.has(plugin.id) || registry.get(plugin.id)?.version === plugin.version })).sort((left, right) => left.id.localeCompare(right.id)); } function recentErrors() { return listLogs({ limit: 50, levels: ["warn", "error"] }).map((entry) => ({ level: entry.level, message: entry.message, details: entry.details, created_at: entry.created_at })); } function boundedBenchmark() { const databaseStarted = performance.now(); for (let index = 0; index < 100; index += 1) db.prepare("SELECT 1 AS ok").get(); const databaseMs = performance.now() - databaseStarted; const pluginStarted = performance.now(); let pluginCount = 0; for (let index = 0; index < 5; index += 1) pluginCount = scanPluginDirectories().length; const pluginMs = performance.now() - pluginStarted; const serializationStarted = performance.now(); const sample = Array.from({ length: 250 }, (_, index) => ({ index, status: "ok", version: packageJson.version })); for (let index = 0; index < 20; index += 1) JSON.stringify(sample); const serializationMs = performance.now() - serializationStarted; return { fixed_workload: true, database_select_100_ms: round(databaseMs), plugin_scan_5_ms: round(pluginMs), plugin_count: pluginCount, json_serialize_20_ms: round(serializationMs) }; } function redactDiagnosticValue(value, key = "", depth = 0) { if (depth > 8) return "[truncated]"; if (/(?:token|secret|password|passwd|authorization|cookie|session|credential|private.?key|api.?key)/i.test(key)) return "[redacted]"; if (typeof value === "string") return redactString(value).slice(0, 4000); if (typeof value === "bigint") return Number(value); if (Array.isArray(value)) return value.slice(0, 100).map((item) => redactDiagnosticValue(item, key, depth + 1)); if (value && typeof value === "object") { return Object.fromEntries(Object.entries(value).slice(0, 100).map(([childKey, child]) => [childKey, redactDiagnosticValue(child, childKey, depth + 1)])); } return value; } function redactString(value) { return String(value) .replace(/\blumi_diag_[A-Za-z0-9_-]+\b/g, "[diagnostics key]") .replace(/(https?:\/\/)[^/@\s:]+:[^/@\s]+@/gi, "$1[credentials]@") .replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g, "[signed token]") .replace(/\b([a-z0-9_-]*(?:token|secret|session|password|passwd|api[_-]?key|auth)[a-z0-9_-]*)\s*[:=]\s*[^,\s;]+/gi, "$1=[redacted]") .replace(/\b(?:bearer|basic)\s+[a-z0-9._~+/=-]+/gi, "[auth header]") .replace(/([?&](?:token|secret|session|password|api_key|apikey|auth|code)=)[^&#\s]+/gi, "$1[redacted]") .replace(/(?:[A-Za-z]:\\|\\\\)[^\s<>\"|?*]+/g, "[local path]") .replace(/\/(?:home|Users|mnt|var|tmp|opt|root)\/[^\s)\]}>]+/g, "[local path]") .replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi, "[email]") .replace(/[A-Za-z0-9+_=-]{40,}/g, (candidate) => /[A-Za-z]/.test(candidate) && /\d/.test(candidate) ? "[high-entropy value]" : candidate); } function hashAccessKey(value) { return crypto.createHash("sha256").update(String(value || "")).digest("hex"); } function verifyAccessKey(candidate, expectedHash) { if (!String(candidate || "").startsWith(TOKEN_PREFIX) || !/^[a-f0-9]{64}$/i.test(String(expectedHash || ""))) return false; const actual = Buffer.from(hashAccessKey(candidate), "hex"); const expected = Buffer.from(expectedHash, "hex"); return actual.length === expected.length && crypto.timingSafeEqual(actual, expected); } function consumeRateLimit(fingerprint, now = Date.now()) { const windowStart = now - (now % 60000); const current = requestWindows.get(fingerprint); const state = !current || current.window_start !== windowStart ? { window_start: windowStart, count: 0 } : current; state.count += 1; requestWindows.set(fingerprint, state); if (requestWindows.size > 1000) { for (const [key, value] of requestWindows.entries()) if (value.window_start < windowStart) requestWindows.delete(key); } return { allowed: state.count <= MAX_REQUESTS_PER_MINUTE, remaining: Math.max(0, MAX_REQUESTS_PER_MINUTE - state.count), retry_after_seconds: Math.max(1, Math.ceil((windowStart + 60000 - now) / 1000)) }; } function isSecureDiagnosticRequest(req) { if (req.secure === true) return true; const address = String(req.socket?.remoteAddress || req.ip || ""); if (isLoopbackAddress(address)) return true; return diagnosticsRequestProtocol(req) === "https"; } function diagnosticsRequestProtocol(req) { return requestProtocol(req); } function isTrustedPrivateProxyAddress(value) { return isTrustedProxyAddress(value); } function summarizeRecovery(marker) { if (!marker) return { active: false }; return { active: ["pending", "applying", "verifying", "failed", "stale"].includes(marker.status), status: marker.status || null, target_kind: marker.target_kind || null, target_id: marker.target_id || null, from_version: marker.from_version || null, to_version: marker.to_version || null, error: marker.error || null, updated_at: marker.updated_at || null }; } function readJson(filePath, fallback) { try { return JSON.parse(fs.readFileSync(filePath, "utf8")); } catch { return fallback; } } function round(value) { return Math.round(Number(value) * 100) / 100; } module.exports = { CHECKS, MAX_REQUESTS_PER_MINUTE, auditDiagnosticRequest, authenticateDiagnosticsRequest, consumeRateLimit, diagnosticsAccessStatus, diagnosticsRequestProtocol, hashAccessKey, issueDiagnosticsAccessKey, isSecureDiagnosticRequest, isTrustedPrivateProxyAddress, recentDiagnosticAudit, redactDiagnosticValue, revokeDiagnosticsAccess, runDiagnosticCheck, verifyAccessKey };