mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-05-13 16:07:30 +00:00
🛡️ fix: Filter user_provided Sentinel in Tool Credential Loading (#12840)
When GOOGLE_KEY=user_provided is set as an endpoint config, the loadAuthValues() function in credentials.js would pass the literal string 'user_provided' to tools via the || fallback chain. This caused Gemini Image Tools to fail at runtime with an invalid API key error, as initializeGeminiClient() received the sentinel value instead of a real key. The fix aligns loadAuthValues() with checkPluginAuth() in format.ts, which already correctly excludes user_provided and empty/whitespace values. Now loadAuthValues() skips these values and continues to the next field in the fallback chain or falls through to user DB values. Added regression tests covering: - user_provided sentinel is skipped, DB value used instead - Fallback chain continues past user_provided to next field - Empty and whitespace env values are skipped - Real env values are returned correctly - Optional fields with sentinel values handled gracefully
This commit is contained in:
parent
89bf2ab7b4
commit
f2df0ea62b
2 changed files with 140 additions and 4 deletions
|
|
@ -1,3 +1,4 @@
|
|||
const { AuthType } = require('librechat-data-provider');
|
||||
const { getUserPluginAuthValue } = require('~/server/services/PluginService');
|
||||
|
||||
/**
|
||||
|
|
@ -19,17 +20,18 @@ const loadAuthValues = async ({ userId, authFields, optional, throwError = true
|
|||
*/
|
||||
const findAuthValue = async (fields) => {
|
||||
for (const field of fields) {
|
||||
let value = process.env[field];
|
||||
if (value) {
|
||||
return { authField: field, authValue: value };
|
||||
const envValue = process.env[field];
|
||||
if (envValue && envValue.trim() !== '' && envValue !== AuthType.USER_PROVIDED) {
|
||||
return { authField: field, authValue: envValue };
|
||||
}
|
||||
let value;
|
||||
try {
|
||||
value = await getUserPluginAuthValue(userId, field, throwError);
|
||||
} catch (err) {
|
||||
if (optional && optional.has(field)) {
|
||||
return { authField: field, authValue: undefined };
|
||||
}
|
||||
if (field === fields[fields.length - 1] && !value) {
|
||||
if (field === fields[fields.length - 1]) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
134
api/server/services/Tools/credentials.spec.js
Normal file
134
api/server/services/Tools/credentials.spec.js
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
const { AuthType } = require('librechat-data-provider');
|
||||
|
||||
jest.mock('~/server/services/PluginService', () => ({
|
||||
getUserPluginAuthValue: jest.fn(),
|
||||
}));
|
||||
|
||||
const { getUserPluginAuthValue } = require('~/server/services/PluginService');
|
||||
const { loadAuthValues } = require('./credentials');
|
||||
|
||||
describe('loadAuthValues', () => {
|
||||
const originalEnv = process.env;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
process.env = { ...originalEnv };
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env = originalEnv;
|
||||
});
|
||||
|
||||
it('should return env value when set to a real key', async () => {
|
||||
process.env.MY_API_KEY = 'real-key-123';
|
||||
|
||||
const result = await loadAuthValues({
|
||||
userId: 'user1',
|
||||
authFields: ['MY_API_KEY'],
|
||||
});
|
||||
|
||||
expect(result).toEqual({ MY_API_KEY: 'real-key-123' });
|
||||
});
|
||||
|
||||
it('should skip user_provided sentinel and try user DB value', async () => {
|
||||
process.env.GOOGLE_KEY = AuthType.USER_PROVIDED;
|
||||
getUserPluginAuthValue.mockResolvedValue('user-stored-key');
|
||||
|
||||
const result = await loadAuthValues({
|
||||
userId: 'user1',
|
||||
authFields: ['GOOGLE_KEY'],
|
||||
});
|
||||
|
||||
expect(getUserPluginAuthValue).toHaveBeenCalledWith('user1', 'GOOGLE_KEY', true);
|
||||
expect(result).toEqual({ GOOGLE_KEY: 'user-stored-key' });
|
||||
});
|
||||
|
||||
it('should skip user_provided and continue to next field in fallback chain', async () => {
|
||||
process.env.GOOGLE_KEY = AuthType.USER_PROVIDED;
|
||||
process.env.GOOGLE_SERVICE_KEY_FILE = '/path/to/service-account.json';
|
||||
getUserPluginAuthValue.mockRejectedValue(new Error('No auth found'));
|
||||
|
||||
const result = await loadAuthValues({
|
||||
userId: 'user1',
|
||||
authFields: ['GEMINI_API_KEY||GOOGLE_KEY||GOOGLE_SERVICE_KEY_FILE'],
|
||||
});
|
||||
|
||||
expect(result).toEqual({ GOOGLE_SERVICE_KEY_FILE: '/path/to/service-account.json' });
|
||||
});
|
||||
|
||||
it('should skip empty and whitespace-only env values', async () => {
|
||||
process.env.EMPTY_KEY = '';
|
||||
process.env.WHITESPACE_KEY = ' ';
|
||||
process.env.REAL_KEY = 'valid';
|
||||
|
||||
const result = await loadAuthValues({
|
||||
userId: 'user1',
|
||||
authFields: ['EMPTY_KEY||WHITESPACE_KEY||REAL_KEY'],
|
||||
});
|
||||
|
||||
expect(result).toEqual({ REAL_KEY: 'valid' });
|
||||
});
|
||||
|
||||
it('should not return user_provided as an auth value', async () => {
|
||||
process.env.GOOGLE_KEY = AuthType.USER_PROVIDED;
|
||||
getUserPluginAuthValue.mockResolvedValue(null);
|
||||
|
||||
const result = await loadAuthValues({
|
||||
userId: 'user1',
|
||||
authFields: ['GOOGLE_KEY'],
|
||||
throwError: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it('should return env value without calling DB when env is valid', async () => {
|
||||
process.env.MY_KEY = 'valid-key';
|
||||
|
||||
const result = await loadAuthValues({
|
||||
userId: 'user1',
|
||||
authFields: ['MY_KEY'],
|
||||
});
|
||||
|
||||
expect(result).toEqual({ MY_KEY: 'valid-key' });
|
||||
expect(getUserPluginAuthValue).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return real env value from first matching field in fallback chain', async () => {
|
||||
process.env.GEMINI_API_KEY = 'gemini-key';
|
||||
process.env.GOOGLE_KEY = 'google-key';
|
||||
|
||||
const result = await loadAuthValues({
|
||||
userId: 'user1',
|
||||
authFields: ['GEMINI_API_KEY||GOOGLE_KEY'],
|
||||
});
|
||||
|
||||
expect(result).toEqual({ GEMINI_API_KEY: 'gemini-key' });
|
||||
});
|
||||
|
||||
it('should return undefined for optional field when sentinel is filtered and DB throws', async () => {
|
||||
process.env.GOOGLE_KEY = AuthType.USER_PROVIDED;
|
||||
getUserPluginAuthValue.mockRejectedValue(new Error('No auth found'));
|
||||
|
||||
const optional = new Set(['GOOGLE_KEY']);
|
||||
const result = await loadAuthValues({
|
||||
userId: 'user1',
|
||||
authFields: ['GOOGLE_KEY'],
|
||||
optional,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ GOOGLE_KEY: undefined });
|
||||
});
|
||||
|
||||
it('should not leak sentinel through catch path when DB lookup throws', async () => {
|
||||
process.env.GOOGLE_KEY = AuthType.USER_PROVIDED;
|
||||
getUserPluginAuthValue.mockRejectedValue(new Error('No auth found'));
|
||||
|
||||
await expect(
|
||||
loadAuthValues({
|
||||
userId: 'user1',
|
||||
authFields: ['GOOGLE_KEY'],
|
||||
}),
|
||||
).rejects.toThrow('No auth found');
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue