/** * 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 };