import { WorkflowNode, WorkflowState } from '../types'; import CodeInterpreter from '@e2b/code-interpreter'; /** * Execute Data Nodes - Transform, Set State * * SECURITY NOTE: Transform Node Script Execution * * E2B SANDBOX INTEGRATION (ENABLED): * - Uses E2B CodeInterpreter for secure sandboxed JavaScript/TypeScript execution * - Executes in isolated cloud environment with 5-minute timeout * - Safe for untrusted code execution * - Requires E2B_API_KEY environment variable * * FALLBACK: If E2B is not available (missing key or server-side only execution), * falls back to Function constructor with security patterns (NOT recommended for production) * * E2B Execution: * - Creates isolated sandbox per execution * - Supports JavaScript and TypeScript * - 5-minute timeout per execution * - Automatic cleanup after execution */ export async function executeDataNode( node: WorkflowNode, state: WorkflowState ): Promise { const { data } = node; const nodeType = data.nodeType || node.type; try { switch (nodeType) { case 'transform': case 'data-transform': return await executeTransform(data, state); case 'set-state': case 'set state': return await executeSetState(data, state); case 'export': return await executeExport(data, state); default: throw new Error(`Unknown data node type: ${nodeType}`); } } catch (error) { // Log error with context console.error(`Data node ${node.id} (${nodeType}) failed:`, error); // Re-throw with more context throw new Error( `Node ${node.id} execution failed: ${ error instanceof Error ? error.message : 'Unknown error' }` ); } } /** * Execute transform using E2B sandbox or fallback to Function constructor */ async function executeTransform(data: any, state: WorkflowState): Promise { // Get the transform script from node data // Support both transformScript (UI) and transformation (templates) const transformScript = data.transformScript || data.transformation; // If no transform script, just pass through the input if (!transformScript || transformScript.trim() === '') { console.log('⚠️ No transform script provided, passing through input'); return state.variables.lastOutput || {}; } // Try E2B execution first (secure sandbox) const useE2B = typeof process !== 'undefined' && process.env?.E2B_API_KEY; if (useE2B) { try { return await executeTransformE2B(transformScript, state); } catch (error) { console.error('❌ E2B execution failed, falling back to Function:', error); // Fall through to fallback execution } } // Fallback to Function constructor (with security patterns) return await executeTransformFallback(transformScript, state); } /** * Execute transform using E2B CodeInterpreter (SECURE) * Now uses JavaScript/TypeScript execution */ async function executeTransformE2B(transformScript: string, state: WorkflowState): Promise { console.log('🔒 Executing transform in E2B sandbox...'); // Prepare the data for the sandbox const sandboxedInput = JSON.parse(JSON.stringify(state.variables.lastOutput || {})); const sandboxedState = { variables: JSON.parse(JSON.stringify(state.variables)) }; // Create E2B sandbox const sandbox = await CodeInterpreter.create({ apiKey: process.env.E2B_API_KEY, }); try { // Prepare the code to execute using JavaScript // We wrap the user's code in a function that provides the context const codeToExecute = ` const input = ${JSON.stringify(sandboxedInput)}; const lastOutput = ${JSON.stringify(sandboxedInput)}; const state = ${JSON.stringify(sandboxedState)}; // Execute user's transform code const transform = () => { ${transformScript.split('\n').map((line: string) => ' ' + line).join('\n')} }; const result = transform(); // Output result as JSON console.log(JSON.stringify(result)); `; console.log('🔍 E2B code to execute:', codeToExecute); // Execute in the sandbox using JavaScript const execution = await sandbox.runCode(codeToExecute); // Check for errors if (execution.error) { throw new Error(`E2B execution error: ${execution.error.name}: ${execution.error.value}`); } // Parse the result from stdout const resultText = execution.logs.stdout.join('\n'); const result = JSON.parse(resultText); console.log('✅ E2B execution successful:', result); // Update state with the result state.variables['lastOutput'] = result; return result; } finally { // Always close the sandbox await sandbox.kill(); } } /** * Fallback execution using Function constructor (LESS SECURE) */ async function executeTransformFallback(transformScript: string, state: WorkflowState): Promise { console.log('⚠️ Using fallback Function execution (not recommended for production)'); // Enhanced security patterns to prevent malicious code execution const dangerousPatterns = [ /require\s*\(/gi, /import\s+/gi, /eval\s*\(/gi, /Function\s*\(/gi, /setTimeout\s*\(/gi, /setInterval\s*\(/gi, /setImmediate\s*\(/gi, /process\./gi, /__dirname/gi, /__filename/gi, /global\./gi, /globalThis\./gi, /window\./gi, /document\./gi, /fetch\s*\(/gi, /XMLHttpRequest/gi, /fs\./gi, /child_process/gi, /exec\s*\(/gi, /spawn\s*\(/gi, ]; for (const pattern of dangerousPatterns) { if (pattern.test(transformScript)) { throw new Error(`Security violation: Script contains forbidden pattern: ${pattern.source}`); } } // Script length limit const MAX_SCRIPT_LENGTH = 5000; if (transformScript.length > MAX_SCRIPT_LENGTH) { throw new Error(`Script too long (max ${MAX_SCRIPT_LENGTH} characters)`); } try { // Create restricted context with deep clones to prevent mutation const sandboxedInput = JSON.parse(JSON.stringify(state.variables.lastOutput || {})); const sandboxedState = { variables: JSON.parse(JSON.stringify(state.variables)) }; // Debug: Log the state structure console.log('🔍 Transform Debug - state.variables.input:', JSON.stringify(state.variables.input, null, 2)); console.log('🔍 Transform Debug - input (lastOutput):', JSON.stringify(sandboxedInput, null, 2)); console.log('🔍 Transform Debug - ALL state.variables keys:', Object.keys(state.variables)); // Use strict mode to prevent certain unsafe operations const strictScript = `"use strict";\n${transformScript}\n//# sourceURL=transform-script.js`; const transformFunction = new Function('input', 'lastOutput', 'state', strictScript); // Execute with timeout protection const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Transform timeout (3s limit)')), 3000) ); const executionPromise = Promise.resolve( transformFunction(sandboxedInput, sandboxedInput, sandboxedState) ); const result = await Promise.race([executionPromise, timeoutPromise]); // Validate output isn't suspiciously large (1MB limit) const resultString = JSON.stringify(result); if (resultString.length > 1000000) { throw new Error('Transform output too large (>1MB)'); } console.log('🔍 Transform Debug - result:', JSON.stringify(result, null, 2)); // Update state with the result state.variables['lastOutput'] = result; return result; } catch (error) { throw new Error(`Transform failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async function executeSetState(data: any, state: WorkflowState): Promise { const key = data.stateKey || 'variable'; try { let rawValue = data.stateValue || null; const valueType = data.valueType || 'string'; console.log('🔧 Set State - Key:', key); console.log('🔧 Set State - Raw Value:', rawValue); console.log('🔧 Set State - Type:', valueType); // Import variable substitution const { substituteVariables } = await import('../variable-substitution'); // Substitute variables in the value (e.g., {{lastOutput.price}}) if (typeof rawValue === 'string') { rawValue = substituteVariables(rawValue, state); console.log('🔧 Set State - After substitution:', rawValue); } // Parse value based on type let finalValue: any; switch (valueType) { case 'number': finalValue = parseFloat(rawValue); if (isNaN(finalValue)) { throw new Error(`Cannot convert "${rawValue}" to number`); } break; case 'boolean': if (typeof rawValue === 'boolean') { finalValue = rawValue; } else { const str = String(rawValue).toLowerCase(); finalValue = str === 'true' || str === '1' || str === 'yes'; } break; case 'json': try { finalValue = typeof rawValue === 'string' ? JSON.parse(rawValue) : rawValue; } catch (e) { throw new Error(`Invalid JSON: ${e instanceof Error ? e.message : 'Parse error'}`); } break; case 'expression': // Evaluate JavaScript expression try { const evalFunction = new Function('input', 'lastOutput', 'state', `return ${rawValue}`); finalValue = evalFunction( state.variables.input, state.variables.lastOutput, state ); } catch (e) { throw new Error(`Expression evaluation failed: ${e instanceof Error ? e.message : 'Unknown error'}`); } break; default: // String - just use the substituted value finalValue = rawValue; } console.log('🔧 Set State - Final Value:', finalValue); // Set the state variable state.variables[key] = finalValue; return { key, value: finalValue, valueType, stateUpdated: true, }; } catch (error) { // Provide context about what failed throw new Error( `Failed to set state variable '${key}': ${ error instanceof Error ? error.message : 'Unknown error' }` ); } } async function executeExport(data: any, state: WorkflowState): Promise { // Simulate delay await new Promise(resolve => setTimeout(resolve, 100)); // Get the data to export from lastOutput const input = state.variables['lastOutput'] || {}; // For now, just return the data formatted for export // In a real implementation, this would handle different export formats return { exportData: input, exported: true, }; } /** * TODO: Real implementation * * For transform: * - Execute JavaScript transform script safely (sandboxed) * - Support common transformation libraries (lodash, etc) * - Handle errors gracefully * - Support async transformations * * For set-state: * - Parse value expressions * - Support references to other variables * - Validate types * - Support nested object paths */