fix: trust private diagnostics proxies
This commit is contained in:
parent
5c17408221
commit
328d06662f
@ -1,5 +1,10 @@
|
||||
# Lumi changelog
|
||||
|
||||
## 0.2.5
|
||||
|
||||
- Fixed diagnostics behind local/private reverse proxies by accepting forwarded HTTPS only from loopback, RFC1918/link-local IPv4, or private/link-local IPv6 proxy addresses.
|
||||
- Updated the local diagnostics client to read either JSON or env-style `.secrets` files, including `LUMI_PROD_URL` and `LUMI_DIAG_KEY` aliases.
|
||||
|
||||
## 0.2.4
|
||||
|
||||
- Changed the production diagnostics endpoint, cURL example, and local-client configuration example to use the actual URL from which the administrator opened Lumi, including trusted reverse-proxy HTTPS handling.
|
||||
|
||||
1
TODO.md
1
TODO.md
@ -677,6 +677,7 @@ This section is for Lumi chat answer feedback and AI Improvement Center work, no
|
||||
|
||||
## Done
|
||||
|
||||
- 2026-07-18: Fixed private reverse-proxy HTTPS recognition for production diagnostics in core 0.2.5 without globally trusting client forwarding headers, and taught the local client to consume the repository's env-style `.secrets` file and short diagnostic variable names directly.
|
||||
- 2026-07-18: Updated production diagnostics examples in core 0.2.4 to derive the full endpoint and client base URL from the administrator's current Lumi request instead of showing a placeholder hostname.
|
||||
- 2026-07-18: Fixed the shared submit-action resolver in core 0.2.3: ordinary buttons now inherit their parent form endpoint unless they explicitly declare `formaction`, restoring timed diagnostics-key creation and preventing async update actions from posting back to the Updates page.
|
||||
- 2026-07-18: Added production-stage plugin update diagnostics in core 0.2.2: selected plugin source is verified before snapshotting, failures record their exact stage and target in update state, and the affected plugin row displays the server error directly.
|
||||
|
||||
@ -43,7 +43,7 @@ The bundled client then runs an allowlisted check and prints its redacted JSON r
|
||||
npm run diagnostics:production -- update_state
|
||||
```
|
||||
|
||||
`LUMI_DIAGNOSTICS_CONFIG` can point to a key file outside the repository. `LUMI_DIAGNOSTICS_URL` and `LUMI_DIAGNOSTICS_KEY` are also supported for ephemeral environments.
|
||||
`LUMI_DIAGNOSTICS_CONFIG` can point to a key file outside the repository. The client also reads an env-style `.secrets` file and accepts the shorter `LUMI_PROD_URL` / `LUMI_DIAG_KEY` names in addition to `LUMI_DIAGNOSTICS_URL` / `LUMI_DIAGNOSTICS_KEY`.
|
||||
|
||||
The supported checks are:
|
||||
|
||||
|
||||
@ -14,7 +14,7 @@ editable: false
|
||||
Lumi is the core web UI and bot runtime.
|
||||
## Runtime
|
||||
Package: lumi-bot
|
||||
Version: 0.2.4
|
||||
Version: 0.2.5
|
||||
## Routes
|
||||
- GET /api/events
|
||||
- POST /api/destructive-confirmations
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "lumi-bot",
|
||||
"version": "0.2.4",
|
||||
"version": "0.2.5",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "lumi-bot",
|
||||
"version": "0.2.4",
|
||||
"version": "0.2.5",
|
||||
"dependencies": {
|
||||
"adm-zip": "^0.5.12",
|
||||
"better-sqlite3": "^11.5.0",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "lumi-bot",
|
||||
"version": "0.2.4",
|
||||
"version": "0.2.5",
|
||||
"private": true,
|
||||
"type": "commonjs",
|
||||
"scripts": {
|
||||
|
||||
@ -2,6 +2,36 @@
|
||||
"schema_version": 1,
|
||||
"channel": "stable",
|
||||
"releases": [
|
||||
{
|
||||
"version": "0.2.5",
|
||||
"ref": "refs/tags/v0.2.5",
|
||||
"released_at": "2026-07-18",
|
||||
"installable": true,
|
||||
"rollback_safe": true,
|
||||
"replaces_versions": [
|
||||
"1.2.0"
|
||||
],
|
||||
"data_policy": "preserve",
|
||||
"dependency_policy": "sync_on_restart",
|
||||
"migration_notes": "Recognizes secured diagnostics forwarded through local/private reverse proxies and supports env-style local diagnostic secrets.",
|
||||
"plugins": {
|
||||
"auto-vc": "0.1.6",
|
||||
"birthday": "0.1.3",
|
||||
"economy-framework": "0.2.10",
|
||||
"economy-games": "0.1.7",
|
||||
"expression-interaction": "0.2.1",
|
||||
"lumi_ai": "0.8.2",
|
||||
"moderation": "0.1.5",
|
||||
"okf": "0.1.1",
|
||||
"quotes": "0.1.2",
|
||||
"sample-plugin": "0.1.0",
|
||||
"throne_wishlist": "0.1.2",
|
||||
"welcome_messages": "0.1.1"
|
||||
},
|
||||
"tools": {
|
||||
"lumi_ai_web_search": "0.1.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "0.2.4",
|
||||
"ref": "refs/tags/v0.2.4",
|
||||
|
||||
@ -4,7 +4,7 @@ const path = require("path");
|
||||
const AdmZip = require("adm-zip");
|
||||
|
||||
const root = path.join(__dirname, "..");
|
||||
const destination = path.join(root, "dist", "lumi-core-1.2.0-to-0.2.4-repair.zip");
|
||||
const destination = path.join(root, "dist", "lumi-core-1.2.0-to-0.2.5-repair.zip");
|
||||
const files = [
|
||||
"CHANGELOG.md",
|
||||
"README.md",
|
||||
@ -53,10 +53,10 @@ for (const relativePath of files) {
|
||||
|
||||
const manifest = {
|
||||
schema_version: 1,
|
||||
name: "Lumi core 0.2.4 repair",
|
||||
name: "Lumi core 0.2.5 repair",
|
||||
target: "core",
|
||||
from_versions: ["0.1.9", "1.2.0", "0.2.0", "0.2.1", "0.2.2", "0.2.3", "0.2.4"],
|
||||
to_version: "0.2.4",
|
||||
from_versions: ["0.1.9", "1.2.0", "0.2.0", "0.2.1", "0.2.2", "0.2.3", "0.2.4", "0.2.5"],
|
||||
to_version: "0.2.5",
|
||||
data_policy: "preserve",
|
||||
dependency_policy: "sync_on_restart",
|
||||
created_at: new Date().toISOString(),
|
||||
|
||||
@ -9,19 +9,25 @@ if (!allowedChecks.has(check)) {
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
const configPath = path.resolve(process.env.LUMI_DIAGNOSTICS_CONFIG || path.join(root, ".secrets", "production-diagnostics.json"));
|
||||
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 = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
||||
fileConfig = parseConfig(fs.readFileSync(configPath, "utf8"));
|
||||
} catch (error) {
|
||||
if (!process.env.LUMI_DIAGNOSTICS_URL || !process.env.LUMI_DIAGNOSTICS_KEY) {
|
||||
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 || fileConfig.base_url || "").trim();
|
||||
const key = String(process.env.LUMI_DIAGNOSTICS_KEY || fileConfig.key || "").trim();
|
||||
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);
|
||||
@ -64,3 +70,29 @@ fetch(endpoint, {
|
||||
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 || ""
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@ const AdmZip = require("adm-zip");
|
||||
const { verifyPatchPackage } = require("../src/services/update-manager");
|
||||
|
||||
const root = path.join(__dirname, "..");
|
||||
const archivePath = path.join(root, "dist", "lumi-core-1.2.0-to-0.2.4-repair.zip");
|
||||
const archivePath = path.join(root, "dist", "lumi-core-1.2.0-to-0.2.5-repair.zip");
|
||||
assert.equal(fs.existsSync(archivePath), true, "build the repair patch first");
|
||||
const zip = new AdmZip(archivePath);
|
||||
const entries = zip.getEntries().filter((entry) => !entry.isDirectory);
|
||||
@ -15,9 +15,9 @@ const names = new Set(entries.map((entry) => entry.entryName.replace(/\\/g, "/")
|
||||
assert.equal(names.has("patch-manifest.json"), true);
|
||||
const manifest = JSON.parse(zip.readAsText("patch-manifest.json"));
|
||||
assert.equal(manifest.target, "core");
|
||||
assert.equal(manifest.to_version, "0.2.4");
|
||||
assert.equal(manifest.to_version, "0.2.5");
|
||||
assert.equal(manifest.data_policy, "preserve");
|
||||
assert.deepEqual(manifest.from_versions, ["0.1.9", "1.2.0", "0.2.0", "0.2.1", "0.2.2", "0.2.3", "0.2.4"]);
|
||||
assert.deepEqual(manifest.from_versions, ["0.1.9", "1.2.0", "0.2.0", "0.2.1", "0.2.2", "0.2.3", "0.2.4", "0.2.5"]);
|
||||
|
||||
const forbidden = /^(?:data|plugins|node_modules|config|storage|uploads|logs|database|databases|knowledge\/(?:community|corrections))(?:\/|$)|^\.env(?:\.|$)|^\.secrets$/;
|
||||
for (const entry of entries) {
|
||||
@ -29,7 +29,7 @@ for (const [relativePath, expected] of Object.entries(manifest.files)) {
|
||||
assert.equal(actual, expected, `${relativePath} checksum`);
|
||||
}
|
||||
assert.equal(Object.keys(manifest.files).length + 1, entries.length, "every repair file must be checksummed");
|
||||
assert.equal(JSON.parse(zip.readAsText("package.json")).version, "0.2.4");
|
||||
assert.equal(JSON.parse(zip.readAsText("package.json")).version, "0.2.5");
|
||||
|
||||
const simulation = fs.mkdtempSync(path.join(os.tmpdir(), "lumi-repair-simulation-"));
|
||||
try {
|
||||
@ -47,11 +47,11 @@ try {
|
||||
fs.writeFileSync(target, `preserve:${sentinel}`);
|
||||
}
|
||||
zip.extractAllTo(simulation, true);
|
||||
assert.equal(verifyPatchPackage(simulation).to_version, "0.2.4");
|
||||
assert.equal(verifyPatchPackage(simulation).to_version, "0.2.5");
|
||||
for (const sentinel of sentinels) {
|
||||
assert.equal(fs.readFileSync(path.join(simulation, sentinel), "utf8"), `preserve:${sentinel}`);
|
||||
}
|
||||
assert.equal(JSON.parse(fs.readFileSync(path.join(simulation, "package.json"), "utf8")).version, "0.2.4");
|
||||
assert.equal(JSON.parse(fs.readFileSync(path.join(simulation, "package.json"), "utf8")).version, "0.2.5");
|
||||
assert.equal(fs.existsSync(path.join(simulation, "src", "services", "dependency-manager.js")), true);
|
||||
assert.equal(fs.existsSync(path.join(simulation, "src", "services", "production-diagnostics.js")), true);
|
||||
assert.equal(fs.existsSync(path.join(simulation, "src", "web", "views", "admin-diagnostics.ejs")), true);
|
||||
|
||||
@ -23,6 +23,18 @@ assert.equal(diagnostics.verifyAccessKey("wrong-prefix", hash), false);
|
||||
assert.equal(diagnostics.isSecureDiagnosticRequest({ secure: true, ip: "203.0.113.10" }), true);
|
||||
assert.equal(diagnostics.isSecureDiagnosticRequest({ secure: false, ip: "127.0.0.1" }), true);
|
||||
assert.equal(diagnostics.isSecureDiagnosticRequest({ secure: false, ip: "203.0.113.10" }), false);
|
||||
const proxiedRequest = {
|
||||
secure: false,
|
||||
protocol: "http",
|
||||
socket: { remoteAddress: "::ffff:172.19.0.4" },
|
||||
get(name) { return name === "x-forwarded-proto" ? "https" : ""; }
|
||||
};
|
||||
assert.equal(diagnostics.isTrustedPrivateProxyAddress("172.19.0.4"), true);
|
||||
assert.equal(diagnostics.isTrustedPrivateProxyAddress("192.168.1.10"), true);
|
||||
assert.equal(diagnostics.isTrustedPrivateProxyAddress("203.0.113.10"), false);
|
||||
assert.equal(diagnostics.diagnosticsRequestProtocol(proxiedRequest), "https");
|
||||
assert.equal(diagnostics.isSecureDiagnosticRequest(proxiedRequest), true);
|
||||
assert.equal(diagnostics.diagnosticsRequestProtocol({ ...proxiedRequest, socket: { remoteAddress: "203.0.113.10" } }), "http");
|
||||
|
||||
const redacted = diagnostics.redactDiagnosticValue({
|
||||
token: "top-secret",
|
||||
@ -82,12 +94,16 @@ assert.match(viewSource, /\.secrets/);
|
||||
assert.match(viewSource, /diagnosticsEndpointUrl/);
|
||||
assert.match(viewSource, /diagnosticsBaseUrl/);
|
||||
assert.doesNotMatch(viewSource, /https:\/\/your-lumi-host/);
|
||||
assert.match(serverSource, /const diagnosticsBaseUrl = `\$\{req\.protocol\}:\/\/\$\{req\.get\("host"\)\}`/);
|
||||
assert.match(serverSource, /const diagnosticsBaseUrl = `\$\{diagnosticsRequestProtocol\(req\)\}:\/\/\$\{req\.get\("host"\)\}`/);
|
||||
|
||||
const clientSource = fs.readFileSync(path.join(root, "scripts", "production-diagnostics-client.js"), "utf8");
|
||||
assert.match(clientSource, /\.secrets["'], "production-diagnostics\.json"/);
|
||||
assert.match(clientSource, /path\.join\(root, "\.secrets"\)/);
|
||||
assert.match(clientSource, /path\.join\(defaultSecretPath, "production-diagnostics\.json"\)/);
|
||||
assert.match(clientSource, /method: "POST"/);
|
||||
assert.match(clientSource, /new URL\("\/api\/diagnostics\/v1\/run"/);
|
||||
assert.match(clientSource, /LUMI_PROD_URL/);
|
||||
assert.match(clientSource, /LUMI_DIAG_KEY/);
|
||||
assert.match(clientSource, /function parseConfig\(raw\)/);
|
||||
assert.doesNotMatch(clientSource, /console\.(?:log|error)\(\s*key\s*\)/, "client must not print its access key");
|
||||
|
||||
console.log("Production diagnostics verification passed: fixed read-only checks, redaction, key validation, rate limiting, HTTPS proxy trust, and admin-only controls.");
|
||||
|
||||
@ -4,8 +4,8 @@ const path = require("path");
|
||||
const { findSafeTarget } = require("../src/services/versioning");
|
||||
|
||||
const root = path.join(__dirname, "..");
|
||||
const releaseVersion = "0.2.4";
|
||||
const previousCoreVersion = "0.2.3";
|
||||
const releaseVersion = "0.2.5";
|
||||
const previousCoreVersion = "0.2.4";
|
||||
const earliestCompatibleCoreVersion = "0.1.9";
|
||||
const changedPlugins = {
|
||||
"auto-vc": { from: "0.1.5", to: "0.1.6", knowledge: "auto-vc" },
|
||||
@ -87,4 +87,4 @@ assert.equal(webSearch.minimum_lumi_version, "0.2.0");
|
||||
assert.equal(webSearch.minimum_lumi_ai_version, changedPlugins.lumi_ai.to);
|
||||
assert.equal(hasVersionHeading(readText("plugins/lumi_ai_web_search/CHANGELOG.md"), webSearch.version), true);
|
||||
|
||||
console.log("Release metadata verification passed: core 0.2.4 and 11 changed plugin/tool packages.");
|
||||
console.log("Release metadata verification passed: core 0.2.5 and 11 changed plugin/tool packages.");
|
||||
|
||||
@ -16,7 +16,7 @@ function readJson(relativePath) {
|
||||
|
||||
const releaseIndex = readJson("release-index.json");
|
||||
const releaseVersions = releaseIndex.releases.map((release) => release.version);
|
||||
assert.deepEqual(releaseVersions, ["0.2.4", "0.2.3", "0.2.2", "0.2.1", "0.2.0", "0.1.9"]);
|
||||
assert.deepEqual(releaseVersions, ["0.2.5", "0.2.4", "0.2.3", "0.2.2", "0.2.1", "0.2.0", "0.1.9"]);
|
||||
assert.equal(new Set(releaseVersions).size, releaseVersions.length, "release versions must be unique");
|
||||
for (const release of releaseIndex.releases) {
|
||||
assert.equal(normalizeRepositoryRef(release.ref), release.ref);
|
||||
@ -35,8 +35,9 @@ for (const [toolId, version] of Object.entries(currentRelease.tools)) {
|
||||
}
|
||||
|
||||
const baseTarget = {
|
||||
current_version: "0.2.3",
|
||||
current_version: "0.2.4",
|
||||
available_versions: [
|
||||
{ version: "0.2.5", ref: "refs/tags/v0.2.5", rollback_safe: true },
|
||||
{ version: "0.2.4", ref: "refs/tags/v0.2.4", rollback_safe: true },
|
||||
{ version: "0.2.3", ref: "refs/tags/v0.2.3", rollback_safe: true },
|
||||
{ version: "0.2.2", ref: "refs/tags/v0.2.2", rollback_safe: true },
|
||||
@ -64,7 +65,7 @@ const corrected = buildStatus({
|
||||
channel: "stable"
|
||||
});
|
||||
assert.equal(corrected.version_correction, true);
|
||||
assert.equal(corrected.safe_target_version, "0.2.4");
|
||||
assert.equal(corrected.safe_target_version, "0.2.5");
|
||||
assert.equal(corrected.update_available, true);
|
||||
assert.equal(corrected.blocked, false);
|
||||
|
||||
|
||||
@ -272,8 +272,39 @@ function consumeRateLimit(fingerprint, now = Date.now()) {
|
||||
|
||||
function isSecureDiagnosticRequest(req) {
|
||||
if (req.secure === true) return true;
|
||||
const address = String(req.ip || req.socket?.remoteAddress || "");
|
||||
return ["127.0.0.1", "::1", "::ffff:127.0.0.1"].includes(address);
|
||||
const address = String(req.socket?.remoteAddress || req.ip || "");
|
||||
if (isLoopbackAddress(address)) return true;
|
||||
return diagnosticsRequestProtocol(req) === "https";
|
||||
}
|
||||
|
||||
function diagnosticsRequestProtocol(req) {
|
||||
if (req.secure === true || String(req.protocol || "").toLowerCase() === "https") return "https";
|
||||
const forwarded = String(req.get?.("x-forwarded-proto") || "")
|
||||
.split(",")[0]
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
const proxyAddress = String(req.socket?.remoteAddress || "");
|
||||
if (forwarded === "https" && isTrustedPrivateProxyAddress(proxyAddress)) return "https";
|
||||
return "http";
|
||||
}
|
||||
|
||||
function isTrustedPrivateProxyAddress(value) {
|
||||
const address = String(value || "").trim().toLowerCase().replace(/^::ffff:/, "");
|
||||
if (isLoopbackAddress(address)) return true;
|
||||
if (/^\d{1,3}(?:\.\d{1,3}){3}$/.test(address)) {
|
||||
const parts = address.split(".").map(Number);
|
||||
if (parts.some((part) => part < 0 || part > 255)) return false;
|
||||
return parts[0] === 10 ||
|
||||
(parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) ||
|
||||
(parts[0] === 192 && parts[1] === 168) ||
|
||||
(parts[0] === 169 && parts[1] === 254);
|
||||
}
|
||||
return address.startsWith("fc") || address.startsWith("fd") || /^fe[89ab]/.test(address);
|
||||
}
|
||||
|
||||
function isLoopbackAddress(value) {
|
||||
const address = String(value || "").trim().toLowerCase();
|
||||
return address === "127.0.0.1" || address === "::1" || address === "::ffff:127.0.0.1";
|
||||
}
|
||||
|
||||
function summarizeRecovery(marker) {
|
||||
@ -305,9 +336,11 @@ module.exports = {
|
||||
authenticateDiagnosticsRequest,
|
||||
consumeRateLimit,
|
||||
diagnosticsAccessStatus,
|
||||
diagnosticsRequestProtocol,
|
||||
hashAccessKey,
|
||||
issueDiagnosticsAccessKey,
|
||||
isSecureDiagnosticRequest,
|
||||
isTrustedPrivateProxyAddress,
|
||||
recentDiagnosticAudit,
|
||||
redactDiagnosticValue,
|
||||
revokeDiagnosticsAccess,
|
||||
|
||||
@ -110,6 +110,7 @@ const {
|
||||
auditDiagnosticRequest,
|
||||
authenticateDiagnosticsRequest,
|
||||
diagnosticsAccessStatus,
|
||||
diagnosticsRequestProtocol,
|
||||
issueDiagnosticsAccessKey,
|
||||
recentDiagnosticAudit,
|
||||
revokeDiagnosticsAccess,
|
||||
@ -5770,7 +5771,7 @@ function createWebServer({ loadPlugins, discordClient }) {
|
||||
});
|
||||
|
||||
const renderDiagnosticsAdmin = (req, res, values = {}) => {
|
||||
const diagnosticsBaseUrl = `${req.protocol}://${req.get("host")}`;
|
||||
const diagnosticsBaseUrl = `${diagnosticsRequestProtocol(req)}://${req.get("host")}`;
|
||||
res.set("Cache-Control", "no-store");
|
||||
res.render("admin-diagnostics", {
|
||||
title: "Production diagnostics",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Lumi Core",
|
||||
"version": "0.2.4",
|
||||
"version": "0.2.5",
|
||||
"channel": "stable",
|
||||
"released_at": "2026-07-18",
|
||||
"compatible_from": "0.1.9",
|
||||
@ -8,7 +8,7 @@
|
||||
"replaces_versions": [
|
||||
"1.2.0"
|
||||
],
|
||||
"migration_notes": "Includes the 1.2.0 version correction, production plugin-update diagnostics, secured read-only production diagnostics with request-aware examples, and the shared form-action fix required by timed confirmations and repository updates. Lumi synchronizes runtime dependencies on restart and preserves settings, databases, plugin data, community knowledge, AI models, runtimes, uploads, logs, and secrets.",
|
||||
"migration_notes": "Includes the 1.2.0 version correction, production plugin-update diagnostics, secured read-only production diagnostics with private-proxy HTTPS support, and the shared form-action fix required by timed confirmations and repository updates. Lumi synchronizes runtime dependencies on restart and preserves settings, databases, plugin data, community knowledge, AI models, runtimes, uploads, logs, and secrets.",
|
||||
"rollback_safe": true,
|
||||
"requirements": [
|
||||
"Node.js 18 or newer"
|
||||
@ -73,6 +73,18 @@
|
||||
],
|
||||
"rollback_safe": true,
|
||||
"migration_notes": "Uses the administrator's current Lumi origin in production diagnostics examples; preserved local data is not replaced."
|
||||
},
|
||||
{
|
||||
"version": "0.2.5",
|
||||
"channel": "stable",
|
||||
"released_at": "2026-07-18",
|
||||
"compatible_from": "0.1.9",
|
||||
"migration_kind": "patch",
|
||||
"replaces_versions": [
|
||||
"1.2.0"
|
||||
],
|
||||
"rollback_safe": true,
|
||||
"migration_notes": "Recognizes forwarded HTTPS from local/private reverse proxies for secured diagnostics; preserved local data is not replaced."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user