mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-16 16:30:15 +01:00
🗝️ refactor: loadServiceKey to Support Stringified JSON and Env Var Renaming (#8317)
* feat: Enhance loadServiceKey to support stringified JSON input * chore: Update GOOGLE_SERVICE_KEY_FILE_PATH to GOOGLE_SERVICE_KEY_FILE for consistency
This commit is contained in:
parent
e57fc83d40
commit
7e37211458
5 changed files with 112 additions and 7 deletions
|
|
@ -22,8 +22,7 @@ async function loadAsyncEndpoints(req) {
|
|||
} else {
|
||||
/** Only attempt to load service key if GOOGLE_KEY is not provided */
|
||||
const serviceKeyPath =
|
||||
process.env.GOOGLE_SERVICE_KEY_FILE_PATH ||
|
||||
path.join(__dirname, '../../..', 'data', 'auth.json');
|
||||
process.env.GOOGLE_SERVICE_KEY_FILE || path.join(__dirname, '../../..', 'data', 'auth.json');
|
||||
|
||||
try {
|
||||
serviceKey = await loadServiceKey(serviceKeyPath);
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ const initializeClient = async ({ req, res, endpointOption, overrideModel, optio
|
|||
/** Only attempt to load service key if GOOGLE_KEY is not provided */
|
||||
try {
|
||||
const serviceKeyPath =
|
||||
process.env.GOOGLE_SERVICE_KEY_FILE_PATH ||
|
||||
process.env.GOOGLE_SERVICE_KEY_FILE ||
|
||||
path.join(__dirname, '../../../..', 'data', 'auth.json');
|
||||
serviceKey = await loadServiceKey(serviceKeyPath);
|
||||
if (!serviceKey) {
|
||||
|
|
|
|||
|
|
@ -442,7 +442,7 @@ async function loadGoogleAuthConfig(): Promise<{
|
|||
}> {
|
||||
/** Path from environment variable or default location */
|
||||
const serviceKeyPath =
|
||||
process.env.GOOGLE_SERVICE_KEY_FILE_PATH ||
|
||||
process.env.GOOGLE_SERVICE_KEY_FILE ||
|
||||
path.join(__dirname, '..', '..', '..', 'api', 'data', 'auth.json');
|
||||
|
||||
const serviceKey = await loadServiceKey(serviceKeyPath);
|
||||
|
|
|
|||
97
packages/api/src/utils/key.test.ts
Normal file
97
packages/api/src/utils/key.test.ts
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import axios from 'axios';
|
||||
import { loadServiceKey } from './key';
|
||||
|
||||
jest.mock('fs');
|
||||
jest.mock('axios');
|
||||
jest.mock('@librechat/data-schemas', () => ({
|
||||
logger: {
|
||||
error: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('loadServiceKey', () => {
|
||||
const mockServiceKey = {
|
||||
type: 'service_account',
|
||||
project_id: 'test-project',
|
||||
private_key_id: 'test-key-id',
|
||||
private_key: '-----BEGIN PRIVATE KEY-----\ntest-key\n-----END PRIVATE KEY-----',
|
||||
client_email: 'test@test-project.iam.gserviceaccount.com',
|
||||
client_id: '123456789',
|
||||
auth_uri: 'https://accounts.google.com/o/oauth2/auth',
|
||||
token_uri: 'https://oauth2.googleapis.com/token',
|
||||
auth_provider_x509_cert_url: 'https://www.googleapis.com/oauth2/v1/certs',
|
||||
client_x509_cert_url:
|
||||
'https://www.googleapis.com/robot/v1/metadata/x509/test%40test-project.iam.gserviceaccount.com',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return null if keyPath is empty', async () => {
|
||||
const result = await loadServiceKey('');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should parse stringified JSON directly', async () => {
|
||||
const jsonString = JSON.stringify(mockServiceKey);
|
||||
const result = await loadServiceKey(jsonString);
|
||||
expect(result).toEqual(mockServiceKey);
|
||||
});
|
||||
|
||||
it('should parse stringified JSON with leading/trailing whitespace', async () => {
|
||||
const jsonString = ` ${JSON.stringify(mockServiceKey)} `;
|
||||
const result = await loadServiceKey(jsonString);
|
||||
expect(result).toEqual(mockServiceKey);
|
||||
});
|
||||
|
||||
it('should load from file path', async () => {
|
||||
const filePath = '/path/to/service-key.json';
|
||||
(fs.readFileSync as jest.Mock).mockReturnValue(JSON.stringify(mockServiceKey));
|
||||
|
||||
const result = await loadServiceKey(filePath);
|
||||
expect(fs.readFileSync).toHaveBeenCalledWith(path.resolve(filePath), 'utf8');
|
||||
expect(result).toEqual(mockServiceKey);
|
||||
});
|
||||
|
||||
it('should load from URL', async () => {
|
||||
const url = 'https://example.com/service-key.json';
|
||||
(axios.get as jest.Mock).mockResolvedValue({ data: mockServiceKey });
|
||||
|
||||
const result = await loadServiceKey(url);
|
||||
expect(axios.get).toHaveBeenCalledWith(url);
|
||||
expect(result).toEqual(mockServiceKey);
|
||||
});
|
||||
|
||||
it('should handle invalid JSON string', async () => {
|
||||
const invalidJson = '{ invalid json }';
|
||||
const result = await loadServiceKey(invalidJson);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle file read errors', async () => {
|
||||
const filePath = '/path/to/nonexistent.json';
|
||||
(fs.readFileSync as jest.Mock).mockImplementation(() => {
|
||||
throw new Error('File not found');
|
||||
});
|
||||
|
||||
const result = await loadServiceKey(filePath);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle URL fetch errors', async () => {
|
||||
const url = 'https://example.com/service-key.json';
|
||||
(axios.get as jest.Mock).mockRejectedValue(new Error('Network error'));
|
||||
|
||||
const result = await loadServiceKey(url);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should validate service key format', async () => {
|
||||
const invalidServiceKey = { invalid: 'key' };
|
||||
const result = await loadServiceKey(JSON.stringify(invalidServiceKey));
|
||||
expect(result).toEqual(invalidServiceKey); // It returns the object as-is, validation is minimal
|
||||
});
|
||||
});
|
||||
|
|
@ -18,8 +18,8 @@ export interface GoogleServiceKey {
|
|||
}
|
||||
|
||||
/**
|
||||
* Load Google service key from file path or URL
|
||||
* @param keyPath - The path or URL to the service key file
|
||||
* Load Google service key from file path, URL, or stringified JSON
|
||||
* @param keyPath - The path to the service key file, URL to fetch it from, or stringified JSON
|
||||
* @returns The parsed service key object or null if failed
|
||||
*/
|
||||
export async function loadServiceKey(keyPath: string): Promise<GoogleServiceKey | null> {
|
||||
|
|
@ -29,8 +29,17 @@ export async function loadServiceKey(keyPath: string): Promise<GoogleServiceKey
|
|||
|
||||
let serviceKey: unknown;
|
||||
|
||||
// Check if it's a stringified JSON (starts with '{')
|
||||
if (keyPath.trim().startsWith('{')) {
|
||||
try {
|
||||
serviceKey = JSON.parse(keyPath);
|
||||
} catch (error) {
|
||||
logger.error('Failed to parse service key from stringified JSON', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
// Check if it's a URL
|
||||
if (/^https?:\/\//.test(keyPath)) {
|
||||
else if (/^https?:\/\//.test(keyPath)) {
|
||||
try {
|
||||
const response = await axios.get(keyPath);
|
||||
serviceKey = response.data;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue