diff --git a/sync-keys.js b/sync-keys.js index 9d471c6..96b54bb 100644 --- a/sync-keys.js +++ b/sync-keys.js @@ -20,6 +20,13 @@ * it refuses to write unless CREDS_KEY can first decrypt an existing key row * (proof it is the app's real key), and unless each fresh ciphertext round-trips * back to the plaintext. + * + * CREDS_KEY/CREDS_IV: a scheduled run happens in the scheduler's own container, + * where the app's process env does not exist (/proc/1/environ there belongs to + * the task launcher). They are read from /app/data/.librechat-creds + * (root:cloudron 0640) instead. If the app's key ever rotates, refresh that file + * — a stale key makes the decrypt guard refuse to write rather than corrupt + * member keys. */ const fs = require("fs"); const os = require("os"); @@ -52,8 +59,32 @@ const MCP_AUTH_FIELD = "IC_API_KEY"; if (!MONGO_URL) { console.error("FATAL: no MongoDB URL in environment"); process.exit(1); } if (!SYNC_TOKEN) { console.error("FATAL: LIBRECHAT_SYNC_TOKEN not set"); process.exit(1); } +// Scheduler tasks run in their OWN container (Cloudron's scheduler addon): the +// app's process env is absent and /proc/1/environ belongs to the task launcher, +// not the app. So the app's CREDS_KEY/CREDS_IV are read from a root- and +// group-readable file on the shared volume, written from the live app env. +// If it is missing or stale, the guards below stop the run instead of writing +// keys the app cannot decrypt. +const CREDS_FILE = "/app/data/.librechat-creds"; +try { + for (const line of fs.readFileSync(CREDS_FILE, "utf8").split("\n")) { + const m = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/); + if (m && /^CREDS_(KEY|IV)$/.test(m[1]) && process.env[m[1]] === undefined) process.env[m[1]] = m[2].trim(); + } +} catch (e) { /* absent is fine — the guard below reports it */ } + if (!process.env.CREDS_KEY || !process.env.CREDS_IV) { - console.error("FATAL: CREDS_KEY/CREDS_IV not available — refusing to write (keys would be undecryptable)"); + const diag = [ + `when=${new Date().toISOString()}`, + `uid=${typeof process.getuid === "function" ? process.getuid() : "?"} gid=${typeof process.getgid === "function" ? process.getgid() : "?"}`, + `cwd=${process.cwd()}`, + `creds_file_exists=${fs.existsSync(CREDS_FILE)}`, + `proc1_environ_readable=${(() => { try { return fs.readFileSync("/proc/1/environ").length > 0; } catch (e) { return false; } })()}`, + `env_names_with_creds=${Object.keys(process.env).filter((k) => /CREDS/i.test(k)).join(",") || "none"}`, + `volume_mounted=${fs.existsSync("/app/data")}`, + ].join("\n"); + try { fs.writeFileSync("/app/data/key-sync-diag.txt", diag + "\n"); } catch (e) { /* volume not mounted */ } + console.error("FATAL: CREDS_KEY/CREDS_IV not available — refusing to write (keys would be undecryptable); diag: /app/data/key-sync-diag.txt"); process.exit(1); }