77 lines
3.3 KiB
JavaScript
77 lines
3.3 KiB
JavaScript
/**
|
|
* Key broker sync — automatically provision a member's gateway key at SSO login.
|
|
*
|
|
* Inference Cooperative patch (MIT, same as LibreChat). When KEY_BROKER_URL is
|
|
* set, an OpenID (Cloudron SSO) login fetches-or-creates the member's chat key
|
|
* via the member portal's broker and stores it as the user's LibreChat key for
|
|
* the configured endpoint — so members never paste a key and all usage bills
|
|
* to their own LiteLLM team/budget.
|
|
*
|
|
* Env:
|
|
* KEY_BROKER_URL e.g. https://portal.inference.coop
|
|
* KEY_BROKER_SECRET the portal's BROKER_SECRET (x-broker-secret header)
|
|
* KEY_BROKER_KEY_NAME portal-side key name, default "chat"
|
|
* KEY_BROKER_ENDPOINT LibreChat endpoint (user-key) name, default "Inference Co-op"
|
|
*
|
|
* Semantics: create-on-first-login. If the member already has a portal-side
|
|
* "chat" key but LibreChat lost its stored copy, we deliberately do NOT rotate
|
|
* (rotation would silently break the member's own API scripts); they can
|
|
* rotate from the dashboard and log in again.
|
|
*/
|
|
const { updateUserKey, getUserKeyExpiry } = require('~/models');
|
|
const { logger } = require('@librechat/data-schemas');
|
|
|
|
async function syncBrokerUserKey(user) {
|
|
const base = process.env.KEY_BROKER_URL;
|
|
const secret = process.env.KEY_BROKER_SECRET;
|
|
const userId = user?._id?.toString?.() || user?.id;
|
|
if (!base || !secret || !userId || !user?.email) return;
|
|
const keyName = process.env.KEY_BROKER_KEY_NAME || 'chat';
|
|
const endpointName = process.env.KEY_BROKER_ENDPOINT || 'Inference Co-op';
|
|
const headers = {
|
|
'x-broker-secret': secret,
|
|
'x-member-email': user.email,
|
|
};
|
|
|
|
// Does the member already have a portal-side chat key?
|
|
const listRes = await fetch(`${base}/broker/keys`, { headers });
|
|
if (!listRes.ok) throw new Error(`broker list ${listRes.status}`);
|
|
const list = await listRes.json();
|
|
const exists = (list.keys || []).some((k) => k.name === keyName);
|
|
|
|
if (!exists) {
|
|
const createRes = await fetch(`${base}/broker/keys`, {
|
|
method: 'POST',
|
|
headers: { ...headers, 'content-type': 'application/json' },
|
|
body: JSON.stringify({ name: keyName }),
|
|
});
|
|
if (!createRes.ok) throw new Error(`broker create ${createRes.status}`);
|
|
const created = await createRes.json();
|
|
if (!created.sk_token) throw new Error('broker returned no sk_token');
|
|
// Store the key in LibreChat (JSON string as required by LibreChat storage)
|
|
await updateUserKey({
|
|
userId: userId,
|
|
name: endpointName,
|
|
value: JSON.stringify({ apiKey: created.sk_token }),
|
|
expiresAt: null,
|
|
});
|
|
logger.info(`[KeyBroker] provisioned "${keyName}" key for ${user.email} (endpoint: ${endpointName})`);
|
|
return;
|
|
}
|
|
|
|
// Portal-side key exists — check whether LibreChat still holds a stored copy.
|
|
try {
|
|
const stored = await getUserKeyExpiry({ userId: user.id, name: endpointName });
|
|
if (!stored) {
|
|
logger.warn(
|
|
`[KeyBroker] ${user.email} has a portal "${keyName}" key but no stored copy for endpoint "${endpointName}". ` +
|
|
'Rotate the key on the dashboard and log in again (manual rotation avoids breaking their API scripts).',
|
|
);
|
|
}
|
|
} catch (e) {
|
|
logger.warn(`[KeyBroker] stored-key check failed for ${user.email}: ${e.message}`);
|
|
}
|
|
}
|
|
|
|
module.exports = { syncBrokerUserKey };
|