| import { extractEnvVariable } from 'librechat-data-provider'; |
| import type { MCPOptions } from 'librechat-data-provider'; |
| import type { IUser } from '@librechat/data-schemas'; |
| import type { RequestBody } from '~/types'; |
| import { extractOpenIDTokenInfo, processOpenIDPlaceholders, isOpenIDTokenValid } from './oidc'; |
|
|
| |
| |
| |
| |
| const ALLOWED_USER_FIELDS = [ |
| 'id', |
| 'name', |
| 'username', |
| 'email', |
| 'provider', |
| 'role', |
| 'googleId', |
| 'facebookId', |
| 'openidId', |
| 'samlId', |
| 'ldapId', |
| 'githubId', |
| 'discordId', |
| 'appleId', |
| 'emailVerified', |
| 'twoFactorEnabled', |
| 'termsAccepted', |
| ] as const; |
|
|
| type AllowedUserField = (typeof ALLOWED_USER_FIELDS)[number]; |
| type SafeUser = Pick<IUser, AllowedUserField>; |
|
|
| |
| |
| |
| |
| |
| |
| |
| export function createSafeUser( |
| user: IUser | null | undefined, |
| ): Partial<SafeUser> & { federatedTokens?: unknown } { |
| if (!user) { |
| return {}; |
| } |
|
|
| const safeUser: Partial<SafeUser> & { federatedTokens?: unknown } = {}; |
| for (const field of ALLOWED_USER_FIELDS) { |
| if (field in user) { |
| safeUser[field] = user[field]; |
| } |
| } |
|
|
| if ('federatedTokens' in user) { |
| safeUser.federatedTokens = user.federatedTokens; |
| } |
|
|
| return safeUser; |
| } |
|
|
| |
| |
| |
| |
| const ALLOWED_BODY_FIELDS = ['conversationId', 'parentMessageId', 'messageId'] as const; |
|
|
| |
| |
| |
| |
| |
| |
| function processUserPlaceholders(value: string, user?: IUser): string { |
| if (!user || typeof value !== 'string') { |
| return value; |
| } |
|
|
| for (const field of ALLOWED_USER_FIELDS) { |
| const placeholder = `{{LIBRECHAT_USER_${field.toUpperCase()}}}`; |
|
|
| if (typeof value !== 'string' || !value.includes(placeholder)) { |
| continue; |
| } |
|
|
| const fieldValue = user[field as keyof IUser]; |
|
|
| |
| if (!(field in user)) { |
| continue; |
| } |
|
|
| |
| if (field === 'id' && (fieldValue === undefined || fieldValue === '')) { |
| continue; |
| } |
|
|
| const replacementValue = fieldValue == null ? '' : String(fieldValue); |
| value = value.replace(new RegExp(placeholder, 'g'), replacementValue); |
| } |
|
|
| return value; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| function processBodyPlaceholders(value: string, body: RequestBody): string { |
| |
| if (typeof value !== 'string') { |
| return value; |
| } |
|
|
| for (const field of ALLOWED_BODY_FIELDS) { |
| const placeholder = `{{LIBRECHAT_BODY_${field.toUpperCase()}}}`; |
| if (!value.includes(placeholder)) { |
| continue; |
| } |
|
|
| const fieldValue = body[field]; |
| const replacementValue = fieldValue == null ? '' : String(fieldValue); |
| value = value.replace(new RegExp(placeholder, 'g'), replacementValue); |
| } |
|
|
| return value; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| function processSingleValue({ |
| originalValue, |
| customUserVars, |
| user, |
| body = undefined, |
| }: { |
| originalValue: string; |
| customUserVars?: Record<string, string>; |
| user?: IUser; |
| body?: RequestBody; |
| }): string { |
| |
| if (typeof originalValue !== 'string') { |
| return String(originalValue); |
| } |
|
|
| let value = originalValue; |
|
|
| if (customUserVars) { |
| for (const [varName, varVal] of Object.entries(customUserVars)) { |
| |
| const escapedVarName = varName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); |
| const placeholderRegex = new RegExp(`\\{\\{${escapedVarName}\\}\\}`, 'g'); |
| value = value.replace(placeholderRegex, varVal); |
| } |
| } |
|
|
| value = processUserPlaceholders(value, user); |
|
|
| const openidTokenInfo = extractOpenIDTokenInfo(user); |
| if (openidTokenInfo && isOpenIDTokenValid(openidTokenInfo)) { |
| value = processOpenIDPlaceholders(value, openidTokenInfo); |
| } |
|
|
| if (body) { |
| value = processBodyPlaceholders(value, body); |
| } |
|
|
| value = extractEnvVariable(value); |
|
|
| return value; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export function processMCPEnv(params: { |
| options: Readonly<MCPOptions>; |
| user?: IUser; |
| customUserVars?: Record<string, string>; |
| body?: RequestBody; |
| }): MCPOptions { |
| const { options, user, customUserVars, body } = params; |
|
|
| if (options === null || options === undefined) { |
| return options; |
| } |
|
|
| const newObj: MCPOptions = structuredClone(options); |
|
|
| if ('env' in newObj && newObj.env) { |
| const processedEnv: Record<string, string> = {}; |
| for (const [key, originalValue] of Object.entries(newObj.env)) { |
| processedEnv[key] = processSingleValue({ originalValue, customUserVars, user, body }); |
| } |
| newObj.env = processedEnv; |
| } |
|
|
| if ('args' in newObj && newObj.args) { |
| const processedArgs: string[] = []; |
| for (const originalValue of newObj.args) { |
| processedArgs.push(processSingleValue({ originalValue, customUserVars, user, body })); |
| } |
| newObj.args = processedArgs; |
| } |
|
|
| |
| |
| if ('headers' in newObj && newObj.headers) { |
| const processedHeaders: Record<string, string> = {}; |
| for (const [key, originalValue] of Object.entries(newObj.headers)) { |
| processedHeaders[key] = processSingleValue({ originalValue, customUserVars, user, body }); |
| } |
| newObj.headers = processedHeaders; |
| } |
|
|
| |
| if ('url' in newObj && newObj.url) { |
| newObj.url = processSingleValue({ originalValue: newObj.url, customUserVars, user, body }); |
| } |
|
|
| |
| if ('oauth' in newObj && newObj.oauth) { |
| const processedOAuth: Record<string, boolean | string | string[] | undefined> = {}; |
| for (const [key, originalValue] of Object.entries(newObj.oauth)) { |
| |
| |
| if (typeof originalValue === 'string') { |
| processedOAuth[key] = processSingleValue({ originalValue, customUserVars, user, body }); |
| } else { |
| processedOAuth[key] = originalValue; |
| } |
| } |
| newObj.oauth = processedOAuth; |
| } |
|
|
| return newObj; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| function processValue( |
| value: unknown, |
| options: { |
| customUserVars?: Record<string, string>; |
| user?: IUser; |
| body?: RequestBody; |
| }, |
| ): unknown { |
| if (typeof value === 'string') { |
| return processSingleValue({ |
| originalValue: value, |
| customUserVars: options.customUserVars, |
| user: options.user, |
| body: options.body, |
| }); |
| } |
|
|
| if (Array.isArray(value)) { |
| return value.map((item) => processValue(item, options)); |
| } |
|
|
| if (value !== null && typeof value === 'object') { |
| const processed: Record<string, unknown> = {}; |
| for (const [key, val] of Object.entries(value)) { |
| processed[key] = processValue(val, options); |
| } |
| return processed; |
| } |
|
|
| return value; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export function resolveNestedObject<T = unknown>(options?: { |
| obj: T | undefined; |
| user?: Partial<IUser> | { id: string }; |
| body?: RequestBody; |
| customUserVars?: Record<string, string>; |
| }): T { |
| const { obj, user, body, customUserVars } = options ?? {}; |
|
|
| if (!obj) { |
| return obj as T; |
| } |
|
|
| return processValue(obj, { |
| customUserVars, |
| user: user as IUser, |
| body, |
| }) as T; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export function resolveHeaders(options?: { |
| headers: Record<string, string> | undefined; |
| user?: Partial<IUser> | { id: string }; |
| body?: RequestBody; |
| customUserVars?: Record<string, string>; |
| }) { |
| const { headers, user, body, customUserVars } = options ?? {}; |
| const inputHeaders = headers ?? {}; |
|
|
| const resolvedHeaders: Record<string, string> = { ...inputHeaders }; |
|
|
| if (inputHeaders && typeof inputHeaders === 'object' && !Array.isArray(inputHeaders)) { |
| Object.keys(inputHeaders).forEach((key) => { |
| resolvedHeaders[key] = processSingleValue({ |
| originalValue: inputHeaders[key], |
| customUserVars, |
| user: user as IUser, |
| body, |
| }); |
| }); |
| } |
|
|
| return resolvedHeaders; |
| } |
|
|