UNPKG

behemoth-cli

Version:

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

398 lines (335 loc) 13 kB
import { N8nWorkflow, N8nNode, SuperCodeNode } from '../core/n8n-types.js'; import { N8N_SUPER_CODE_GLOBAL_LIBRARIES, SUPER_CODE_NODE_TYPE } from './constants.js'; import { parse } from 'acorn'; export interface ValidationResult { valid: boolean; errors: ValidationError[]; warnings: ValidationWarning[]; } export interface ValidationError { type: 'error'; code: string; message: string; nodeId?: string; nodeName?: string; path?: string; } export interface ValidationWarning { type: 'warning'; code: string; message: string; nodeId?: string; nodeName?: string; path?: string; } export class N8nWorkflowValidator { private workflow: N8nWorkflow; private errors: ValidationError[] = []; private warnings: ValidationWarning[] = []; constructor(workflow: N8nWorkflow) { this.workflow = workflow; } validate(): ValidationResult { 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 }; } private validateWorkflowStructure(): void { 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'); } } private validateNodes(): void { const nodeIds = new Set<string>(); 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); }); } private validateNodeParameters(node: N8nNode): void { 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 as SuperCodeNode); 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); } } private validateManualTriggerParameters(node: N8nNode): void { // Manual triggers typically don't need specific parameters } private validateWebhookParameters(node: N8nNode): void { 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); } } private validateScheduleParameters(node: N8nNode): void { 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); } } private validateHttpRequestParameters(node: N8nNode): void { 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); } } private validateSuperCodeParameters(node: SuperCodeNode): void { 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); } private validateJavaScriptCode(code: string, nodeId?: string, nodeName?: string): void { 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); } } private validateConnections(): void { 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); } }); }); } private validateSuperCodeNodes(): void { 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 }); } private validateWorkflowLogic(): void { // 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(); } private detectPotentialLoops(): void { if (!this.workflow.connections) return; // Simple cycle detection using DFS const visited = new Set<string>(); const recursionStack = new Set<string>(); const hasCycle = (nodeId: string): boolean => { 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 } } } private addError(code: string, message: string, nodeId?: string, nodeName?: string, path?: string): void { this.errors.push({ type: 'error', code, message, nodeId, nodeName, path }); } private addWarning(code: string, message: string, nodeId?: string, nodeName?: string, path?: string): void { this.warnings.push({ type: 'warning', code, message, nodeId, nodeName, path }); } } // Utility functions export function validateWorkflow(workflow: N8nWorkflow): ValidationResult { const validator = new N8nWorkflowValidator(workflow); return validator.validate(); } export function validateWorkflowFile(filePath: string): Promise<ValidationResult> { return new Promise((resolve, reject) => { const fs = require('fs'); fs.readFile(filePath, 'utf8', (err: any, data: string) => { 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: ValidationResult): string { 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; }