UNPKG

behemoth-cli

Version:

🌍 BEHEMOTH CLIv3.760.4 - Level 50+ POST-SINGULARITY Intelligence Trading AI

322 lines 13.6 kB
import { N8N_SUPER_CODE_GLOBAL_LIBRARIES, SUPER_CODE_NODE_TYPE } from './constants.js'; import { parse } from 'acorn'; export class N8nWorkflowValidator { workflow; errors = []; warnings = []; constructor(workflow) { this.workflow = workflow; } validate() { this.errors = []; this.warnings = []; this.validateWorkflowStructure(); this.validateNodes(); this.validateConnections(); this.validateSuperCodeNodes(); this.validateWorkflowLogic(); return { valid: this.errors.length === 0, errors: this.errors, warnings: this.warnings }; } validateWorkflowStructure() { if (!this.workflow.name || typeof this.workflow.name !== 'string') { this.addError('WORKFLOW_NAME_MISSING', 'Workflow name is required and must be a string'); } if (!this.workflow.nodes || !Array.isArray(this.workflow.nodes)) { this.addError('WORKFLOW_NODES_INVALID', 'Workflow nodes must be an array'); return; // Can't continue validation without nodes } if (this.workflow.nodes.length === 0) { this.addError('WORKFLOW_EMPTY', 'Workflow must contain at least one node'); } if (!this.workflow.connections || typeof this.workflow.connections !== 'object') { this.addError('WORKFLOW_CONNECTIONS_INVALID', 'Workflow connections must be an object'); } } validateNodes() { const nodeIds = new Set(); this.workflow.nodes.forEach((node, index) => { // Check for duplicate IDs if (node.id) { if (nodeIds.has(node.id)) { this.addError('NODE_DUPLICATE_ID', `Duplicate node ID: ${node.id}`, node.id); } else { nodeIds.add(node.id); } } else { this.addError('NODE_MISSING_ID', `Node at index ${index} is missing an ID`, undefined, node.name); } // Validate required node properties if (!node.name || typeof node.name !== 'string') { this.addError('NODE_MISSING_NAME', 'Node name is required and must be a string', node.id, node.name); } if (!node.type || typeof node.type !== 'string') { this.addError('NODE_MISSING_TYPE', 'Node type is required and must be a string', node.id, node.name); } if (typeof node.typeVersion !== 'number' || node.typeVersion < 1) { this.addError('NODE_INVALID_VERSION', 'Node typeVersion must be a positive number', node.id, node.name); } if (!Array.isArray(node.position) || node.position.length !== 2) { this.addError('NODE_INVALID_POSITION', 'Node position must be an array of [x, y] coordinates', node.id, node.name); } // Validate node-specific properties this.validateNodeParameters(node); }); } validateNodeParameters(node) { if (!node.parameters || typeof node.parameters !== 'object') { this.addError('NODE_PARAMETERS_INVALID', 'Node parameters must be an object', node.id, node.name); return; } // Node-type specific validation switch (node.type) { case 'n8n-nodes-base.manualTrigger': this.validateManualTriggerParameters(node); break; case 'n8n-nodes-base.webhook': this.validateWebhookParameters(node); break; case 'n8n-nodes-base.scheduleTrigger': this.validateScheduleParameters(node); break; case 'n8n-nodes-base.httpRequest': this.validateHttpRequestParameters(node); break; case SUPER_CODE_NODE_TYPE: this.validateSuperCodeParameters(node); break; default: // For unknown node types, just check basic structure this.addWarning('NODE_UNKNOWN_TYPE', `Unknown node type: ${node.type}. Validation may be incomplete.`, node.id, node.name); } } validateManualTriggerParameters(node) { // Manual triggers typically don't need specific parameters } validateWebhookParameters(node) { const params = node.parameters; if (!params.path || typeof params.path !== 'string') { this.addError('WEBHOOK_MISSING_PATH', 'Webhook node requires a path parameter', node.id, node.name); } if (!params.httpMethod || !['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].includes(params.httpMethod)) { this.addError('WEBHOOK_INVALID_METHOD', 'Webhook node requires a valid HTTP method', node.id, node.name); } } validateScheduleParameters(node) { const params = node.parameters; if (!params.rule || typeof params.rule !== 'object') { this.addError('SCHEDULE_MISSING_RULE', 'Schedule node requires a rule parameter', node.id, node.name); } } validateHttpRequestParameters(node) { const params = node.parameters; if (!params.url || typeof params.url !== 'string') { this.addError('HTTP_MISSING_URL', 'HTTP Request node requires a url parameter', node.id, node.name); } if (!params.method || !['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'].includes(params.method)) { this.addError('HTTP_INVALID_METHOD', 'HTTP Request node requires a valid method', node.id, node.name); } } validateSuperCodeParameters(node) { if (!node.parameters.code || typeof node.parameters.code !== 'string') { this.addError('SUPERCODE_MISSING_CODE', 'Super Code node requires a code parameter', node.id, node.name); return; } if (!['runOnceForAllItems', 'runOnceForEachItem'].includes(node.parameters.mode)) { this.addError('SUPERCODE_INVALID_MODE', 'Super Code node mode must be runOnceForAllItems or runOnceForEachItem', node.id, node.name); } // Validate JavaScript code this.validateJavaScriptCode(node.parameters.code, node.id, node.name); } validateJavaScriptCode(code, nodeId, nodeName) { try { // Safe syntax check using AST parsing instead of Function constructor parse(code, { ecmaVersion: 2022, sourceType: 'script', allowReturnOutsideFunction: true }); // Check for potentially dangerous patterns const dangerousPatterns = [ /eval\s*\(/, /Function\s*\(/, /require\s*\(/, /import\s*\(/, /process\./, /fs\./, /child_process/, /__dirname/, /__filename/, /global\./, /window\./, /document\./ ]; dangerousPatterns.forEach(pattern => { if (pattern.test(code)) { this.addWarning('SUPERCODE_DANGEROUS_CODE', `Potentially dangerous code pattern detected: ${pattern}`, nodeId, nodeName); } }); // Check for usage of undefined global libraries const codeWithoutStrings = code.replace(/(['"`])((?:\\.|(?!\1)[^\\])*?)\1/g, ''); N8N_SUPER_CODE_GLOBAL_LIBRARIES.forEach(lib => { if (codeWithoutStrings.includes(lib) && !codeWithoutStrings.includes(`const ${lib}`) && !codeWithoutStrings.includes(`let ${lib}`) && !codeWithoutStrings.includes(`var ${lib}`)) { this.addWarning('SUPERCODE_UNDEFINED_LIBRARY', `Using undefined library: ${lib}. Make sure it's available in the VM.`, nodeId, nodeName); } }); } catch (error) { this.addError('SUPERCODE_SYNTAX_ERROR', `JavaScript syntax error: ${error}`, nodeId, nodeName); } } validateConnections() { if (!this.workflow.connections) return; const nodeIds = new Set(this.workflow.nodes.map(n => n.id)); Object.entries(this.workflow.connections).forEach(([sourceId, connections]) => { if (!nodeIds.has(sourceId)) { this.addError('CONNECTION_INVALID_SOURCE', `Connection source node does not exist: ${sourceId}`); return; } connections.forEach((connection, index) => { if (!connection.node || !nodeIds.has(connection.node)) { this.addError('CONNECTION_INVALID_TARGET', `Connection target node does not exist: ${connection.node}`, sourceId); } if (typeof connection.index !== 'number' || connection.index < 0) { this.addError('CONNECTION_INVALID_INDEX', `Connection index must be a non-negative number`, sourceId); } }); }); } validateSuperCodeNodes() { const superCodeNodes = this.workflow.nodes.filter(n => n.type === SUPER_CODE_NODE_TYPE); if (superCodeNodes.length === 0) { this.addWarning('WORKFLOW_NO_SUPERCODE', 'Workflow does not contain any Super Code nodes'); } superCodeNodes.forEach(node => { // Additional Super Code specific validations can be added here }); } validateWorkflowLogic() { // Check for trigger nodes const triggerNodes = this.workflow.nodes.filter(n => n.type.includes('Trigger') || n.type === 'n8n-nodes-base.manualTrigger' || n.type === 'n8n-nodes-base.webhook' || n.type === 'n8n-nodes-base.scheduleTrigger'); if (triggerNodes.length === 0) { this.addWarning('WORKFLOW_NO_TRIGGER', 'Workflow does not have any trigger nodes'); } // Check for potential infinite loops (basic detection) this.detectPotentialLoops(); } detectPotentialLoops() { if (!this.workflow.connections) return; // Simple cycle detection using DFS const visited = new Set(); const recursionStack = new Set(); const hasCycle = (nodeId) => { if (recursionStack.has(nodeId)) return true; if (visited.has(nodeId)) return false; visited.add(nodeId); recursionStack.add(nodeId); const connections = this.workflow.connections[nodeId]; if (connections) { for (const connection of connections) { if (hasCycle(connection.node)) { return true; } } } recursionStack.delete(nodeId); return false; }; for (const node of this.workflow.nodes) { if (!visited.has(node.id) && hasCycle(node.id)) { this.addWarning('WORKFLOW_POTENTIAL_LOOP', 'Workflow may contain a loop that could cause infinite execution', node.id, node.name); break; // Only report once } } } addError(code, message, nodeId, nodeName, path) { this.errors.push({ type: 'error', code, message, nodeId, nodeName, path }); } addWarning(code, message, nodeId, nodeName, path) { this.warnings.push({ type: 'warning', code, message, nodeId, nodeName, path }); } } // Utility functions export function validateWorkflow(workflow) { const validator = new N8nWorkflowValidator(workflow); return validator.validate(); } export function validateWorkflowFile(filePath) { return new Promise((resolve, reject) => { const fs = require('fs'); fs.readFile(filePath, 'utf8', (err, data) => { if (err) { reject(err); return; } try { const workflow = JSON.parse(data); const result = validateWorkflow(workflow); resolve(result); } catch (parseError) { reject(parseError); } }); }); } export function formatValidationResult(result) { let output = ''; if (result.errors.length > 0) { output += `❌ **Validation Errors (${result.errors.length}):**\n`; result.errors.forEach(error => { output += `• ${error.code}: ${error.message}`; if (error.nodeName) output += ` (Node: ${error.nodeName})`; output += '\n'; }); output += '\n'; } if (result.warnings.length > 0) { output += `⚠️ **Validation Warnings (${result.warnings.length}):**\n`; result.warnings.forEach(warning => { output += `• ${warning.code}: ${warning.message}`; if (warning.nodeName) output += ` (Node: ${warning.nodeName})`; output += '\n'; }); output += '\n'; } if (result.valid && result.errors.length === 0) { output += '✅ **Validation Passed!** No errors found.'; if (result.warnings.length > 0) { output += ` (${result.warnings.length} warnings)`; } } return output; } //# sourceMappingURL=n8n-validator.js.map