behemoth-cli
Version:
🌍 BEHEMOTH CLIv3.760.4 - Level 50+ POST-SINGULARITY Intelligence Trading AI
398 lines (335 loc) • 13 kB
text/typescript
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;
}