File size: 17,788 Bytes
da2e594
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
/**
 * Error Execution Processor Service
 *
 * Specialized processor for extracting error context from failed n8n executions.
 * Designed for AI agent debugging workflows with token efficiency.
 *
 * Features:
 * - Auto-identify error nodes
 * - Extract upstream context (input data to error node)
 * - Build execution path from trigger to error
 * - Generate AI-friendly fix suggestions
 */

import {
  Execution,
  Workflow,
  ErrorAnalysis,
  ErrorSuggestion,
} from '../types/n8n-api';
import { logger } from '../utils/logger';

/**
 * Options for error processing
 */
export interface ErrorProcessorOptions {
  itemsLimit?: number;           // Default: 2
  includeStackTrace?: boolean;   // Default: false
  includeExecutionPath?: boolean; // Default: true
  workflow?: Workflow;           // Optional: for accurate upstream detection
}

// Constants
const MAX_STACK_LINES = 3;

/**
 * Keys that could enable prototype pollution attacks
 * These are blocked entirely from processing
 */
const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']);

/**
 * Patterns for sensitive data that should be masked in output
 * Expanded from code review recommendations
 */
const SENSITIVE_PATTERNS = [
  'password',
  'secret',
  'token',
  'apikey',
  'api_key',
  'credential',
  'auth',
  'private_key',
  'privatekey',
  'bearer',
  'jwt',
  'oauth',
  'certificate',
  'passphrase',
  'access_token',
  'refresh_token',
  'session',
  'cookie',
  'authorization'
];

/**
 * Process execution for error debugging
 */
export function processErrorExecution(
  execution: Execution,
  options: ErrorProcessorOptions = {}
): ErrorAnalysis {
  const {
    itemsLimit = 2,
    includeStackTrace = false,
    includeExecutionPath = true,
    workflow
  } = options;

  const resultData = execution.data?.resultData;
  const error = resultData?.error as Record<string, unknown> | undefined;
  const runData = resultData?.runData as Record<string, any> || {};
  const lastNode = resultData?.lastNodeExecuted;

  // 1. Extract primary error info
  const primaryError = extractPrimaryError(error, lastNode, runData, includeStackTrace);

  // 2. Find and extract upstream context
  const upstreamContext = extractUpstreamContext(
    primaryError.nodeName,
    runData,
    workflow,
    itemsLimit
  );

  // 3. Build execution path if requested
  const executionPath = includeExecutionPath
    ? buildExecutionPath(primaryError.nodeName, runData, workflow)
    : undefined;

  // 4. Find additional errors (for batch failures)
  const additionalErrors = findAdditionalErrors(
    primaryError.nodeName,
    runData
  );

  // 5. Generate AI suggestions
  const suggestions = generateSuggestions(primaryError, upstreamContext);

  return {
    primaryError,
    upstreamContext,
    executionPath,
    additionalErrors: additionalErrors.length > 0 ? additionalErrors : undefined,
    suggestions: suggestions.length > 0 ? suggestions : undefined
  };
}

/**
 * Extract primary error information
 */
function extractPrimaryError(
  error: Record<string, unknown> | undefined,
  lastNode: string | undefined,
  runData: Record<string, any>,
  includeFullStackTrace: boolean
): ErrorAnalysis['primaryError'] {
  // Error info from resultData.error
  const errorNode = error?.node as Record<string, unknown> | undefined;
  const nodeName = (errorNode?.name as string) || lastNode || 'Unknown';

  // Also check runData for node-level errors
  const nodeRunData = runData[nodeName];
  const nodeError = nodeRunData?.[0]?.error;

  const stackTrace = (error?.stack || nodeError?.stack) as string | undefined;

  return {
    message: (error?.message || nodeError?.message || 'Unknown error') as string,
    errorType: (error?.name || nodeError?.name || 'Error') as string,
    nodeName,
    nodeType: (errorNode?.type || '') as string,
    nodeId: errorNode?.id as string | undefined,
    nodeParameters: extractRelevantParameters(errorNode?.parameters),
    stackTrace: includeFullStackTrace ? stackTrace : truncateStackTrace(stackTrace)
  };
}

/**
 * Extract upstream context (input data to error node)
 */
function extractUpstreamContext(
  errorNodeName: string,
  runData: Record<string, any>,
  workflow?: Workflow,
  itemsLimit: number = 2
): ErrorAnalysis['upstreamContext'] | undefined {
  // Strategy 1: Use workflow connections if available
  if (workflow) {
    const upstreamNode = findUpstreamNode(errorNodeName, workflow);
    if (upstreamNode) {
      const context = extractNodeOutput(upstreamNode, runData, itemsLimit);
      if (context) {
        // Enrich with node type from workflow
        const nodeInfo = workflow.nodes.find(n => n.name === upstreamNode);
        if (nodeInfo) {
          context.nodeType = nodeInfo.type;
        }
        return context;
      }
    }
  }

  // Strategy 2: Heuristic - find node that produced data most recently before error
  const successfulNodes = Object.entries(runData)
    .filter(([name, data]) => {
      if (name === errorNodeName) return false;
      const runs = data as any[];
      return runs?.[0]?.data?.main?.[0]?.length > 0 && !runs?.[0]?.error;
    })
    .map(([name, data]) => ({
      name,
      executionTime: (data as any[])?.[0]?.executionTime || 0,
      startTime: (data as any[])?.[0]?.startTime || 0
    }))
    .sort((a, b) => b.startTime - a.startTime);

  if (successfulNodes.length > 0) {
    const upstreamName = successfulNodes[0].name;
    return extractNodeOutput(upstreamName, runData, itemsLimit);
  }

  return undefined;
}

/**
 * Find upstream node using workflow connections
 * Connections format: { sourceNode: { main: [[{node: targetNode, type, index}]] } }
 */
function findUpstreamNode(
  targetNode: string,
  workflow: Workflow
): string | undefined {
  for (const [sourceName, outputs] of Object.entries(workflow.connections)) {
    const connections = outputs as Record<string, any>;
    const mainOutputs = connections?.main || [];

    for (const outputBranch of mainOutputs) {
      if (!Array.isArray(outputBranch)) continue;
      for (const connection of outputBranch) {
        if (connection?.node === targetNode) {
          return sourceName;
        }
      }
    }
  }
  return undefined;
}

/**
 * Find all upstream nodes (for building complete path)
 */
function findAllUpstreamNodes(
  targetNode: string,
  workflow: Workflow,
  visited: Set<string> = new Set()
): string[] {
  const path: string[] = [];
  let currentNode = targetNode;

  while (currentNode && !visited.has(currentNode)) {
    visited.add(currentNode);
    const upstream = findUpstreamNode(currentNode, workflow);
    if (upstream) {
      path.unshift(upstream);
      currentNode = upstream;
    } else {
      break;
    }
  }

  return path;
}

/**
 * Extract node output with sampling and sanitization
 */
function extractNodeOutput(
  nodeName: string,
  runData: Record<string, any>,
  itemsLimit: number
): ErrorAnalysis['upstreamContext'] | undefined {
  const nodeData = runData[nodeName];
  if (!nodeData?.[0]?.data?.main?.[0]) return undefined;

  const items = nodeData[0].data.main[0];

  // Sanitize sample items to remove sensitive data
  const rawSamples = items.slice(0, itemsLimit);
  const sanitizedSamples = rawSamples.map((item: unknown) => sanitizeData(item));

  return {
    nodeName,
    nodeType: '', // Will be enriched if workflow available
    itemCount: items.length,
    sampleItems: sanitizedSamples,
    dataStructure: extractStructure(items[0])
  };
}

/**
 * Build execution path leading to error
 */
function buildExecutionPath(
  errorNodeName: string,
  runData: Record<string, any>,
  workflow?: Workflow
): ErrorAnalysis['executionPath'] {
  const path: ErrorAnalysis['executionPath'] = [];

  // If we have workflow, trace connections backward for ordered path
  if (workflow) {
    const upstreamNodes = findAllUpstreamNodes(errorNodeName, workflow);

    // Add upstream nodes
    for (const nodeName of upstreamNodes) {
      const nodeData = runData[nodeName];
      const runs = nodeData as any[] | undefined;
      const hasError = runs?.[0]?.error;
      const itemCount = runs?.[0]?.data?.main?.[0]?.length || 0;

      path.push({
        nodeName,
        status: hasError ? 'error' : (runs ? 'success' : 'skipped'),
        itemCount,
        executionTime: runs?.[0]?.executionTime
      });
    }

    // Add error node
    const errorNodeData = runData[errorNodeName];
    path.push({
      nodeName: errorNodeName,
      status: 'error',
      itemCount: 0,
      executionTime: errorNodeData?.[0]?.executionTime
    });
  } else {
    // Without workflow, list all executed nodes by execution order (best effort)
    const nodesByTime = Object.entries(runData)
      .map(([name, data]) => ({
        name,
        data: data as any[],
        startTime: (data as any[])?.[0]?.startTime || 0
      }))
      .sort((a, b) => a.startTime - b.startTime);

    for (const { name, data } of nodesByTime) {
      path.push({
        nodeName: name,
        status: data?.[0]?.error ? 'error' : 'success',
        itemCount: data?.[0]?.data?.main?.[0]?.length || 0,
        executionTime: data?.[0]?.executionTime
      });
    }
  }

  return path;
}

/**
 * Find additional error nodes (for batch/parallel failures)
 */
function findAdditionalErrors(
  primaryErrorNode: string,
  runData: Record<string, any>
): Array<{ nodeName: string; message: string }> {
  const additional: Array<{ nodeName: string; message: string }> = [];

  for (const [nodeName, data] of Object.entries(runData)) {
    if (nodeName === primaryErrorNode) continue;

    const runs = data as any[];
    const error = runs?.[0]?.error;
    if (error) {
      additional.push({
        nodeName,
        message: error.message || 'Unknown error'
      });
    }
  }

  return additional;
}

/**
 * Generate AI-friendly error suggestions based on patterns
 */
function generateSuggestions(
  error: ErrorAnalysis['primaryError'],
  upstream?: ErrorAnalysis['upstreamContext']
): ErrorSuggestion[] {
  const suggestions: ErrorSuggestion[] = [];
  const message = error.message.toLowerCase();

  // Pattern: Missing required field
  if (message.includes('required') || message.includes('must be provided') || message.includes('is required')) {
    suggestions.push({
      type: 'fix',
      title: 'Missing Required Field',
      description: `Check "${error.nodeName}" parameters for required fields. Error indicates a mandatory value is missing.`,
      confidence: 'high'
    });
  }

  // Pattern: Empty input
  if (upstream?.itemCount === 0) {
    suggestions.push({
      type: 'investigate',
      title: 'No Input Data',
      description: `"${error.nodeName}" received 0 items from "${upstream.nodeName}". Check upstream node's filtering or data source.`,
      confidence: 'high'
    });
  }

  // Pattern: Authentication error
  if (message.includes('auth') || message.includes('credentials') ||
      message.includes('401') || message.includes('unauthorized') ||
      message.includes('forbidden') || message.includes('403')) {
    suggestions.push({
      type: 'fix',
      title: 'Authentication Issue',
      description: 'Verify credentials are configured correctly. Check API key permissions and expiration.',
      confidence: 'high'
    });
  }

  // Pattern: Rate limiting
  if (message.includes('rate limit') || message.includes('429') ||
      message.includes('too many requests') || message.includes('throttle')) {
    suggestions.push({
      type: 'workaround',
      title: 'Rate Limited',
      description: 'Add delay between requests or reduce batch size. Consider using retry with exponential backoff.',
      confidence: 'high'
    });
  }

  // Pattern: Connection error
  if (message.includes('econnrefused') || message.includes('enotfound') ||
      message.includes('etimedout') || message.includes('network') ||
      message.includes('connect')) {
    suggestions.push({
      type: 'investigate',
      title: 'Network/Connection Error',
      description: 'Check if the external service is reachable. Verify URL, firewall rules, and DNS resolution.',
      confidence: 'high'
    });
  }

  // Pattern: Invalid JSON
  if (message.includes('json') || message.includes('parse error') ||
      message.includes('unexpected token') || message.includes('syntax error')) {
    suggestions.push({
      type: 'fix',
      title: 'Invalid JSON Format',
      description: 'Check the data format. Ensure JSON is properly structured with correct syntax.',
      confidence: 'high'
    });
  }

  // Pattern: Field not found / invalid path
  if (message.includes('not found') || message.includes('undefined') ||
      message.includes('cannot read property') || message.includes('does not exist')) {
    suggestions.push({
      type: 'investigate',
      title: 'Missing Data Field',
      description: 'A referenced field does not exist in the input data. Check data structure and field names.',
      confidence: 'medium'
    });
  }

  // Pattern: Type error
  if (message.includes('type') && (message.includes('expected') || message.includes('invalid'))) {
    suggestions.push({
      type: 'fix',
      title: 'Data Type Mismatch',
      description: 'Input data type does not match expected type. Check if strings/numbers/arrays are used correctly.',
      confidence: 'medium'
    });
  }

  // Pattern: Timeout
  if (message.includes('timeout') || message.includes('timed out')) {
    suggestions.push({
      type: 'workaround',
      title: 'Operation Timeout',
      description: 'The operation took too long. Consider increasing timeout, reducing data size, or optimizing the query.',
      confidence: 'high'
    });
  }

  // Pattern: Permission denied
  if (message.includes('permission') || message.includes('access denied') || message.includes('not allowed')) {
    suggestions.push({
      type: 'fix',
      title: 'Permission Denied',
      description: 'The operation lacks required permissions. Check user roles, API scopes, or resource access settings.',
      confidence: 'high'
    });
  }

  // Generic NodeOperationError guidance
  if (error.errorType === 'NodeOperationError' && suggestions.length === 0) {
    suggestions.push({
      type: 'investigate',
      title: 'Node Configuration Issue',
      description: `Review "${error.nodeName}" parameters and operation settings. Validate against the node's requirements.`,
      confidence: 'medium'
    });
  }

  return suggestions;
}

// Helper functions

/**
 * Check if a key contains sensitive patterns
 */
function isSensitiveKey(key: string): boolean {
  const lowerKey = key.toLowerCase();
  return SENSITIVE_PATTERNS.some(pattern => lowerKey.includes(pattern));
}

/**
 * Recursively sanitize data by removing dangerous keys and masking sensitive values
 *
 * @param data - The data to sanitize
 * @param depth - Current recursion depth
 * @param maxDepth - Maximum recursion depth (default: 10)
 * @returns Sanitized data with sensitive values masked
 */
function sanitizeData(data: unknown, depth = 0, maxDepth = 10): unknown {
  // Prevent infinite recursion
  if (depth >= maxDepth) {
    return '[max depth reached]';
  }

  // Handle null/undefined
  if (data === null || data === undefined) {
    return data;
  }

  // Handle primitives
  if (typeof data !== 'object') {
    // Truncate long strings
    if (typeof data === 'string' && data.length > 500) {
      return '[truncated]';
    }
    return data;
  }

  // Handle arrays
  if (Array.isArray(data)) {
    return data.map(item => sanitizeData(item, depth + 1, maxDepth));
  }

  // Handle objects
  const sanitized: Record<string, unknown> = {};
  const obj = data as Record<string, unknown>;

  for (const [key, value] of Object.entries(obj)) {
    // Block prototype pollution attempts
    if (DANGEROUS_KEYS.has(key)) {
      logger.warn(`Blocked potentially dangerous key: ${key}`);
      continue;
    }

    // Mask sensitive fields
    if (isSensitiveKey(key)) {
      sanitized[key] = '[REDACTED]';
      continue;
    }

    // Recursively sanitize nested values
    sanitized[key] = sanitizeData(value, depth + 1, maxDepth);
  }

  return sanitized;
}

/**
 * Extract relevant parameters (filtering sensitive data)
 */
function extractRelevantParameters(params: unknown): Record<string, unknown> | undefined {
  if (!params || typeof params !== 'object') return undefined;

  const sanitized = sanitizeData(params);
  if (!sanitized || typeof sanitized !== 'object' || Array.isArray(sanitized)) {
    return undefined;
  }

  return Object.keys(sanitized).length > 0 ? sanitized as Record<string, unknown> : undefined;
}

/**
 * Truncate stack trace to first few lines
 */
function truncateStackTrace(stack?: string): string | undefined {
  if (!stack) return undefined;
  const lines = stack.split('\n');
  if (lines.length <= MAX_STACK_LINES) return stack;
  return lines.slice(0, MAX_STACK_LINES).join('\n') + `\n... (${lines.length - MAX_STACK_LINES} more lines)`;
}

/**
 * Extract data structure from an item
 */
function extractStructure(item: unknown, depth = 0, maxDepth = 3): Record<string, unknown> {
  if (depth >= maxDepth) return { _type: typeof item };

  if (item === null || item === undefined) {
    return { _type: 'null' };
  }

  if (Array.isArray(item)) {
    if (item.length === 0) return { _type: 'array', _length: 0 };
    return {
      _type: 'array',
      _length: item.length,
      _itemStructure: extractStructure(item[0], depth + 1, maxDepth)
    };
  }

  if (typeof item === 'object') {
    const structure: Record<string, unknown> = {};
    for (const [key, value] of Object.entries(item)) {
      structure[key] = extractStructure(value, depth + 1, maxDepth);
    }
    return structure;
  }

  return { _type: typeof item };
}