@n8n/eslint-plugin-community-nodes 0.36.0 → 0.38.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/README.md CHANGED
@@ -77,6 +77,7 @@ export default [
77
77
  | [node-connection-type-literal](docs/rules/node-connection-type-literal.md) | Disallow string literals in node description `inputs`/`outputs` — use `NodeConnectionTypes` enum instead | ✅ ☑️ | | 🔧 | | |
78
78
  | [node-registration-complete](docs/rules/node-registration-complete.md) | Ensure every `.node.ts` file in the `nodes/` directory is registered in the "n8n.nodes" array of package.json | | ✅ ☑️ | | | |
79
79
  | [node-usable-as-tool](docs/rules/node-usable-as-tool.md) | Ensure node classes have usableAsTool property | ✅ ☑️ | | 🔧 | | |
80
+ | [open-ended-node-fields](docs/rules/open-ended-node-fields.md) | Discourage optional open-ended JSON fields in node properties | | ✅ ☑️ | | | |
80
81
  | [package-name-convention](docs/rules/package-name-convention.md) | Enforce correct package naming convention for n8n community nodes | ✅ ☑️ | | | 💡 | |
81
82
  | [require-files-array](docs/rules/require-files-array.md) | Require a non-empty "files" array in package.json | | ✅ ☑️ | | | |
82
83
  | [require-homepage](docs/rules/require-homepage.md) | Require a "homepage" field with a valid URL in package.json | | ✅ ☑️ | | | |
package/dist/plugin.d.ts CHANGED
@@ -23,6 +23,7 @@ declare const configs: {
23
23
  '@n8n/community-nodes/no-deprecated-workflow-functions': "error";
24
24
  '@n8n/community-nodes/no-emoji-in-options': "error";
25
25
  '@n8n/community-nodes/node-usable-as-tool': "error";
26
+ '@n8n/community-nodes/open-ended-node-fields': "warn";
26
27
  '@n8n/community-nodes/package-name-convention': "error";
27
28
  '@n8n/community-nodes/credential-test-required': "error";
28
29
  '@n8n/community-nodes/no-credential-reuse': "error";
@@ -80,6 +81,7 @@ declare const configs: {
80
81
  '@n8n/community-nodes/no-deprecated-workflow-functions': "error";
81
82
  '@n8n/community-nodes/no-emoji-in-options': "error";
82
83
  '@n8n/community-nodes/node-usable-as-tool': "error";
84
+ '@n8n/community-nodes/open-ended-node-fields': "warn";
83
85
  '@n8n/community-nodes/package-name-convention': "error";
84
86
  '@n8n/community-nodes/credential-test-required': "error";
85
87
  '@n8n/community-nodes/no-credential-reuse': "error";
@@ -148,6 +150,7 @@ declare const pluginWithConfigs: {
148
150
  '@n8n/community-nodes/no-deprecated-workflow-functions': "error";
149
151
  '@n8n/community-nodes/no-emoji-in-options': "error";
150
152
  '@n8n/community-nodes/node-usable-as-tool': "error";
153
+ '@n8n/community-nodes/open-ended-node-fields': "warn";
151
154
  '@n8n/community-nodes/package-name-convention': "error";
152
155
  '@n8n/community-nodes/credential-test-required': "error";
153
156
  '@n8n/community-nodes/no-credential-reuse': "error";
@@ -205,6 +208,7 @@ declare const pluginWithConfigs: {
205
208
  '@n8n/community-nodes/no-deprecated-workflow-functions': "error";
206
209
  '@n8n/community-nodes/no-emoji-in-options': "error";
207
210
  '@n8n/community-nodes/node-usable-as-tool': "error";
211
+ '@n8n/community-nodes/open-ended-node-fields': "warn";
208
212
  '@n8n/community-nodes/package-name-convention': "error";
209
213
  '@n8n/community-nodes/credential-test-required': "error";
210
214
  '@n8n/community-nodes/no-credential-reuse': "error";
@@ -274,6 +278,7 @@ declare const n8nCommunityNodesPlugin: {
274
278
  '@n8n/community-nodes/no-deprecated-workflow-functions': "error";
275
279
  '@n8n/community-nodes/no-emoji-in-options': "error";
276
280
  '@n8n/community-nodes/node-usable-as-tool': "error";
281
+ '@n8n/community-nodes/open-ended-node-fields': "warn";
277
282
  '@n8n/community-nodes/package-name-convention': "error";
278
283
  '@n8n/community-nodes/credential-test-required': "error";
279
284
  '@n8n/community-nodes/no-credential-reuse': "error";
@@ -331,6 +336,7 @@ declare const n8nCommunityNodesPlugin: {
331
336
  '@n8n/community-nodes/no-deprecated-workflow-functions': "error";
332
337
  '@n8n/community-nodes/no-emoji-in-options': "error";
333
338
  '@n8n/community-nodes/node-usable-as-tool': "error";
339
+ '@n8n/community-nodes/open-ended-node-fields': "warn";
334
340
  '@n8n/community-nodes/package-name-convention': "error";
335
341
  '@n8n/community-nodes/credential-test-required': "error";
336
342
  '@n8n/community-nodes/no-credential-reuse': "error";
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAU,MAAM,QAAQ,CAAC;AAG7C,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAYzC,QAAA,MAAM,OAAO;;;;YAIV,sBAAsB;;oBAZvB,IAAI;oBACJ,OAAO;oBACP,SAAS;;uBAGM,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;YA2DpC,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+CgB,CAAC;AAE1C,QAAA,MAAM,iBAAiB;;QAjHrB,IAAI;QACJ,OAAO;QACP,SAAS;;WAGM,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC;;;;;gBAOpC,sBAAsB;;wBAZvB,IAAI;wBACJ,OAAO;wBACP,SAAS;;2BAGM,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gBA2DpC,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiD+C,CAAC;AAEzE,QAAA,MAAM,uBAAuB;;QAnH3B,IAAI;QACJ,OAAO;QACP,SAAS;;WAGM,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC;;;;;gBAOpC,sBAAsB;;wBAZvB,IAAI;wBACJ,OAAO;wBACP,SAAS;;2BAGM,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gBA2DpC,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmDwB,CAAC;eACnC,iBAAiB;AAChC,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,uBAAuB,EAAE,CAAC"}
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAU,MAAM,QAAQ,CAAC;AAG7C,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAYzC,QAAA,MAAM,OAAO;;;;YAIV,sBAAsB;;oBAZvB,IAAI;oBACJ,OAAO;oBACP,SAAS;;uBAGM,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;YA4DpC,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgDgB,CAAC;AAE1C,QAAA,MAAM,iBAAiB;;QAnHrB,IAAI;QACJ,OAAO;QACP,SAAS;;WAGM,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC;;;;;gBAOpC,sBAAsB;;wBAZvB,IAAI;wBACJ,OAAO;wBACP,SAAS;;2BAGM,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gBA4DpC,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkD+C,CAAC;AAEzE,QAAA,MAAM,uBAAuB;;QArH3B,IAAI;QACJ,OAAO;QACP,SAAS;;WAGM,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC;;;;;gBAOpC,sBAAsB;;wBAZvB,IAAI;wBACJ,OAAO;wBACP,SAAS;;2BAGM,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gBA4DpC,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoDwB,CAAC;eACnC,iBAAiB;AAChC,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,uBAAuB,EAAE,CAAC"}
package/dist/plugin.js CHANGED
@@ -25,6 +25,7 @@ const configs = {
25
25
  '@n8n/community-nodes/no-deprecated-workflow-functions': 'error',
26
26
  '@n8n/community-nodes/no-emoji-in-options': 'error',
27
27
  '@n8n/community-nodes/node-usable-as-tool': 'error',
28
+ '@n8n/community-nodes/open-ended-node-fields': 'warn',
28
29
  '@n8n/community-nodes/package-name-convention': 'error',
29
30
  '@n8n/community-nodes/credential-test-required': 'error',
30
31
  '@n8n/community-nodes/no-credential-reuse': 'error',
@@ -75,6 +76,7 @@ const configs = {
75
76
  '@n8n/community-nodes/no-deprecated-workflow-functions': 'error',
76
77
  '@n8n/community-nodes/no-emoji-in-options': 'error',
77
78
  '@n8n/community-nodes/node-usable-as-tool': 'error',
79
+ '@n8n/community-nodes/open-ended-node-fields': 'warn',
78
80
  '@n8n/community-nodes/package-name-convention': 'error',
79
81
  '@n8n/community-nodes/credential-test-required': 'error',
80
82
  '@n8n/community-nodes/no-credential-reuse': 'error',
@@ -20,6 +20,9 @@ export declare const rules: {
20
20
  'node-usable-as-tool': import("@typescript-eslint/utils/ts-eslint").RuleModule<"aiOnlyUsableAsTool" | "missingUsableAsTool" | "triggerUsableAsTool", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
21
21
  name: string;
22
22
  };
23
+ 'open-ended-node-fields': import("@typescript-eslint/utils/ts-eslint").RuleModule<"optionalJson", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
24
+ name: string;
25
+ };
23
26
  'package-name-convention': import("@typescript-eslint/utils/ts-eslint").RuleModule<"defaultPlaceholderName" | "invalidPackageName" | "missingName" | "renameTo", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
24
27
  name: string;
25
28
  };
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/rules/index.ts"],"names":[],"mappings":"AAgDA,eAAO,MAAM,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8CuB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/rules/index.ts"],"names":[],"mappings":"AAiDA,eAAO,MAAM,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+CuB,CAAC"}
@@ -29,6 +29,7 @@ import { NodeClassDescriptionIconMissingRule } from './node-class-description-ic
29
29
  import { NodeConnectionTypeLiteralRule } from './node-connection-type-literal.js';
30
30
  import { NodeRegistrationCompleteRule } from './node-registration-complete.js';
31
31
  import { NodeUsableAsToolRule } from './node-usable-as-tool.js';
32
+ import { OpenEndedNodeFieldsRule } from './open-ended-node-fields.js';
32
33
  import { PackageNameConventionRule } from './package-name-convention.js';
33
34
  import { RequireFilesArrayRule } from './require-files-array.js';
34
35
  import { RequireHomepageRule } from './require-homepage.js';
@@ -51,6 +52,7 @@ export const rules = {
51
52
  'no-deprecated-workflow-functions': NoDeprecatedWorkflowFunctionsRule,
52
53
  'no-emoji-in-options': NoEmojiInOptionsRule,
53
54
  'node-usable-as-tool': NodeUsableAsToolRule,
55
+ 'open-ended-node-fields': OpenEndedNodeFieldsRule,
54
56
  'package-name-convention': PackageNameConventionRule,
55
57
  'credential-test-required': CredentialTestRequiredRule,
56
58
  'credential-unnecessary-password': CredentialUnnecessaryPasswordRule,
@@ -1 +1 @@
1
- {"version":3,"file":"no-deprecated-workflow-functions.d.ts","sourceRoot":"","sources":["../../src/rules/no-deprecated-workflow-functions.ts"],"names":[],"mappings":"AAyBA,eAAO,MAAM,iCAAiC;;CA6I5C,CAAC"}
1
+ {"version":3,"file":"no-deprecated-workflow-functions.d.ts","sourceRoot":"","sources":["../../src/rules/no-deprecated-workflow-functions.ts"],"names":[],"mappings":"AA8BA,eAAO,MAAM,iCAAiC;;CA0J5C,CAAC"}
@@ -1,5 +1,5 @@
1
1
  import { AST_NODE_TYPES } from '@typescript-eslint/utils';
2
- import { createRule, isThisHelpersAccess } from '../utils/index.js';
2
+ import { createRule, isHelpersAccess, isThisHelpersAccess, EXECUTION_CONTEXT_TYPES, } from '../utils/index.js';
3
3
  const DEPRECATED_FUNCTIONS = {
4
4
  request: 'httpRequest',
5
5
  requestWithAuthentication: 'httpRequestWithAuthentication',
@@ -38,21 +38,32 @@ export const NoDeprecatedWorkflowFunctionsRule = createRule({
38
38
  defaultOptions: [],
39
39
  create(context) {
40
40
  const n8nWorkflowTypes = new Set();
41
- return {
42
- ImportDeclaration(node) {
43
- if (node.source.value === 'n8n-workflow') {
44
- node.specifiers.forEach((specifier) => {
45
- if (specifier.type === AST_NODE_TYPES.ImportSpecifier &&
46
- specifier.imported.type === AST_NODE_TYPES.Identifier) {
47
- n8nWorkflowTypes.add(specifier.local.name);
48
- }
49
- });
41
+ let importsExecutionContext = false;
42
+ // Collect n8n-workflow imports up front. ESLint visits nodes in source order, so a
43
+ // `helpers` access above its import statement would otherwise be checked before the
44
+ // import is seen.
45
+ for (const statement of context.sourceCode.ast.body) {
46
+ if (statement.type !== AST_NODE_TYPES.ImportDeclaration ||
47
+ statement.source.value !== 'n8n-workflow') {
48
+ continue;
49
+ }
50
+ for (const specifier of statement.specifiers) {
51
+ if (specifier.type === AST_NODE_TYPES.ImportSpecifier &&
52
+ specifier.imported.type === AST_NODE_TYPES.Identifier) {
53
+ n8nWorkflowTypes.add(specifier.local.name);
54
+ if (EXECUTION_CONTEXT_TYPES.has(specifier.imported.name)) {
55
+ importsExecutionContext = true;
56
+ }
50
57
  }
51
- },
58
+ }
59
+ }
60
+ return {
52
61
  MemberExpression(node) {
53
62
  if (node.property.type === AST_NODE_TYPES.Identifier &&
54
63
  isDeprecatedFunctionName(node.property.name)) {
55
- if (!isThisHelpersAccess(node)) {
64
+ // `this.helpers` is unambiguous on its own; any other receiver only counts in a file
65
+ // that imports an execution context type, which is what makes its `helpers` n8n's.
66
+ if (!isThisHelpersAccess(node) && !(importsExecutionContext && isHelpersAccess(node))) {
56
67
  return;
57
68
  }
58
69
  const functionName = node.property.name;
@@ -0,0 +1,4 @@
1
+ export declare const OpenEndedNodeFieldsRule: import("@typescript-eslint/utils/ts-eslint").RuleModule<"optionalJson", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
2
+ name: string;
3
+ };
4
+ //# sourceMappingURL=open-ended-node-fields.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"open-ended-node-fields.d.ts","sourceRoot":"","sources":["../../src/rules/open-ended-node-fields.ts"],"names":[],"mappings":"AAaA,eAAO,MAAM,uBAAuB;;CAyClC,CAAC"}
@@ -0,0 +1,44 @@
1
+ import { AST_NODE_TYPES } from '@typescript-eslint/utils';
2
+ import { createRule, findArrayLiteralProperty, findNodeDescriptionObject, findObjectProperty, getBooleanLiteralValue, getStringLiteralValue, isFileType, isNodeTypeClass, } from '../utils/index.js';
3
+ export const OpenEndedNodeFieldsRule = createRule({
4
+ name: 'open-ended-node-fields',
5
+ meta: {
6
+ type: 'suggestion',
7
+ docs: {
8
+ description: 'Discourage optional open-ended JSON fields in node properties',
9
+ },
10
+ messages: {
11
+ optionalJson: 'Optional JSON fields can hide configuration. Use typed parameters under Additional Options or Additional Fields instead.',
12
+ },
13
+ schema: [],
14
+ },
15
+ defaultOptions: [],
16
+ create(context) {
17
+ if (!isFileType(context.filename, '.node.ts'))
18
+ return {};
19
+ return {
20
+ ClassDeclaration(node) {
21
+ if (!isNodeTypeClass(node))
22
+ return;
23
+ const description = findNodeDescriptionObject(node);
24
+ if (!description)
25
+ return;
26
+ const properties = findArrayLiteralProperty(description, 'properties');
27
+ if (!properties)
28
+ return;
29
+ for (const property of properties.elements) {
30
+ if (property?.type !== AST_NODE_TYPES.ObjectExpression)
31
+ continue;
32
+ const type = findObjectProperty(property, 'type');
33
+ if (getStringLiteralValue(type?.value ?? null) !== 'json')
34
+ continue;
35
+ const required = findObjectProperty(property, 'required');
36
+ if (getBooleanLiteralValue(required?.value ?? null) === true)
37
+ continue;
38
+ context.report({ node: property, messageId: 'optionalJson' });
39
+ }
40
+ },
41
+ };
42
+ },
43
+ });
44
+ //# sourceMappingURL=open-ended-node-fields.js.map
@@ -45,6 +45,14 @@ export declare function extractCredentialNameFromArray(element: TSESTree.ArrayEx
45
45
  } | null;
46
46
  /** Matches the `this.helpers` MemberExpression (the object part of `this.helpers.foo`). */
47
47
  export declare function isThisHelpersAccess(node: TSESTree.MemberExpression): boolean;
48
+ /** Execution context interfaces from `n8n-workflow` that expose a `helpers` object. */
49
+ export declare const EXECUTION_CONTEXT_TYPES: Set<string>;
50
+ /**
51
+ * Matches `<receiver>.helpers.<method>` for any receiver, and a bare `helpers.<method>` left by
52
+ * destructuring. Deliberately does not identify the receiver: the rule runs without type
53
+ * information, so callers gate this on the file importing an execution context type instead.
54
+ */
55
+ export declare function isHelpersAccess(node: TSESTree.MemberExpression): boolean;
48
56
  /** Matches a call expression of the form `this.methodName(...)`. */
49
57
  export declare function isThisMethodCall(node: TSESTree.CallExpression, method: string): boolean;
50
58
  /** Matches a call expression of the form `this.helpers.methodName(...)`. */
@@ -1 +1 @@
1
- {"version":3,"file":"ast-utils.d.ts","sourceRoot":"","sources":["../../src/utils/ast-utils.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ast-utils.d.ts","sourceRoot":"","sources":["../../src/utils/ast-utils.ts"],"names":[],"mappings":"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"}
@@ -219,6 +219,33 @@ export function isThisHelpersAccess(node) {
219
219
  node.object.property?.type === AST_NODE_TYPES.Identifier &&
220
220
  node.object.property.name === 'helpers');
221
221
  }
222
+ /** Execution context interfaces from `n8n-workflow` that expose a `helpers` object. */
223
+ export const EXECUTION_CONTEXT_TYPES = new Set([
224
+ 'IAllExecuteFunctions',
225
+ 'ICredentialTestFunctions',
226
+ 'IExecuteFunctions',
227
+ 'IExecutePaginationFunctions',
228
+ 'IExecuteSingleFunctions',
229
+ 'IHookFunctions',
230
+ 'ILoadOptionsFunctions',
231
+ 'IPollFunctions',
232
+ 'ISupplyDataFunctions',
233
+ 'ITriggerFunctions',
234
+ 'IWebhookFunctions',
235
+ ]);
236
+ /**
237
+ * Matches `<receiver>.helpers.<method>` for any receiver, and a bare `helpers.<method>` left by
238
+ * destructuring. Deliberately does not identify the receiver: the rule runs without type
239
+ * information, so callers gate this on the file importing an execution context type instead.
240
+ */
241
+ export function isHelpersAccess(node) {
242
+ if (node.object.type === AST_NODE_TYPES.Identifier) {
243
+ return node.object.name === 'helpers';
244
+ }
245
+ return (node.object.type === AST_NODE_TYPES.MemberExpression &&
246
+ node.object.property.type === AST_NODE_TYPES.Identifier &&
247
+ node.object.property.name === 'helpers');
248
+ }
222
249
  /** Matches a call expression of the form `this.methodName(...)`. */
223
250
  export function isThisMethodCall(node, method) {
224
251
  return (node.callee.type === AST_NODE_TYPES.MemberExpression &&
@@ -1 +1 @@
1
- {"version":3,"file":"credential-fields.d.ts","sourceRoot":"","sources":["../../src/utils/credential-fields.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAkBpD;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAC9B,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;CAAO,GACvD,OAAO,CAST;AAED,mFAAmF;AACnF,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,QAAQ,CAAC,gBAAgB,GAAG,OAAO,CAWjF"}
1
+ {"version":3,"file":"credential-fields.d.ts","sourceRoot":"","sources":["../../src/utils/credential-fields.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAmBpD;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAC9B,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;CAAO,GACvD,OAAO,CAST;AAED,mFAAmF;AACnF,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,QAAQ,CAAC,gBAAgB,GAAG,OAAO,CAWjF"}
@@ -7,6 +7,7 @@ const SENSITIVE_PATTERNS = [
7
7
  'cert',
8
8
  'passphrase',
9
9
  'apikey',
10
+ 'accesskey',
10
11
  'secretkey',
11
12
  'privatekey',
12
13
  'authkey',
@@ -10,6 +10,26 @@
10
10
 
11
11
  Prevents usage of deprecated functions from n8n-workflow package and suggests modern alternatives.
12
12
 
13
+ A node reaches these helpers through its execution context, which it can hold in
14
+ several ways — all of them are reported:
15
+
16
+ ```typescript
17
+ this.helpers.request(options); // execute() bound to the context
18
+ context.helpers.requestOAuth2.call(context, 'oAuth2Api', options); // context passed into a transport helper
19
+ this.executeFunctions.helpers.request(options); // context stored on a class field
20
+ const { helpers } = this;
21
+ helpers.request(options); // destructured off the context, reported on the call
22
+ ```
23
+
24
+ > [!NOTE]
25
+ > The rule runs without type information, so it cannot tell which object a
26
+ > `helpers` property belongs to. Apart from `this.helpers`, it therefore only
27
+ > reports in files that import an execution context interface
28
+ > (`IExecuteFunctions`, `ILoadOptionsFunctions`, …) from `n8n-workflow`. The
29
+ > check is file-scoped rather than binding-scoped: in such a file, an unrelated
30
+ > `someClient.helpers.request()` would also be reported. Silence the rare false
31
+ > positive with `// eslint-disable-next-line`.
32
+
13
33
  ## Examples
14
34
 
15
35
  ### ❌ Incorrect
@@ -0,0 +1,35 @@
1
+ # Discourage optional open-ended JSON fields in node properties (`@n8n/community-nodes/open-ended-node-fields`)
2
+
3
+ ⚠️ This rule _warns_ in the following configs: ✅ `recommended`, ☑️ `recommendedWithoutN8nCloudSupport`.
4
+
5
+ <!-- end auto-generated rule header -->
6
+
7
+ ## Rule Details
8
+
9
+ This rule warns about optional top-level JSON properties in node descriptions. A JSON configuration field can hide parameters from the editor. Typed parameters make configuration easier to find, validate, and change.
10
+
11
+ Place optional parameters under Additional Options or Additional Fields. The rule does not report required JSON properties or typed collection options.
12
+
13
+ ## Examples
14
+
15
+ ### Incorrect
16
+
17
+ ```typescript
18
+ properties: [
19
+ { displayName: 'Configuration', name: 'configuration', type: 'json', default: '{}' },
20
+ ];
21
+ ```
22
+
23
+ ### Correct
24
+
25
+ ```typescript
26
+ properties: [
27
+ {
28
+ displayName: 'Additional Fields',
29
+ name: 'additionalFields',
30
+ type: 'collection',
31
+ default: {},
32
+ options: [{ displayName: 'Limit', name: 'limit', type: 'number', default: 10 }],
33
+ },
34
+ ];
35
+ ```
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@n8n/eslint-plugin-community-nodes",
3
3
  "type": "module",
4
- "version": "0.36.0",
4
+ "version": "0.38.0",
5
5
  "main": "./dist/plugin.js",
6
6
  "types": "./dist/plugin.d.ts",
7
7
  "exports": {
@@ -11,7 +11,7 @@
11
11
  }
12
12
  },
13
13
  "dependencies": {
14
- "@n8n/utils": "1.51.0",
14
+ "@n8n/utils": "1.53.0",
15
15
  "@typescript-eslint/typescript-estree": "8.70.0",
16
16
  "@typescript-eslint/utils": "8.70.0",
17
17
  "fastest-levenshtein": "1.0.16"
@@ -20,14 +20,14 @@
20
20
  "@n8n/typescript-config": "1.12.0",
21
21
  "@n8n/vitest-config": "1.24.0",
22
22
  "@types/node": "24.10.1",
23
- "n8n-workflow": "2.44.0",
23
+ "n8n-workflow": "2.46.0",
24
24
  "@typescript-eslint/rule-tester": "8.70.0",
25
25
  "eslint-doc-generator": "^2.2.2",
26
26
  "eslint-plugin-eslint-plugin": "^7.0.0",
27
27
  "rimraf": "6.0.1",
28
28
  "@typescript/native": "npm:typescript@7.0.2",
29
29
  "typescript": "npm:@typescript/typescript6@6.0.2",
30
- "vite": "^8.0.2",
30
+ "vite": "^8.1.6",
31
31
  "vitest": "5.0.1"
32
32
  },
33
33
  "peerDependencies": {
package/src/plugin.ts CHANGED
@@ -29,6 +29,7 @@ const configs = {
29
29
  '@n8n/community-nodes/no-deprecated-workflow-functions': 'error',
30
30
  '@n8n/community-nodes/no-emoji-in-options': 'error',
31
31
  '@n8n/community-nodes/node-usable-as-tool': 'error',
32
+ '@n8n/community-nodes/open-ended-node-fields': 'warn',
32
33
  '@n8n/community-nodes/package-name-convention': 'error',
33
34
  '@n8n/community-nodes/credential-test-required': 'error',
34
35
  '@n8n/community-nodes/no-credential-reuse': 'error',
@@ -79,6 +80,7 @@ const configs = {
79
80
  '@n8n/community-nodes/no-deprecated-workflow-functions': 'error',
80
81
  '@n8n/community-nodes/no-emoji-in-options': 'error',
81
82
  '@n8n/community-nodes/node-usable-as-tool': 'error',
83
+ '@n8n/community-nodes/open-ended-node-fields': 'warn',
82
84
  '@n8n/community-nodes/package-name-convention': 'error',
83
85
  '@n8n/community-nodes/credential-test-required': 'error',
84
86
  '@n8n/community-nodes/no-credential-reuse': 'error',
@@ -166,6 +166,12 @@ ruleTester.run('credential-password-field', CredentialPasswordFieldRule, {
166
166
  errors: [{ messageId: 'missingPasswordOption', data: { fieldName: 'apiKey' } }],
167
167
  output: createCredentialCode([createProperty('API Key', 'apiKey', { password: true })]),
168
168
  },
169
+ {
170
+ name: 'access key field missing typeOptions.password = true',
171
+ code: createCredentialCode([createProperty('Access Key', 'accessKey')]),
172
+ errors: [{ messageId: 'missingPasswordOption', data: { fieldName: 'accessKey' } }],
173
+ output: createCredentialCode([createProperty('Access Key', 'accessKey', { password: true })]),
174
+ },
169
175
  {
170
176
  name: 'secret field missing typeOptions.password = true',
171
177
  code: createCredentialCode([createProperty('Client Secret', 'clientSecret')]),
@@ -31,6 +31,7 @@ import { NodeClassDescriptionIconMissingRule } from './node-class-description-ic
31
31
  import { NodeConnectionTypeLiteralRule } from './node-connection-type-literal.js';
32
32
  import { NodeRegistrationCompleteRule } from './node-registration-complete.js';
33
33
  import { NodeUsableAsToolRule } from './node-usable-as-tool.js';
34
+ import { OpenEndedNodeFieldsRule } from './open-ended-node-fields.js';
34
35
  import { PackageNameConventionRule } from './package-name-convention.js';
35
36
  import { RequireFilesArrayRule } from './require-files-array.js';
36
37
  import { RequireHomepageRule } from './require-homepage.js';
@@ -54,6 +55,7 @@ export const rules = {
54
55
  'no-deprecated-workflow-functions': NoDeprecatedWorkflowFunctionsRule,
55
56
  'no-emoji-in-options': NoEmojiInOptionsRule,
56
57
  'node-usable-as-tool': NodeUsableAsToolRule,
58
+ 'open-ended-node-fields': OpenEndedNodeFieldsRule,
57
59
  'package-name-convention': PackageNameConventionRule,
58
60
  'credential-test-required': CredentialTestRequiredRule,
59
61
  'credential-unnecessary-password': CredentialUnnecessaryPasswordRule,
@@ -47,6 +47,17 @@ import { IRequestOptions } from 'some-other-package';
47
47
 
48
48
  function test(options: IRequestOptions) {
49
49
  return options.url;
50
+ }`,
51
+ },
52
+ {
53
+ name: 'non-this helpers in a file that does not handle execution contexts',
54
+ code: `
55
+ import { SomeApiClient } from 'some-other-package';
56
+
57
+ function makeRequest(client: SomeApiClient) {
58
+ const { helpers } = client;
59
+ helpers.request('https://example.com');
60
+ return client.helpers.request('https://example.com');
50
61
  }`,
51
62
  },
52
63
  ],
@@ -183,5 +194,216 @@ return this.helpers.prepareOutputData([{ json: response }]);`,
183
194
  },
184
195
  ],
185
196
  },
197
+ {
198
+ name: 'execution context passed in as a parameter',
199
+ code: `
200
+ import type { IExecuteFunctions } from 'n8n-workflow';
201
+
202
+ export async function apiRequest(context: IExecuteFunctions, options) {
203
+ return await context.helpers.requestOAuth2.call(context, 'exampleOAuth2Api', options);
204
+ }`,
205
+ errors: [
206
+ {
207
+ messageId: 'deprecatedRequestFunction',
208
+ data: { functionName: 'requestOAuth2', replacement: 'httpRequestWithAuthentication' },
209
+ suggestions: [
210
+ {
211
+ messageId: 'suggestReplaceFunction',
212
+ data: { functionName: 'requestOAuth2', replacement: 'httpRequestWithAuthentication' },
213
+ output: `
214
+ import type { IExecuteFunctions } from 'n8n-workflow';
215
+
216
+ export async function apiRequest(context: IExecuteFunctions, options) {
217
+ return await context.helpers.httpRequestWithAuthentication.call(context, 'exampleOAuth2Api', options);
218
+ }`,
219
+ },
220
+ ],
221
+ },
222
+ ],
223
+ },
224
+ {
225
+ name: 'execution context union type parameter',
226
+ code: `
227
+ import type { IExecuteFunctions, ILoadOptionsFunctions } from 'n8n-workflow';
228
+
229
+ async function loadOptions(ctx: IExecuteFunctions | ILoadOptionsFunctions) {
230
+ return await ctx.helpers.request({ url: 'https://example.com' });
231
+ }`,
232
+ errors: [
233
+ {
234
+ messageId: 'deprecatedRequestFunction',
235
+ data: { functionName: 'request', replacement: 'httpRequest' },
236
+ suggestions: [
237
+ {
238
+ messageId: 'suggestReplaceFunction',
239
+ data: { functionName: 'request', replacement: 'httpRequest' },
240
+ output: `
241
+ import type { IExecuteFunctions, ILoadOptionsFunctions } from 'n8n-workflow';
242
+
243
+ async function loadOptions(ctx: IExecuteFunctions | ILoadOptionsFunctions) {
244
+ return await ctx.helpers.httpRequest({ url: 'https://example.com' });
245
+ }`,
246
+ },
247
+ ],
248
+ },
249
+ ],
250
+ },
251
+ {
252
+ name: 'helpers destructured from this',
253
+ code: `
254
+ import type { IExecuteFunctions } from 'n8n-workflow';
255
+
256
+ async function run(this: IExecuteFunctions) {
257
+ const { helpers } = this;
258
+ return await helpers.requestWithAuthentication('exampleApi', options);
259
+ }`,
260
+ errors: [
261
+ {
262
+ messageId: 'deprecatedRequestFunction',
263
+ data: {
264
+ functionName: 'requestWithAuthentication',
265
+ replacement: 'httpRequestWithAuthentication',
266
+ },
267
+ suggestions: [
268
+ {
269
+ messageId: 'suggestReplaceFunction',
270
+ data: {
271
+ functionName: 'requestWithAuthentication',
272
+ replacement: 'httpRequestWithAuthentication',
273
+ },
274
+ output: `
275
+ import type { IExecuteFunctions } from 'n8n-workflow';
276
+
277
+ async function run(this: IExecuteFunctions) {
278
+ const { helpers } = this;
279
+ return await helpers.httpRequestWithAuthentication('exampleApi', options);
280
+ }`,
281
+ },
282
+ ],
283
+ },
284
+ ],
285
+ },
286
+ {
287
+ name: 'execution context held in a class field',
288
+ code: `
289
+ import type { IExecuteFunctions } from 'n8n-workflow';
290
+
291
+ class ExampleApi {
292
+ private executeFunctions: IExecuteFunctions;
293
+
294
+ async request(options) {
295
+ return await this.executeFunctions.helpers.requestWithAuthentication.call(
296
+ this.executeFunctions,
297
+ 'exampleApi',
298
+ options,
299
+ );
300
+ }
301
+ }`,
302
+ errors: [
303
+ {
304
+ messageId: 'deprecatedRequestFunction',
305
+ data: {
306
+ functionName: 'requestWithAuthentication',
307
+ replacement: 'httpRequestWithAuthentication',
308
+ },
309
+ suggestions: [
310
+ {
311
+ messageId: 'suggestReplaceFunction',
312
+ data: {
313
+ functionName: 'requestWithAuthentication',
314
+ replacement: 'httpRequestWithAuthentication',
315
+ },
316
+ output: `
317
+ import type { IExecuteFunctions } from 'n8n-workflow';
318
+
319
+ class ExampleApi {
320
+ private executeFunctions: IExecuteFunctions;
321
+
322
+ async request(options) {
323
+ return await this.executeFunctions.helpers.httpRequestWithAuthentication.call(
324
+ this.executeFunctions,
325
+ 'exampleApi',
326
+ options,
327
+ );
328
+ }
329
+ }`,
330
+ },
331
+ ],
332
+ },
333
+ ],
334
+ },
335
+ {
336
+ name: 'execution context declared as a constructor parameter property',
337
+ code: `
338
+ import type { IExecuteFunctions } from 'n8n-workflow';
339
+
340
+ class ExampleApi {
341
+ constructor(private readonly ctx: IExecuteFunctions) {}
342
+
343
+ async run() {
344
+ return await this.ctx.helpers.prepareOutputData([]);
345
+ }
346
+ }`,
347
+ errors: [
348
+ {
349
+ messageId: 'deprecatedWithoutReplacement',
350
+ data: { functionName: 'prepareOutputData' },
351
+ },
352
+ ],
353
+ },
354
+ {
355
+ name: 'execution context used above its import statement',
356
+ code: `
357
+ async function apiRequest(context: IExecuteFunctions, options) {
358
+ return await context.helpers.request(options);
359
+ }
360
+
361
+ import type { IExecuteFunctions } from 'n8n-workflow';`,
362
+ errors: [
363
+ {
364
+ messageId: 'deprecatedRequestFunction',
365
+ data: { functionName: 'request', replacement: 'httpRequest' },
366
+ suggestions: [
367
+ {
368
+ messageId: 'suggestReplaceFunction',
369
+ data: { functionName: 'request', replacement: 'httpRequest' },
370
+ output: `
371
+ async function apiRequest(context: IExecuteFunctions, options) {
372
+ return await context.helpers.httpRequest(options);
373
+ }
374
+
375
+ import type { IExecuteFunctions } from 'n8n-workflow';`,
376
+ },
377
+ ],
378
+ },
379
+ ],
380
+ },
381
+ {
382
+ name: 'credential test context passed in as a parameter',
383
+ code: `
384
+ import type { ICredentialTestFunctions } from 'n8n-workflow';
385
+
386
+ export async function testCredential(context: ICredentialTestFunctions, options) {
387
+ return await context.helpers.request(options);
388
+ }`,
389
+ errors: [
390
+ {
391
+ messageId: 'deprecatedRequestFunction',
392
+ data: { functionName: 'request', replacement: 'httpRequest' },
393
+ suggestions: [
394
+ {
395
+ messageId: 'suggestReplaceFunction',
396
+ data: { functionName: 'request', replacement: 'httpRequest' },
397
+ output: `
398
+ import type { ICredentialTestFunctions } from 'n8n-workflow';
399
+
400
+ export async function testCredential(context: ICredentialTestFunctions, options) {
401
+ return await context.helpers.httpRequest(options);
402
+ }`,
403
+ },
404
+ ],
405
+ },
406
+ ],
407
+ },
186
408
  ],
187
409
  });
@@ -1,6 +1,11 @@
1
1
  import { AST_NODE_TYPES } from '@typescript-eslint/utils';
2
2
 
3
- import { createRule, isThisHelpersAccess } from '../utils/index.js';
3
+ import {
4
+ createRule,
5
+ isHelpersAccess,
6
+ isThisHelpersAccess,
7
+ EXECUTION_CONTEXT_TYPES,
8
+ } from '../utils/index.js';
4
9
 
5
10
  const DEPRECATED_FUNCTIONS = {
6
11
  request: 'httpRequest',
@@ -46,27 +51,40 @@ export const NoDeprecatedWorkflowFunctionsRule = createRule({
46
51
  defaultOptions: [],
47
52
  create(context) {
48
53
  const n8nWorkflowTypes = new Set<string>();
49
-
50
- return {
51
- ImportDeclaration(node) {
52
- if (node.source.value === 'n8n-workflow') {
53
- node.specifiers.forEach((specifier) => {
54
- if (
55
- specifier.type === AST_NODE_TYPES.ImportSpecifier &&
56
- specifier.imported.type === AST_NODE_TYPES.Identifier
57
- ) {
58
- n8nWorkflowTypes.add(specifier.local.name);
59
- }
60
- });
54
+ let importsExecutionContext = false;
55
+
56
+ // Collect n8n-workflow imports up front. ESLint visits nodes in source order, so a
57
+ // `helpers` access above its import statement would otherwise be checked before the
58
+ // import is seen.
59
+ for (const statement of context.sourceCode.ast.body) {
60
+ if (
61
+ statement.type !== AST_NODE_TYPES.ImportDeclaration ||
62
+ statement.source.value !== 'n8n-workflow'
63
+ ) {
64
+ continue;
65
+ }
66
+ for (const specifier of statement.specifiers) {
67
+ if (
68
+ specifier.type === AST_NODE_TYPES.ImportSpecifier &&
69
+ specifier.imported.type === AST_NODE_TYPES.Identifier
70
+ ) {
71
+ n8nWorkflowTypes.add(specifier.local.name);
72
+ if (EXECUTION_CONTEXT_TYPES.has(specifier.imported.name)) {
73
+ importsExecutionContext = true;
74
+ }
61
75
  }
62
- },
76
+ }
77
+ }
63
78
 
79
+ return {
64
80
  MemberExpression(node) {
65
81
  if (
66
82
  node.property.type === AST_NODE_TYPES.Identifier &&
67
83
  isDeprecatedFunctionName(node.property.name)
68
84
  ) {
69
- if (!isThisHelpersAccess(node)) {
85
+ // `this.helpers` is unambiguous on its own; any other receiver only counts in a file
86
+ // that imports an execution context type, which is what makes its `helpers` n8n's.
87
+ if (!isThisHelpersAccess(node) && !(importsExecutionContext && isHelpersAccess(node))) {
70
88
  return;
71
89
  }
72
90
 
@@ -0,0 +1,122 @@
1
+ import { Linter } from 'eslint';
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ import { configs } from '../plugin.js';
5
+
6
+ const linter = new Linter();
7
+
8
+ function createNodeCode(property: string): string {
9
+ return `
10
+ export class TestNode extends Node {
11
+ description = {
12
+ displayName: 'Test Node',
13
+ name: 'testNode',
14
+ group: ['transform'],
15
+ version: 1,
16
+ inputs: ['main'],
17
+ outputs: ['main'],
18
+ properties: [${property}],
19
+ };
20
+ }`;
21
+ }
22
+
23
+ function lintNode(code: string, config: keyof typeof configs) {
24
+ return linter.verify(code, [{ ...configs[config], files: ['**/*.node.ts'] }], {
25
+ filename: 'src/rules/TestNode.node.ts',
26
+ });
27
+ }
28
+
29
+ describe.each(['recommended', 'recommendedWithoutN8nCloudSupport'] as const)(
30
+ '%s node property review',
31
+ (config) => {
32
+ // CE-1879: Warn when an optional JSON field replaces typed node parameters.
33
+ it('warns about an optional JSON configuration catch-all', () => {
34
+ const code = createNodeCode(`{
35
+ displayName: 'Configuration',
36
+ name: 'configuration',
37
+ type: 'json',
38
+ required: false,
39
+ default: '{}',
40
+ }`);
41
+
42
+ const warnings = lintNode(code, config).filter(
43
+ ({ severity, message }) => severity === 1 && /Additional (Options|Fields)/i.test(message),
44
+ );
45
+
46
+ expect(warnings).toEqual([
47
+ expect.objectContaining({
48
+ severity: 1,
49
+ message: expect.stringMatching(/Additional (Options|Fields)/i),
50
+ }),
51
+ ]);
52
+ });
53
+
54
+ it('does not warn about typed parameters inside Additional Fields', () => {
55
+ const code = createNodeCode(`{
56
+ displayName: 'Additional Fields',
57
+ name: 'additionalFields',
58
+ type: 'collection',
59
+ default: {},
60
+ options: [{ displayName: 'Limit', name: 'limit', type: 'number', default: 10 }],
61
+ }`);
62
+
63
+ const messages = lintNode(code, config);
64
+ expect(messages).toEqual(
65
+ expect.arrayContaining([
66
+ expect.objectContaining({
67
+ ruleId: '@n8n/community-nodes/require-node-description-fields',
68
+ }),
69
+ ]),
70
+ );
71
+
72
+ const warnings = messages.filter(
73
+ ({ severity, message }) => severity === 1 && /Additional (Options|Fields)/i.test(message),
74
+ );
75
+
76
+ expect(warnings).toEqual([]);
77
+ });
78
+
79
+ it('warns when an open-ended JSON field does not declare required', () => {
80
+ const code = createNodeCode(`{
81
+ displayName: 'Configuration',
82
+ name: 'configuration',
83
+ type: 'json',
84
+ default: '{}',
85
+ }`);
86
+
87
+ expect(
88
+ lintNode(code, config).filter(
89
+ ({ ruleId }) => ruleId === '@n8n/community-nodes/open-ended-node-fields',
90
+ ),
91
+ ).toHaveLength(1);
92
+ });
93
+
94
+ it('does not warn about required JSON fields', () => {
95
+ const code = createNodeCode(`{
96
+ displayName: 'Request Body',
97
+ name: 'body',
98
+ type: 'json',
99
+ required: true,
100
+ default: '{}',
101
+ }`);
102
+
103
+ expect(
104
+ lintNode(code, config).filter(
105
+ ({ ruleId }) => ruleId === '@n8n/community-nodes/open-ended-node-fields',
106
+ ),
107
+ ).toEqual([]);
108
+ });
109
+
110
+ it('does not inspect classes that are not nodes', () => {
111
+ const code = `class Helper {
112
+ description = { properties: [{ type: 'json', required: false }] };
113
+ }`;
114
+
115
+ expect(
116
+ lintNode(code, config).filter(
117
+ ({ ruleId }) => ruleId === '@n8n/community-nodes/open-ended-node-fields',
118
+ ),
119
+ ).toEqual([]);
120
+ });
121
+ },
122
+ );
@@ -0,0 +1,55 @@
1
+ import { AST_NODE_TYPES } from '@typescript-eslint/utils';
2
+
3
+ import {
4
+ createRule,
5
+ findArrayLiteralProperty,
6
+ findNodeDescriptionObject,
7
+ findObjectProperty,
8
+ getBooleanLiteralValue,
9
+ getStringLiteralValue,
10
+ isFileType,
11
+ isNodeTypeClass,
12
+ } from '../utils/index.js';
13
+
14
+ export const OpenEndedNodeFieldsRule = createRule({
15
+ name: 'open-ended-node-fields',
16
+ meta: {
17
+ type: 'suggestion',
18
+ docs: {
19
+ description: 'Discourage optional open-ended JSON fields in node properties',
20
+ },
21
+ messages: {
22
+ optionalJson:
23
+ 'Optional JSON fields can hide configuration. Use typed parameters under Additional Options or Additional Fields instead.',
24
+ },
25
+ schema: [],
26
+ },
27
+ defaultOptions: [],
28
+ create(context) {
29
+ if (!isFileType(context.filename, '.node.ts')) return {};
30
+
31
+ return {
32
+ ClassDeclaration(node) {
33
+ if (!isNodeTypeClass(node)) return;
34
+
35
+ const description = findNodeDescriptionObject(node);
36
+ if (!description) return;
37
+
38
+ const properties = findArrayLiteralProperty(description, 'properties');
39
+ if (!properties) return;
40
+
41
+ for (const property of properties.elements) {
42
+ if (property?.type !== AST_NODE_TYPES.ObjectExpression) continue;
43
+
44
+ const type = findObjectProperty(property, 'type');
45
+ if (getStringLiteralValue(type?.value ?? null) !== 'json') continue;
46
+
47
+ const required = findObjectProperty(property, 'required');
48
+ if (getBooleanLiteralValue(required?.value ?? null) === true) continue;
49
+
50
+ context.report({ node: property, messageId: 'optionalJson' });
51
+ }
52
+ },
53
+ };
54
+ },
55
+ });
@@ -327,6 +327,38 @@ export function isThisHelpersAccess(node: TSESTree.MemberExpression): boolean {
327
327
  );
328
328
  }
329
329
 
330
+ /** Execution context interfaces from `n8n-workflow` that expose a `helpers` object. */
331
+ export const EXECUTION_CONTEXT_TYPES = new Set([
332
+ 'IAllExecuteFunctions',
333
+ 'ICredentialTestFunctions',
334
+ 'IExecuteFunctions',
335
+ 'IExecutePaginationFunctions',
336
+ 'IExecuteSingleFunctions',
337
+ 'IHookFunctions',
338
+ 'ILoadOptionsFunctions',
339
+ 'IPollFunctions',
340
+ 'ISupplyDataFunctions',
341
+ 'ITriggerFunctions',
342
+ 'IWebhookFunctions',
343
+ ]);
344
+
345
+ /**
346
+ * Matches `<receiver>.helpers.<method>` for any receiver, and a bare `helpers.<method>` left by
347
+ * destructuring. Deliberately does not identify the receiver: the rule runs without type
348
+ * information, so callers gate this on the file importing an execution context type instead.
349
+ */
350
+ export function isHelpersAccess(node: TSESTree.MemberExpression): boolean {
351
+ if (node.object.type === AST_NODE_TYPES.Identifier) {
352
+ return node.object.name === 'helpers';
353
+ }
354
+
355
+ return (
356
+ node.object.type === AST_NODE_TYPES.MemberExpression &&
357
+ node.object.property.type === AST_NODE_TYPES.Identifier &&
358
+ node.object.property.name === 'helpers'
359
+ );
360
+ }
361
+
330
362
  /** Matches a call expression of the form `this.methodName(...)`. */
331
363
  export function isThisMethodCall(node: TSESTree.CallExpression, method: string): boolean {
332
364
  return (
@@ -9,6 +9,7 @@ const SENSITIVE_PATTERNS = [
9
9
  'cert',
10
10
  'passphrase',
11
11
  'apikey',
12
+ 'accesskey',
12
13
  'secretkey',
13
14
  'privatekey',
14
15
  'authkey',