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.
- package/dist/mcp/handlers-n8n-manager.d.ts.map +1 -1
- package/dist/mcp/handlers-n8n-manager.js +29 -3
- package/dist/mcp/handlers-n8n-manager.js.map +1 -1
- package/dist/mcp/handlers-workflow-diff.d.ts.map +1 -1
- package/dist/mcp/handlers-workflow-diff.js +110 -9
- package/dist/mcp/handlers-workflow-diff.js.map +1 -1
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +12 -4
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tool-docs/workflow_management/n8n-autofix-workflow.d.ts.map +1 -1
- package/dist/mcp/tool-docs/workflow_management/n8n-autofix-workflow.js +2 -1
- package/dist/mcp/tool-docs/workflow_management/n8n-autofix-workflow.js.map +1 -1
- package/dist/mcp/tool-docs/workflow_management/n8n-update-full-workflow.d.ts.map +1 -1
- package/dist/mcp/tool-docs/workflow_management/n8n-update-full-workflow.js +2 -1
- package/dist/mcp/tool-docs/workflow_management/n8n-update-full-workflow.js.map +1 -1
- package/dist/mcp/tool-docs/workflow_management/n8n-update-partial-workflow.d.ts.map +1 -1
- package/dist/mcp/tool-docs/workflow_management/n8n-update-partial-workflow.js +2 -1
- package/dist/mcp/tool-docs/workflow_management/n8n-update-partial-workflow.js.map +1 -1
- package/dist/mcp/tools-documentation.js +127 -230
- package/dist/mcp/tools-documentation.js.map +1 -1
- package/dist/mcp-tools-engine.d.ts +1 -1
- package/dist/mcp-tools-engine.d.ts.map +1 -1
- package/dist/mcp-tools-engine.js +1 -3
- package/dist/mcp-tools-engine.js.map +1 -1
- package/dist/services/config-validator.d.ts.map +1 -1
- package/dist/services/config-validator.js +8 -53
- package/dist/services/config-validator.js.map +1 -1
- package/dist/services/enhanced-config-validator.d.ts +1 -0
- package/dist/services/enhanced-config-validator.d.ts.map +1 -1
- package/dist/services/enhanced-config-validator.js +46 -6
- package/dist/services/enhanced-config-validator.js.map +1 -1
- package/dist/services/example-generator.d.ts.map +1 -1
- package/dist/services/example-generator.js +28 -29
- package/dist/services/example-generator.js.map +1 -1
- package/dist/services/expression-validator.d.ts +2 -0
- package/dist/services/expression-validator.d.ts.map +1 -1
- package/dist/services/expression-validator.js +27 -1
- package/dist/services/expression-validator.js.map +1 -1
- package/dist/services/mcp-exposure.d.ts.map +1 -1
- package/dist/services/mcp-exposure.js +43 -6
- package/dist/services/mcp-exposure.js.map +1 -1
- package/dist/services/n8n-api-client.d.ts +1 -4
- package/dist/services/n8n-api-client.d.ts.map +1 -1
- package/dist/services/n8n-api-client.js +0 -30
- package/dist/services/n8n-api-client.js.map +1 -1
- package/dist/services/n8n-validation.d.ts.map +1 -1
- package/dist/services/n8n-validation.js +28 -33
- package/dist/services/n8n-validation.js.map +1 -1
- package/dist/services/node-specific-validators.d.ts +22 -0
- package/dist/services/node-specific-validators.d.ts.map +1 -1
- package/dist/services/node-specific-validators.js +701 -88
- package/dist/services/node-specific-validators.js.map +1 -1
- package/dist/services/task-templates.d.ts.map +1 -1
- package/dist/services/task-templates.js +54 -49
- package/dist/services/task-templates.js.map +1 -1
- package/dist/services/workflow-validator.d.ts +1 -0
- package/dist/services/workflow-validator.d.ts.map +1 -1
- package/dist/services/workflow-validator.js +58 -63
- package/dist/services/workflow-validator.js.map +1 -1
- package/dist/services/workflow-versioning-service.d.ts +2 -0
- package/dist/services/workflow-versioning-service.d.ts.map +1 -1
- package/dist/services/workflow-versioning-service.js +14 -0
- package/dist/services/workflow-versioning-service.js.map +1 -1
- package/dist/types/n8n-api.d.ts +0 -40
- package/dist/types/n8n-api.d.ts.map +1 -1
- package/dist/utils/jmespath-checks.d.ts +16 -0
- package/dist/utils/jmespath-checks.d.ts.map +1 -0
- package/dist/utils/jmespath-checks.js +357 -0
- package/dist/utils/jmespath-checks.js.map +1 -0
- package/dist/utils/n8n-errors.d.ts.map +1 -1
- package/dist/utils/n8n-errors.js +9 -0
- package/dist/utils/n8n-errors.js.map +1 -1
- 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
|
-
|
|
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
|
-
|
|
973
|
-
this.
|
|
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
|
|
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
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
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:
|
|
1059
|
-
fix:
|
|
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 =
|
|
1079
|
-
? this.stripNestedJavaScriptFunctionBodies(
|
|
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 (
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
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
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
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
|
|
1137
|
-
|
|
1138
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 +=
|
|
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' ?
|
|
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
|
|
1386
|
-
? (
|
|
1387
|
-
|
|
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
|
-
: ['
|
|
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
|
-
:
|
|
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:
|
|
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
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
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
|
-
|
|
1492
|
-
|
|
1493
|
-
const
|
|
1494
|
-
|
|
1495
|
-
|
|
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:
|
|
1503
|
-
message:
|
|
1504
|
-
|
|
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
|
-
|
|
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
|
]);
|