| | const jwt = require('jsonwebtoken'); |
| | const { nanoid } = require('nanoid'); |
| | const { tool } = require('@langchain/core/tools'); |
| | const { logger } = require('@librechat/data-schemas'); |
| | const { GraphEvents, sleep } = require('@librechat/agents'); |
| | const { |
| | sendEvent, |
| | encryptV2, |
| | decryptV2, |
| | logAxiosError, |
| | refreshAccessToken, |
| | } = require('@librechat/api'); |
| | const { |
| | Time, |
| | CacheKeys, |
| | StepTypes, |
| | Constants, |
| | AuthTypeEnum, |
| | actionDelimiter, |
| | isImageVisionTool, |
| | actionDomainSeparator, |
| | } = require('librechat-data-provider'); |
| | const { findToken, updateToken, createToken } = require('~/models'); |
| | const { getActions, deleteActions } = require('~/models/Action'); |
| | const { deleteAssistant } = require('~/models/Assistant'); |
| | const { getFlowStateManager } = require('~/config'); |
| | const { getLogStores } = require('~/cache'); |
| |
|
| | const JWT_SECRET = process.env.JWT_SECRET; |
| | const toolNameRegex = /^[a-zA-Z0-9_-]+$/; |
| | const replaceSeparatorRegex = new RegExp(actionDomainSeparator, 'g'); |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | const validateAndUpdateTool = async ({ req, tool, assistant_id }) => { |
| | let actions; |
| | if (isImageVisionTool(tool)) { |
| | return null; |
| | } |
| | if (!toolNameRegex.test(tool.function.name)) { |
| | const [functionName, domain] = tool.function.name.split(actionDelimiter); |
| | actions = await getActions({ assistant_id, user: req.user.id }, true); |
| | const matchingActions = actions.filter((action) => { |
| | const metadata = action.metadata; |
| | return metadata && metadata.domain === domain; |
| | }); |
| | const action = matchingActions[0]; |
| | if (!action) { |
| | return null; |
| | } |
| |
|
| | const parsedDomain = await domainParser(domain, true); |
| |
|
| | if (!parsedDomain) { |
| | return null; |
| | } |
| |
|
| | tool.function.name = `${functionName}${actionDelimiter}${parsedDomain}`; |
| | } |
| | return tool; |
| | }; |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | async function domainParser(domain, inverse = false) { |
| | if (!domain) { |
| | return; |
| | } |
| | const domainsCache = getLogStores(CacheKeys.ENCODED_DOMAINS); |
| | const cachedDomain = await domainsCache.get(domain); |
| | if (inverse && cachedDomain) { |
| | return domain; |
| | } |
| |
|
| | if (inverse && domain.length <= Constants.ENCODED_DOMAIN_LENGTH) { |
| | return domain.replace(/\./g, actionDomainSeparator); |
| | } |
| |
|
| | if (inverse) { |
| | const modifiedDomain = Buffer.from(domain).toString('base64'); |
| | const key = modifiedDomain.substring(0, Constants.ENCODED_DOMAIN_LENGTH); |
| | await domainsCache.set(key, modifiedDomain); |
| | return key; |
| | } |
| |
|
| | if (!cachedDomain) { |
| | return domain.replace(replaceSeparatorRegex, '.'); |
| | } |
| |
|
| | try { |
| | return Buffer.from(cachedDomain, 'base64').toString('utf-8'); |
| | } catch (error) { |
| | logger.error(`Failed to parse domain (possibly not base64): ${domain}`, error); |
| | return domain; |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | async function loadActionSets(searchParams) { |
| | return await getActions(searchParams, true); |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | async function createActionTool({ |
| | userId, |
| | res, |
| | action, |
| | requestBuilder, |
| | zodSchema, |
| | name, |
| | description, |
| | encrypted, |
| | }) { |
| | |
| | const _call = async (toolInput, config) => { |
| | try { |
| | |
| | const metadata = action.metadata; |
| | const executor = requestBuilder.createExecutor(); |
| | const preparedExecutor = executor.setParams(toolInput ?? {}); |
| |
|
| | if (metadata.auth && metadata.auth.type !== AuthTypeEnum.None) { |
| | try { |
| | if (metadata.auth.type === AuthTypeEnum.OAuth && metadata.auth.authorization_url) { |
| | const action_id = action.action_id; |
| | const identifier = `${userId}:${action.action_id}`; |
| | const requestLogin = async () => { |
| | const { args: _args, stepId, ...toolCall } = config.toolCall ?? {}; |
| | if (!stepId) { |
| | throw new Error('Tool call is missing stepId'); |
| | } |
| | const statePayload = { |
| | nonce: nanoid(), |
| | user: userId, |
| | action_id, |
| | }; |
| |
|
| | const stateToken = jwt.sign(statePayload, JWT_SECRET, { expiresIn: '10m' }); |
| | try { |
| | const redirectUri = `${process.env.DOMAIN_CLIENT}/api/actions/${action_id}/oauth/callback`; |
| | const params = new URLSearchParams({ |
| | client_id: metadata.oauth_client_id, |
| | scope: metadata.auth.scope, |
| | redirect_uri: redirectUri, |
| | access_type: 'offline', |
| | response_type: 'code', |
| | state: stateToken, |
| | }); |
| |
|
| | const authURL = `${metadata.auth.authorization_url}?${params.toString()}`; |
| | |
| | const data = { |
| | id: stepId, |
| | delta: { |
| | type: StepTypes.TOOL_CALLS, |
| | tool_calls: [{ ...toolCall, args: '' }], |
| | auth: authURL, |
| | expires_at: Date.now() + Time.TWO_MINUTES, |
| | }, |
| | }; |
| | const flowsCache = getLogStores(CacheKeys.FLOWS); |
| | const flowManager = getFlowStateManager(flowsCache); |
| | await flowManager.createFlowWithHandler( |
| | `${identifier}:oauth_login:${config.metadata.thread_id}:${config.metadata.run_id}`, |
| | 'oauth_login', |
| | async () => { |
| | sendEvent(res, { event: GraphEvents.ON_RUN_STEP_DELTA, data }); |
| | logger.debug('Sent OAuth login request to client', { action_id, identifier }); |
| | return true; |
| | }, |
| | config?.signal, |
| | ); |
| | logger.debug('Waiting for OAuth Authorization response', { action_id, identifier }); |
| | const result = await flowManager.createFlow( |
| | identifier, |
| | 'oauth', |
| | { |
| | state: stateToken, |
| | userId: userId, |
| | client_url: metadata.auth.client_url, |
| | redirect_uri: `${process.env.DOMAIN_SERVER}/api/actions/${action_id}/oauth/callback`, |
| | token_exchange_method: metadata.auth.token_exchange_method, |
| | |
| | encrypted_oauth_client_id: encrypted.oauth_client_id, |
| | encrypted_oauth_client_secret: encrypted.oauth_client_secret, |
| | }, |
| | config?.signal, |
| | ); |
| | logger.debug('Received OAuth Authorization response', { action_id, identifier }); |
| | data.delta.auth = undefined; |
| | data.delta.expires_at = undefined; |
| | sendEvent(res, { event: GraphEvents.ON_RUN_STEP_DELTA, data }); |
| | await sleep(3000); |
| | metadata.oauth_access_token = result.access_token; |
| | metadata.oauth_refresh_token = result.refresh_token; |
| | const expiresAt = new Date(Date.now() + result.expires_in * 1000); |
| | metadata.oauth_token_expires_at = expiresAt.toISOString(); |
| | } catch (error) { |
| | const errorMessage = 'Failed to authenticate OAuth tool'; |
| | logger.error(errorMessage, error); |
| | throw new Error(errorMessage); |
| | } |
| | }; |
| |
|
| | const tokenPromises = []; |
| | tokenPromises.push(findToken({ userId, type: 'oauth', identifier })); |
| | tokenPromises.push( |
| | findToken({ |
| | userId, |
| | type: 'oauth_refresh', |
| | identifier: `${identifier}:refresh`, |
| | }), |
| | ); |
| | const [tokenData, refreshTokenData] = await Promise.all(tokenPromises); |
| |
|
| | if (tokenData) { |
| | |
| | metadata.oauth_access_token = await decryptV2(tokenData.token); |
| | if (refreshTokenData) { |
| | metadata.oauth_refresh_token = await decryptV2(refreshTokenData.token); |
| | } |
| | metadata.oauth_token_expires_at = tokenData.expiresAt.toISOString(); |
| | } else if (!refreshTokenData) { |
| | |
| | await requestLogin(); |
| | } else if (refreshTokenData) { |
| | |
| | try { |
| | const refresh_token = await decryptV2(refreshTokenData.token); |
| | const refreshTokens = async () => |
| | await refreshAccessToken( |
| | { |
| | userId, |
| | identifier, |
| | refresh_token, |
| | client_url: metadata.auth.client_url, |
| | encrypted_oauth_client_id: encrypted.oauth_client_id, |
| | token_exchange_method: metadata.auth.token_exchange_method, |
| | encrypted_oauth_client_secret: encrypted.oauth_client_secret, |
| | }, |
| | { |
| | findToken, |
| | updateToken, |
| | createToken, |
| | }, |
| | ); |
| | const flowsCache = getLogStores(CacheKeys.FLOWS); |
| | const flowManager = getFlowStateManager(flowsCache); |
| | const refreshData = await flowManager.createFlowWithHandler( |
| | `${identifier}:refresh`, |
| | 'oauth_refresh', |
| | refreshTokens, |
| | config?.signal, |
| | ); |
| | metadata.oauth_access_token = refreshData.access_token; |
| | if (refreshData.refresh_token) { |
| | metadata.oauth_refresh_token = refreshData.refresh_token; |
| | } |
| | const expiresAt = new Date(Date.now() + refreshData.expires_in * 1000); |
| | metadata.oauth_token_expires_at = expiresAt.toISOString(); |
| | } catch (error) { |
| | logger.error('Failed to refresh token, requesting new login:', error); |
| | await requestLogin(); |
| | } |
| | } else { |
| | await requestLogin(); |
| | } |
| | } |
| |
|
| | await preparedExecutor.setAuth(metadata); |
| | } catch (error) { |
| | if ( |
| | error.message.includes('No access token found') || |
| | error.message.includes('Access token is expired') |
| | ) { |
| | throw error; |
| | } |
| | throw new Error(`Authentication failed: ${error.message}`); |
| | } |
| | } |
| |
|
| | const response = await preparedExecutor.execute(); |
| |
|
| | if (typeof response.data === 'object') { |
| | return JSON.stringify(response.data); |
| | } |
| | return response.data; |
| | } catch (error) { |
| | const message = `API call to ${action.metadata.domain} failed:`; |
| | return logAxiosError({ message, error }); |
| | } |
| | }; |
| |
|
| | if (name) { |
| | return tool(_call, { |
| | name: name.replace(replaceSeparatorRegex, '_'), |
| | description: description || '', |
| | schema: zodSchema, |
| | }); |
| | } |
| |
|
| | return { |
| | _call, |
| | }; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | async function encryptSensitiveValue(value) { |
| | |
| | const encodedValue = encodeURIComponent(value); |
| | return await encryptV2(encodedValue); |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | async function decryptSensitiveValue(value) { |
| | const decryptedValue = await decryptV2(value); |
| | return decodeURIComponent(decryptedValue); |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | async function encryptMetadata(metadata) { |
| | const encryptedMetadata = { ...metadata }; |
| |
|
| | |
| | if (metadata.auth && metadata.auth.type === AuthTypeEnum.ServiceHttp) { |
| | if (metadata.api_key) { |
| | encryptedMetadata.api_key = await encryptSensitiveValue(metadata.api_key); |
| | } |
| | } |
| |
|
| | |
| | else if (metadata.auth && metadata.auth.type === AuthTypeEnum.OAuth) { |
| | if (metadata.oauth_client_id) { |
| | encryptedMetadata.oauth_client_id = await encryptSensitiveValue(metadata.oauth_client_id); |
| | } |
| | if (metadata.oauth_client_secret) { |
| | encryptedMetadata.oauth_client_secret = await encryptSensitiveValue( |
| | metadata.oauth_client_secret, |
| | ); |
| | } |
| | } |
| |
|
| | return encryptedMetadata; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | async function decryptMetadata(metadata) { |
| | const decryptedMetadata = { ...metadata }; |
| |
|
| | |
| | if (metadata.auth && metadata.auth.type === AuthTypeEnum.ServiceHttp) { |
| | if (metadata.api_key) { |
| | decryptedMetadata.api_key = await decryptSensitiveValue(metadata.api_key); |
| | } |
| | } |
| |
|
| | |
| | else if (metadata.auth && metadata.auth.type === AuthTypeEnum.OAuth) { |
| | if (metadata.oauth_client_id) { |
| | decryptedMetadata.oauth_client_id = await decryptSensitiveValue(metadata.oauth_client_id); |
| | } |
| | if (metadata.oauth_client_secret) { |
| | decryptedMetadata.oauth_client_secret = await decryptSensitiveValue( |
| | metadata.oauth_client_secret, |
| | ); |
| | } |
| | } |
| |
|
| | return decryptedMetadata; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | const deleteAssistantActions = async ({ req, assistant_id }) => { |
| | try { |
| | await deleteActions({ assistant_id, user: req.user.id }); |
| | await deleteAssistant({ assistant_id, user: req.user.id }); |
| | } catch (error) { |
| | const message = 'Trouble deleting Assistant Actions for Assistant ID: ' + assistant_id; |
| | logger.error(message, error); |
| | throw new Error(message); |
| | } |
| | }; |
| |
|
| | module.exports = { |
| | deleteAssistantActions, |
| | validateAndUpdateTool, |
| | createActionTool, |
| | encryptMetadata, |
| | decryptMetadata, |
| | loadActionSets, |
| | domainParser, |
| | }; |
| |
|