n8n-mcp 2.86.0 → 2.87.0

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 (74) hide show
  1. package/dist/mcp/handlers-n8n-manager.d.ts.map +1 -1
  2. package/dist/mcp/handlers-n8n-manager.js +29 -3
  3. package/dist/mcp/handlers-n8n-manager.js.map +1 -1
  4. package/dist/mcp/handlers-workflow-diff.d.ts.map +1 -1
  5. package/dist/mcp/handlers-workflow-diff.js +110 -9
  6. package/dist/mcp/handlers-workflow-diff.js.map +1 -1
  7. package/dist/mcp/server.d.ts +1 -0
  8. package/dist/mcp/server.d.ts.map +1 -1
  9. package/dist/mcp/server.js +12 -4
  10. package/dist/mcp/server.js.map +1 -1
  11. package/dist/mcp/tool-docs/workflow_management/n8n-autofix-workflow.d.ts.map +1 -1
  12. package/dist/mcp/tool-docs/workflow_management/n8n-autofix-workflow.js +2 -1
  13. package/dist/mcp/tool-docs/workflow_management/n8n-autofix-workflow.js.map +1 -1
  14. package/dist/mcp/tool-docs/workflow_management/n8n-update-full-workflow.d.ts.map +1 -1
  15. package/dist/mcp/tool-docs/workflow_management/n8n-update-full-workflow.js +2 -1
  16. package/dist/mcp/tool-docs/workflow_management/n8n-update-full-workflow.js.map +1 -1
  17. package/dist/mcp/tool-docs/workflow_management/n8n-update-partial-workflow.d.ts.map +1 -1
  18. package/dist/mcp/tool-docs/workflow_management/n8n-update-partial-workflow.js +2 -1
  19. package/dist/mcp/tool-docs/workflow_management/n8n-update-partial-workflow.js.map +1 -1
  20. package/dist/mcp/tools-documentation.js +127 -230
  21. package/dist/mcp/tools-documentation.js.map +1 -1
  22. package/dist/mcp-tools-engine.d.ts +1 -1
  23. package/dist/mcp-tools-engine.d.ts.map +1 -1
  24. package/dist/mcp-tools-engine.js +1 -3
  25. package/dist/mcp-tools-engine.js.map +1 -1
  26. package/dist/services/config-validator.d.ts.map +1 -1
  27. package/dist/services/config-validator.js +8 -53
  28. package/dist/services/config-validator.js.map +1 -1
  29. package/dist/services/enhanced-config-validator.d.ts +1 -0
  30. package/dist/services/enhanced-config-validator.d.ts.map +1 -1
  31. package/dist/services/enhanced-config-validator.js +46 -6
  32. package/dist/services/enhanced-config-validator.js.map +1 -1
  33. package/dist/services/example-generator.d.ts.map +1 -1
  34. package/dist/services/example-generator.js +28 -29
  35. package/dist/services/example-generator.js.map +1 -1
  36. package/dist/services/expression-validator.d.ts +2 -0
  37. package/dist/services/expression-validator.d.ts.map +1 -1
  38. package/dist/services/expression-validator.js +27 -1
  39. package/dist/services/expression-validator.js.map +1 -1
  40. package/dist/services/mcp-exposure.d.ts.map +1 -1
  41. package/dist/services/mcp-exposure.js +43 -6
  42. package/dist/services/mcp-exposure.js.map +1 -1
  43. package/dist/services/n8n-api-client.d.ts +1 -4
  44. package/dist/services/n8n-api-client.d.ts.map +1 -1
  45. package/dist/services/n8n-api-client.js +0 -30
  46. package/dist/services/n8n-api-client.js.map +1 -1
  47. package/dist/services/n8n-validation.d.ts.map +1 -1
  48. package/dist/services/n8n-validation.js +28 -33
  49. package/dist/services/n8n-validation.js.map +1 -1
  50. package/dist/services/node-specific-validators.d.ts +22 -0
  51. package/dist/services/node-specific-validators.d.ts.map +1 -1
  52. package/dist/services/node-specific-validators.js +701 -88
  53. package/dist/services/node-specific-validators.js.map +1 -1
  54. package/dist/services/task-templates.d.ts.map +1 -1
  55. package/dist/services/task-templates.js +54 -49
  56. package/dist/services/task-templates.js.map +1 -1
  57. package/dist/services/workflow-validator.d.ts +1 -0
  58. package/dist/services/workflow-validator.d.ts.map +1 -1
  59. package/dist/services/workflow-validator.js +58 -63
  60. package/dist/services/workflow-validator.js.map +1 -1
  61. package/dist/services/workflow-versioning-service.d.ts +2 -0
  62. package/dist/services/workflow-versioning-service.d.ts.map +1 -1
  63. package/dist/services/workflow-versioning-service.js +14 -0
  64. package/dist/services/workflow-versioning-service.js.map +1 -1
  65. package/dist/types/n8n-api.d.ts +0 -40
  66. package/dist/types/n8n-api.d.ts.map +1 -1
  67. package/dist/utils/jmespath-checks.d.ts +16 -0
  68. package/dist/utils/jmespath-checks.d.ts.map +1 -0
  69. package/dist/utils/jmespath-checks.js +357 -0
  70. package/dist/utils/jmespath-checks.js.map +1 -0
  71. package/dist/utils/n8n-errors.d.ts.map +1 -1
  72. package/dist/utils/n8n-errors.js +9 -0
  73. package/dist/utils/n8n-errors.js.map +1 -1
  74. package/package.json +1 -1
@@ -1,9 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.NodeSpecificValidators = void 0;
4
+ const jmespath_checks_1 = require("../utils/jmespath-checks");
4
5
  const MAX_CODE_LENGTH = 200000;
5
6
  const MAX_SHORT_INPUT_LENGTH = 2000;
6
7
  const MAX_PARAM_SCAN = 2000;
8
+ const MAX_HEADER_LINES = 50;
9
+ const MAX_RETURN_LOOKAHEAD = 5000;
10
+ const MAX_RETURN_TOTAL_SCAN = 200000;
7
11
  const JS_PRIMITIVE_RETURN_RE = /return\s+(?:(?:true|false|null|undefined)\b|\d+|['"`])/m;
8
12
  class NodeSpecificValidators {
9
13
  static validateSlack(context) {
@@ -391,7 +395,8 @@ class NodeSpecificValidators {
391
395
  static validateMongoDB(context) {
392
396
  const { config, errors, warnings, autofix } = context;
393
397
  const { operation } = config;
394
- if (!config.collection) {
398
+ const collectionAlreadyReported = errors.some(e => e.property === 'collection');
399
+ if (!config.collection && !collectionAlreadyReported) {
395
400
  errors.push({
396
401
  type: 'missing_required',
397
402
  property: 'collection',
@@ -963,15 +968,16 @@ class NodeSpecificValidators {
963
968
  });
964
969
  return;
965
970
  }
971
+ const mode = config.mode || 'runOnceForAllItems';
972
+ const modeIsKnown = !(typeof config.mode === 'string' && config.mode.startsWith('='));
966
973
  if (language === 'javaScript') {
967
974
  this.validateJavaScriptCode(code, errors, warnings, suggestions);
968
975
  }
969
976
  else if (language === 'python') {
970
- this.validatePythonCode(code, errors, warnings, suggestions);
977
+ this.validatePythonCode(code, errors, warnings, suggestions, mode, modeIsKnown);
971
978
  }
972
- const mode = config.mode || 'runOnceForAllItems';
973
- this.validateReturnStatement(code, language, errors, warnings, suggestions, mode);
974
- this.validateN8nVariables(code, language, warnings, suggestions, errors);
979
+ this.validateReturnStatement(code, language, errors, warnings, suggestions, mode, modeIsKnown);
980
+ this.validateN8nVariables(code, language, warnings, suggestions, errors, mode);
975
981
  this.validateCodeSecurity(code, language, warnings);
976
982
  if (!config.onError && code.length > 100) {
977
983
  warnings.push({
@@ -982,7 +988,7 @@ class NodeSpecificValidators {
982
988
  });
983
989
  autofix.onError = 'continueRegularOutput';
984
990
  }
985
- if (config.mode === 'runOnceForEachItem' && code.includes('items')) {
991
+ if (language === 'javaScript' && config.mode === 'runOnceForEachItem' && code.includes('items')) {
986
992
  warnings.push({
987
993
  type: 'best_practice',
988
994
  message: 'In "Run Once for Each Item" mode, use $json instead of items array',
@@ -1035,8 +1041,369 @@ class NodeSpecificValidators {
1035
1041
  });
1036
1042
  }
1037
1043
  }
1038
- static validatePythonCode(code, errors, warnings, suggestions) {
1044
+ static pythonRemovedGlobalFix(name, isEachItem) {
1045
+ switch (name) {
1046
+ case '_input':
1047
+ return isEachItem ? 'Use _item (the current item dict)' : 'Use _items (the list of item dicts)';
1048
+ case '_json':
1049
+ return isEachItem ? 'Use _item["json"]' : 'Use _items[0]["json"]';
1050
+ case '_node':
1051
+ return 'No equivalent: merge the other branch upstream, or read it in JavaScript';
1052
+ case '_now':
1053
+ case '_today':
1054
+ return 'No equivalent: pass the timestamp in from an expression, or import datetime if this instance allowlists it';
1055
+ default:
1056
+ return 'No equivalent: use $jmespath in an expression, or a list comprehension';
1057
+ }
1058
+ }
1059
+ static withinCapOrRaw(code, strip) {
1060
+ return code.length <= MAX_CODE_LENGTH ? strip(code) : code;
1061
+ }
1062
+ static pythonHeaderColonIndex(text, startDepth = 0) {
1063
+ let depth = startDepth;
1064
+ for (let i = 0; i < text.length; i++) {
1065
+ const char = text[i];
1066
+ if (char === '(' || char === '[' || char === '{')
1067
+ depth++;
1068
+ else if (char === ')' || char === ']' || char === '}')
1069
+ depth--;
1070
+ else if (char === ':' && depth === 0)
1071
+ return i;
1072
+ }
1073
+ return -1;
1074
+ }
1075
+ static pythonBracketDelta(text) {
1076
+ let delta = 0;
1077
+ for (const char of text) {
1078
+ if (char === '(' || char === '[' || char === '{')
1079
+ delta++;
1080
+ else if (char === ')' || char === ']' || char === '}')
1081
+ delta--;
1082
+ }
1083
+ return delta;
1084
+ }
1085
+ static pythonTargetName(target) {
1086
+ const bare = target.trim().replace(/^[(\[\s]+|[)\]\s]+$/g, '').replace(/^\*+/, '').trim();
1087
+ const name = bare.split(/[:=]/)[0].trim();
1088
+ return /^[A-Za-z_]\w*$/.test(name) ? name : null;
1089
+ }
1090
+ static pythonSplitTopLevel(text) {
1091
+ const parts = [];
1092
+ let depth = 0;
1093
+ let current = '';
1094
+ for (const char of text) {
1095
+ if (char === '(' || char === '[' || char === '{')
1096
+ depth++;
1097
+ else if (char === ')' || char === ']' || char === '}')
1098
+ depth--;
1099
+ if (char === ',' && depth === 0) {
1100
+ parts.push(current);
1101
+ current = '';
1102
+ continue;
1103
+ }
1104
+ current += char;
1105
+ }
1106
+ parts.push(current);
1107
+ return parts;
1108
+ }
1109
+ static pythonParameterNames(header) {
1110
+ const open = header.indexOf('(');
1111
+ if (open === -1)
1112
+ return [];
1113
+ let depth = 0;
1114
+ let close = -1;
1115
+ for (let i = open; i < header.length; i++) {
1116
+ if (header[i] === '(')
1117
+ depth++;
1118
+ else if (header[i] === ')') {
1119
+ depth--;
1120
+ if (depth === 0) {
1121
+ close = i;
1122
+ break;
1123
+ }
1124
+ }
1125
+ }
1126
+ if (close === -1)
1127
+ return [];
1128
+ return this.pythonSplitTopLevel(header.slice(open + 1, close))
1129
+ .map(part => this.pythonTargetName(part))
1130
+ .filter((name) => name !== null);
1131
+ }
1132
+ static pythonLogicalLines(lines) {
1133
+ const logical = lines.map(() => '');
1134
+ const continuation = /\\[ \t]*$/;
1135
+ const withoutMarker = (line) => line.replace(continuation, '');
1136
+ for (let i = 0; i < lines.length; i++) {
1137
+ const start = i;
1138
+ let statement = withoutMarker(lines[i]);
1139
+ let depth = this.pythonBracketDelta(lines[i]);
1140
+ let continued = continuation.test(lines[i]);
1141
+ while ((depth > 0 || continued) && i + 1 < lines.length && statement.length <= MAX_SHORT_INPUT_LENGTH) {
1142
+ i++;
1143
+ statement += ` ${withoutMarker(lines[i]).trim()}`;
1144
+ depth += this.pythonBracketDelta(lines[i]);
1145
+ continued = continuation.test(lines[i]);
1146
+ }
1147
+ logical[start] = statement;
1148
+ }
1149
+ return logical;
1150
+ }
1151
+ static pythonImportBindings(statement) {
1152
+ const text = statement.trim().replace(/[()]/g, ' ');
1153
+ const words = text.split(/[ \t]+/).filter(Boolean);
1154
+ if (words.length === 0)
1155
+ return [];
1156
+ let list;
1157
+ if (words[0] === 'import') {
1158
+ list = words.slice(1);
1159
+ }
1160
+ else if (words[0] === 'from') {
1161
+ const keyword = words.indexOf('import');
1162
+ if (keyword === -1)
1163
+ return [];
1164
+ list = words.slice(keyword + 1);
1165
+ }
1166
+ else {
1167
+ return [];
1168
+ }
1169
+ const names = [];
1170
+ for (const part of list.join(' ').split(',')) {
1171
+ const pieces = part.trim().split(/[ \t]+/).filter(Boolean);
1172
+ if (pieces.length === 0)
1173
+ continue;
1174
+ const alias = pieces.indexOf('as');
1175
+ const target = alias === -1 ? pieces[0].split('.')[0] : pieces[alias + 1];
1176
+ if (target && /^[A-Za-z_]\w*$/.test(target))
1177
+ names.push(target);
1178
+ }
1179
+ return names;
1180
+ }
1181
+ static pythonBindingsOnLine(line) {
1182
+ const names = [];
1183
+ const assignment = /^[ \t]*([^=\n]+?)(?<![=!<>+\-*/%&|^~])=(?!=)/.exec(line);
1184
+ if (assignment) {
1185
+ this.pythonSplitTopLevel(assignment[1]).forEach(target => {
1186
+ const name = this.pythonTargetName(target);
1187
+ if (name)
1188
+ names.push(name);
1189
+ });
1190
+ }
1191
+ const forStatement = /^[ \t]*(?:async[ \t]+)?for[ \t]+(.+?)[ \t]+in[ \t]/.exec(line);
1192
+ if (forStatement) {
1193
+ this.pythonSplitTopLevel(forStatement[1]).forEach(target => {
1194
+ const name = this.pythonTargetName(target);
1195
+ if (name)
1196
+ names.push(name);
1197
+ });
1198
+ }
1199
+ const asTargets = /\bas[ \t]+(\w+)/g;
1200
+ let match;
1201
+ while ((match = asTargets.exec(line)) !== null)
1202
+ names.push(match[1]);
1203
+ this.pythonImportBindings(line).forEach(name => names.push(name));
1204
+ return names;
1205
+ }
1206
+ static pythonFormatsDunder(formatFields) {
1207
+ const field = /\{[^{}\n]{0,200}\.__\w+__[^{}\n]{0,200}\}/g;
1208
+ let match;
1209
+ while ((match = field.exec(formatFields)) !== null) {
1210
+ const after = formatFields.slice(match.index + match[0].length, match.index + match[0].length + 200);
1211
+ if (/['"][ \t]*\.[ \t]*format[ \t]*\(/.test(after))
1212
+ return true;
1213
+ }
1214
+ return false;
1215
+ }
1216
+ static pythonEnclosingGroup(line, index) {
1217
+ let closers = 0;
1218
+ let start = -1;
1219
+ for (let i = index - 1; i >= 0; i--) {
1220
+ const char = line[i];
1221
+ if (char === ')' || char === ']' || char === '}')
1222
+ closers++;
1223
+ else if (char === '(' || char === '[' || char === '{') {
1224
+ if (closers === 0) {
1225
+ start = i;
1226
+ break;
1227
+ }
1228
+ closers--;
1229
+ }
1230
+ }
1231
+ if (start === -1)
1232
+ return null;
1233
+ let depth = 0;
1234
+ for (let i = start; i < line.length; i++) {
1235
+ const char = line[i];
1236
+ if (char === '(' || char === '[' || char === '{')
1237
+ depth++;
1238
+ else if (char === ')' || char === ']' || char === '}') {
1239
+ depth--;
1240
+ if (depth === 0)
1241
+ return { start, end: i };
1242
+ }
1243
+ }
1244
+ return { start, end: line.length };
1245
+ }
1246
+ static pythonLineLocalBindings(line) {
1247
+ const bindings = [];
1248
+ const add = (targets, range) => {
1249
+ this.pythonSplitTopLevel(targets).forEach(target => {
1250
+ const name = this.pythonTargetName(target);
1251
+ if (name)
1252
+ bindings.push({ name, ...range });
1253
+ });
1254
+ };
1255
+ const forTargets = /\bfor[ \t]+(.+?)[ \t]+in\b/g;
1256
+ let match;
1257
+ while ((match = forTargets.exec(line)) !== null) {
1258
+ add(match[1], this.pythonEnclosingGroup(line, match.index) ?? { start: match.index, end: line.length });
1259
+ }
1260
+ const lambdas = /\blambda\b([^:\n]*):/g;
1261
+ while ((match = lambdas.exec(line)) !== null) {
1262
+ const group = this.pythonEnclosingGroup(line, match.index);
1263
+ add(match[1], { start: match.index, end: group ? group.end : line.length });
1264
+ }
1265
+ return bindings;
1266
+ }
1267
+ static pythonMaskHeaderBindings(header) {
1268
+ const chars = header.split('');
1269
+ const blank = (from, to) => {
1270
+ for (let i = from; i < to; i++)
1271
+ if (chars[i] !== '\n')
1272
+ chars[i] = ' ';
1273
+ };
1274
+ const named = /(\bdef[ \t]+)(\w+)/.exec(header);
1275
+ if (named)
1276
+ blank(named.index + named[1].length, named.index + named[0].length);
1277
+ const open = header.indexOf('(');
1278
+ if (open === -1)
1279
+ return chars.join('');
1280
+ let depth = 0;
1281
+ let close = header.length;
1282
+ for (let i = open; i < header.length; i++) {
1283
+ if (header[i] === '(')
1284
+ depth++;
1285
+ else if (header[i] === ')') {
1286
+ depth--;
1287
+ if (depth === 0) {
1288
+ close = i;
1289
+ break;
1290
+ }
1291
+ }
1292
+ }
1293
+ let partStart = open + 1;
1294
+ let partDepth = 0;
1295
+ for (let i = open + 1; i <= close; i++) {
1296
+ if (i === close || (header[i] === ',' && partDepth === 0)) {
1297
+ const part = header.slice(partStart, i);
1298
+ const annotated = part.search(/[:=]/);
1299
+ blank(partStart, partStart + (annotated === -1 ? part.length : annotated));
1300
+ partStart = i + 1;
1301
+ continue;
1302
+ }
1303
+ const char = header[i];
1304
+ if (char === '(' || char === '[' || char === '{')
1305
+ partDepth++;
1306
+ else if (char === ')' || char === ']' || char === '}')
1307
+ partDepth--;
1308
+ }
1309
+ return chars.join('');
1310
+ }
1311
+ static pythonScopes(scan, logicalLines) {
1312
+ const lines = scan.split('\n');
1313
+ const logical = logicalLines ?? this.pythonLogicalLines(lines);
1314
+ const scopes = [{ parent: -1, names: new Set() }];
1315
+ const lineScope = new Array(lines.length).fill(0);
1316
+ const lineLocal = lines.map(() => []);
1317
+ const referenceText = [...lines];
1318
+ const stack = [{ scope: 0, indent: -1 }];
1319
+ let i = 0;
1320
+ while (i < lines.length) {
1321
+ const line = lines[i];
1322
+ if (line.trim() === '') {
1323
+ lineScope[i] = stack[stack.length - 1].scope;
1324
+ i++;
1325
+ continue;
1326
+ }
1327
+ const indent = (line.match(/^[ \t]*/) || [''])[0].length;
1328
+ while (stack.length > 1 && indent <= stack[stack.length - 1].indent)
1329
+ stack.pop();
1330
+ const enclosing = stack[stack.length - 1].scope;
1331
+ const definition = /^[ \t]*(?:async[ \t]+)?def[ \t]+(\w+)[ \t]*\(/.exec(line);
1332
+ if (!definition) {
1333
+ lineScope[i] = enclosing;
1334
+ i++;
1335
+ continue;
1336
+ }
1337
+ let header = line;
1338
+ let last = i;
1339
+ let headerDepth = this.pythonBracketDelta(line);
1340
+ while (this.pythonHeaderColonIndex(line, headerDepth - this.pythonBracketDelta(line)) === -1
1341
+ && last + 1 < lines.length
1342
+ && last - i < MAX_HEADER_LINES) {
1343
+ const next = lines[last + 1];
1344
+ const nextIndent = (next.match(/^[ \t]*/) || [''])[0].length;
1345
+ if (headerDepth <= 0 && next.trim() !== '' && nextIndent <= indent)
1346
+ break;
1347
+ last++;
1348
+ header += `\n${next}`;
1349
+ headerDepth += this.pythonBracketDelta(next);
1350
+ if (this.pythonHeaderColonIndex(next, headerDepth - this.pythonBracketDelta(next)) !== -1)
1351
+ break;
1352
+ }
1353
+ const masked = this.pythonMaskHeaderBindings(header).split('\n');
1354
+ const body = scopes.push({ parent: enclosing, names: new Set(this.pythonParameterNames(header)) }) - 1;
1355
+ const colon = this.pythonHeaderColonIndex(header);
1356
+ const inlineSuite = colon !== -1 && header.slice(colon + 1).trim() !== '';
1357
+ for (let k = i; k <= last; k++) {
1358
+ lineScope[k] = k === last && inlineSuite ? body : enclosing;
1359
+ referenceText[k] = masked[k - i];
1360
+ }
1361
+ scopes[enclosing].names.add(definition[1]);
1362
+ stack.push({ scope: body, indent });
1363
+ i = last + 1;
1364
+ }
1365
+ lines.forEach((line, index) => {
1366
+ const statement = logical[index] || line;
1367
+ this.pythonBindingsOnLine(statement).forEach(name => scopes[lineScope[index]].names.add(name));
1368
+ lineLocal[index] = this.pythonLineLocalBindings(line);
1369
+ });
1370
+ return { lines, lineScope, scopes, lineLocal, referenceText };
1371
+ }
1372
+ static pythonHasUnboundReference(index, name, reference) {
1373
+ const { lineScope, scopes, lineLocal, referenceText } = index;
1374
+ const matcher = new RegExp(reference.source, reference.flags.includes('g') ? reference.flags : `${reference.flags}g`);
1375
+ return referenceText.some((line, lineNumber) => {
1376
+ let bound = false;
1377
+ for (let scope = lineScope[lineNumber]; scope !== -1; scope = scopes[scope].parent) {
1378
+ if (scopes[scope].names.has(name)) {
1379
+ bound = true;
1380
+ break;
1381
+ }
1382
+ }
1383
+ if (bound)
1384
+ return false;
1385
+ matcher.lastIndex = 0;
1386
+ let match;
1387
+ while ((match = matcher.exec(line)) !== null) {
1388
+ if (match[0].length === 0)
1389
+ matcher.lastIndex++;
1390
+ const at = match.index;
1391
+ const shadowed = lineLocal[lineNumber].some(local => local.name === name && at >= local.start && at <= local.end);
1392
+ if (!shadowed)
1393
+ return true;
1394
+ }
1395
+ return false;
1396
+ });
1397
+ }
1398
+ static pythonHasGlobalInsideFunction(index) {
1399
+ return index.lines.some((line, line_index) => index.lineScope[line_index] !== 0 && /^[ \t]*global[ \t]+\w/.test(line));
1400
+ }
1401
+ static validatePythonCode(code, errors, warnings, suggestions, mode = 'runOnceForAllItems', modeIsKnown = true) {
1039
1402
  const lines = code.split('\n');
1403
+ const isEachItem = mode === 'runOnceForEachItem';
1404
+ const scan = this.withinCapOrRaw(code, c => this.stripPythonStringsAndComments(c));
1405
+ const logicalLines = this.pythonLogicalLines(scan.split('\n'));
1406
+ const scopeIndex = code.length <= MAX_CODE_LENGTH ? this.pythonScopes(scan, logicalLines) : null;
1040
1407
  if (code.includes('__name__') && code.includes('__main__')) {
1041
1408
  warnings.push({
1042
1409
  type: 'inefficient',
@@ -1044,22 +1411,127 @@ class NodeSpecificValidators {
1044
1411
  suggestion: 'Code node Python runs directly - remove the main check'
1045
1412
  });
1046
1413
  }
1047
- const unavailableImports = [
1048
- { module: 'requests', suggestion: 'Use JavaScript Code node with $helpers.httpRequest for HTTP requests' },
1049
- { module: 'pandas', suggestion: 'Use built-in list/dict operations or JavaScript for data manipulation' },
1050
- { module: 'numpy', suggestion: 'Use standard Python math operations' },
1051
- { module: 'pip', suggestion: 'External packages cannot be installed in Code nodes' }
1052
- ];
1053
- unavailableImports.forEach(({ module, suggestion }) => {
1054
- if (code.includes(`import ${module}`) || code.includes(`from ${module}`)) {
1414
+ if (scopeIndex) {
1415
+ for (const name of this.PYTHON_REMOVED_GLOBALS) {
1416
+ if (this.pythonHasUnboundReference(scopeIndex, name, new RegExp(`(?<![\\w.])${name}\\b`))) {
1417
+ errors.push({
1418
+ type: 'invalid_value',
1419
+ property: 'pythonCode',
1420
+ message: `${name} does not exist in native Python - it was removed with the Pyodide runtime`,
1421
+ fix: this.pythonRemovedGlobalFix(name, isEachItem)
1422
+ });
1423
+ }
1424
+ }
1425
+ if (this.pythonHasUnboundReference(scopeIndex, 'items', /(?<![\w.])items\b(?![ \t]*=(?!=))/)) {
1055
1426
  errors.push({
1056
1427
  type: 'invalid_value',
1057
1428
  property: 'pythonCode',
1058
- message: `Module '${module}' is not available in Code nodes`,
1059
- fix: suggestion
1429
+ message: 'items does not exist in native Python; use _items (all-items mode) or _item (each-item mode)',
1430
+ fix: isEachItem ? 'Use _item (the current item dict)' : 'Use _items (the list of item dicts)'
1060
1431
  });
1061
1432
  }
1062
- });
1433
+ }
1434
+ if (modeIsKnown && scopeIndex) {
1435
+ if (isEachItem && this.pythonHasUnboundReference(scopeIndex, '_items', /(?<![\w.])_items\b/)) {
1436
+ errors.push({
1437
+ type: 'invalid_value',
1438
+ property: 'pythonCode',
1439
+ message: '_items does not exist in "Run Once for Each Item" mode',
1440
+ fix: 'Use _item, or switch mode to runOnceForAllItems'
1441
+ });
1442
+ }
1443
+ if (!isEachItem && this.pythonHasUnboundReference(scopeIndex, '_item', /(?<![\w.])_item\b/)) {
1444
+ errors.push({
1445
+ type: 'invalid_value',
1446
+ property: 'pythonCode',
1447
+ message: '_item does not exist in "Run Once for All Items" mode',
1448
+ fix: 'Use _items, or switch mode to runOnceForEachItem'
1449
+ });
1450
+ }
1451
+ }
1452
+ if (/\.json\b(?!\s*\()/.test(scan)) {
1453
+ errors.push({
1454
+ type: 'invalid_value',
1455
+ property: 'pythonCode',
1456
+ message: 'Items are dicts: .json attribute access raises AttributeError',
1457
+ fix: 'Use dict access: item["json"]["field"] or item["json"].get("field")'
1458
+ });
1459
+ }
1460
+ const importedModules = new Set();
1461
+ const importStatement = /(?:^|[:;])[ \t]*import[ \t]+([^\n;]+)/gm;
1462
+ const fromImportStatement = /(?:^|[:;])[ \t]*from[ \t]+([\w.]+)[ \t]+import\b/gm;
1463
+ const rootModule = (token) => {
1464
+ const trimmed = token.trim();
1465
+ const space = trimmed.search(/[ \t]/);
1466
+ const first = space === -1 ? trimmed : trimmed.slice(0, space);
1467
+ return first.split('.')[0];
1468
+ };
1469
+ const addModule = (token) => {
1470
+ const name = rootModule(token);
1471
+ if (/^[A-Za-z_]\w*$/.test(name))
1472
+ importedModules.add(name);
1473
+ };
1474
+ for (const statement of logicalLines) {
1475
+ if (!statement)
1476
+ continue;
1477
+ let importMatch;
1478
+ importStatement.lastIndex = 0;
1479
+ while ((importMatch = importStatement.exec(statement)) !== null) {
1480
+ importMatch[1].split(',').forEach(addModule);
1481
+ }
1482
+ fromImportStatement.lastIndex = 0;
1483
+ while ((importMatch = fromImportStatement.exec(statement)) !== null) {
1484
+ if (rootModule(importMatch[1]))
1485
+ addModule(importMatch[1]);
1486
+ else
1487
+ this.pythonImportBindings(statement).forEach(name => importedModules.add(name));
1488
+ }
1489
+ }
1490
+ for (const moduleName of importedModules) {
1491
+ warnings.push({
1492
+ type: 'security',
1493
+ property: 'pythonCode',
1494
+ message: `import ${moduleName} is blocked unless this instance allowlists the module (n8n Cloud allows none)`,
1495
+ suggestion: `Write import-free code, or confirm '${moduleName}' is allowlisted on the Python task runner first`
1496
+ });
1497
+ }
1498
+ if (/^[ \t]*class[ \t]+\w/m.test(scan)) {
1499
+ errors.push({
1500
+ type: 'invalid_value',
1501
+ property: 'pythonCode',
1502
+ message: 'class definitions fail in the sandbox: __build_class__ not found',
1503
+ fix: 'Use dicts and plain functions instead of a class'
1504
+ });
1505
+ }
1506
+ for (const [name, fix] of Object.entries(scopeIndex ? this.PYTHON_DENIED_BUILTINS : {})) {
1507
+ if (this.pythonHasUnboundReference(scopeIndex, name, new RegExp(`(?<![\\w.])${name}\\b(?![ \\t]*=(?!=))`))) {
1508
+ errors.push({
1509
+ type: 'invalid_value',
1510
+ property: 'pythonCode',
1511
+ message: `${name}() is denied in the Python sandbox and raises NameError`,
1512
+ fix
1513
+ });
1514
+ }
1515
+ }
1516
+ const formatFields = this.withinCapOrRaw(code, c => this.stripPythonStringsAndComments(c, true, true));
1517
+ if (/\.__\w+__/.test(scan) || /\b__class__\b/.test(scan) || /\b__builtins__\b/.test(scan)
1518
+ || /(?<![\w.])__import__[ \t]*\(/.test(scan)
1519
+ || this.pythonFormatsDunder(formatFields)) {
1520
+ errors.push({
1521
+ type: 'invalid_value',
1522
+ property: 'pythonCode',
1523
+ message: 'Dunder access is rejected before the code runs: Security violations detected',
1524
+ fix: 'Remove __class__, __import__ and other dunder access'
1525
+ });
1526
+ }
1527
+ if (scopeIndex && this.pythonHasGlobalInsideFunction(scopeIndex)) {
1528
+ errors.push({
1529
+ type: 'invalid_value',
1530
+ property: 'pythonCode',
1531
+ message: 'global does not work: your code runs inside a wrapper function',
1532
+ fix: 'Use nonlocal instead of global'
1533
+ });
1534
+ }
1063
1535
  lines.forEach((line, i) => {
1064
1536
  if (line.trim().endsWith(':') && i < lines.length - 1) {
1065
1537
  const nextLine = lines[i + 1];
@@ -1074,9 +1546,9 @@ class NodeSpecificValidators {
1074
1546
  }
1075
1547
  });
1076
1548
  }
1077
- static validateReturnStatement(code, language, errors, warnings, suggestions, mode = 'runOnceForAllItems') {
1078
- const returnScanCode = (language === 'javaScript' && code.length <= MAX_CODE_LENGTH)
1079
- ? this.stripNestedJavaScriptFunctionBodies(code)
1549
+ static validateReturnStatement(code, language, errors, warnings, suggestions, mode = 'runOnceForAllItems', modeIsKnown = true) {
1550
+ const returnScanCode = language === 'javaScript'
1551
+ ? this.withinCapOrRaw(code, c => this.stripNestedJavaScriptFunctionBodies(c))
1080
1552
  : code;
1081
1553
  const hasReturn = /return\s+/.test(returnScanCode);
1082
1554
  if (!hasReturn) {
@@ -1113,31 +1585,109 @@ class NodeSpecificValidators {
1113
1585
  'To modify: return items.map(item => ({json: {...item.json, newField: "value"}}))');
1114
1586
  }
1115
1587
  }
1116
- if (language === 'python') {
1588
+ if (language === 'python' && modeIsKnown) {
1117
1589
  const isRunOncePerItem = mode === 'runOnceForEachItem';
1118
- if (!isRunOncePerItem && /return\s+{(?!.*\[).*}$/s.test(code)) {
1119
- errors.push({
1120
- type: 'invalid_value',
1121
- property: 'pythonCode',
1122
- message: 'Return value must be a list of dicts',
1123
- fix: 'Wrap in list: return [{"json": your_dict}]'
1124
- });
1590
+ if (isRunOncePerItem) {
1591
+ const strippedTopLevel = this.withinCapOrRaw(code, c => this.stripPythonFunctionBodies(this.stripPythonStringsAndComments(c)));
1592
+ const parenthesisedList = (inner) => /^\[[\s\S]*\]$/.test(inner) || inner === '_items' || /^list[ \t]*\(/.test(inner);
1593
+ if (this.pythonReturnsWholeGroup(strippedTopLevel, /^[ \t]*return[ \t]+\[/)
1594
+ || this.pythonReturnsWholeGroup(strippedTopLevel, /^[ \t]*return[ \t]+list[ \t]*\(/)
1595
+ || this.pythonReturnsWholeGroup(strippedTopLevel, /^[ \t]*return[ \t]*\(/, parenthesisedList)
1596
+ || /^[ \t]*return[ \t]+_items[ \t;]*$/m.test(strippedTopLevel)) {
1597
+ errors.push({
1598
+ type: 'invalid_value',
1599
+ property: 'pythonCode',
1600
+ message: 'Returning a list in "Run Once for Each Item" mode fails: a \'json\' property isn\'t a dictionary',
1601
+ fix: 'Return a single dict: return {"json": {"value": your_data}}'
1602
+ });
1603
+ }
1125
1604
  }
1126
- if (!isRunOncePerItem && /return\s+(True|False|None|\d+|['"`])/m.test(code)) {
1127
- errors.push({
1128
- type: 'invalid_value',
1129
- property: 'pythonCode',
1130
- message: 'Cannot return primitive values directly',
1131
- fix: 'Return list of dicts: return [{"json": {"value": your_data}}]'
1132
- });
1605
+ else {
1606
+ const topLevel = this.withinCapOrRaw(code, c => this.stripPythonFunctionBodies(this.stripPythonStringsAndComments(c, true)));
1607
+ if (/return\s+(?:(?:True|False|None)\b|[+-]?(?:\d|\.\d)|[rbfu]{0,2}['"])/m.test(topLevel)) {
1608
+ errors.push({
1609
+ type: 'invalid_value',
1610
+ property: 'pythonCode',
1611
+ message: 'Cannot return primitive values directly',
1612
+ fix: 'Return list of dicts: return [{"json": {"value": your_data}}]'
1613
+ });
1614
+ }
1133
1615
  }
1134
1616
  }
1135
1617
  }
1136
- static hasTopLevelPrimitiveReturn(code) {
1137
- if (code.length > MAX_CODE_LENGTH) {
1138
- return JS_PRIMITIVE_RETURN_RE.test(code);
1618
+ static pythonReturnsWholeGroup(scan, prefix, innerTest) {
1619
+ const matcher = new RegExp(prefix.source, 'gm');
1620
+ let budget = MAX_RETURN_TOTAL_SCAN;
1621
+ let match;
1622
+ while ((match = matcher.exec(scan)) !== null) {
1623
+ let position = match.index + match[0].length - 1;
1624
+ const limit = Math.min(scan.length, position + MAX_RETURN_LOOKAHEAD, position + budget);
1625
+ let depth = 0;
1626
+ let closed = -1;
1627
+ for (; position < limit; position++) {
1628
+ const char = scan[position];
1629
+ if (char === '[' || char === '(' || char === '{')
1630
+ depth++;
1631
+ else if (char === ']' || char === ')' || char === '}') {
1632
+ depth--;
1633
+ if (depth === 0) {
1634
+ closed = position;
1635
+ break;
1636
+ }
1637
+ }
1638
+ }
1639
+ budget -= position - (match.index + match[0].length - 1);
1640
+ if (budget <= 0)
1641
+ return false;
1642
+ if (closed === -1)
1643
+ continue;
1644
+ const lineEnd = scan.indexOf('\n', closed);
1645
+ const rest = scan.slice(closed + 1, lineEnd === -1 ? scan.length : lineEnd);
1646
+ if (rest.replace(/;+[ \t]*$/, '').trim() !== '')
1647
+ continue;
1648
+ if (!innerTest)
1649
+ return true;
1650
+ const opened = match.index + match[0].length - 1;
1651
+ if (innerTest(scan.slice(opened + 1, closed).trim()))
1652
+ return true;
1653
+ }
1654
+ return false;
1655
+ }
1656
+ static stripPythonFunctionBodies(code) {
1657
+ const lines = code.split('\n');
1658
+ const result = [];
1659
+ let blockIndent = null;
1660
+ let header = null;
1661
+ for (const line of lines) {
1662
+ const indentMatch = line.match(/^[ \t]*/);
1663
+ const indent = indentMatch ? indentMatch[0].length : 0;
1664
+ const isBlank = line.trim() === '';
1665
+ if (header !== null) {
1666
+ header += `\n${line}`;
1667
+ result.push('');
1668
+ if (this.pythonHeaderColonIndex(header) !== -1)
1669
+ header = null;
1670
+ continue;
1671
+ }
1672
+ if (blockIndent !== null) {
1673
+ if (isBlank || indent > blockIndent) {
1674
+ result.push('');
1675
+ continue;
1676
+ }
1677
+ blockIndent = null;
1678
+ }
1679
+ if (/^[ \t]*(?:async[ \t]+)?(?:def|class)[ \t]+\w/.test(line)) {
1680
+ blockIndent = indent;
1681
+ header = this.pythonHeaderColonIndex(line) === -1 ? line : null;
1682
+ result.push('');
1683
+ continue;
1684
+ }
1685
+ result.push(line);
1139
1686
  }
1140
- const topLevelCode = this.stripNestedJavaScriptFunctionBodies(code);
1687
+ return result.join('\n');
1688
+ }
1689
+ static hasTopLevelPrimitiveReturn(code) {
1690
+ const topLevelCode = this.withinCapOrRaw(code, c => this.stripNestedJavaScriptFunctionBodies(c));
1141
1691
  return JS_PRIMITIVE_RETURN_RE.test(topLevelCode);
1142
1692
  }
1143
1693
  static stripStringsCommentsRegex(code) {
@@ -1275,7 +1825,18 @@ class NodeSpecificValidators {
1275
1825
  }
1276
1826
  return result;
1277
1827
  }
1278
- static stripPythonStringsAndComments(code) {
1828
+ static isFStringPrefix(code, end) {
1829
+ let start = end;
1830
+ while (start > 0 && /[A-Za-z]/.test(code[start - 1]))
1831
+ start--;
1832
+ if (start === end || end - start > 2)
1833
+ return false;
1834
+ if (start > 0 && /[\w$]/.test(code[start - 1]))
1835
+ return false;
1836
+ const prefix = code.slice(start, end);
1837
+ return /^[rbuRBU]?[fF]$|^[fF][rbuRBU]?$/.test(prefix);
1838
+ }
1839
+ static stripPythonStringsAndComments(code, keepDelimiters = false, keepContent = false) {
1279
1840
  let result = '';
1280
1841
  let i = 0;
1281
1842
  while (i < code.length) {
@@ -1290,7 +1851,9 @@ class NodeSpecificValidators {
1290
1851
  if (char === "'" || char === '"') {
1291
1852
  const triple = code.slice(i, i + 3) === char.repeat(3);
1292
1853
  const delim = triple ? char.repeat(3) : char;
1293
- result += ' '.repeat(delim.length);
1854
+ const blankedDelim = keepDelimiters || keepContent ? delim : ' '.repeat(delim.length);
1855
+ const isFString = !keepContent && this.isFStringPrefix(code, i);
1856
+ result += blankedDelim;
1294
1857
  i += delim.length;
1295
1858
  while (i < code.length) {
1296
1859
  if (code[i] === '\\') {
@@ -1299,16 +1862,44 @@ class NodeSpecificValidators {
1299
1862
  continue;
1300
1863
  }
1301
1864
  if (code.slice(i, i + delim.length) === delim) {
1302
- result += ' '.repeat(delim.length);
1865
+ result += blankedDelim;
1303
1866
  i += delim.length;
1304
1867
  break;
1305
1868
  }
1869
+ if (isFString && code[i] === '{' && code[i + 1] !== '{') {
1870
+ let depth = 0;
1871
+ let nestedQuote = '';
1872
+ do {
1873
+ const current = code[i];
1874
+ if (nestedQuote) {
1875
+ if (current === nestedQuote)
1876
+ nestedQuote = '';
1877
+ result += current === '\n' ? '\n' : ' ';
1878
+ i++;
1879
+ continue;
1880
+ }
1881
+ if (current === "'" || current === '"')
1882
+ nestedQuote = current;
1883
+ else if (current === '{')
1884
+ depth++;
1885
+ else if (current === '}')
1886
+ depth--;
1887
+ result += current === '\n' ? '\n' : current;
1888
+ i++;
1889
+ } while (i < code.length && depth > 0);
1890
+ continue;
1891
+ }
1892
+ if (isFString && code[i] === '{' && code[i + 1] === '{') {
1893
+ result += ' ';
1894
+ i += 2;
1895
+ continue;
1896
+ }
1306
1897
  if (code[i] === '\n' && !triple) {
1307
1898
  result += '\n';
1308
1899
  i++;
1309
1900
  break;
1310
1901
  }
1311
- result += code[i] === '\n' ? '\n' : ' ';
1902
+ result += keepContent || code[i] === '\n' ? code[i] : ' ';
1312
1903
  i++;
1313
1904
  }
1314
1905
  continue;
@@ -1381,23 +1972,24 @@ class NodeSpecificValidators {
1381
1972
  }
1382
1973
  return true;
1383
1974
  }
1384
- static validateN8nVariables(code, language, warnings, suggestions, errors) {
1385
- const scanView = code.length <= MAX_CODE_LENGTH
1386
- ? (language === 'javaScript'
1387
- ? this.stripStringsCommentsRegex(code)
1388
- : this.stripPythonStringsAndComments(code))
1389
- : code;
1975
+ static validateN8nVariables(code, language, warnings, suggestions, errors, mode = 'runOnceForAllItems') {
1976
+ const scanView = this.withinCapOrRaw(code, c => language === 'javaScript'
1977
+ ? this.stripStringsCommentsRegex(c)
1978
+ : this.stripPythonStringsAndComments(c));
1390
1979
  const inputPatterns = language === 'javaScript'
1391
1980
  ? ['items', '$input', '$json', '$node', '$prevNode', '$(', '$getWorkflowStaticData', '$workflow', '$execution', '$vars']
1392
- : ['items', '_input'];
1981
+ : ['_items', '_item', '_input', '_json'];
1393
1982
  const usesInput = inputPatterns.some(pattern => scanView.includes(pattern));
1394
1983
  if (!usesInput && code.length > 50) {
1984
+ const pythonSuggestion = mode === 'runOnceForEachItem'
1985
+ ? 'Access input with: _item (the current item dict)'
1986
+ : 'Access input with: _items (the list of item dicts)';
1395
1987
  warnings.push({
1396
1988
  type: 'best_practice',
1397
1989
  message: 'Code doesn\'t reference input data',
1398
1990
  suggestion: language === 'javaScript'
1399
1991
  ? 'Access input with: items, $input.all(), or $json (single-item mode)'
1400
- : 'Access input with: items variable'
1992
+ : pythonSuggestion
1401
1993
  });
1402
1994
  }
1403
1995
  if (scanView.includes('{{') && scanView.includes('}}')) {
@@ -1408,25 +2000,27 @@ class NodeSpecificValidators {
1408
2000
  fix: 'Use regular JavaScript/Python syntax without double curly braces'
1409
2001
  });
1410
2002
  }
1411
- if (code.includes('$node[')) {
2003
+ if (language === 'javaScript' && code.includes('$node[')) {
1412
2004
  warnings.push({
1413
2005
  type: 'invalid_value',
1414
- property: language === 'python' ? 'pythonCode' : 'jsCode',
2006
+ property: 'jsCode',
1415
2007
  message: 'Use $(\'Node Name\') instead of $node[\'Node Name\'] in Code nodes',
1416
2008
  suggestion: 'Replace $node[\'NodeName\'] with $(\'NodeName\')'
1417
2009
  });
1418
2010
  }
1419
- const expressionOnlyFunctions = ['$now()', '$today()', '$tomorrow()', '.unique()', '.pluck(', '.keys()', '.hash('];
1420
- expressionOnlyFunctions.forEach(func => {
1421
- if (code.includes(func)) {
1422
- warnings.push({
1423
- type: 'invalid_value',
1424
- property: language === 'python' ? 'pythonCode' : 'jsCode',
1425
- message: `${func} is an expression-only function not available in Code nodes`,
1426
- suggestion: 'See Code node documentation for alternatives'
1427
- });
1428
- }
1429
- });
2011
+ if (language === 'javaScript') {
2012
+ const expressionOnlyFunctions = ['$now()', '$today()', '$tomorrow()', '.unique()', '.pluck(', '.keys()', '.hash('];
2013
+ expressionOnlyFunctions.forEach(func => {
2014
+ if (code.includes(func)) {
2015
+ warnings.push({
2016
+ type: 'invalid_value',
2017
+ property: 'jsCode',
2018
+ message: `${func} is an expression-only function not available in Code nodes`,
2019
+ suggestion: 'See Code node documentation for alternatives'
2020
+ });
2021
+ }
2022
+ });
2023
+ }
1430
2024
  if (language === 'javaScript') {
1431
2025
  if (/\$(?![a-zA-Z_(])/.test(scanView) && !scanView.includes('${')) {
1432
2026
  warnings.push({
@@ -1461,14 +2055,6 @@ class NodeSpecificValidators {
1461
2055
  fix: 'Use $getWorkflowStaticData("global") or $getWorkflowStaticData("node") directly'
1462
2056
  });
1463
2057
  }
1464
- if (code.includes('$jmespath(') && /\$jmespath\s*\(\s*['"`]/.test(code)) {
1465
- warnings.push({
1466
- type: 'invalid_value',
1467
- property: 'jsCode',
1468
- message: 'Code node $jmespath has reversed parameter order: $jmespath(data, query)',
1469
- suggestion: 'Use: $jmespath(dataObject, "query.path") not $jmespath("query.path", dataObject)'
1470
- });
1471
- }
1472
2058
  if (code.includes('items[0].json') && !code.includes('.json.body')) {
1473
2059
  if (code.includes('Webhook') || code.includes('webhook') ||
1474
2060
  code.includes('$("Webhook")') || code.includes("$('Webhook')")) {
@@ -1488,31 +2074,39 @@ class NodeSpecificValidators {
1488
2074
  }
1489
2075
  }
1490
2076
  }
1491
- const jmespathFunction = language === 'javaScript' ? '$jmespath' : '_jmespath';
1492
- if (code.length <= MAX_CODE_LENGTH && code.includes(jmespathFunction + '(')) {
1493
- const filterPattern = /\[?\?[^[\]]*(?:>=?|<=?|==|!=)\s*(\d+(?:\.\d+)?)\s*\]/g;
1494
- let match;
1495
- while ((match = filterPattern.exec(code)) !== null) {
1496
- const number = match[1];
1497
- const beforeNumber = code.substring(match.index, match.index + match[0].indexOf(number));
1498
- const afterNumber = code.substring(match.index + match[0].indexOf(number) + number.length);
1499
- if (!beforeNumber.includes('`') || !afterNumber.startsWith('`')) {
1500
- errors.push({
2077
+ if (language === 'javaScript' && code.length <= MAX_CODE_LENGTH && code.includes('$jmespath')) {
2078
+ const calls = (0, jmespath_checks_1.findJmespathCalls)(code);
2079
+ for (const call of calls) {
2080
+ if (call.queryIsFirstArgument) {
2081
+ warnings.push({
1501
2082
  type: 'invalid_value',
1502
- property: language === 'python' ? 'pythonCode' : 'jsCode',
1503
- message: `JMESPath numeric literal ${number} must be wrapped in backticks`,
1504
- fix: `Change [?field >= ${number}] to [?field >= \`${number}\`]`
2083
+ property: 'jsCode',
2084
+ message: 'Code node $jmespath has reversed parameter order: $jmespath(data, query)',
2085
+ suggestion: 'Use: $jmespath(dataObject, "query.path") not $jmespath("query.path", dataObject)'
1505
2086
  });
2087
+ continue;
2088
+ }
2089
+ if (call.query === undefined)
2090
+ continue;
2091
+ for (const finding of (0, jmespath_checks_1.checkJmespathQuery)(call.query)) {
2092
+ if (finding.severity === 'error') {
2093
+ errors.push({ type: 'invalid_value', property: 'jsCode', message: finding.message, fix: finding.fix });
2094
+ }
2095
+ else {
2096
+ warnings.push({ type: 'invalid_value', property: 'jsCode', message: finding.message, suggestion: finding.fix });
2097
+ }
1506
2098
  }
1507
2099
  }
1508
- suggestions.push('JMESPath in n8n requires backticks around numeric literals in filters: [?age >= `18`]');
2100
+ if (calls.length > 0) {
2101
+ suggestions.push('JMESPath in n8n requires backticks around numeric literals in filters: [?age >= `18`]');
2102
+ }
1509
2103
  }
1510
2104
  }
1511
2105
  static validateCodeSecurity(code, language, warnings) {
1512
2106
  const securityView = language === 'javaScript'
1513
2107
  ? this.stripStringsCommentsRegex(code)
1514
2108
  : this.stripPythonStringsAndComments(code);
1515
- const dangerousPatterns = [
2109
+ const dangerousPatterns = language !== 'javaScript' ? [] : [
1516
2110
  { pattern: /(?<![.\w$])eval\s*\(/, message: 'Avoid eval() - it\'s a security risk' },
1517
2111
  { pattern: /(?<![.\w$])Function\s*\(/, message: 'Avoid Function constructor - use regular functions' },
1518
2112
  { pattern: /(?:window|globalThis)\s*\.\s*eval\s*\(/, message: 'Avoid eval() - it\'s a security risk' },
@@ -1625,6 +2219,25 @@ class NodeSpecificValidators {
1625
2219
  }
1626
2220
  }
1627
2221
  exports.NodeSpecificValidators = NodeSpecificValidators;
2222
+ NodeSpecificValidators.PYTHON_REMOVED_GLOBALS = ['_input', '_json', '_node', '_now', '_today', '_jmespath'];
2223
+ NodeSpecificValidators.PYTHON_DENIED_BUILTINS = {
2224
+ eval: 'Compute the value directly instead of evaluating a string',
2225
+ exec: 'Compute the value directly instead of executing a string',
2226
+ compile: 'Compute the value directly instead of compiling a string',
2227
+ open: 'Read and write files with n8n file nodes',
2228
+ input: 'Pass values in from a previous node',
2229
+ type: 'Use isinstance(x, dict) to check a type',
2230
+ getattr: 'Use dict access: d.get(key)',
2231
+ setattr: 'Use dict access: d[key] = value',
2232
+ hasattr: 'Use dict access: key in d',
2233
+ vars: 'Use dict access: d.get(key)',
2234
+ dir: 'Use dict access: key in d',
2235
+ globals: 'Pass values through function arguments',
2236
+ locals: 'Pass values through function arguments',
2237
+ object: 'Use dicts instead of objects',
2238
+ memoryview: 'Work with lists, dicts and strings',
2239
+ breakpoint: 'Use print() for debugging'
2240
+ };
1628
2241
  NodeSpecificValidators.NON_FUNCTION_HEADS = new Set([
1629
2242
  'if', 'for', 'while', 'switch', 'catch', 'with', 'await',
1630
2243
  ]);