diff --git a/api/server/services/Config/loadAsyncEndpoints.js b/api/server/services/Config/loadAsyncEndpoints.js index edded906d8..56f693c779 100644 --- a/api/server/services/Config/loadAsyncEndpoints.js +++ b/api/server/services/Config/loadAsyncEndpoints.js @@ -1,5 +1,5 @@ -const fs = require('fs'); const path = require('path'); +const { loadServiceKey } = require('@librechat/api'); const { EModelEndpoint } = require('librechat-data-provider'); const { isUserProvided } = require('~/server/utils'); const { config } = require('./EndpointService'); @@ -18,15 +18,7 @@ async function loadAsyncEndpoints(req) { path.join(__dirname, '../../..', 'data', 'auth.json'); try { - if (process.env.GOOGLE_SERVICE_KEY_FILE_PATH) { - const absolutePath = path.isAbsolute(serviceKeyPath) - ? serviceKeyPath - : path.resolve(serviceKeyPath); - const fileContent = fs.readFileSync(absolutePath, 'utf8'); - serviceKey = JSON.parse(fileContent); - } else { - serviceKey = require('~/data/auth.json'); - } + serviceKey = await loadServiceKey(serviceKeyPath); } catch { if (i === 0) { i++; diff --git a/api/server/services/Endpoints/google/initialize.js b/api/server/services/Endpoints/google/initialize.js index 169d625e3f..871feda604 100644 --- a/api/server/services/Endpoints/google/initialize.js +++ b/api/server/services/Endpoints/google/initialize.js @@ -1,7 +1,6 @@ -const fs = require('fs'); const path = require('path'); -const { getGoogleConfig, isEnabled } = require('@librechat/api'); const { EModelEndpoint, AuthKeys } = require('librechat-data-provider'); +const { getGoogleConfig, isEnabled, loadServiceKey } = require('@librechat/api'); const { getUserKey, checkUserKeyExpiry } = require('~/server/services/UserService'); const { GoogleClient } = require('~/app'); @@ -19,17 +18,12 @@ const initializeClient = async ({ req, res, endpointOption, overrideModel, optio let serviceKey = {}; try { - if (process.env.GOOGLE_SERVICE_KEY_FILE_PATH) { - const serviceKeyPath = - process.env.GOOGLE_SERVICE_KEY_FILE_PATH || - path.join(__dirname, '../../../../..', 'data', 'auth.json'); - const absolutePath = path.isAbsolute(serviceKeyPath) - ? serviceKeyPath - : path.resolve(serviceKeyPath); - const fileContent = fs.readFileSync(absolutePath, 'utf8'); - serviceKey = JSON.parse(fileContent); - } else { - serviceKey = require('~/data/auth.json'); + const serviceKeyPath = + process.env.GOOGLE_SERVICE_KEY_FILE_PATH || + path.join(__dirname, '../../../..', 'data', 'auth.json'); + serviceKey = await loadServiceKey(serviceKeyPath); + if (!serviceKey) { + serviceKey = {}; } } catch (_e) { // Do nothing diff --git a/packages/api/src/files/mistral/crud.ts b/packages/api/src/files/mistral/crud.ts index 5b0d10659a..5aef8edc2b 100644 --- a/packages/api/src/files/mistral/crud.ts +++ b/packages/api/src/files/mistral/crud.ts @@ -21,6 +21,7 @@ import type { OCRImage, } from '~/types'; import { logAxiosError, createAxiosInstance } from '~/utils/axios'; +import { loadServiceKey } from '~/utils/key'; const axios = createAxiosInstance(); const DEFAULT_MISTRAL_BASE_URL = 'https://api.mistral.ai/v1'; @@ -443,27 +444,24 @@ async function loadGoogleAuthConfig(): Promise<{ const serviceKeyPath = process.env.GOOGLE_SERVICE_KEY_FILE_PATH || path.join(__dirname, '..', '..', '..', 'api', 'data', 'auth.json'); - const absolutePath = path.isAbsolute(serviceKeyPath) - ? serviceKeyPath - : path.resolve(serviceKeyPath); - let serviceKey: GoogleServiceAccount; - try { - const authJsonContent = fs.readFileSync(absolutePath, 'utf8'); - serviceKey = JSON.parse(authJsonContent) as GoogleServiceAccount; - } catch { - throw new Error(`Google service account not found at ${absolutePath}`); + const serviceKey = await loadServiceKey(serviceKeyPath); + + if (!serviceKey) { + throw new Error( + `Google service account not found or could not be loaded from ${serviceKeyPath}`, + ); } if (!serviceKey.client_email || !serviceKey.private_key || !serviceKey.project_id) { throw new Error('Invalid Google service account configuration'); } - const jwt = await createJWT(serviceKey); + const jwt = await createJWT(serviceKey as GoogleServiceAccount); const accessToken = await exchangeJWTForAccessToken(jwt); return { - serviceAccount: serviceKey, + serviceAccount: serviceKey as GoogleServiceAccount, accessToken, }; } diff --git a/packages/api/src/utils/index.ts b/packages/api/src/utils/index.ts index dea9472b0e..db304d4c0f 100644 --- a/packages/api/src/utils/index.ts +++ b/packages/api/src/utils/index.ts @@ -5,6 +5,7 @@ export * from './env'; export * from './events'; export * from './files'; export * from './generators'; +export * from './key'; export * from './llm'; export * from './math'; export * from './openid'; diff --git a/packages/api/src/utils/key.ts b/packages/api/src/utils/key.ts new file mode 100644 index 0000000000..7d603afd3f --- /dev/null +++ b/packages/api/src/utils/key.ts @@ -0,0 +1,70 @@ +import fs from 'fs'; +import path from 'path'; +import axios from 'axios'; +import { logger } from '@librechat/data-schemas'; + +export interface GoogleServiceKey { + type?: string; + project_id?: string; + private_key_id?: string; + private_key?: string; + client_email?: string; + client_id?: string; + auth_uri?: string; + token_uri?: string; + auth_provider_x509_cert_url?: string; + client_x509_cert_url?: string; + [key: string]: unknown; +} + +/** + * Load Google service key from file path or URL + * @param keyPath - The path or URL to the service key file + * @returns The parsed service key object or null if failed + */ +export async function loadServiceKey(keyPath: string): Promise { + if (!keyPath) { + return null; + } + + let serviceKey: unknown; + + // Check if it's a URL + if (/^https?:\/\//.test(keyPath)) { + try { + const response = await axios.get(keyPath); + serviceKey = response.data; + } catch (error) { + logger.error(`Failed to fetch the service key from URL: ${keyPath}`, error); + return null; + } + } else { + // It's a file path + try { + const absolutePath = path.isAbsolute(keyPath) ? keyPath : path.resolve(keyPath); + const fileContent = fs.readFileSync(absolutePath, 'utf8'); + serviceKey = JSON.parse(fileContent); + } catch (error) { + logger.error(`Failed to load service key from file: ${keyPath}`, error); + return null; + } + } + + // If the response is a string (e.g., from a URL that returns JSON as text), parse it + if (typeof serviceKey === 'string') { + try { + serviceKey = JSON.parse(serviceKey); + } catch (parseError) { + logger.error(`Failed to parse service key JSON from ${keyPath}`, parseError); + return null; + } + } + + // Validate the service key has required fields + if (!serviceKey || typeof serviceKey !== 'object') { + logger.error(`Invalid service key format from ${keyPath}`); + return null; + } + + return serviceKey as GoogleServiceKey; +}