Merge branch 'main' into chore/Allow-Built-In-Dep

This commit is contained in:
Henry 2025-09-28 13:32:22 +01:00
commit 6a2816798e
52 changed files with 658 additions and 140 deletions

View File

@ -14,6 +14,7 @@ DATABASE_PATH=/root/.flowise
# DATABASE_USER=root
# DATABASE_PASSWORD=mypassword
# DATABASE_SSL=true
# DATABASE_REJECT_UNAUTHORIZED=true
# DATABASE_SSL_KEY_BASE64=<Self signed certificate in BASE64>
@ -163,4 +164,14 @@ JWT_REFRESH_TOKEN_EXPIRY_IN_MINUTES=43200
# REDIS_KEY=
# REDIS_CA=
# REDIS_KEEP_ALIVE=
# ENABLE_BULLMQ_DASHBOARD=
# ENABLE_BULLMQ_DASHBOARD=
############################################################################################################
############################################## SECURITY ####################################################
############################################################################################################
# HTTP_DENY_LIST=
# CUSTOM_MCP_SECURITY_CHECK=true
# CUSTOM_MCP_PROTOCOL=sse #(stdio | sse)
# TRUST_PROXY=true #(true | false | 1 | loopback| linklocal | uniquelocal | IP addresses | loopback, IP addresses)

View File

@ -139,6 +139,12 @@ services:
- REDIS_CA=${REDIS_CA}
- REDIS_KEEP_ALIVE=${REDIS_KEEP_ALIVE}
- ENABLE_BULLMQ_DASHBOARD=${ENABLE_BULLMQ_DASHBOARD}
# SECURITY
- CUSTOM_MCP_SECURITY_CHECK=${CUSTOM_MCP_SECURITY_CHECK}
- CUSTOM_MCP_PROTOCOL=${CUSTOM_MCP_PROTOCOL}
- HTTP_DENY_LIST=${HTTP_DENY_LIST}
- TRUST_PROXY=${TRUST_PROXY}
healthcheck:
test: ['CMD', 'curl', '-f', 'http://localhost:${PORT:-3000}/api/v1/ping']
interval: 10s
@ -276,6 +282,12 @@ services:
- REDIS_CA=${REDIS_CA}
- REDIS_KEEP_ALIVE=${REDIS_KEEP_ALIVE}
- ENABLE_BULLMQ_DASHBOARD=${ENABLE_BULLMQ_DASHBOARD}
# SECURITY
- CUSTOM_MCP_SECURITY_CHECK=${CUSTOM_MCP_SECURITY_CHECK}
- CUSTOM_MCP_PROTOCOL=${CUSTOM_MCP_PROTOCOL}
- HTTP_DENY_LIST=${HTTP_DENY_LIST}
- TRUST_PROXY=${TRUST_PROXY}
healthcheck:
test: ['CMD', 'curl', '-f', 'http://localhost:${WORKER_PORT:-5566}/healthz']
interval: 10s

View File

@ -124,6 +124,12 @@ services:
- REDIS_CA=${REDIS_CA}
- REDIS_KEEP_ALIVE=${REDIS_KEEP_ALIVE}
- ENABLE_BULLMQ_DASHBOARD=${ENABLE_BULLMQ_DASHBOARD}
# SECURITY
- CUSTOM_MCP_SECURITY_CHECK=${CUSTOM_MCP_SECURITY_CHECK}
- CUSTOM_MCP_PROTOCOL=${CUSTOM_MCP_PROTOCOL}
- HTTP_DENY_LIST=${HTTP_DENY_LIST}
- TRUST_PROXY=${TRUST_PROXY}
ports:
- '${PORT}:${PORT}'
healthcheck:

View File

@ -14,6 +14,7 @@ DATABASE_PATH=/root/.flowise
# DATABASE_USER=root
# DATABASE_PASSWORD=mypassword
# DATABASE_SSL=true
# DATABASE_REJECT_UNAUTHORIZED=true
# DATABASE_SSL_KEY_BASE64=<Self signed certificate in BASE64>
@ -163,4 +164,14 @@ JWT_REFRESH_TOKEN_EXPIRY_IN_MINUTES=43200
# REDIS_KEY=
# REDIS_CA=
# REDIS_KEEP_ALIVE=
# ENABLE_BULLMQ_DASHBOARD=
# ENABLE_BULLMQ_DASHBOARD=
############################################################################################################
############################################## SECURITY ####################################################
############################################################################################################
# HTTP_DENY_LIST=
# CUSTOM_MCP_SECURITY_CHECK=true
# CUSTOM_MCP_PROTOCOL=sse #(stdio | sse)
# TRUST_PROXY=true #(true | false | 1 | loopback| linklocal | uniquelocal | IP addresses | loopback, IP addresses)

View File

@ -124,6 +124,12 @@ services:
- REDIS_CA=${REDIS_CA}
- REDIS_KEEP_ALIVE=${REDIS_KEEP_ALIVE}
- ENABLE_BULLMQ_DASHBOARD=${ENABLE_BULLMQ_DASHBOARD}
# SECURITY
- CUSTOM_MCP_SECURITY_CHECK=${CUSTOM_MCP_SECURITY_CHECK}
- CUSTOM_MCP_PROTOCOL=${CUSTOM_MCP_PROTOCOL}
- HTTP_DENY_LIST=${HTTP_DENY_LIST}
- TRUST_PROXY=${TRUST_PROXY}
ports:
- '${WORKER_PORT}:${WORKER_PORT}'
healthcheck:

View File

@ -0,0 +1,26 @@
import { INodeParams, INodeCredential } from '../src/Interface'
class TeradataBearerTokenCredential implements INodeCredential {
label: string
name: string
description: string
version: number
inputs: INodeParams[]
constructor() {
this.label = 'Teradata Bearer Token'
this.name = 'teradataBearerToken'
this.version = 1.0
this.description =
'Refer to <a target="_blank" href="https://docs.teradata.com/r/Enterprise_IntelliFlex_VMware/Teradata-Vector-Store-User-Guide/Setting-up-Vector-Store/Importing-Modules-Required-for-Vector-Store">official guide</a> on how to get Teradata Bearer Token'
this.inputs = [
{
label: 'Token',
name: 'token',
type: 'password'
}
]
}
}
module.exports = { credClass: TeradataBearerTokenCredential }

View File

@ -0,0 +1,28 @@
import { INodeParams, INodeCredential } from '../src/Interface'
class TeradataTD2Credential implements INodeCredential {
label: string
name: string
version: number
inputs: INodeParams[]
constructor() {
this.label = 'Teradata TD2 Auth'
this.name = 'teradataTD2Auth'
this.version = 1.0
this.inputs = [
{
label: 'Teradata TD2 Auth Username',
name: 'tdUsername',
type: 'string'
},
{
label: 'Teradata TD2 Auth Password',
name: 'tdPassword',
type: 'password'
}
]
}
}
module.exports = { credClass: TeradataTD2Credential }

View File

@ -175,8 +175,7 @@ class CustomFunction_Agentflow implements INode {
try {
const response = await executeJavaScriptCode(javascriptFunction, sandbox, {
libraries: ['axios'],
streamOutput,
timeout: 10000
streamOutput
})
let finalOutput = response

View File

@ -8,8 +8,7 @@ import {
IServerSideEventStreamer
} from '../../../src/Interface'
import axios, { AxiosRequestConfig } from 'axios'
import { getCredentialData, getCredentialParam, processTemplateVariables } from '../../../src/utils'
import JSON5 from 'json5'
import { getCredentialData, getCredentialParam, processTemplateVariables, parseJsonBody } from '../../../src/utils'
import { DataSource } from 'typeorm'
import { BaseMessageLike } from '@langchain/core/messages'
import { updateFlowState } from '../utils'
@ -168,7 +167,7 @@ class ExecuteFlow_Agentflow implements INode {
let overrideConfig = nodeData.inputs?.executeFlowOverrideConfig
if (typeof overrideConfig === 'string' && overrideConfig.startsWith('{') && overrideConfig.endsWith('}')) {
try {
overrideConfig = JSON5.parse(overrideConfig)
overrideConfig = parseJsonBody(overrideConfig)
} catch (parseError) {
throw new Error(`Invalid JSON in executeFlowOverrideConfig: ${parseError.message}`)
}

View File

@ -2,9 +2,8 @@ import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Inter
import { AxiosRequestConfig, Method, ResponseType } from 'axios'
import FormData from 'form-data'
import * as querystring from 'querystring'
import { getCredentialData, getCredentialParam } from '../../../src/utils'
import { getCredentialData, getCredentialParam, parseJsonBody } from '../../../src/utils'
import { secureAxiosRequest } from '../../../src/httpSecurity'
import JSON5 from 'json5'
class HTTP_Agentflow implements INode {
label: string
@ -20,16 +19,6 @@ class HTTP_Agentflow implements INode {
credential: INodeParams
inputs: INodeParams[]
private parseJsonBody(body: string): any {
try {
return JSON5.parse(body)
} catch (error) {
throw new Error(
`Invalid JSON format in body. Original error: ${error.message}. Please ensure your JSON is properly formatted with quoted strings and valid escape sequences.`
)
}
}
constructor() {
this.label = 'HTTP'
this.name = 'httpAgentflow'
@ -285,7 +274,7 @@ class HTTP_Agentflow implements INode {
if (method !== 'GET' && body) {
switch (bodyType) {
case 'json': {
requestConfig.data = typeof body === 'string' ? this.parseJsonBody(body) : body
requestConfig.data = typeof body === 'string' ? parseJsonBody(body) : body
requestHeaders['Content-Type'] = 'application/json'
break
}
@ -303,7 +292,7 @@ class HTTP_Agentflow implements INode {
break
}
case 'xWwwFormUrlencoded':
requestConfig.data = querystring.stringify(typeof body === 'string' ? this.parseJsonBody(body) : body)
requestConfig.data = querystring.stringify(typeof body === 'string' ? parseJsonBody(body) : body)
requestHeaders['Content-Type'] = 'application/x-www-form-urlencoded'
break
}

View File

@ -1,5 +1,5 @@
import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface'
import JSON5 from 'json5'
import { parseJsonBody } from '../../../src/utils'
class Iteration_Agentflow implements INode {
label: string
@ -42,10 +42,10 @@ class Iteration_Agentflow implements INode {
// Helper function to clean JSON strings with redundant backslashes
const safeParseJson = (str: string): string => {
try {
return JSON5.parse(str)
return parseJsonBody(str)
} catch {
// Try parsing after cleaning
return JSON5.parse(str.replace(/\\(["'[\]{}])/g, '$1'))
return parseJsonBody(str.replace(/\\(["'[\]{}])/g, '$1'))
}
}

View File

@ -1,5 +1,12 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.72492 9.35559L6.5 24L15 5.5L6.33616 7.15025C5.30261 7.34712 4.59832 8.3111 4.72492 9.35559Z" fill="#97D700" stroke="#97D700" stroke-width="2" stroke-linejoin="round"/>
<path d="M26.6204 20.5943L26.5699 20.6161L19.5 4.5L24.0986 4.14626C25.163 4.06438 26.1041 4.83296 26.2365 5.8923L27.8137 18.5094C27.9241 19.3925 27.4377 20.2422 26.6204 20.5943Z" fill="#71C5E8" stroke="#71C5E8" stroke-width="2" stroke-linejoin="round"/>
<path d="M17.5 10L9.5 28L23 22L17.5 10Z" fill="#FF9114" stroke="#FF9114" stroke-width="2" stroke-linejoin="round"/>
<svg width="200" height="200" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_267_4154)">
<path d="M114.695 0H196.97C198.643 0 200 1.35671 200 3.03031V128.766C200 131.778 196.083 132.945 194.434 130.425L112.159 4.68953C110.841 2.67412 112.287 0 114.695 0Z" fill="#246DFF"/>
<path d="M85.3048 0H3.0303C1.35671 0 0 1.35671 0 3.03031V128.766C0 131.778 3.91698 132.945 5.566 130.425L87.8405 4.68953C89.1593 2.67412 87.7134 0 85.3048 0Z" fill="#20A34E"/>
<path d="M98.5909 100.668L5.12683 194.835C3.22886 196.747 4.58334 200 7.27759 200H192.8C195.483 200 196.842 196.77 194.967 194.852L102.908 100.685C101.726 99.4749 99.7824 99.4676 98.5909 100.668Z" fill="#F86606"/>
</g>
<defs>
<clipPath id="clip0_267_4154">
<rect width="200" height="200" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 653 B

After

Width:  |  Height:  |  Size: 827 B

View File

@ -119,8 +119,7 @@ class CustomDocumentLoader_DocumentLoaders implements INode {
try {
const response = await executeJavaScriptCode(javascriptFunction, sandbox, {
libraries: ['axios'],
timeout: 10000
libraries: ['axios']
})
if (output === 'document' && Array.isArray(response)) {

View File

@ -130,8 +130,7 @@ class ChatPromptTemplate_Prompts implements INode {
try {
const response = await executeJavaScriptCode(messageHistoryCode, sandbox, {
libraries: ['axios', '@langchain/core'],
timeout: 10000
libraries: ['axios', '@langchain/core']
})
const parsedResponse = JSON.parse(response)

View File

@ -940,9 +940,7 @@ const getReturnOutput = async (nodeData: INodeData, input: string, options: ICom
const sandbox = createCodeExecutionSandbox(input, variables, flow)
try {
const response = await executeJavaScriptCode(updateStateMemoryCode, sandbox, {
timeout: 10000
})
const response = await executeJavaScriptCode(updateStateMemoryCode, sandbox)
if (typeof response !== 'object') throw new Error('Return output must be an object')
return response

View File

@ -282,9 +282,7 @@ const runCondition = async (nodeData: INodeData, input: string, options: ICommon
const sandbox = createCodeExecutionSandbox(input, variables, flow)
try {
const response = await executeJavaScriptCode(conditionFunction, sandbox, {
timeout: 10000
})
const response = await executeJavaScriptCode(conditionFunction, sandbox)
if (typeof response !== 'string') throw new Error('Condition function must return a string')
return response

View File

@ -549,9 +549,7 @@ const runCondition = async (
const sandbox = createCodeExecutionSandbox(input, variables, flow)
try {
const response = await executeJavaScriptCode(conditionFunction, sandbox, {
timeout: 10000
})
const response = await executeJavaScriptCode(conditionFunction, sandbox)
if (typeof response !== 'string') throw new Error('Condition function must return a string')
return response

View File

@ -166,9 +166,7 @@ class CustomFunction_SeqAgents implements INode {
const sandbox = createCodeExecutionSandbox(input, variables, flow, additionalSandbox)
try {
const response = await executeJavaScriptCode(javascriptFunction, sandbox, {
timeout: 10000
})
const response = await executeJavaScriptCode(javascriptFunction, sandbox)
if (returnValueAs === 'stateObj') {
if (typeof response !== 'object') {

View File

@ -264,8 +264,7 @@ class ExecuteFlow_SeqAgents implements INode {
try {
let response = await executeJavaScriptCode(code, sandbox, {
useSandbox: false,
timeout: 10000
useSandbox: false
})
if (typeof response === 'object') {

View File

@ -712,9 +712,7 @@ const getReturnOutput = async (nodeData: INodeData, input: string, options: ICom
const sandbox = createCodeExecutionSandbox(input, variables, flow)
try {
const response = await executeJavaScriptCode(updateStateMemoryCode, sandbox, {
timeout: 10000
})
const response = await executeJavaScriptCode(updateStateMemoryCode, sandbox)
if (typeof response !== 'object') throw new Error('Return output must be an object')
return response

View File

@ -204,9 +204,7 @@ class State_SeqAgents implements INode {
const sandbox = createCodeExecutionSandbox('', variables, flow)
try {
const response = await executeJavaScriptCode(`return ${stateMemoryCode}`, sandbox, {
timeout: 10000
})
const response = await executeJavaScriptCode(`return ${stateMemoryCode}`, sandbox)
if (typeof response !== 'object') throw new Error('State must be an object')
const returnOutput: ISeqAgentNode = {

View File

@ -575,9 +575,7 @@ const getReturnOutput = async (
const sandbox = createCodeExecutionSandbox(input, variables, flow)
try {
const response = await executeJavaScriptCode(updateStateMemoryCode, sandbox, {
timeout: 10000
})
const response = await executeJavaScriptCode(updateStateMemoryCode, sandbox)
if (typeof response !== 'object') throw new Error('Return output must be an object')
return response

View File

@ -396,9 +396,7 @@ export const checkMessageHistory = async (
const sandbox = createCodeExecutionSandbox('', variables, flow)
try {
const response = await executeJavaScriptCode(messageHistory, sandbox, {
timeout: 10000
})
const response = await executeJavaScriptCode(messageHistory, sandbox)
if (!Array.isArray(response)) throw new Error('Returned message history must be an array')
if (sysPrompt) {

View File

@ -364,8 +364,7 @@ try {
const sandbox = createCodeExecutionSandbox('', [], {}, additionalSandbox)
let response = await executeJavaScriptCode(code, sandbox, {
useSandbox: false,
timeout: 10000
useSandbox: false
})
if (typeof response === 'object') {

View File

@ -372,8 +372,7 @@ try {
const sandbox = createCodeExecutionSandbox('', [], {}, additionalSandbox)
let response = await executeJavaScriptCode(code, sandbox, {
useSandbox: false,
timeout: 10000
useSandbox: false
})
if (typeof response === 'object') {

View File

@ -124,9 +124,7 @@ export class DynamicStructuredTool<
const sandbox = createCodeExecutionSandbox('', this.variables || [], flow, additionalSandbox)
let response = await executeJavaScriptCode(this.code, sandbox, {
timeout: 10000
})
let response = await executeJavaScriptCode(this.code, sandbox)
if (typeof response === 'object') {
response = JSON.stringify(response)

View File

@ -1,10 +1,9 @@
import { Tool } from '@langchain/core/tools'
import { ICommonObject, IDatabaseEntity, INode, INodeData, INodeOptionsValue, INodeParams } from '../../../../src/Interface'
import { MCPToolkit } from '../core'
import { getVars, prepareSandboxVars } from '../../../../src/utils'
import { MCPToolkit, validateMCPServerConfig } from '../core'
import { getVars, prepareSandboxVars, parseJsonBody } from '../../../../src/utils'
import { DataSource } from 'typeorm'
import hash from 'object-hash'
import JSON5 from 'json5'
const mcpServerConfig = `{
"command": "npx",
@ -75,8 +74,8 @@ class Custom_MCP implements INode {
},
placeholder: mcpServerConfig,
warning:
process.env.CUSTOM_MCP_SECURITY_CHECK === 'true'
? 'In next release, only Remote MCP with url is supported. Read more <a href="https://docs.flowiseai.com/tutorials/tools-and-mcp#streamable-http-recommended" target="_blank">here</a>'
process.env.CUSTOM_MCP_PROTOCOL === 'sse'
? 'Only Remote MCP with url is supported. Read more <a href="https://docs.flowiseai.com/tutorials/tools-and-mcp#streamable-http-recommended" target="_blank">here</a>'
: undefined
},
{
@ -174,6 +173,14 @@ class Custom_MCP implements INode {
serverParams = JSON.parse(serverParamsString)
}
if (process.env.CUSTOM_MCP_SECURITY_CHECK !== 'false') {
try {
validateMCPServerConfig(serverParams)
} catch (error) {
throw new Error(`Security validation failed: ${error.message}`)
}
}
// Compatible with stdio and SSE
let toolkit: MCPToolkit
if (process.env.CUSTOM_MCP_PROTOCOL === 'sse') {
@ -262,7 +269,7 @@ function substituteVariablesInString(str: string, sandbox: any): string {
function convertToValidJSONString(inputString: string) {
try {
const jsObject = JSON5.parse(inputString)
const jsObject = parseJsonBody(inputString)
return JSON.stringify(jsObject, null, 2)
} catch (error) {
console.error('Error converting to JSON:', error)

View File

@ -1,7 +1,7 @@
import { Tool } from '@langchain/core/tools'
import { ICommonObject, INode, INodeData, INodeOptionsValue, INodeParams } from '../../../../src/Interface'
import { getNodeModulesPackagePath } from '../../../../src/utils'
import { MCPToolkit, validateArgsForLocalFileAccess } from '../core'
import { MCPToolkit, validateMCPServerConfig } from '../core'
class Supergateway_MCP implements INode {
label: string
@ -106,9 +106,9 @@ class Supergateway_MCP implements INode {
args: [packagePath, ...processedArgs]
}
if (process.env.CUSTOM_MCP_SECURITY_CHECK === 'true') {
if (process.env.CUSTOM_MCP_SECURITY_CHECK !== 'false') {
try {
validateArgsForLocalFileAccess(processedArgs)
validateMCPServerConfig(serverParams)
} catch (error) {
throw new Error(`Security validation failed: ${error.message}`)
}

View File

@ -0,0 +1,147 @@
import { Tool } from '@langchain/core/tools'
import { ICommonObject, INode, INodeData, INodeOptionsValue, INodeParams } from '../../../../src/Interface'
import { getCredentialData, getCredentialParam } from '../../../../src/utils'
import { MCPToolkit } from '../core'
import hash from 'object-hash'
class Teradata_MCP implements INode {
label: string
name: string
version: number
description: string
type: string
icon: string
category: string
baseClasses: string[]
documentation: string
credential: INodeParams
inputs: INodeParams[]
constructor() {
this.label = 'Teradata MCP'
this.name = 'teradataMCP'
this.version = 1.0
this.type = 'Teradata MCP Tool'
this.icon = 'teradata.svg'
this.category = 'Tools (MCP)'
this.description = 'MCP Server for Teradata (remote HTTP streamable)'
this.documentation = 'https://github.com/Teradata/teradata-mcp-server'
this.credential = {
label: 'Connect Credential',
name: 'credential',
type: 'credential',
credentialNames: ['teradataTD2Auth', 'teradataBearerToken'],
description: 'Needed when using Teradata MCP server with authentication'
}
this.inputs = [
{
label: 'MCP Server URL',
name: 'mcpUrl',
type: 'string',
placeholder: 'http://teradata-mcp-server:8001/mcp',
description: 'URL of your Teradata MCP server',
optional: false
},
{
label: 'Bearer Token',
name: 'bearerToken',
type: 'string',
optional: true,
description: 'Optional to override Default set credentials'
},
{
label: 'Available Actions',
name: 'mcpActions',
type: 'asyncMultiOptions',
loadMethod: 'listActions',
refresh: true
}
]
this.baseClasses = ['Tool']
}
//@ts-ignore
loadMethods = {
listActions: async (nodeData: INodeData, options: ICommonObject): Promise<INodeOptionsValue[]> => {
try {
const toolset = await this.getTools(nodeData, options)
toolset.sort((a: any, b: any) => a.name.localeCompare(b.name))
return toolset.map(({ name, ...rest }) => ({
label: name.toUpperCase(),
name: name,
description: rest.description || name
}))
} catch (error) {
console.error('Error listing actions:', error)
return [
{
label: 'No Available Actions',
name: 'error',
description: 'No available actions, please check your MCP server URL and credentials, then refresh.'
}
]
}
}
}
async init(nodeData: INodeData, _: string, options: ICommonObject): Promise<any> {
const tools = await this.getTools(nodeData, options)
const _mcpActions = nodeData.inputs?.mcpActions
let mcpActions = []
if (_mcpActions) {
try {
mcpActions = typeof _mcpActions === 'string' ? JSON.parse(_mcpActions) : _mcpActions
} catch (error) {
console.error('Error parsing mcp actions:', error)
}
}
return tools.filter((tool: any) => mcpActions.includes(tool.name))
}
async getTools(nodeData: INodeData, options: ICommonObject): Promise<Tool[]> {
const credentialData = await getCredentialData(nodeData.credential ?? '', options)
const mcpUrl = nodeData.inputs?.mcpUrl || 'http://teradata-mcp-server:8001/mcp'
if (!mcpUrl) {
throw new Error('Missing MCP Server URL')
}
// Determine auth method from credentials
let serverParams: any = {
url: mcpUrl,
headers: {}
}
// Get Bearer token from node input (from agent flow) or credential store
const bearerToken = nodeData.inputs?.bearerToken || getCredentialParam('token', credentialData, nodeData)
const username = getCredentialParam('tdUsername', credentialData, nodeData)
const password = getCredentialParam('tdPassword', credentialData, nodeData)
if (bearerToken) {
serverParams.headers['Authorization'] = `Bearer ${bearerToken}`
} else if (username && password) {
serverParams.headers['Authorization'] = 'Basic ' + Buffer.from(`${username}:${password}`).toString('base64')
} else {
throw new Error('Missing credentials: provide Bearer token from flow/credentials OR username/password from credentials')
}
const workspaceId = options?.searchOptions?.workspaceId?._value || options?.workspaceId || 'tdws_default'
let sandbox: ICommonObject = {}
const cacheKey = hash({ workspaceId, serverParams, sandbox })
if (options.cachePool) {
const cachedResult = await options.cachePool.getMCPCache(cacheKey)
if (cachedResult) {
if (cachedResult.tools.length > 0) {
return cachedResult.tools
}
}
}
// Use SSE for remote HTTP MCP servers
const toolkit = new MCPToolkit(serverParams, 'sse')
await toolkit.initialize()
const tools = toolkit.tools ?? []
if (options.cachePool) {
await options.cachePool.addMCPCache(cacheKey, { toolkit, tools })
}
return tools as Tool[]
}
}
module.exports = { nodeClass: Teradata_MCP }

View File

@ -0,0 +1,19 @@
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d_15769_12621)">
<path d="M49.3232 8H14.6768C13.1984 8 12 9.19843 12 10.6768V45.3232C12 46.8016 13.1984 48 14.6768 48H49.3232C50.8016 48 52 46.8016 52 45.3232V10.6768C52 9.19843 50.8016 8 49.3232 8Z" fill="#FF5F02"/>
<path d="M25.098 32.467V15.5882H30.1292V20.2286H35.7465V24.6834H30.1292V32.467C30.1292 34.4794 31.1745 35.1364 32.6447 35.1364H35.7391V39.5863H32.6447C27.4915 39.5814 25.098 37.3369 25.098 32.467Z" fill="white"/>
<path d="M37.8688 37.376C37.8688 36.668 38.1501 35.989 38.6507 35.4884C39.1513 34.9878 39.8303 34.7066 40.5383 34.7066C41.2462 34.7066 41.9252 34.9878 42.4258 35.4884C42.9265 35.989 43.2077 36.668 43.2077 37.376C43.2077 38.084 42.9265 38.7629 42.4258 39.2636C41.9252 39.7642 41.2462 40.0454 40.5383 40.0454C39.8303 40.0454 39.1513 39.7642 38.6507 39.2636C38.1501 38.7629 37.8688 38.084 37.8688 37.376Z" fill="white"/>
</g>
<defs>
<filter id="filter0_d_15769_12621" x="0" y="0" width="64" height="64" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="6"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_15769_12621"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_15769_12621" result="shape"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -219,3 +219,67 @@ export const validateArgsForLocalFileAccess = (args: string[]): void => {
}
}
}
export const validateCommandInjection = (args: string[]): void => {
const dangerousPatterns = [
// Shell metacharacters
/[;&|`$(){}[\]<>]/,
// Command chaining
/&&|\|\||;;/,
// Redirections
/>>|<<|>/,
// Backticks and command substitution
/`|\$\(/,
// Process substitution
/<\(|>\(/
]
for (const arg of args) {
if (typeof arg !== 'string') continue
for (const pattern of dangerousPatterns) {
if (pattern.test(arg)) {
throw new Error(`Argument contains potentially dangerous characters: "${arg}"`)
}
}
}
}
export const validateEnvironmentVariables = (env: Record<string, any>): void => {
const dangerousEnvVars = ['PATH', 'LD_LIBRARY_PATH', 'DYLD_LIBRARY_PATH']
for (const [key, value] of Object.entries(env)) {
if (dangerousEnvVars.includes(key)) {
throw new Error(`Environment variable '${key}' modification is not allowed`)
}
if (typeof value === 'string' && value.includes('\0')) {
throw new Error(`Environment variable '${key}' contains null byte`)
}
}
}
export const validateMCPServerConfig = (serverParams: any): void => {
// Validate the entire server configuration
if (!serverParams || typeof serverParams !== 'object') {
throw new Error('Invalid server configuration')
}
// Command allowlist - only allow specific safe commands
const allowedCommands = ['node', 'npx', 'python', 'python3', 'docker']
if (serverParams.command && !allowedCommands.includes(serverParams.command)) {
throw new Error(`Command '${serverParams.command}' is not allowed. Allowed commands: ${allowedCommands.join(', ')}`)
}
// Validate arguments if present
if (serverParams.args && Array.isArray(serverParams.args)) {
validateArgsForLocalFileAccess(serverParams.args)
validateCommandInjection(serverParams.args)
}
// Validate environment variables
if (serverParams.env) {
validateEnvironmentVariables(serverParams.env)
}
}

View File

@ -253,9 +253,7 @@ export class DynamicStructuredTool<
const sandbox = createCodeExecutionSandbox('', this.variables || [], flow, additionalSandbox)
let response = await executeJavaScriptCode(this.customCode || defaultCode, sandbox, {
timeout: 10000
})
let response = await executeJavaScriptCode(this.customCode || defaultCode, sandbox)
if (typeof response === 'object') {
response = JSON.stringify(response)

View File

@ -1,7 +1,6 @@
import { INode, INodeData, INodeParams } from '../../../src/Interface'
import { getBaseClasses, stripHTMLFromToolInput } from '../../../src/utils'
import { getBaseClasses, stripHTMLFromToolInput, parseJsonBody } from '../../../src/utils'
import { desc, RequestParameters, RequestsDeleteTool } from './core'
import JSON5 from 'json5'
const codeExample = `{
"id": {
@ -131,7 +130,7 @@ class RequestsDelete_Tools implements INode {
if (queryParamsSchema) obj.queryParamsSchema = queryParamsSchema
if (maxOutputLength) obj.maxOutputLength = parseInt(maxOutputLength, 10)
if (headers) {
const parsedHeaders = typeof headers === 'object' ? headers : JSON5.parse(stripHTMLFromToolInput(headers))
const parsedHeaders = typeof headers === 'object' ? headers : parseJsonBody(stripHTMLFromToolInput(headers))
obj.headers = parsedHeaders
}

View File

@ -1,7 +1,7 @@
import { z } from 'zod'
import { DynamicStructuredTool } from '../OpenAPIToolkit/core'
import { secureFetch } from '../../../src/httpSecurity'
import JSON5 from 'json5'
import { parseJsonBody } from '../../../src/utils'
export const desc = `Use this when you need to execute a DELETE request to remove data from a website.`
@ -23,7 +23,7 @@ const createRequestsDeleteSchema = (queryParamsSchema?: string) => {
// If queryParamsSchema is provided, parse it and add dynamic query params
if (queryParamsSchema) {
try {
const parsedSchema = JSON5.parse(queryParamsSchema)
const parsedSchema = parseJsonBody(queryParamsSchema)
const queryParamsObject: Record<string, z.ZodTypeAny> = {}
Object.entries(parsedSchema).forEach(([key, config]: [string, any]) => {
@ -109,7 +109,7 @@ export class RequestsDeleteTool extends DynamicStructuredTool {
if (this.queryParamsSchema && params.queryParams && Object.keys(params.queryParams).length > 0) {
try {
const parsedSchema = JSON5.parse(this.queryParamsSchema)
const parsedSchema = parseJsonBody(this.queryParamsSchema)
const pathParams: Array<{ key: string; value: string }> = []
Object.entries(params.queryParams).forEach(([key, value]) => {

View File

@ -1,7 +1,6 @@
import { INode, INodeData, INodeParams } from '../../../src/Interface'
import { getBaseClasses, stripHTMLFromToolInput } from '../../../src/utils'
import { getBaseClasses, stripHTMLFromToolInput, parseJsonBody } from '../../../src/utils'
import { desc, RequestParameters, RequestsGetTool } from './core'
import JSON5 from 'json5'
const codeExample = `{
"id": {
@ -131,7 +130,7 @@ class RequestsGet_Tools implements INode {
if (queryParamsSchema) obj.queryParamsSchema = queryParamsSchema
if (maxOutputLength) obj.maxOutputLength = parseInt(maxOutputLength, 10)
if (headers) {
const parsedHeaders = typeof headers === 'object' ? headers : JSON5.parse(stripHTMLFromToolInput(headers))
const parsedHeaders = typeof headers === 'object' ? headers : parseJsonBody(stripHTMLFromToolInput(headers))
obj.headers = parsedHeaders
}

View File

@ -1,7 +1,7 @@
import { z } from 'zod'
import { DynamicStructuredTool } from '../OpenAPIToolkit/core'
import { secureFetch } from '../../../src/httpSecurity'
import JSON5 from 'json5'
import { parseJsonBody } from '../../../src/utils'
export const desc = `Use this when you need to execute a GET request to get data from a website.`
@ -23,7 +23,7 @@ const createRequestsGetSchema = (queryParamsSchema?: string) => {
// If queryParamsSchema is provided, parse it and add dynamic query params
if (queryParamsSchema) {
try {
const parsedSchema = JSON5.parse(queryParamsSchema)
const parsedSchema = parseJsonBody(queryParamsSchema)
const queryParamsObject: Record<string, z.ZodTypeAny> = {}
Object.entries(parsedSchema).forEach(([key, config]: [string, any]) => {
@ -109,7 +109,7 @@ export class RequestsGetTool extends DynamicStructuredTool {
if (this.queryParamsSchema && params.queryParams && Object.keys(params.queryParams).length > 0) {
try {
const parsedSchema = JSON5.parse(this.queryParamsSchema)
const parsedSchema = parseJsonBody(this.queryParamsSchema)
const pathParams: Array<{ key: string; value: string }> = []
Object.entries(params.queryParams).forEach(([key, value]) => {

View File

@ -1,7 +1,6 @@
import { INode, INodeData, INodeParams } from '../../../src/Interface'
import { getBaseClasses, stripHTMLFromToolInput } from '../../../src/utils'
import { getBaseClasses, stripHTMLFromToolInput, parseJsonBody } from '../../../src/utils'
import { RequestParameters, desc, RequestsPostTool } from './core'
import JSON5 from 'json5'
const codeExample = `{
"name": {
@ -141,11 +140,11 @@ class RequestsPost_Tools implements INode {
if (bodySchema) obj.bodySchema = stripHTMLFromToolInput(bodySchema)
if (maxOutputLength) obj.maxOutputLength = parseInt(maxOutputLength, 10)
if (headers) {
const parsedHeaders = typeof headers === 'object' ? headers : JSON5.parse(stripHTMLFromToolInput(headers))
const parsedHeaders = typeof headers === 'object' ? headers : parseJsonBody(stripHTMLFromToolInput(headers))
obj.headers = parsedHeaders
}
if (body) {
const parsedBody = typeof body === 'object' ? body : JSON5.parse(body)
const parsedBody = typeof body === 'object' ? body : parseJsonBody(body)
obj.body = parsedBody
}

View File

@ -1,7 +1,7 @@
import { z } from 'zod'
import { DynamicStructuredTool } from '../OpenAPIToolkit/core'
import { secureFetch } from '../../../src/httpSecurity'
import JSON5 from 'json5'
import { parseJsonBody } from '../../../src/utils'
export const desc = `Use this when you want to execute a POST request to create or update a resource.`
@ -28,7 +28,7 @@ const createRequestsPostSchema = (bodySchema?: string) => {
// If bodySchema is provided, parse it and add dynamic body params
if (bodySchema) {
try {
const parsedSchema = JSON5.parse(bodySchema)
const parsedSchema = parseJsonBody(bodySchema)
const bodyParamsObject: Record<string, z.ZodTypeAny> = {}
Object.entries(parsedSchema).forEach(([key, config]: [string, any]) => {

View File

@ -1,7 +1,6 @@
import { INode, INodeData, INodeParams } from '../../../src/Interface'
import { getBaseClasses, stripHTMLFromToolInput } from '../../../src/utils'
import { getBaseClasses, stripHTMLFromToolInput, parseJsonBody } from '../../../src/utils'
import { RequestParameters, desc, RequestsPutTool } from './core'
import JSON5 from 'json5'
const codeExample = `{
"name": {
@ -141,11 +140,11 @@ class RequestsPut_Tools implements INode {
if (bodySchema) obj.bodySchema = stripHTMLFromToolInput(bodySchema)
if (maxOutputLength) obj.maxOutputLength = parseInt(maxOutputLength, 10)
if (headers) {
const parsedHeaders = typeof headers === 'object' ? headers : JSON5.parse(stripHTMLFromToolInput(headers))
const parsedHeaders = typeof headers === 'object' ? headers : parseJsonBody(stripHTMLFromToolInput(headers))
obj.headers = parsedHeaders
}
if (body) {
const parsedBody = typeof body === 'object' ? body : JSON5.parse(body)
const parsedBody = typeof body === 'object' ? body : parseJsonBody(body)
obj.body = parsedBody
}

View File

@ -1,7 +1,7 @@
import { z } from 'zod'
import { DynamicStructuredTool } from '../OpenAPIToolkit/core'
import { secureFetch } from '../../../src/httpSecurity'
import JSON5 from 'json5'
import { parseJsonBody } from '../../../src/utils'
export const desc = `Use this when you want to execute a PUT request to update or replace a resource.`
@ -28,7 +28,7 @@ const createRequestsPutSchema = (bodySchema?: string) => {
// If bodySchema is provided, parse it and add dynamic body params
if (bodySchema) {
try {
const parsedSchema = JSON5.parse(bodySchema)
const parsedSchema = parseJsonBody(bodySchema)
const bodyParamsObject: Record<string, z.ZodTypeAny> = {}
Object.entries(parsedSchema).forEach(([key, config]: [string, any]) => {

View File

@ -132,9 +132,7 @@ class CustomFunction_Utilities implements INode {
const sandbox = createCodeExecutionSandbox(input, variables, flow, additionalSandbox)
try {
const response = await executeJavaScriptCode(javascriptFunction, sandbox, {
timeout: 10000
})
const response = await executeJavaScriptCode(javascriptFunction, sandbox)
if (typeof response === 'string' && !isEndingNode) {
return handleEscapeCharacters(response, false)

View File

@ -131,16 +131,12 @@ class IfElseFunction_Utilities implements INode {
const sandbox = createCodeExecutionSandbox(input, variables, flow, additionalSandbox)
try {
const responseTrue = await executeJavaScriptCode(ifFunction, sandbox, {
timeout: 10000
})
const responseTrue = await executeJavaScriptCode(ifFunction, sandbox)
if (responseTrue)
return { output: typeof responseTrue === 'string' ? handleEscapeCharacters(responseTrue, false) : responseTrue, type: true }
const responseFalse = await executeJavaScriptCode(elseFunction, sandbox, {
timeout: 10000
})
const responseFalse = await executeJavaScriptCode(elseFunction, sandbox)
return { output: typeof responseFalse === 'string' ? handleEscapeCharacters(responseFalse, false) : responseFalse, type: false }
} catch (e) {

View File

@ -1408,7 +1408,7 @@ const parseOutput = (output: any): any => {
// Check if it looks like JSON (starts with { or [)
if ((trimmedOutput.startsWith('{') && trimmedOutput.endsWith('}')) || (trimmedOutput.startsWith('[') && trimmedOutput.endsWith(']'))) {
try {
const parsedOutput = JSON5.parse(trimmedOutput)
const parsedOutput = parseJsonBody(trimmedOutput)
return parsedOutput
} catch (e) {
return output
@ -1439,6 +1439,10 @@ export const executeJavaScriptCode = async (
): Promise<any> => {
const { timeout = 300000, useSandbox = true, streamOutput, libraries = [], nodeVMOptions = {} } = options
const shouldUseSandbox = useSandbox && process.env.E2B_APIKEY
let timeoutMs = timeout
if (process.env.SANDBOX_TIMEOUT) {
timeoutMs = parseInt(process.env.SANDBOX_TIMEOUT, 10)
}
if (shouldUseSandbox) {
try {
@ -1495,7 +1499,7 @@ export const executeJavaScriptCode = async (
}
}
const sbx = await Sandbox.create({ apiKey: process.env.E2B_APIKEY, timeoutMs: timeout })
const sbx = await Sandbox.create({ apiKey: process.env.E2B_APIKEY, timeoutMs })
// Install libraries
for (const library of libraries) {
@ -1554,7 +1558,7 @@ export const executeJavaScriptCode = async (
},
eval: false,
wasm: false,
timeout
timeout: timeoutMs
}
// Merge with custom nodeVMOptions if provided
@ -1659,3 +1663,71 @@ export const processTemplateVariables = (state: ICommonObject, finalOutput: any)
return newState
}
/**
* Parse JSON body with comprehensive error handling and cleanup
* @param {string} body - The JSON string to parse
* @returns {any} - The parsed JSON object
* @throws {Error} - Detailed error message with suggestions for common JSON issues
*/
export const parseJsonBody = (body: string): any => {
try {
// First try to parse as-is with JSON5 (which handles more cases than standard JSON)
return JSON5.parse(body)
} catch (error) {
try {
// If that fails, try to clean up common issues
let cleanedBody = body
// 1. Remove unnecessary backslash escapes for square brackets and braces
// eslint-disable-next-line
cleanedBody = cleanedBody.replace(/\\(?=[\[\]{}])/g, '')
// 2. Fix single quotes to double quotes (but preserve quotes inside strings)
cleanedBody = cleanedBody.replace(/'/g, '"')
// 3. Remove trailing commas before closing brackets/braces
cleanedBody = cleanedBody.replace(/,(\s*[}\]])/g, '$1')
// 4. Remove comments (// and /* */)
cleanedBody = cleanedBody
.replace(/\/\/.*$/gm, '') // Remove single-line comments
.replace(/\/\*[\s\S]*?\*\//g, '') // Remove multi-line comments
return JSON5.parse(cleanedBody)
} catch (secondError) {
try {
// 3rd attempt: try with standard JSON.parse on original body
return JSON.parse(body)
} catch (thirdError) {
try {
// 4th attempt: try with standard JSON.parse on cleaned body
const finalCleanedBody = body
// eslint-disable-next-line
.replace(/\\(?=[\[\]{}])/g, '') // Basic escape cleanup
.replace(/,(\s*[}\]])/g, '$1') // Remove trailing commas
.trim()
return JSON.parse(finalCleanedBody)
} catch (fourthError) {
// Provide comprehensive error message with suggestions
const suggestions = [
'• Ensure all strings are enclosed in double quotes',
'• Remove trailing commas',
'• Remove comments (// or /* */)',
'• Escape special characters properly (\\n for newlines, \\" for quotes)',
'• Use double quotes instead of single quotes',
'• Remove unnecessary backslashes before brackets [ ] { }'
]
throw new Error(
`Invalid JSON format in body. Original error: ${error.message}. ` +
`After cleanup attempts: ${secondError.message}. 3rd attempt: ${thirdError.message}. Final attempt: ${fourthError.message}.\n\n` +
`Common fixes:\n${suggestions.join('\n')}\n\n` +
`Received body: ${body.substring(0, 200)}${body.length > 200 ? '...' : ''}`
)
}
}
}
}
}

View File

@ -14,6 +14,7 @@ PORT=3000
# DATABASE_USER=root
# DATABASE_PASSWORD=mypassword
# DATABASE_SSL=true
# DATABASE_REJECT_UNAUTHORIZED=true
# DATABASE_SSL_KEY_BASE64=<Self signed certificate in BASE64>
@ -171,6 +172,9 @@ JWT_REFRESH_TOKEN_EXPIRY_IN_MINUTES=43200
############################################################################################################
# HTTP_DENY_LIST=
# CUSTOM_MCP_SECURITY_CHECK=true
# CUSTOM_MCP_PROTOCOL=sse #(stdio | sse)
# TRUST_PROXY=true #(true | false | 1 | loopback| linklocal | uniquelocal | IP addresses | loopback, IP addresses)
############################################################################################################
@ -178,4 +182,4 @@ JWT_REFRESH_TOKEN_EXPIRY_IN_MINUTES=43200
############################################################################################################
# PUPPETEER_EXECUTABLE_FILE_PATH='C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe'
# PLAYWRIGHT_EXECUTABLE_FILE_PATH='C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe'
# PLAYWRIGHT_EXECUTABLE_FILE_PATH='C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe'

View File

@ -111,7 +111,7 @@ export function getDataSource(): DataSource {
export const getDatabaseSSLFromEnv = () => {
if (process.env.DATABASE_SSL_KEY_BASE64) {
return {
rejectUnauthorized: false,
rejectUnauthorized: process.env.DATABASE_REJECT_UNAUTHORIZED === 'true',
ca: Buffer.from(process.env.DATABASE_SSL_KEY_BASE64, 'base64')
}
} else if (process.env.DATABASE_SSL === 'true') {

View File

@ -74,7 +74,11 @@ export abstract class BaseCommand extends Command {
REDIS_KEY: Flags.string(),
REDIS_CA: Flags.string(),
REDIS_KEEP_ALIVE: Flags.string(),
ENABLE_BULLMQ_DASHBOARD: Flags.string()
ENABLE_BULLMQ_DASHBOARD: Flags.string(),
CUSTOM_MCP_SECURITY_CHECK: Flags.string(),
CUSTOM_MCP_PROTOCOL: Flags.string(),
HTTP_DENY_LIST: Flags.string(),
TRUST_PROXY: Flags.string()
}
protected async stopProcess() {
@ -202,5 +206,11 @@ export abstract class BaseCommand extends Command {
if (flags.REMOVE_ON_COUNT) process.env.REMOVE_ON_COUNT = flags.REMOVE_ON_COUNT
if (flags.REDIS_KEEP_ALIVE) process.env.REDIS_KEEP_ALIVE = flags.REDIS_KEEP_ALIVE
if (flags.ENABLE_BULLMQ_DASHBOARD) process.env.ENABLE_BULLMQ_DASHBOARD = flags.ENABLE_BULLMQ_DASHBOARD
// Security
if (flags.CUSTOM_MCP_SECURITY_CHECK) process.env.CUSTOM_MCP_SECURITY_CHECK = flags.CUSTOM_MCP_SECURITY_CHECK
if (flags.CUSTOM_MCP_PROTOCOL) process.env.CUSTOM_MCP_PROTOCOL = flags.CUSTOM_MCP_PROTOCOL
if (flags.HTTP_DENY_LIST) process.env.HTTP_DENY_LIST = flags.HTTP_DENY_LIST
if (flags.TRUST_PROXY) process.env.TRUST_PROXY = flags.TRUST_PROXY
}
}

View File

@ -163,7 +163,19 @@ export class App {
this.app.use(express.urlencoded({ limit: flowise_file_size_limit, extended: true }))
// Enhanced trust proxy settings for load balancer
this.app.set('trust proxy', true) // Trust all proxies
let trustProxy: string | boolean | number | undefined = process.env.TRUST_PROXY
if (typeof trustProxy === 'undefined' || trustProxy.trim() === '' || trustProxy === 'true') {
// Default to trust all proxies
trustProxy = true
} else if (trustProxy === 'false') {
// Disable trust proxy
trustProxy = false
} else if (!isNaN(Number(trustProxy))) {
// Number: Trust specific number of proxies
trustProxy = Number(trustProxy)
}
this.app.set('trust proxy', trustProxy)
// Allow access from specified domains
this.app.use(cors(getCorsOptions()))

View File

@ -26,6 +26,7 @@
"@mui/x-tree-view": "^7.25.0",
"@reduxjs/toolkit": "^2.2.7",
"@tabler/icons-react": "^3.30.0",
"@tiptap/extension-code-block-lowlight": "^3.4.3",
"@tiptap/extension-mention": "^2.11.5",
"@tiptap/extension-placeholder": "^2.11.5",
"@tiptap/pm": "^2.11.5",
@ -46,6 +47,7 @@
"history": "^5.0.0",
"html-react-parser": "^3.0.4",
"lodash": "^4.17.21",
"lowlight": "^3.3.0",
"moment": "^2.29.3",
"notistack": "^2.0.4",
"prop-types": "^15.7.2",

View File

@ -132,6 +132,80 @@
content: '\200B';
}
}
pre {
background: var(--code-bg, #2d2d2d) !important;
border-radius: 0.5rem;
color: var(--code-color, #d4d4d4) !important;
font-family: 'JetBrainsMono', 'Fira Code', 'Monaco', 'Cascadia Code', 'Roboto Mono', monospace;
margin: 1.5rem 0;
padding: 0.75rem 1rem;
code {
background: none !important;
color: inherit !important;
font-size: 0.8rem;
padding: 0;
}
/* Syntax highlighting matching the screenshot colors */
.hljs-comment,
.hljs-quote {
color: var(--hljs-comment, #6a9955) !important;
}
.hljs-variable,
.hljs-name {
color: var(--hljs-variable, #9cdcfe) !important; /* Light blue for variables */
}
.hljs-number,
.hljs-literal {
color: var(--hljs-number, #b5cea8) !important; /* Light green for numbers */
}
.hljs-string {
color: var(--hljs-string, #ce9178) !important; /* Orange/peach for strings */
}
.hljs-title,
.hljs-built_in,
.hljs-builtin-name {
color: var(--hljs-title, #dcdcaa) !important; /* Yellow for function names */
}
.hljs-keyword,
.hljs-selector-tag {
color: var(--hljs-keyword, #569cd6) !important; /* Blue for keywords */
}
/* Additional elements that should match the base text color */
.hljs-operator,
.hljs-punctuation,
.hljs-template-variable,
.hljs-attribute,
.hljs-tag,
.hljs-regexp,
.hljs-link,
.hljs-selector-id,
.hljs-selector-class,
.hljs-meta,
.hljs-type,
.hljs-params,
.hljs-symbol,
.hljs-bullet,
.hljs-section {
color: var(--code-color, #d4d4d4) !important; /* Default text color */
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: 700;
}
}
}
.spin-animation {

View File

@ -1,6 +1,6 @@
import { createPortal } from 'react-dom'
import { useState, useEffect } from 'react'
import { useDispatch } from 'react-redux'
import { useDispatch, useSelector } from 'react-redux'
import PropTypes from 'prop-types'
import PerfectScrollbar from 'react-perfect-scrollbar'
@ -17,14 +17,18 @@ import Placeholder from '@tiptap/extension-placeholder'
import { mergeAttributes } from '@tiptap/core'
import StarterKit from '@tiptap/starter-kit'
import Mention from '@tiptap/extension-mention'
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight'
import { common, createLowlight } from 'lowlight'
import { suggestionOptions } from '@/ui-component/input/suggestionOption'
import { getAvailableNodesForVariable } from '@/utils/genericHelper'
const lowlight = createLowlight(common)
// Store
import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from '@/store/actions'
// Add styled component for editor wrapper
const StyledEditorContent = styled(EditorContent)(({ theme, rows }) => ({
const StyledEditorContent = styled(EditorContent)(({ theme, rows, disabled, isDarkMode }) => ({
'& .ProseMirror': {
padding: '0px 14px',
height: rows ? `${rows * 1.4375}rem` : '2.4rem',
@ -32,40 +36,48 @@ const StyledEditorContent = styled(EditorContent)(({ theme, rows }) => ({
overflowX: rows ? 'auto' : 'hidden',
lineHeight: rows ? '1.4375em' : '0.875em',
fontWeight: 500,
color: theme.palette.grey[900],
border: `1px solid ${theme.palette.textBackground.border}`,
color: disabled ? theme.palette.action.disabled : theme.palette.grey[900],
border: `1px solid ${theme.palette.grey[900] + 25}`,
borderRadius: '10px',
backgroundColor: theme.palette.textBackground.main,
boxSizing: 'border-box',
whiteSpace: rows ? 'pre-wrap' : 'nowrap',
'&:hover': {
borderColor: theme.palette.text.primary,
cursor: 'text'
borderColor: disabled ? `${theme.palette.grey[900] + 25}` : theme.palette.text.primary,
cursor: disabled ? 'default' : 'text'
},
'&:focus': {
borderColor: theme.palette.primary.main,
boxShadow: `0 0 0 0px ${theme.palette.primary.main}`,
borderColor: disabled ? `${theme.palette.grey[900] + 25}` : theme.palette.primary.main,
outline: 'none'
},
'&[disabled]': {
backgroundColor: theme.palette.action.disabledBackground,
color: theme.palette.action.disabled
},
// Placeholder for first paragraph when editor is empty
'& p.is-editor-empty:first-of-type::before': {
content: 'attr(data-placeholder)',
float: 'left',
color: theme.palette.text.primary,
opacity: 0.4,
color: disabled ? theme.palette.action.disabled : theme.palette.text.primary,
opacity: disabled ? 0.6 : 0.4,
pointerEvents: 'none',
height: 0
}
},
// Set CSS custom properties for theme-aware styling based on the screenshot
'--code-bg': isDarkMode ? '#2d2d2d' : '#f5f5f5',
'--code-color': isDarkMode ? '#d4d4d4' : '#333333',
'--hljs-comment': isDarkMode ? '#6a9955' : '#6a9955',
'--hljs-variable': isDarkMode ? '#9cdcfe' : '#d73a49', // Light blue for variables (var, i)
'--hljs-number': isDarkMode ? '#b5cea8' : '#e36209', // Light green for numbers (1, 20, 15, etc.)
'--hljs-string': isDarkMode ? '#ce9178' : '#22863a', // Orange/peach for strings ("FizzBuzz", "Fizz", "Buzz")
'--hljs-title': isDarkMode ? '#dcdcaa' : '#6f42c1', // Yellow for function names (log)
'--hljs-keyword': isDarkMode ? '#569cd6' : '#005cc5', // Blue for keywords (for, if, else)
'--hljs-operator': isDarkMode ? '#d4d4d4' : '#333333', // White/gray for operators (=, %, ==, etc.)
'--hljs-punctuation': isDarkMode ? '#d4d4d4' : '#333333' // White/gray for punctuation ({, }, ;, etc.)
}
}))
// define your extension array
const extensions = (availableNodesForVariable, availableState, acceptNodeOutputAsVariable, nodes, nodeData, isNodeInsideInteration) => [
StarterKit,
StarterKit.configure({
codeBlock: false
}),
Mention.configure({
HTMLAttributes: {
class: 'variable'
@ -86,6 +98,11 @@ const extensions = (availableNodesForVariable, availableState, acceptNodeOutputA
isNodeInsideInteration
),
deleteTriggerWithBackspace: true
}),
CodeBlockLowlight.configure({
lowlight,
enableTabIndentation: true,
tabSize: 2
})
]
@ -93,6 +110,8 @@ const ExpandRichInputDialog = ({ show, dialogProps, onCancel, onInputHintDialogC
const portalElement = document.getElementById('portal')
const dispatch = useDispatch()
const customization = useSelector((state) => state.customization)
const isDarkMode = customization.isDarkMode
const [inputValue, setInputValue] = useState('')
const [inputParam, setInputParam] = useState(null)
@ -201,7 +220,12 @@ const ExpandRichInputDialog = ({ show, dialogProps, onCancel, onInputHintDialogC
}}
>
<Box sx={{ mt: 1, border: '' }}>
<StyledEditorContent editor={editor} rows={15} />
<StyledEditorContent
editor={editor}
rows={15}
disabled={dialogProps.disabled}
isDarkMode={isDarkMode}
/>
</Box>
</PerfectScrollbar>
</div>

View File

@ -1,5 +1,6 @@
import { useState, useEffect } from 'react'
import PropTypes from 'prop-types'
import { useSelector } from 'react-redux'
import { useEditor, EditorContent } from '@tiptap/react'
import Placeholder from '@tiptap/extension-placeholder'
import { mergeAttributes } from '@tiptap/core'
@ -7,12 +8,18 @@ import StarterKit from '@tiptap/starter-kit'
import { styled } from '@mui/material/styles'
import { Box } from '@mui/material'
import Mention from '@tiptap/extension-mention'
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight'
import { common, createLowlight } from 'lowlight'
import { suggestionOptions } from './suggestionOption'
import { getAvailableNodesForVariable } from '@/utils/genericHelper'
const lowlight = createLowlight(common)
// define your extension array
const extensions = (availableNodesForVariable, availableState, acceptNodeOutputAsVariable, nodes, nodeData, isNodeInsideInteration) => [
StarterKit,
StarterKit.configure({
codeBlock: false
}),
Mention.configure({
HTMLAttributes: {
class: 'variable'
@ -33,11 +40,16 @@ const extensions = (availableNodesForVariable, availableState, acceptNodeOutputA
isNodeInsideInteration
),
deleteTriggerWithBackspace: true
}),
CodeBlockLowlight.configure({
lowlight,
enableTabIndentation: true,
tabSize: 2
})
]
// Add styled component for editor wrapper
const StyledEditorContent = styled(EditorContent)(({ theme, rows, disabled }) => ({
const StyledEditorContent = styled(EditorContent)(({ theme, rows, disabled, isDarkMode }) => ({
'& .ProseMirror': {
padding: '0px 14px',
height: rows ? `${rows * 1.4375}rem` : '2.4rem',
@ -67,11 +79,24 @@ const StyledEditorContent = styled(EditorContent)(({ theme, rows, disabled }) =>
opacity: disabled ? 0.6 : 0.4,
pointerEvents: 'none',
height: 0
}
},
// Set CSS custom properties for theme-aware styling based on the screenshot
'--code-bg': isDarkMode ? '#2d2d2d' : '#f5f5f5',
'--code-color': isDarkMode ? '#d4d4d4' : '#333333',
'--hljs-comment': isDarkMode ? '#6a9955' : '#6a9955',
'--hljs-variable': isDarkMode ? '#9cdcfe' : '#d73a49', // Light blue for variables (var, i)
'--hljs-number': isDarkMode ? '#b5cea8' : '#e36209', // Light green for numbers (1, 20, 15, etc.)
'--hljs-string': isDarkMode ? '#ce9178' : '#22863a', // Orange/peach for strings ("FizzBuzz", "Fizz", "Buzz")
'--hljs-title': isDarkMode ? '#dcdcaa' : '#6f42c1', // Yellow for function names (log)
'--hljs-keyword': isDarkMode ? '#569cd6' : '#005cc5', // Blue for keywords (for, if, else)
'--hljs-operator': isDarkMode ? '#d4d4d4' : '#333333', // White/gray for operators (=, %, ==, etc.)
'--hljs-punctuation': isDarkMode ? '#d4d4d4' : '#333333' // White/gray for punctuation ({, }, ;, etc.)
}
}))
export const RichInput = ({ inputParam, value, nodes, edges, nodeId, onChange, disabled = false }) => {
const customization = useSelector((state) => state.customization)
const isDarkMode = customization.isDarkMode
const [availableNodesForVariable, setAvailableNodesForVariable] = useState([])
const [availableState, setAvailableState] = useState([])
const [nodeData, setNodeData] = useState({})
@ -117,7 +142,7 @@ export const RichInput = ({ inputParam, value, nodes, edges, nodeId, onChange, d
return (
<Box sx={{ mt: 1, border: '' }}>
<StyledEditorContent editor={editor} rows={inputParam?.rows} disabled={disabled} />
<StyledEditorContent editor={editor} rows={inputParam?.rows} disabled={disabled} isDarkMode={isDarkMode} />
</Box>
)
}

View File

@ -6,7 +6,6 @@ import { CodeBlock } from '../markdown/CodeBlock'
import remarkGfm from 'remark-gfm'
import remarkMath from 'remark-math'
import rehypeMathjax from 'rehype-mathjax'
import rehypeRaw from 'rehype-raw'
/**
* Checks if text likely contains LaTeX math notation
@ -91,7 +90,7 @@ export const MemoizedReactMarkdown = memo(
const rehypePlugins = useMemo(() => {
if (props.rehypePlugins) return props.rehypePlugins
return shouldEnableMath ? [rehypeMathjax, rehypeRaw] : [rehypeRaw]
return shouldEnableMath ? [rehypeMathjax] : []
}, [props.rehypePlugins, shouldEnableMath])
return (