n8n-mcp 2.7.9 → 2.7.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/README.md +20 -42
  2. package/data/nodes.db +0 -0
  3. package/dist/database/database-adapter.d.ts +1 -0
  4. package/dist/database/database-adapter.d.ts.map +1 -1
  5. package/dist/database/database-adapter.js +20 -0
  6. package/dist/database/database-adapter.js.map +1 -1
  7. package/dist/http-server-single-session.d.ts +2 -0
  8. package/dist/http-server-single-session.d.ts.map +1 -1
  9. package/dist/http-server-single-session.js +65 -11
  10. package/dist/http-server-single-session.js.map +1 -1
  11. package/dist/http-server.d.ts.map +1 -1
  12. package/dist/http-server.js +41 -8
  13. package/dist/http-server.js.map +1 -1
  14. package/dist/mcp/server.d.ts +7 -0
  15. package/dist/mcp/server.d.ts.map +1 -1
  16. package/dist/mcp/server.js +334 -10
  17. package/dist/mcp/server.js.map +1 -1
  18. package/dist/mcp/tools-documentation.d.ts.map +1 -1
  19. package/dist/mcp/tools-documentation.js +832 -52
  20. package/dist/mcp/tools-documentation.js.map +1 -1
  21. package/dist/mcp/tools-n8n-manager.d.ts.map +1 -1
  22. package/dist/mcp/tools-n8n-manager.js +10 -112
  23. package/dist/mcp/tools-n8n-manager.js.map +1 -1
  24. package/dist/mcp/tools.d.ts.map +1 -1
  25. package/dist/mcp/tools.js +42 -36
  26. package/dist/mcp/tools.js.map +1 -1
  27. package/dist/scripts/fetch-templates.d.ts.map +1 -1
  28. package/dist/scripts/fetch-templates.js +37 -0
  29. package/dist/scripts/fetch-templates.js.map +1 -1
  30. package/dist/scripts/test-error-handling-validation.d.ts +3 -0
  31. package/dist/scripts/test-error-handling-validation.d.ts.map +1 -0
  32. package/dist/scripts/test-error-handling-validation.js +340 -0
  33. package/dist/scripts/test-error-handling-validation.js.map +1 -0
  34. package/dist/scripts/test-node-level-properties.d.ts +3 -0
  35. package/dist/scripts/test-node-level-properties.d.ts.map +1 -0
  36. package/dist/scripts/test-node-level-properties.js +196 -0
  37. package/dist/scripts/test-node-level-properties.js.map +1 -0
  38. package/dist/services/config-validator.d.ts +2 -2
  39. package/dist/services/config-validator.d.ts.map +1 -1
  40. package/dist/services/config-validator.js +123 -5
  41. package/dist/services/config-validator.js.map +1 -1
  42. package/dist/services/enhanced-config-validator.d.ts +2 -0
  43. package/dist/services/enhanced-config-validator.d.ts.map +1 -1
  44. package/dist/services/enhanced-config-validator.js +31 -1
  45. package/dist/services/enhanced-config-validator.js.map +1 -1
  46. package/dist/services/example-generator.d.ts.map +1 -1
  47. package/dist/services/example-generator.js +442 -28
  48. package/dist/services/example-generator.js.map +1 -1
  49. package/dist/services/n8n-validation.d.ts +10 -10
  50. package/dist/services/node-specific-validators.d.ts +8 -1
  51. package/dist/services/node-specific-validators.d.ts.map +1 -1
  52. package/dist/services/node-specific-validators.js +608 -59
  53. package/dist/services/node-specific-validators.js.map +1 -1
  54. package/dist/services/task-templates.d.ts +1 -0
  55. package/dist/services/task-templates.d.ts.map +1 -1
  56. package/dist/services/task-templates.js +858 -16
  57. package/dist/services/task-templates.js.map +1 -1
  58. package/dist/services/workflow-diff-engine.d.ts.map +1 -1
  59. package/dist/services/workflow-diff-engine.js +1 -0
  60. package/dist/services/workflow-diff-engine.js.map +1 -1
  61. package/dist/services/workflow-validator.d.ts +9 -0
  62. package/dist/services/workflow-validator.d.ts.map +1 -1
  63. package/dist/services/workflow-validator.js +270 -0
  64. package/dist/services/workflow-validator.js.map +1 -1
  65. package/dist/sse-server.d.ts +8 -0
  66. package/dist/sse-server.d.ts.map +1 -0
  67. package/dist/sse-server.js +652 -0
  68. package/dist/sse-server.js.map +1 -0
  69. package/dist/templates/template-repository.d.ts +4 -0
  70. package/dist/templates/template-repository.d.ts.map +1 -1
  71. package/dist/templates/template-repository.js +119 -7
  72. package/dist/templates/template-repository.js.map +1 -1
  73. package/dist/templates/template-service.d.ts.map +1 -1
  74. package/dist/templates/template-service.js +2 -0
  75. package/dist/templates/template-service.js.map +1 -1
  76. package/dist/types/n8n-api.d.ts +2 -1
  77. package/dist/types/n8n-api.d.ts.map +1 -1
  78. package/dist/types/n8n-api.js.map +1 -1
  79. package/dist/types/sse.d.ts +42 -0
  80. package/dist/types/sse.d.ts.map +1 -0
  81. package/dist/types/sse.js +3 -0
  82. package/dist/types/sse.js.map +1 -0
  83. package/dist/utils/sse-session-manager.d.ts +23 -0
  84. package/dist/utils/sse-session-manager.d.ts.map +1 -0
  85. package/dist/utils/sse-session-manager.js +178 -0
  86. package/dist/utils/sse-session-manager.js.map +1 -0
  87. package/package.json +1 -1
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.NodeSpecificValidators = void 0;
4
4
  class NodeSpecificValidators {
5
5
  static validateSlack(context) {
6
- const { config, errors, warnings, suggestions } = context;
6
+ const { config, errors, warnings, suggestions, autofix } = context;
7
7
  const { resource, operation } = config;
8
8
  if (resource === 'message') {
9
9
  switch (operation) {
@@ -38,6 +38,26 @@ class NodeSpecificValidators {
38
38
  });
39
39
  }
40
40
  }
41
+ if (!config.onError && !config.retryOnFail && !config.continueOnFail) {
42
+ warnings.push({
43
+ type: 'best_practice',
44
+ property: 'errorHandling',
45
+ message: 'Slack API can have rate limits and transient failures',
46
+ suggestion: 'Add onError: "continueRegularOutput" with retryOnFail for resilience'
47
+ });
48
+ autofix.onError = 'continueRegularOutput';
49
+ autofix.retryOnFail = true;
50
+ autofix.maxTries = 2;
51
+ autofix.waitBetweenTries = 3000;
52
+ }
53
+ if (config.continueOnFail !== undefined) {
54
+ warnings.push({
55
+ type: 'deprecated',
56
+ property: 'continueOnFail',
57
+ message: 'continueOnFail is deprecated. Use onError instead',
58
+ suggestion: 'Replace with onError: "continueRegularOutput"'
59
+ });
60
+ }
41
61
  }
42
62
  static validateSlackSendMessage(context) {
43
63
  const { config, errors, warnings, suggestions, autofix } = context;
@@ -293,7 +313,7 @@ class NodeSpecificValidators {
293
313
  }
294
314
  }
295
315
  static validateOpenAI(context) {
296
- const { config, errors, warnings, suggestions } = context;
316
+ const { config, errors, warnings, suggestions, autofix } = context;
297
317
  const { resource, operation } = config;
298
318
  if (resource === 'chat' && operation === 'create') {
299
319
  if (!config.model) {
@@ -342,9 +362,30 @@ class NodeSpecificValidators {
342
362
  }
343
363
  }
344
364
  }
365
+ if (!config.onError && !config.retryOnFail && !config.continueOnFail) {
366
+ warnings.push({
367
+ type: 'best_practice',
368
+ property: 'errorHandling',
369
+ message: 'AI APIs have rate limits and can return errors',
370
+ suggestion: 'Add onError: "continueRegularOutput" with retryOnFail and longer wait times'
371
+ });
372
+ autofix.onError = 'continueRegularOutput';
373
+ autofix.retryOnFail = true;
374
+ autofix.maxTries = 3;
375
+ autofix.waitBetweenTries = 5000;
376
+ autofix.alwaysOutputData = true;
377
+ }
378
+ if (config.continueOnFail !== undefined) {
379
+ warnings.push({
380
+ type: 'deprecated',
381
+ property: 'continueOnFail',
382
+ message: 'continueOnFail is deprecated. Use onError instead',
383
+ suggestion: 'Replace with onError: "continueRegularOutput"'
384
+ });
385
+ }
345
386
  }
346
387
  static validateMongoDB(context) {
347
- const { config, errors, warnings } = context;
388
+ const { config, errors, warnings, autofix } = context;
348
389
  const { operation } = config;
349
390
  if (!config.collection) {
350
391
  errors.push({
@@ -400,70 +441,37 @@ class NodeSpecificValidators {
400
441
  }
401
442
  break;
402
443
  }
403
- }
404
- static validateWebhook(context) {
405
- const { config, errors, warnings, suggestions } = context;
406
- if (!config.path) {
407
- errors.push({
408
- type: 'missing_required',
409
- property: 'path',
410
- message: 'Webhook path is required',
411
- fix: 'Set a unique path like "my-webhook" (no leading slash)'
412
- });
413
- }
414
- else {
415
- const path = config.path;
416
- if (path.startsWith('/')) {
444
+ if (!config.onError && !config.retryOnFail && !config.continueOnFail) {
445
+ if (operation === 'find') {
417
446
  warnings.push({
418
- type: 'inefficient',
419
- property: 'path',
420
- message: 'Webhook path should not start with /',
421
- suggestion: 'Remove the leading slash: use "my-webhook" instead of "/my-webhook"'
422
- });
423
- }
424
- if (path.includes(' ')) {
425
- errors.push({
426
- type: 'invalid_value',
427
- property: 'path',
428
- message: 'Webhook path cannot contain spaces',
429
- fix: 'Replace spaces with hyphens or underscores'
447
+ type: 'best_practice',
448
+ property: 'errorHandling',
449
+ message: 'MongoDB queries can fail due to connection issues',
450
+ suggestion: 'Add onError: "continueRegularOutput" with retryOnFail'
430
451
  });
452
+ autofix.onError = 'continueRegularOutput';
453
+ autofix.retryOnFail = true;
454
+ autofix.maxTries = 3;
431
455
  }
432
- if (!/^[a-zA-Z0-9\-_\/]+$/.test(path.replace(/^\//, ''))) {
456
+ else if (['insert', 'update', 'delete'].includes(operation)) {
433
457
  warnings.push({
434
- type: 'inefficient',
435
- property: 'path',
436
- message: 'Webhook path contains special characters',
437
- suggestion: 'Use only letters, numbers, hyphens, and underscores'
458
+ type: 'best_practice',
459
+ property: 'errorHandling',
460
+ message: 'MongoDB write operations should handle errors carefully',
461
+ suggestion: 'Add onError: "continueErrorOutput" to handle write failures separately'
438
462
  });
463
+ autofix.onError = 'continueErrorOutput';
464
+ autofix.retryOnFail = true;
465
+ autofix.maxTries = 2;
466
+ autofix.waitBetweenTries = 1000;
439
467
  }
440
468
  }
441
- if (config.responseMode === 'responseNode') {
442
- suggestions.push('Add a "Respond to Webhook" node to send custom responses');
443
- if (!config.responseData) {
444
- warnings.push({
445
- type: 'missing_common',
446
- property: 'responseData',
447
- message: 'Response data not configured for responseNode mode',
448
- suggestion: 'Add a "Respond to Webhook" node or change responseMode'
449
- });
450
- }
451
- }
452
- if (config.httpMethod && Array.isArray(config.httpMethod)) {
453
- if (config.httpMethod.length === 0) {
454
- errors.push({
455
- type: 'invalid_value',
456
- property: 'httpMethod',
457
- message: 'At least one HTTP method must be selected',
458
- fix: 'Select GET, POST, or other methods your webhook should accept'
459
- });
460
- }
461
- }
462
- if (!config.authentication || config.authentication === 'none') {
469
+ if (config.continueOnFail !== undefined) {
463
470
  warnings.push({
464
- type: 'security',
465
- message: 'Webhook has no authentication',
466
- suggestion: 'Consider adding authentication to prevent unauthorized access'
471
+ type: 'deprecated',
472
+ property: 'continueOnFail',
473
+ message: 'continueOnFail is deprecated. Use onError instead',
474
+ suggestion: 'Replace with onError: "continueRegularOutput" or "continueErrorOutput"'
467
475
  });
468
476
  }
469
477
  }
@@ -542,6 +550,39 @@ class NodeSpecificValidators {
542
550
  if (config.connectionTimeout === undefined) {
543
551
  suggestions.push('Consider setting connectionTimeout to handle slow connections');
544
552
  }
553
+ if (!config.onError && !config.retryOnFail && !config.continueOnFail) {
554
+ if (operation === 'execute' && config.query?.toLowerCase().includes('select')) {
555
+ warnings.push({
556
+ type: 'best_practice',
557
+ property: 'errorHandling',
558
+ message: 'Database reads can fail due to connection issues',
559
+ suggestion: 'Add onError: "continueRegularOutput" and retryOnFail: true'
560
+ });
561
+ autofix.onError = 'continueRegularOutput';
562
+ autofix.retryOnFail = true;
563
+ autofix.maxTries = 3;
564
+ }
565
+ else if (['insert', 'update', 'delete'].includes(operation)) {
566
+ warnings.push({
567
+ type: 'best_practice',
568
+ property: 'errorHandling',
569
+ message: 'Database writes should handle errors carefully',
570
+ suggestion: 'Add onError: "stopWorkflow" with retryOnFail for transient failures'
571
+ });
572
+ autofix.onError = 'stopWorkflow';
573
+ autofix.retryOnFail = true;
574
+ autofix.maxTries = 2;
575
+ autofix.waitBetweenTries = 2000;
576
+ }
577
+ }
578
+ if (config.continueOnFail !== undefined) {
579
+ warnings.push({
580
+ type: 'deprecated',
581
+ property: 'continueOnFail',
582
+ message: 'continueOnFail is deprecated. Use onError instead',
583
+ suggestion: 'Replace with onError: "continueRegularOutput" or "stopWorkflow"'
584
+ });
585
+ }
545
586
  }
546
587
  static validateMySQL(context) {
547
588
  const { config, errors, warnings, suggestions } = context;
@@ -602,6 +643,24 @@ class NodeSpecificValidators {
602
643
  if (config.timezone === undefined) {
603
644
  suggestions.push('Consider setting timezone to ensure consistent date/time handling');
604
645
  }
646
+ if (!config.onError && !config.retryOnFail && !config.continueOnFail) {
647
+ if (operation === 'execute' && config.query?.toLowerCase().includes('select')) {
648
+ warnings.push({
649
+ type: 'best_practice',
650
+ property: 'errorHandling',
651
+ message: 'Database queries can fail due to connection issues',
652
+ suggestion: 'Add onError: "continueRegularOutput" and retryOnFail: true'
653
+ });
654
+ }
655
+ else if (['insert', 'update', 'delete'].includes(operation)) {
656
+ warnings.push({
657
+ type: 'best_practice',
658
+ property: 'errorHandling',
659
+ message: 'Database modifications should handle errors carefully',
660
+ suggestion: 'Add onError: "stopWorkflow" with retryOnFail for transient failures'
661
+ });
662
+ }
663
+ }
605
664
  }
606
665
  static validateSQLQuery(context, dbType = 'generic') {
607
666
  const { config, errors, warnings, suggestions } = context;
@@ -661,6 +720,496 @@ class NodeSpecificValidators {
661
720
  }
662
721
  }
663
722
  }
723
+ static validateHttpRequest(context) {
724
+ const { config, errors, warnings, suggestions, autofix } = context;
725
+ const { method = 'GET', url, sendBody, authentication } = config;
726
+ if (!url) {
727
+ errors.push({
728
+ type: 'missing_required',
729
+ property: 'url',
730
+ message: 'URL is required for HTTP requests',
731
+ fix: 'Provide the full URL including protocol (https://...)'
732
+ });
733
+ }
734
+ else if (!url.startsWith('http://') && !url.startsWith('https://') && !url.includes('{{')) {
735
+ warnings.push({
736
+ type: 'invalid_value',
737
+ property: 'url',
738
+ message: 'URL should start with http:// or https://',
739
+ suggestion: 'Use https:// for secure connections'
740
+ });
741
+ }
742
+ if (['POST', 'PUT', 'PATCH'].includes(method) && !sendBody) {
743
+ warnings.push({
744
+ type: 'missing_common',
745
+ property: 'sendBody',
746
+ message: `${method} requests typically include a body`,
747
+ suggestion: 'Set sendBody: true and configure the body content'
748
+ });
749
+ }
750
+ if (!config.retryOnFail && !config.onError && !config.continueOnFail) {
751
+ warnings.push({
752
+ type: 'best_practice',
753
+ property: 'errorHandling',
754
+ message: 'HTTP requests can fail due to network issues or server errors',
755
+ suggestion: 'Add onError: "continueRegularOutput" and retryOnFail: true for resilience'
756
+ });
757
+ autofix.onError = 'continueRegularOutput';
758
+ autofix.retryOnFail = true;
759
+ autofix.maxTries = 3;
760
+ autofix.waitBetweenTries = 1000;
761
+ }
762
+ if (config.continueOnFail !== undefined) {
763
+ warnings.push({
764
+ type: 'deprecated',
765
+ property: 'continueOnFail',
766
+ message: 'continueOnFail is deprecated. Use onError instead',
767
+ suggestion: 'Replace with onError: "continueRegularOutput"'
768
+ });
769
+ autofix.onError = config.continueOnFail ? 'continueRegularOutput' : 'stopWorkflow';
770
+ delete autofix.continueOnFail;
771
+ }
772
+ if (config.retryOnFail) {
773
+ if (!['GET', 'HEAD', 'OPTIONS'].includes(method) && (!config.maxTries || config.maxTries > 3)) {
774
+ warnings.push({
775
+ type: 'best_practice',
776
+ property: 'maxTries',
777
+ message: `${method} requests might not be idempotent. Use fewer retries.`,
778
+ suggestion: 'Set maxTries: 2 for non-idempotent operations'
779
+ });
780
+ }
781
+ if (!config.alwaysOutputData) {
782
+ suggestions.push('Enable alwaysOutputData to capture error responses for debugging');
783
+ autofix.alwaysOutputData = true;
784
+ }
785
+ }
786
+ if (url && url.includes('api') && !authentication) {
787
+ warnings.push({
788
+ type: 'security',
789
+ property: 'authentication',
790
+ message: 'API endpoints typically require authentication',
791
+ suggestion: 'Configure authentication method (Bearer token, API key, etc.)'
792
+ });
793
+ }
794
+ if (!config.timeout) {
795
+ suggestions.push('Consider setting a timeout to prevent hanging requests');
796
+ }
797
+ }
798
+ static validateWebhook(context) {
799
+ const { config, errors, warnings, suggestions, autofix } = context;
800
+ const { path, httpMethod = 'POST', responseMode } = config;
801
+ if (!path) {
802
+ errors.push({
803
+ type: 'missing_required',
804
+ property: 'path',
805
+ message: 'Webhook path is required',
806
+ fix: 'Provide a unique path like "my-webhook" or "github-events"'
807
+ });
808
+ }
809
+ else if (path.startsWith('/')) {
810
+ warnings.push({
811
+ type: 'invalid_value',
812
+ property: 'path',
813
+ message: 'Webhook path should not start with /',
814
+ suggestion: 'Use "webhook-name" instead of "/webhook-name"'
815
+ });
816
+ }
817
+ if (!config.onError && !config.continueOnFail) {
818
+ warnings.push({
819
+ type: 'best_practice',
820
+ property: 'onError',
821
+ message: 'Webhooks should always send a response, even on error',
822
+ suggestion: 'Set onError: "continueRegularOutput" to ensure webhook responses'
823
+ });
824
+ autofix.onError = 'continueRegularOutput';
825
+ }
826
+ if (config.continueOnFail !== undefined) {
827
+ warnings.push({
828
+ type: 'deprecated',
829
+ property: 'continueOnFail',
830
+ message: 'continueOnFail is deprecated. Use onError instead',
831
+ suggestion: 'Replace with onError: "continueRegularOutput"'
832
+ });
833
+ autofix.onError = 'continueRegularOutput';
834
+ delete autofix.continueOnFail;
835
+ }
836
+ if (responseMode === 'responseNode' && !config.onError && !config.continueOnFail) {
837
+ errors.push({
838
+ type: 'invalid_configuration',
839
+ property: 'responseMode',
840
+ message: 'responseNode mode requires onError: "continueRegularOutput"',
841
+ fix: 'Set onError to ensure response is always sent'
842
+ });
843
+ }
844
+ if (!config.alwaysOutputData) {
845
+ suggestions.push('Enable alwaysOutputData to debug webhook payloads');
846
+ autofix.alwaysOutputData = true;
847
+ }
848
+ suggestions.push('Consider adding webhook validation (HMAC signature verification)');
849
+ suggestions.push('Implement rate limiting for public webhooks');
850
+ }
851
+ static validateCode(context) {
852
+ const { config, errors, warnings, suggestions, autofix } = context;
853
+ const language = config.language || 'javaScript';
854
+ const codeField = language === 'python' ? 'pythonCode' : 'jsCode';
855
+ const code = config[codeField] || '';
856
+ if (!code || code.trim() === '') {
857
+ errors.push({
858
+ type: 'missing_required',
859
+ property: codeField,
860
+ message: 'Code cannot be empty',
861
+ fix: 'Add your code logic. Start with: return [{json: {result: "success"}}]'
862
+ });
863
+ return;
864
+ }
865
+ if (language === 'javaScript') {
866
+ this.validateJavaScriptCode(code, errors, warnings, suggestions);
867
+ }
868
+ else if (language === 'python') {
869
+ this.validatePythonCode(code, errors, warnings, suggestions);
870
+ }
871
+ this.validateReturnStatement(code, language, errors, warnings, suggestions);
872
+ this.validateN8nVariables(code, language, warnings, suggestions, errors);
873
+ this.validateCodeSecurity(code, language, warnings);
874
+ if (!config.onError && code.length > 100) {
875
+ warnings.push({
876
+ type: 'best_practice',
877
+ property: 'errorHandling',
878
+ message: 'Code nodes can throw errors - consider error handling',
879
+ suggestion: 'Add onError: "continueRegularOutput" to handle errors gracefully'
880
+ });
881
+ autofix.onError = 'continueRegularOutput';
882
+ }
883
+ if (config.mode === 'runOnceForEachItem' && code.includes('items')) {
884
+ warnings.push({
885
+ type: 'best_practice',
886
+ message: 'In "Run Once for Each Item" mode, use $json instead of items array',
887
+ suggestion: 'Access current item data with $json.fieldName'
888
+ });
889
+ }
890
+ if (!config.mode && code.includes('$json')) {
891
+ warnings.push({
892
+ type: 'best_practice',
893
+ message: '$json only works in "Run Once for Each Item" mode',
894
+ suggestion: 'Either set mode: "runOnceForEachItem" or use items[0].json'
895
+ });
896
+ }
897
+ }
898
+ static validateJavaScriptCode(code, errors, warnings, suggestions) {
899
+ const syntaxPatterns = [
900
+ { pattern: /const\s+const/, message: 'Duplicate const declaration' },
901
+ { pattern: /let\s+let/, message: 'Duplicate let declaration' },
902
+ { pattern: /\)\s*\)\s*{/, message: 'Extra closing parenthesis before {' },
903
+ { pattern: /}\s*}$/, message: 'Extra closing brace at end' }
904
+ ];
905
+ syntaxPatterns.forEach(({ pattern, message }) => {
906
+ if (pattern.test(code)) {
907
+ errors.push({
908
+ type: 'invalid_value',
909
+ property: 'jsCode',
910
+ message: `Syntax error: ${message}`,
911
+ fix: 'Check your JavaScript syntax'
912
+ });
913
+ }
914
+ });
915
+ const functionWithAwait = /function\s+\w*\s*\([^)]*\)\s*{[^}]*await/;
916
+ const arrowWithAwait = /\([^)]*\)\s*=>\s*{[^}]*await/;
917
+ if ((functionWithAwait.test(code) || arrowWithAwait.test(code)) && !code.includes('async')) {
918
+ warnings.push({
919
+ type: 'best_practice',
920
+ message: 'Using await inside a non-async function',
921
+ suggestion: 'Add async keyword to the function, or use top-level await (Code nodes support it)'
922
+ });
923
+ }
924
+ if (code.includes('$helpers.httpRequest')) {
925
+ suggestions.push('$helpers.httpRequest is async - use: const response = await $helpers.httpRequest(...)');
926
+ }
927
+ if (code.includes('DateTime') && !code.includes('DateTime.')) {
928
+ warnings.push({
929
+ type: 'best_practice',
930
+ message: 'DateTime is from Luxon library',
931
+ suggestion: 'Use DateTime.now() or DateTime.fromISO() for date operations'
932
+ });
933
+ }
934
+ }
935
+ static validatePythonCode(code, errors, warnings, suggestions) {
936
+ const lines = code.split('\n');
937
+ if (code.includes('__name__') && code.includes('__main__')) {
938
+ warnings.push({
939
+ type: 'inefficient',
940
+ message: 'if __name__ == "__main__" is not needed in Code nodes',
941
+ suggestion: 'Code node Python runs directly - remove the main check'
942
+ });
943
+ }
944
+ const unavailableImports = [
945
+ { module: 'requests', suggestion: 'Use JavaScript Code node with $helpers.httpRequest for HTTP requests' },
946
+ { module: 'pandas', suggestion: 'Use built-in list/dict operations or JavaScript for data manipulation' },
947
+ { module: 'numpy', suggestion: 'Use standard Python math operations' },
948
+ { module: 'pip', suggestion: 'External packages cannot be installed in Code nodes' }
949
+ ];
950
+ unavailableImports.forEach(({ module, suggestion }) => {
951
+ if (code.includes(`import ${module}`) || code.includes(`from ${module}`)) {
952
+ errors.push({
953
+ type: 'invalid_value',
954
+ property: 'pythonCode',
955
+ message: `Module '${module}' is not available in Code nodes`,
956
+ fix: suggestion
957
+ });
958
+ }
959
+ });
960
+ lines.forEach((line, i) => {
961
+ if (line.trim().endsWith(':') && i < lines.length - 1) {
962
+ const nextLine = lines[i + 1];
963
+ if (nextLine.trim() && !nextLine.startsWith(' ') && !nextLine.startsWith('\t')) {
964
+ errors.push({
965
+ type: 'invalid_value',
966
+ property: 'pythonCode',
967
+ message: `Missing indentation after line ${i + 1}`,
968
+ fix: 'Indent the line after the colon'
969
+ });
970
+ }
971
+ }
972
+ });
973
+ }
974
+ static validateReturnStatement(code, language, errors, warnings, suggestions) {
975
+ const hasReturn = /return\s+/.test(code);
976
+ if (!hasReturn) {
977
+ errors.push({
978
+ type: 'missing_required',
979
+ property: language === 'python' ? 'pythonCode' : 'jsCode',
980
+ message: 'Code must return data for the next node',
981
+ fix: language === 'python'
982
+ ? 'Add: return [{"json": {"result": "success"}}]'
983
+ : 'Add: return [{json: {result: "success"}}]'
984
+ });
985
+ return;
986
+ }
987
+ if (language === 'javaScript') {
988
+ if (/return\s+{(?!.*\[).*}\s*;?$/s.test(code) && !code.includes('json:')) {
989
+ errors.push({
990
+ type: 'invalid_value',
991
+ property: 'jsCode',
992
+ message: 'Return value must be an array of objects',
993
+ fix: 'Wrap in array: return [{json: yourObject}]'
994
+ });
995
+ }
996
+ if (/return\s+(true|false|null|undefined|\d+|['"`])/m.test(code)) {
997
+ errors.push({
998
+ type: 'invalid_value',
999
+ property: 'jsCode',
1000
+ message: 'Cannot return primitive values directly',
1001
+ fix: 'Return array of objects: return [{json: {value: yourData}}]'
1002
+ });
1003
+ }
1004
+ if (/return\s+\[[\s\n]*['"`\d]/.test(code)) {
1005
+ errors.push({
1006
+ type: 'invalid_value',
1007
+ property: 'jsCode',
1008
+ message: 'Array items must be objects with json property',
1009
+ fix: 'Use: return [{json: {value: "data"}}] not return ["data"]'
1010
+ });
1011
+ }
1012
+ if (/return\s+items\s*;?$/.test(code) && !code.includes('map')) {
1013
+ suggestions.push('Returning items directly is fine if they already have {json: ...} structure. ' +
1014
+ 'To modify: return items.map(item => ({json: {...item.json, newField: "value"}}))');
1015
+ }
1016
+ }
1017
+ if (language === 'python') {
1018
+ if (/return\s+{(?!.*\[).*}$/s.test(code)) {
1019
+ errors.push({
1020
+ type: 'invalid_value',
1021
+ property: 'pythonCode',
1022
+ message: 'Return value must be a list of dicts',
1023
+ fix: 'Wrap in list: return [{"json": your_dict}]'
1024
+ });
1025
+ }
1026
+ if (/return\s+(True|False|None|\d+|['"`])/m.test(code)) {
1027
+ errors.push({
1028
+ type: 'invalid_value',
1029
+ property: 'pythonCode',
1030
+ message: 'Cannot return primitive values directly',
1031
+ fix: 'Return list of dicts: return [{"json": {"value": your_data}}]'
1032
+ });
1033
+ }
1034
+ }
1035
+ }
1036
+ static validateN8nVariables(code, language, warnings, suggestions, errors) {
1037
+ const inputPatterns = language === 'javaScript'
1038
+ ? ['items', '$input', '$json', '$node', '$prevNode']
1039
+ : ['items', '_input'];
1040
+ const usesInput = inputPatterns.some(pattern => code.includes(pattern));
1041
+ if (!usesInput && code.length > 50) {
1042
+ warnings.push({
1043
+ type: 'missing_common',
1044
+ message: 'Code doesn\'t reference input data',
1045
+ suggestion: language === 'javaScript'
1046
+ ? 'Access input with: items, $input.all(), or $json (single-item mode)'
1047
+ : 'Access input with: items variable'
1048
+ });
1049
+ }
1050
+ if (code.includes('{{') && code.includes('}}')) {
1051
+ errors.push({
1052
+ type: 'invalid_value',
1053
+ property: language === 'python' ? 'pythonCode' : 'jsCode',
1054
+ message: 'Expression syntax {{...}} is not valid in Code nodes',
1055
+ fix: 'Use regular JavaScript/Python syntax without double curly braces'
1056
+ });
1057
+ }
1058
+ if (code.includes('$node[')) {
1059
+ warnings.push({
1060
+ type: 'invalid_value',
1061
+ property: language === 'python' ? 'pythonCode' : 'jsCode',
1062
+ message: 'Use $(\'Node Name\') instead of $node[\'Node Name\'] in Code nodes',
1063
+ suggestion: 'Replace $node[\'NodeName\'] with $(\'NodeName\')'
1064
+ });
1065
+ }
1066
+ const expressionOnlyFunctions = ['$now()', '$today()', '$tomorrow()', '.unique()', '.pluck(', '.keys()', '.hash('];
1067
+ expressionOnlyFunctions.forEach(func => {
1068
+ if (code.includes(func)) {
1069
+ warnings.push({
1070
+ type: 'invalid_value',
1071
+ property: language === 'python' ? 'pythonCode' : 'jsCode',
1072
+ message: `${func} is an expression-only function not available in Code nodes`,
1073
+ suggestion: 'See Code node documentation for alternatives'
1074
+ });
1075
+ }
1076
+ });
1077
+ if (language === 'javaScript') {
1078
+ if (/\$(?![a-zA-Z])/.test(code) && !code.includes('${')) {
1079
+ warnings.push({
1080
+ type: 'best_practice',
1081
+ message: 'Invalid $ usage detected',
1082
+ suggestion: 'n8n variables start with $: $json, $input, $node, $workflow, $execution'
1083
+ });
1084
+ }
1085
+ if (code.includes('helpers.') && !code.includes('$helpers')) {
1086
+ warnings.push({
1087
+ type: 'invalid_value',
1088
+ property: 'jsCode',
1089
+ message: 'Use $helpers not helpers',
1090
+ suggestion: 'Change helpers. to $helpers.'
1091
+ });
1092
+ }
1093
+ if (code.includes('$helpers') && !code.includes('typeof $helpers')) {
1094
+ warnings.push({
1095
+ type: 'best_practice',
1096
+ message: '$helpers availability varies by n8n version',
1097
+ suggestion: 'Check availability first: if (typeof $helpers !== "undefined" && $helpers.httpRequest) { ... }'
1098
+ });
1099
+ }
1100
+ if (code.includes('$helpers')) {
1101
+ suggestions.push('Common $helpers methods: httpRequest(), prepareBinaryData(). Note: getWorkflowStaticData is a standalone function - use $getWorkflowStaticData() instead');
1102
+ }
1103
+ if (code.includes('$helpers.getWorkflowStaticData')) {
1104
+ errors.push({
1105
+ type: 'invalid_value',
1106
+ property: 'jsCode',
1107
+ message: '$helpers.getWorkflowStaticData() will cause "$helpers is not defined" error',
1108
+ fix: 'Use $getWorkflowStaticData("global") or $getWorkflowStaticData("node") directly'
1109
+ });
1110
+ }
1111
+ if (code.includes('$jmespath(') && /\$jmespath\s*\(\s*['"`]/.test(code)) {
1112
+ warnings.push({
1113
+ type: 'invalid_value',
1114
+ property: 'jsCode',
1115
+ message: 'Code node $jmespath has reversed parameter order: $jmespath(data, query)',
1116
+ suggestion: 'Use: $jmespath(dataObject, "query.path") not $jmespath("query.path", dataObject)'
1117
+ });
1118
+ }
1119
+ if (code.includes('items[0].json') && !code.includes('.json.body')) {
1120
+ if (code.includes('Webhook') || code.includes('webhook') ||
1121
+ code.includes('$("Webhook")') || code.includes("$('Webhook')")) {
1122
+ warnings.push({
1123
+ type: 'invalid_value',
1124
+ property: 'jsCode',
1125
+ message: 'Webhook data is nested under .body property',
1126
+ suggestion: 'Use items[0].json.body.fieldName instead of items[0].json.fieldName for webhook data'
1127
+ });
1128
+ }
1129
+ else if (/items\[0\]\.json\.(payload|data|command|action|event|message)\b/.test(code)) {
1130
+ warnings.push({
1131
+ type: 'best_practice',
1132
+ message: 'If processing webhook data, remember it\'s nested under .body',
1133
+ suggestion: 'Webhook payloads are at items[0].json.body, not items[0].json'
1134
+ });
1135
+ }
1136
+ }
1137
+ }
1138
+ const jmespathFunction = language === 'javaScript' ? '$jmespath' : '_jmespath';
1139
+ if (code.includes(jmespathFunction + '(')) {
1140
+ const filterPattern = /\[?\?[^[\]]*(?:>=?|<=?|==|!=)\s*(\d+(?:\.\d+)?)\s*\]/g;
1141
+ let match;
1142
+ while ((match = filterPattern.exec(code)) !== null) {
1143
+ const number = match[1];
1144
+ const beforeNumber = code.substring(match.index, match.index + match[0].indexOf(number));
1145
+ const afterNumber = code.substring(match.index + match[0].indexOf(number) + number.length);
1146
+ if (!beforeNumber.includes('`') || !afterNumber.startsWith('`')) {
1147
+ errors.push({
1148
+ type: 'invalid_value',
1149
+ property: language === 'python' ? 'pythonCode' : 'jsCode',
1150
+ message: `JMESPath numeric literal ${number} must be wrapped in backticks`,
1151
+ fix: `Change [?field >= ${number}] to [?field >= \`${number}\`]`
1152
+ });
1153
+ }
1154
+ }
1155
+ suggestions.push('JMESPath in n8n requires backticks around numeric literals in filters: [?age >= `18`]');
1156
+ }
1157
+ }
1158
+ static validateCodeSecurity(code, language, warnings) {
1159
+ const dangerousPatterns = [
1160
+ { pattern: /eval\s*\(/, message: 'Avoid eval() - it\'s a security risk' },
1161
+ { pattern: /Function\s*\(/, message: 'Avoid Function constructor - use regular functions' },
1162
+ { pattern: language === 'python' ? /exec\s*\(/ : /exec\s*\(/, message: 'Avoid exec() - it\'s a security risk' },
1163
+ { pattern: /process\.env/, message: 'Limited environment access in Code nodes' },
1164
+ { pattern: /import\s+\*/, message: 'Avoid import * - be specific about imports' }
1165
+ ];
1166
+ dangerousPatterns.forEach(({ pattern, message }) => {
1167
+ if (pattern.test(code)) {
1168
+ warnings.push({
1169
+ type: 'security',
1170
+ message,
1171
+ suggestion: 'Use safer alternatives or built-in functions'
1172
+ });
1173
+ }
1174
+ });
1175
+ if (code.includes('require(')) {
1176
+ const builtinModules = ['crypto', 'util', 'querystring', 'url', 'buffer'];
1177
+ const requirePattern = /require\s*\(\s*['"`](\w+)['"`]\s*\)/g;
1178
+ let match;
1179
+ while ((match = requirePattern.exec(code)) !== null) {
1180
+ const moduleName = match[1];
1181
+ if (!builtinModules.includes(moduleName)) {
1182
+ warnings.push({
1183
+ type: 'security',
1184
+ message: `Cannot require('${moduleName}') - only built-in Node.js modules are available`,
1185
+ suggestion: `Available modules: ${builtinModules.join(', ')}`
1186
+ });
1187
+ }
1188
+ }
1189
+ if (/require\s*\([^'"`]/.test(code)) {
1190
+ warnings.push({
1191
+ type: 'security',
1192
+ message: 'Dynamic require() not supported',
1193
+ suggestion: 'Use static require with string literals: require("crypto")'
1194
+ });
1195
+ }
1196
+ }
1197
+ if ((code.includes('crypto.') || code.includes('randomBytes') || code.includes('randomUUID')) &&
1198
+ !code.includes('require') && language === 'javaScript') {
1199
+ warnings.push({
1200
+ type: 'invalid_value',
1201
+ message: 'Using crypto without require statement',
1202
+ suggestion: 'Add: const crypto = require("crypto"); at the beginning (ignore editor warnings)'
1203
+ });
1204
+ }
1205
+ if (/\b(fs|path|child_process)\b/.test(code)) {
1206
+ warnings.push({
1207
+ type: 'security',
1208
+ message: 'File system and process access not available in Code nodes',
1209
+ suggestion: 'Use other n8n nodes for file operations (e.g., Read/Write Files node)'
1210
+ });
1211
+ }
1212
+ }
664
1213
  }
665
1214
  exports.NodeSpecificValidators = NodeSpecificValidators;
666
1215
  //# sourceMappingURL=node-specific-validators.js.map