mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-03-27 18:16:33 +01:00
* chore: move database model methods to /packages/data-schemas * chore: add TypeScript ESLint rule to warn on unused variables * refactor: model imports to streamline access - Consolidated model imports across various files to improve code organization and reduce redundancy. - Updated imports for models such as Assistant, Message, Conversation, and others to a unified import path. - Adjusted middleware and service files to reflect the new import structure, ensuring functionality remains intact. - Enhanced test files to align with the new import paths, maintaining test coverage and integrity. * chore: migrate database models to packages/data-schemas and refactor all direct Mongoose Model usage outside of data-schemas * test: update agent model mocks in unit tests - Added `getAgent` mock to `client.test.js` to enhance test coverage for agent-related functionality. - Removed redundant `getAgent` and `getAgents` mocks from `openai.spec.js` and `responses.unit.spec.js` to streamline test setup and reduce duplication. - Ensured consistency in agent mock implementations across test files. * fix: update types in data-schemas * refactor: enhance type definitions in transaction and spending methods - Updated type definitions in `checkBalance.ts` to use specific request and response types. - Refined `spendTokens.ts` to utilize a new `SpendTxData` interface for better clarity and type safety. - Improved transaction handling in `transaction.ts` by introducing `TransactionResult` and `TxData` interfaces, ensuring consistent data structures across methods. - Adjusted unit tests in `transaction.spec.ts` to accommodate new type definitions and enhance robustness. * refactor: streamline model imports and enhance code organization - Consolidated model imports across various controllers and services to a unified import path, improving code clarity and reducing redundancy. - Updated multiple files to reflect the new import structure, ensuring all functionalities remain intact. - Enhanced overall code organization by removing duplicate import statements and optimizing the usage of model methods. * feat: implement loadAddedAgent and refactor agent loading logic - Introduced `loadAddedAgent` function to handle loading agents from added conversations, supporting multi-convo parallel execution. - Created a new `load.ts` file to encapsulate agent loading functionalities, including `loadEphemeralAgent` and `loadAgent`. - Updated the `index.ts` file to export the new `load` module instead of the deprecated `loadAgent`. - Enhanced type definitions and improved error handling in the agent loading process. - Adjusted unit tests to reflect changes in the agent loading structure and ensure comprehensive coverage. * refactor: enhance balance handling with new update interface - Introduced `IBalanceUpdate` interface to streamline balance update operations across the codebase. - Updated `upsertBalanceFields` method signatures in `balance.ts`, `transaction.ts`, and related tests to utilize the new interface for improved type safety. - Adjusted type imports in `balance.spec.ts` to include `IBalanceUpdate`, ensuring consistency in balance management functionalities. - Enhanced overall code clarity and maintainability by refining type definitions related to balance operations. * feat: add unit tests for loadAgent functionality and enhance agent loading logic - Introduced comprehensive unit tests for the `loadAgent` function, covering various scenarios including null and empty agent IDs, loading of ephemeral agents, and permission checks. - Enhanced the `initializeClient` function by moving `getConvoFiles` to the correct position in the database method exports, ensuring proper functionality. - Improved test coverage for agent loading, including handling of non-existent agents and user permissions. * chore: reorder memory method exports for consistency - Moved `deleteAllUserMemories` to the correct position in the exported memory methods, ensuring a consistent and logical order of method exports in `memory.ts`.
559 lines
19 KiB
TypeScript
559 lines
19 KiB
TypeScript
import { Types } from 'mongoose';
|
|
import {
|
|
ResourceType,
|
|
AccessRoleIds,
|
|
PrincipalType,
|
|
PermissionBits,
|
|
} from 'librechat-data-provider';
|
|
import { logger, encryptV2, decryptV2, createMethods } from '@librechat/data-schemas';
|
|
import type { AllMethods, MCPServerDocument } from '@librechat/data-schemas';
|
|
import type { IServerConfigsRepositoryInterface } from '~/mcp/registry/ServerConfigsRepositoryInterface';
|
|
import type { ParsedServerConfig, AddServerResult } from '~/mcp/types';
|
|
import { AccessControlService } from '~/acl/accessControlService';
|
|
|
|
/**
|
|
* Regex patterns for credential/env placeholders that should not be allowed in user-provided configs.
|
|
* These would substitute server credentials or the CALLING user's data, creating exfiltration risks
|
|
* when MCP servers are shared between users.
|
|
*
|
|
* Safe placeholders like {{MCP_API_KEY}} are allowed as they resolve from the user's own plugin auth.
|
|
*/
|
|
const DANGEROUS_CREDENTIAL_PATTERNS = [
|
|
/\$\{[^}]+\}/g,
|
|
/\{\{LIBRECHAT_OPENID_[^}]+\}\}/g,
|
|
/\{\{LIBRECHAT_USER_[^}]+\}\}/g,
|
|
/\{\{LIBRECHAT_GRAPH_[^}]+\}\}/g,
|
|
/\{\{LIBRECHAT_BODY_[^}]+\}\}/g,
|
|
];
|
|
|
|
/**
|
|
* Sanitizes headers by removing dangerous credential placeholders.
|
|
* This prevents credential exfiltration when MCP servers are shared between users.
|
|
*
|
|
* @param headers - The headers object to sanitize
|
|
* @returns Sanitized headers with dangerous placeholders removed
|
|
*/
|
|
function sanitizeCredentialPlaceholders(
|
|
headers?: Record<string, string>,
|
|
): Record<string, string> | undefined {
|
|
if (!headers) {
|
|
return headers;
|
|
}
|
|
|
|
const sanitized: Record<string, string> = {};
|
|
for (const [key, value] of Object.entries(headers)) {
|
|
let sanitizedValue = value;
|
|
for (const pattern of DANGEROUS_CREDENTIAL_PATTERNS) {
|
|
sanitizedValue = sanitizedValue.replace(pattern, '');
|
|
}
|
|
sanitized[key] = sanitizedValue;
|
|
}
|
|
return sanitized;
|
|
}
|
|
|
|
/**
|
|
* DB backed config storage
|
|
* Handles CRUD Methods of dynamic mcp servers
|
|
* Will handle Permission ACL
|
|
*/
|
|
export class ServerConfigsDB implements IServerConfigsRepositoryInterface {
|
|
private _dbMethods: AllMethods;
|
|
private _aclService: AccessControlService;
|
|
private _mongoose: typeof import('mongoose');
|
|
|
|
constructor(mongoose: typeof import('mongoose')) {
|
|
if (!mongoose) {
|
|
throw new Error('ServerConfigsDB requires mongoose instance');
|
|
}
|
|
this._mongoose = mongoose;
|
|
this._dbMethods = createMethods(mongoose);
|
|
this._aclService = new AccessControlService(mongoose);
|
|
}
|
|
|
|
/**
|
|
* Checks if user has access to an MCP server via an agent they can VIEW.
|
|
* @param serverName - The MCP server name to check
|
|
* @param userId - The user ID (optional - if not provided, checks publicly accessible agents)
|
|
* @returns true if user has VIEW access to at least one agent that has this MCP server
|
|
*/
|
|
private async hasAccessViaAgent(serverName: string, userId?: string): Promise<boolean> {
|
|
let accessibleAgentIds: Types.ObjectId[];
|
|
|
|
if (!userId) {
|
|
/** Publicly accessible agents */
|
|
accessibleAgentIds = await this._aclService.findPubliclyAccessibleResources({
|
|
resourceType: ResourceType.AGENT,
|
|
requiredPermissions: PermissionBits.VIEW,
|
|
});
|
|
} else {
|
|
/** User-accessible agents */
|
|
accessibleAgentIds = await this._aclService.findAccessibleResources({
|
|
userId,
|
|
requiredPermissions: PermissionBits.VIEW,
|
|
resourceType: ResourceType.AGENT,
|
|
});
|
|
}
|
|
|
|
if (accessibleAgentIds.length === 0) {
|
|
return false;
|
|
}
|
|
|
|
const Agent = this._mongoose.model('Agent');
|
|
const exists = await Agent.exists({
|
|
_id: { $in: accessibleAgentIds },
|
|
mcpServerNames: serverName,
|
|
});
|
|
|
|
return exists !== null;
|
|
}
|
|
|
|
/**
|
|
* Creates a new MCP server and grants owner permissions to the user.
|
|
* @param serverName - Temporary server name (not persisted) will be replaced by the nano id generated by the db method
|
|
* @param config - Server configuration to store
|
|
* @param userId - ID of the user creating the server (required)
|
|
* @returns The created server result with serverName and config (including dbId)
|
|
* @throws Error if userId is not provided
|
|
*/
|
|
public async add(
|
|
serverName: string,
|
|
config: ParsedServerConfig,
|
|
userId?: string,
|
|
): Promise<AddServerResult> {
|
|
logger.debug(
|
|
`[ServerConfigsDB.add] Starting Creating server with temp servername: ${serverName} for the user with the ID ${userId}`,
|
|
);
|
|
if (!userId) {
|
|
throw new Error(
|
|
'[ServerConfigsDB.add] User ID is required to create a database-stored MCP server.',
|
|
);
|
|
}
|
|
|
|
const sanitizedConfig = {
|
|
...config,
|
|
headers: sanitizeCredentialPlaceholders(
|
|
(config as ParsedServerConfig & { headers?: Record<string, string> }).headers,
|
|
),
|
|
} as ParsedServerConfig;
|
|
|
|
/** Transformed user-provided API key config (adds customUserVars and headers) */
|
|
const transformedConfig = this.transformUserApiKeyConfig(sanitizedConfig);
|
|
/** Encrypted config before storing in database */
|
|
const encryptedConfig = await this.encryptConfig(transformedConfig);
|
|
const createdServer = await this._dbMethods.createMCPServer({
|
|
config: encryptedConfig,
|
|
author: userId,
|
|
});
|
|
await this._aclService.grantPermission({
|
|
principalType: PrincipalType.USER,
|
|
principalId: userId,
|
|
resourceType: ResourceType.MCPSERVER,
|
|
resourceId: createdServer._id,
|
|
accessRoleId: AccessRoleIds.MCPSERVER_OWNER,
|
|
grantedBy: userId,
|
|
});
|
|
return {
|
|
serverName: createdServer.serverName,
|
|
config: await this.mapDBServerToParsedConfig(createdServer),
|
|
};
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param serverName mcp server unique identifier "serverName"
|
|
* @param config new Configuration to update
|
|
* @param userId user id required to update DB server config
|
|
*/
|
|
public async update(
|
|
serverName: string,
|
|
config: ParsedServerConfig,
|
|
userId?: string,
|
|
): Promise<void> {
|
|
if (!userId) {
|
|
throw new Error(
|
|
'[ServerConfigsDB.update] User ID is required to update a database-stored MCP server.',
|
|
);
|
|
}
|
|
|
|
const existingServer = await this._dbMethods.findMCPServerByServerName(serverName);
|
|
|
|
let configToSave: ParsedServerConfig = {
|
|
...config,
|
|
headers: sanitizeCredentialPlaceholders(
|
|
(config as ParsedServerConfig & { headers?: Record<string, string> }).headers,
|
|
),
|
|
} as ParsedServerConfig;
|
|
|
|
/** Transformed user-provided API key config (adds customUserVars and headers) */
|
|
configToSave = this.transformUserApiKeyConfig(configToSave);
|
|
|
|
/** Encrypted config before storing in database */
|
|
configToSave = await this.encryptConfig(configToSave);
|
|
|
|
if (!config.oauth?.client_secret && existingServer?.config?.oauth?.client_secret) {
|
|
configToSave = {
|
|
...configToSave,
|
|
oauth: {
|
|
...configToSave.oauth,
|
|
client_secret: existingServer.config.oauth.client_secret,
|
|
},
|
|
};
|
|
}
|
|
|
|
if (
|
|
config.apiKey?.source === 'admin' &&
|
|
!config.apiKey?.key &&
|
|
existingServer?.config?.apiKey?.source === 'admin' &&
|
|
existingServer?.config?.apiKey?.key
|
|
) {
|
|
configToSave = {
|
|
...configToSave,
|
|
apiKey: {
|
|
source: configToSave.apiKey!.source,
|
|
authorization_type: configToSave.apiKey!.authorization_type,
|
|
custom_header: configToSave.apiKey?.custom_header,
|
|
key: existingServer.config.apiKey.key,
|
|
},
|
|
};
|
|
}
|
|
|
|
await this._dbMethods.updateMCPServer(serverName, { config: configToSave });
|
|
}
|
|
|
|
/**
|
|
* Deletes an MCP server and removes all associated ACL entries.
|
|
* @param serverName - The serverName of the server to remove
|
|
* @param userId - User performing the deletion (for logging)
|
|
*/
|
|
public async remove(serverName: string, userId?: string): Promise<void> {
|
|
logger.debug(`[ServerConfigsDB.remove] removing ${serverName}. UserId: ${userId}`);
|
|
const deletedServer = await this._dbMethods.deleteMCPServer(serverName);
|
|
if (deletedServer && deletedServer._id) {
|
|
logger.debug(`[ServerConfigsDB.remove] removing all permissions entries of ${serverName}.`);
|
|
await this._aclService.removeAllPermissions({
|
|
resourceType: ResourceType.MCPSERVER,
|
|
resourceId: deletedServer._id!,
|
|
});
|
|
return;
|
|
}
|
|
logger.warn(`[ServerConfigsDB.remove] server with serverName ${serverName} does not exist`);
|
|
}
|
|
|
|
/**
|
|
* Retrieves a single MCP server configuration by its serverName.
|
|
* @param serverName - The serverName of the server to retrieve
|
|
* @param userId - the user id provide the scope of the request. If the user Id is not provided, only publicly visible servers are returned.
|
|
* @returns The parsed server config or undefined if not found. If accessed via agent, consumeOnly will be true.
|
|
*/
|
|
public async get(serverName: string, userId?: string): Promise<ParsedServerConfig | undefined> {
|
|
const server = await this._dbMethods.findMCPServerByServerName(serverName);
|
|
if (!server) return undefined;
|
|
|
|
if (!userId) {
|
|
const directlyAccessibleMCPIds = (
|
|
await this._aclService.findPubliclyAccessibleResources({
|
|
resourceType: ResourceType.MCPSERVER,
|
|
requiredPermissions: PermissionBits.VIEW,
|
|
})
|
|
).map((id) => id.toString());
|
|
if (directlyAccessibleMCPIds.indexOf(server._id.toString()) > -1) {
|
|
return await this.mapDBServerToParsedConfig(server);
|
|
}
|
|
|
|
const hasAgentAccess = await this.hasAccessViaAgent(serverName);
|
|
if (hasAgentAccess) {
|
|
logger.debug(
|
|
`[ServerConfigsDB.get] accessing ${serverName} via public agent (consumeOnly)`,
|
|
);
|
|
return {
|
|
...(await this.mapDBServerToParsedConfig(server)),
|
|
consumeOnly: true,
|
|
};
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
const userHasDirectAccess = await this._aclService.checkPermission({
|
|
userId,
|
|
resourceType: ResourceType.MCPSERVER,
|
|
requiredPermission: PermissionBits.VIEW,
|
|
resourceId: server._id,
|
|
});
|
|
|
|
if (userHasDirectAccess) {
|
|
logger.debug(
|
|
`[ServerConfigsDB.get] getting ${serverName} for user with the UserId: ${userId}`,
|
|
);
|
|
return await this.mapDBServerToParsedConfig(server);
|
|
}
|
|
|
|
/** Check agent access (user can VIEW an agent that has this MCP server) */
|
|
const hasAgentAccess = await this.hasAccessViaAgent(serverName, userId);
|
|
if (hasAgentAccess) {
|
|
logger.debug(
|
|
`[ServerConfigsDB.get] user ${userId} accessing ${serverName} via agent (consumeOnly)`,
|
|
);
|
|
return {
|
|
...(await this.mapDBServerToParsedConfig(server)),
|
|
consumeOnly: true,
|
|
};
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
/**
|
|
* Return all DB stored configs (scoped by user Id if provided)
|
|
* @param userId optional user id. if not provided only publicly shared mcp configs will be returned
|
|
* @returns record of parsed configs
|
|
*/
|
|
public async getAll(userId?: string): Promise<Record<string, ParsedServerConfig>> {
|
|
let directlyAccessibleMCPIds: Types.ObjectId[] = [];
|
|
if (!userId) {
|
|
logger.debug(`[ServerConfigsDB.getAll] fetching all publicly shared mcp servers`);
|
|
directlyAccessibleMCPIds = await this._aclService.findPubliclyAccessibleResources({
|
|
resourceType: ResourceType.MCPSERVER,
|
|
requiredPermissions: PermissionBits.VIEW,
|
|
});
|
|
} else {
|
|
logger.debug(
|
|
`[ServerConfigsDB.getAll] fetching mcp servers directly shared with the user with ID: ${userId}`,
|
|
);
|
|
directlyAccessibleMCPIds = await this._aclService.findAccessibleResources({
|
|
userId,
|
|
requiredPermissions: PermissionBits.VIEW,
|
|
resourceType: ResourceType.MCPSERVER,
|
|
});
|
|
}
|
|
|
|
let agentMCPServerNames: string[] = [];
|
|
let accessibleAgentIds: Types.ObjectId[] = [];
|
|
|
|
if (!userId) {
|
|
accessibleAgentIds = await this._aclService.findPubliclyAccessibleResources({
|
|
resourceType: ResourceType.AGENT,
|
|
requiredPermissions: PermissionBits.VIEW,
|
|
});
|
|
} else {
|
|
accessibleAgentIds = await this._aclService.findAccessibleResources({
|
|
userId,
|
|
requiredPermissions: PermissionBits.VIEW,
|
|
resourceType: ResourceType.AGENT,
|
|
});
|
|
}
|
|
|
|
if (accessibleAgentIds.length > 0) {
|
|
const Agent = this._mongoose.model('Agent');
|
|
const agentsWithMCP = await Agent.find(
|
|
{
|
|
_id: { $in: accessibleAgentIds },
|
|
mcpServerNames: { $exists: true, $not: { $size: 0 } },
|
|
},
|
|
{ mcpServerNames: 1 },
|
|
).lean();
|
|
|
|
agentMCPServerNames = [
|
|
...new Set(
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
agentsWithMCP.flatMap((a: any) => a.mcpServerNames || []),
|
|
),
|
|
];
|
|
}
|
|
|
|
const directResults = await this._dbMethods.getListMCPServersByIds({
|
|
ids: directlyAccessibleMCPIds,
|
|
});
|
|
|
|
const parsedConfigs: Record<string, ParsedServerConfig> = {};
|
|
const directData = directResults.data || [];
|
|
const directServerNames = new Set(directData.map((s: MCPServerDocument) => s.serverName));
|
|
|
|
const directParsed = await Promise.all(
|
|
directData.map((s: MCPServerDocument) => this.mapDBServerToParsedConfig(s)),
|
|
);
|
|
directData.forEach((s: MCPServerDocument, i: number) => {
|
|
parsedConfigs[s.serverName] = directParsed[i];
|
|
});
|
|
|
|
const agentOnlyServerNames = agentMCPServerNames.filter((name) => !directServerNames.has(name));
|
|
|
|
if (agentOnlyServerNames.length > 0) {
|
|
const agentServers = await this._dbMethods.getListMCPServersByNames({
|
|
names: agentOnlyServerNames,
|
|
});
|
|
|
|
const agentData = agentServers.data || [];
|
|
const agentParsed = await Promise.all(
|
|
agentData.map((s: MCPServerDocument) => this.mapDBServerToParsedConfig(s)),
|
|
);
|
|
agentData.forEach((s: MCPServerDocument, i: number) => {
|
|
parsedConfigs[s.serverName] = { ...agentParsed[i], consumeOnly: true };
|
|
});
|
|
}
|
|
|
|
return parsedConfigs;
|
|
}
|
|
|
|
/** No-op for DB storage; logs a warning if called. */
|
|
public async reset(): Promise<void> {
|
|
logger.warn('Attempt to reset the DB config storage');
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* Maps a MongoDB server document to the ParsedServerConfig format.
|
|
* Decrypts sensitive fields (oauth.client_secret) after retrieval.
|
|
*/
|
|
private async mapDBServerToParsedConfig(
|
|
serverDBDoc: MCPServerDocument,
|
|
): Promise<ParsedServerConfig> {
|
|
const config: ParsedServerConfig = {
|
|
...serverDBDoc.config,
|
|
dbId: (serverDBDoc._id as Types.ObjectId).toString(),
|
|
updatedAt: serverDBDoc.updatedAt?.getTime(),
|
|
};
|
|
return await this.decryptConfig(config);
|
|
}
|
|
|
|
/**
|
|
* Transforms user-provided API key config by auto-generating customUserVars and headers.
|
|
* This is a config transformation, not encryption.
|
|
* @param config - The server config to transform
|
|
* @returns The transformed config with customUserVars and headers set up
|
|
*/
|
|
private transformUserApiKeyConfig(config: ParsedServerConfig): ParsedServerConfig {
|
|
if (!config.apiKey || config.apiKey.source !== 'user') {
|
|
return config;
|
|
}
|
|
|
|
const result = { ...config };
|
|
const headerName =
|
|
result.apiKey!.authorization_type === 'custom'
|
|
? result.apiKey!.custom_header || 'X-Api-Key'
|
|
: 'Authorization';
|
|
|
|
let headerValue: string;
|
|
if (result.apiKey!.authorization_type === 'basic') {
|
|
headerValue = 'Basic {{MCP_API_KEY}}';
|
|
} else if (result.apiKey!.authorization_type === 'bearer') {
|
|
headerValue = 'Bearer {{MCP_API_KEY}}';
|
|
} else {
|
|
headerValue = '{{MCP_API_KEY}}';
|
|
}
|
|
|
|
result.customUserVars = {
|
|
...result.customUserVars,
|
|
MCP_API_KEY: {
|
|
title: 'API Key',
|
|
description: 'Your API key for this MCP server',
|
|
},
|
|
};
|
|
|
|
/** Cast to access headers property (not available on Stdio type) */
|
|
const resultWithHeaders = result as ParsedServerConfig & {
|
|
headers?: Record<string, string>;
|
|
};
|
|
resultWithHeaders.headers = {
|
|
...resultWithHeaders.headers,
|
|
[headerName]: headerValue,
|
|
};
|
|
|
|
// Remove key field since it's user-provided (destructure to omit, not set to undefined)
|
|
|
|
const { key: _removed, ...apiKeyWithoutKey } = result.apiKey!;
|
|
result.apiKey = apiKeyWithoutKey;
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Encrypts sensitive fields in config before database storage.
|
|
* Encrypts oauth.client_secret and apiKey.key (when source === 'admin').
|
|
* Throws on failure to prevent storing plaintext secrets.
|
|
*/
|
|
private async encryptConfig(config: ParsedServerConfig): Promise<ParsedServerConfig> {
|
|
let result = { ...config };
|
|
|
|
if (result.apiKey?.source === 'admin' && result.apiKey.key) {
|
|
try {
|
|
result.apiKey = {
|
|
...result.apiKey,
|
|
key: await encryptV2(result.apiKey.key),
|
|
};
|
|
} catch (error) {
|
|
logger.error('[ServerConfigsDB.encryptConfig] Failed to encrypt apiKey.key', error);
|
|
throw new Error('Failed to encrypt MCP server configuration');
|
|
}
|
|
}
|
|
|
|
if (result.oauth?.client_secret) {
|
|
try {
|
|
result = {
|
|
...result,
|
|
oauth: {
|
|
...result.oauth,
|
|
client_secret: await encryptV2(result.oauth.client_secret),
|
|
},
|
|
};
|
|
} catch (error) {
|
|
logger.error('[ServerConfigsDB.encryptConfig] Failed to encrypt client_secret', error);
|
|
throw new Error('Failed to encrypt MCP server configuration');
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Decrypts sensitive fields in config after database retrieval.
|
|
* Decrypts oauth.client_secret and apiKey.key (when source === 'admin').
|
|
* Returns config without secret on failure (graceful degradation).
|
|
*/
|
|
private async decryptConfig(config: ParsedServerConfig): Promise<ParsedServerConfig> {
|
|
let result = { ...config };
|
|
|
|
if (result.apiKey?.source === 'admin' && result.apiKey.key) {
|
|
try {
|
|
result.apiKey = {
|
|
...result.apiKey,
|
|
key: await decryptV2(result.apiKey.key),
|
|
};
|
|
} catch (error) {
|
|
logger.warn(
|
|
'[ServerConfigsDB.decryptConfig] Failed to decrypt apiKey.key, returning config without key',
|
|
error,
|
|
);
|
|
|
|
const { key: _removedKey, ...apiKeyWithoutKey } = result.apiKey;
|
|
result.apiKey = apiKeyWithoutKey;
|
|
}
|
|
}
|
|
|
|
if (result.oauth?.client_secret) {
|
|
const oauthConfig = result.oauth as { client_secret: string } & typeof result.oauth;
|
|
try {
|
|
result = {
|
|
...result,
|
|
oauth: {
|
|
...oauthConfig,
|
|
client_secret: await decryptV2(oauthConfig.client_secret),
|
|
},
|
|
};
|
|
} catch (error) {
|
|
logger.warn(
|
|
'[ServerConfigsDB.decryptConfig] Failed to decrypt client_secret, returning config without secret',
|
|
error,
|
|
);
|
|
|
|
const { client_secret: _removed, ...oauthWithoutSecret } = oauthConfig;
|
|
result = {
|
|
...result,
|
|
oauth: oauthWithoutSecret,
|
|
};
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
}
|