Per-member key auto-load: seed keys via sync-keys.js (portal feed); restore customUserVars for image billing
This commit is contained in:
1 parent
126972beeb
commit
65c6730463
2 files changed
+105
No files matched your search
@@ -178,3 +178,8 @@ mcpServers:
|
||||
# Per-user credentials aren't available at boot; each member initializes
|
||||
# their own connection after saving their key in the MCP panel.
|
||||
startup: false
|
||||
customUserVars:
|
||||
IC_API_KEY:
|
||||
title: "Inference Co-op API key"
|
||||
description: "Auto-loaded from your membership — no action needed."
|
||||
sensitive: true
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
#!/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: <sk>} (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); });
|
||||
Reference in new issue
Block a user