import { logger } from '@librechat/data-schemas'; import type { IUser } from '@librechat/data-schemas'; import { GRAPH_TOKEN_PLACEHOLDER, DEFAULT_GRAPH_SCOPES, extractOpenIDTokenInfo, isOpenIDTokenValid, } from './oidc'; /** * Pre-computed regex for matching the Graph token placeholder. * Escapes curly braces in the placeholder string for safe regex use. */ const GRAPH_TOKEN_REGEX = new RegExp( GRAPH_TOKEN_PLACEHOLDER.replace(/[{}]/g, '\\$&'), 'g', ); /** * Response from a Graph API token exchange. */ export interface GraphTokenResponse { access_token: string; token_type: string; expires_in: number; scope: string; } /** * Function type for resolving Graph API tokens via OBO flow. * This function is injected from the main API layer since it requires * access to OpenID configuration and caching services. */ export type GraphTokenResolver = ( user: IUser, accessToken: string, scopes: string, fromCache?: boolean, ) => Promise; /** * Options for processing Graph token placeholders. */ export interface GraphTokenOptions { user?: IUser; graphTokenResolver?: GraphTokenResolver; scopes?: string; } /** * Checks if a string contains the Graph token placeholder. * @param value - The string to check * @returns True if the placeholder is present */ export function containsGraphTokenPlaceholder(value: string): boolean { return typeof value === 'string' && value.includes(GRAPH_TOKEN_PLACEHOLDER); } /** * Checks if any value in a record contains the Graph token placeholder. * @param record - The record to check (e.g., headers, env vars) * @returns True if any value contains the placeholder */ export function recordContainsGraphTokenPlaceholder( record: Record | undefined, ): boolean { if (!record || typeof record !== 'object') { return false; } return Object.values(record).some(containsGraphTokenPlaceholder); } /** * Checks if MCP options contain the Graph token placeholder in headers, env, or url. * @param options - The MCP options object * @returns True if any field contains the placeholder */ export function mcpOptionsContainGraphTokenPlaceholder(options: { headers?: Record; env?: Record; url?: string; }): boolean { if (options.url && containsGraphTokenPlaceholder(options.url)) { return true; } if (recordContainsGraphTokenPlaceholder(options.headers)) { return true; } if (recordContainsGraphTokenPlaceholder(options.env)) { return true; } return false; } /** * Asynchronously resolves Graph token placeholders in a string. * This function must be called before the synchronous processMCPEnv pipeline. * * @param value - The string containing the placeholder * @param options - Options including user and graph token resolver * @returns The string with Graph token placeholder replaced */ export async function resolveGraphTokenPlaceholder( value: string, options: GraphTokenOptions, ): Promise { if (!containsGraphTokenPlaceholder(value)) { return value; } const { user, graphTokenResolver, scopes } = options; if (!user || !graphTokenResolver) { logger.warn( '[resolveGraphTokenPlaceholder] User or graphTokenResolver not provided, cannot resolve Graph token', ); return value; } const tokenInfo = extractOpenIDTokenInfo(user); if (!tokenInfo || !isOpenIDTokenValid(tokenInfo)) { logger.warn( '[resolveGraphTokenPlaceholder] No valid OpenID token available for Graph token exchange', ); return value; } if (!tokenInfo.accessToken) { logger.warn('[resolveGraphTokenPlaceholder] No access token available for OBO exchange'); return value; } try { const graphScopes = scopes || process.env.GRAPH_API_SCOPES || DEFAULT_GRAPH_SCOPES; const graphTokenResponse = await graphTokenResolver( user, tokenInfo.accessToken, graphScopes, true, // Use cache ); if (graphTokenResponse?.access_token) { return value.replace(GRAPH_TOKEN_REGEX, graphTokenResponse.access_token); } logger.warn('[resolveGraphTokenPlaceholder] Graph token exchange did not return an access token'); return value; } catch (error) { logger.error('[resolveGraphTokenPlaceholder] Failed to exchange token for Graph API:', error); return value; } } /** * Asynchronously resolves Graph token placeholders in a record of string values. * * @param record - The record containing placeholders (e.g., headers) * @param options - Options including user and graph token resolver * @returns The record with Graph token placeholders replaced */ export async function resolveGraphTokensInRecord( record: Record | undefined, options: GraphTokenOptions, ): Promise | undefined> { if (!record || typeof record !== 'object') { return record; } if (!recordContainsGraphTokenPlaceholder(record)) { return record; } const resolved: Record = {}; for (const [key, value] of Object.entries(record)) { resolved[key] = await resolveGraphTokenPlaceholder(value, options); } return resolved; } /** * Pre-processes MCP options to resolve Graph token placeholders. * This must be called before processMCPEnv since Graph token resolution is async. * * @param options - The MCP options object * @param graphOptions - Options for Graph token resolution * @returns The options with Graph token placeholders resolved */ export async function preProcessGraphTokens; env?: Record; url?: string; }>( options: T, graphOptions: GraphTokenOptions, ): Promise { if (!mcpOptionsContainGraphTokenPlaceholder(options)) { return options; } const result = { ...options }; if (result.url && containsGraphTokenPlaceholder(result.url)) { result.url = await resolveGraphTokenPlaceholder(result.url, graphOptions); } if (result.headers) { result.headers = await resolveGraphTokensInRecord(result.headers, graphOptions); } if (result.env) { result.env = await resolveGraphTokensInRecord(result.env, graphOptions); } return result; }