#!/usr/bin/env node /* * sync-keys.js — seed every active member's chat key into LibreChat's * per-user key store so members NEVER see an API-key prompt. * * keys[name="Inference Co-op"] <- {apiKey: } (chat endpoint) * pluginauths[mcp:inference-image] <- IC_API_KEY (image MCP) * * Values are encrypted with the SAME crypto LibreChat uses (data-schemas * encrypt(): AES-256-CBC, CREDS_KEY/CREDS_IV from the app process env), so * LibreChat decrypts them natively. * * Run: cd /app/code/librechat/api && node /app/data/sync-keys.js * (requires CLOUDRON_MONGODB_URL in the environment — present in `cloudron exec`) * Idempotent: safe to run repeatedly (e.g. nightly steward cron or after * onboarding). */ const fs = require("fs"); const os = require("os"); const path = require("path"); // --- env assembly: exec sessions lack the app's process env --------------- function loadEnvFile(p) { try { for (const line of fs.readFileSync(p, "utf8").split("\n")) { const m = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/); if (m && process.env[m[1]] === undefined) process.env[m[1]] = m[2].replace(/^["']|["']$/g, ""); } } catch (e) { /* optional */ } } loadEnvFile("/app/data/env"); try { // CREDS_KEY / CREDS_IV / MONGODB live in the app process env for (const line of fs.readFileSync("/proc/1/environ", "utf8").split("\0")) { const m = line.match(/^([A-Z_][A-Z0-9_]*)=(.*)$/); if (m && process.env[m[1]] === undefined) process.env[m[1]] = m[2]; } } catch (e) { /* optional */ } const MONGO_URL = process.env.CLOUDRON_MONGODB_URL || process.env.MONGODB_URL; const PORTAL_URL = process.env.PORTAL_URL || "https://portal.inference.coop"; const SYNC_TOKEN = process.env.LIBRECHAT_SYNC_TOKEN || ""; const ENDPOINT_NAME = "Inference Co-op"; const MCP_PLUGIN_KEY = "mcp:inference-image"; 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); } // data-schemas resolves CREDS_KEY/CREDS_IV at require time — env is ready above const { encrypt } = require("/app/code/librechat/node_modules/@librechat/data-schemas"); const mongoose = require("/app/code/librechat/node_modules/mongoose"); (async () => { // 1. Fetch active members' chat keys from the portal (dedicated sync token) const res = await fetch(`${PORTAL_URL}/admin/librechat/keys`, { headers: { "X-Sync-Token": SYNC_TOKEN }, }); if (!res.ok) { console.error(`FATAL: portal key feed returned ${res.status}`); process.exit(1); } const { members } = await res.json(); console.log(`portal feed: ${members.length} active member key(s)`); await mongoose.connect(MONGO_URL); const db = mongoose.connection.db; const users = db.collection("users"); const keys = db.collection("keys"); const auths = db.collection("pluginauths"); let seeded = 0, skipped = 0; for (const m of members) { const email = String(m.email || "").toLowerCase(); const sk = String(m.key || ""); if (!email || !sk) continue; const user = await users.findOne({ email: { $regex: new RegExp(`^${email.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`, "i") }, }); if (!user) { skipped++; continue; } // not logged in to the sandbox yet const uid = user._id; // chat endpoint key: value is a JSON object string, same shape the UI saves const encChat = await encrypt(JSON.stringify({ apiKey: sk })); await keys.updateOne( { userId: uid, name: ENDPOINT_NAME }, { $set: { userId: uid, name: ENDPOINT_NAME, value: encChat } }, { upsert: true } ); // image MCP credential const encImage = await encrypt(sk); await auths.updateOne( { userId: uid, pluginKey: MCP_PLUGIN_KEY, authField: MCP_AUTH_FIELD }, { $set: { userId: uid, pluginKey: MCP_PLUGIN_KEY, authField: MCP_AUTH_FIELD, value: encImage } }, { upsert: true } ); seeded++; console.log(`seeded ${email} (user ${String(uid).slice(-6)})`); } console.log(`done: ${seeded} seeded, ${skipped} skipped (no sandbox account yet)`); await mongoose.disconnect(); })().catch((e) => { console.error("FATAL:", e); process.exit(1); });