@n8n/eslint-plugin-community-nodes 0.31.0 → 0.33.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 (191) hide show
  1. package/README.md +2 -1
  2. package/dist/plugin.d.ts +6 -0
  3. package/dist/plugin.d.ts.map +1 -1
  4. package/dist/plugin.js +2 -0
  5. package/dist/rules/ai-node-package-json.d.ts +3 -1
  6. package/dist/rules/ai-node-package-json.d.ts.map +1 -1
  7. package/dist/rules/cred-class-field-icon-missing.d.ts +3 -1
  8. package/dist/rules/cred-class-field-icon-missing.d.ts.map +1 -1
  9. package/dist/rules/cred-class-oauth2-naming.d.ts +3 -1
  10. package/dist/rules/cred-class-oauth2-naming.d.ts.map +1 -1
  11. package/dist/rules/credential-documentation-url.d.ts +3 -1
  12. package/dist/rules/credential-documentation-url.d.ts.map +1 -1
  13. package/dist/rules/credential-password-field.d.ts +3 -1
  14. package/dist/rules/credential-password-field.d.ts.map +1 -1
  15. package/dist/rules/credential-password-field.js +1 -2
  16. package/dist/rules/credential-test-required.d.ts +3 -1
  17. package/dist/rules/credential-test-required.d.ts.map +1 -1
  18. package/dist/rules/credential-unnecessary-password.d.ts +3 -1
  19. package/dist/rules/credential-unnecessary-password.d.ts.map +1 -1
  20. package/dist/rules/credential-unnecessary-password.js +1 -2
  21. package/dist/rules/icon-prefer-themed-variants.d.ts +3 -1
  22. package/dist/rules/icon-prefer-themed-variants.d.ts.map +1 -1
  23. package/dist/rules/icon-validation.d.ts +3 -1
  24. package/dist/rules/icon-validation.d.ts.map +1 -1
  25. package/dist/rules/icon-validation.js +5 -15
  26. package/dist/rules/index.d.ts +132 -43
  27. package/dist/rules/index.d.ts.map +1 -1
  28. package/dist/rules/index.js +2 -0
  29. package/dist/rules/missing-paired-item.d.ts +3 -1
  30. package/dist/rules/missing-paired-item.d.ts.map +1 -1
  31. package/dist/rules/n8n-object-validation.d.ts +3 -1
  32. package/dist/rules/n8n-object-validation.d.ts.map +1 -1
  33. package/dist/rules/no-builder-hint-leakage.d.ts +3 -1
  34. package/dist/rules/no-builder-hint-leakage.d.ts.map +1 -1
  35. package/dist/rules/no-credential-reuse.d.ts +3 -1
  36. package/dist/rules/no-credential-reuse.d.ts.map +1 -1
  37. package/dist/rules/no-credential-reuse.js +4 -6
  38. package/dist/rules/no-dangerous-functions.d.ts +3 -1
  39. package/dist/rules/no-dangerous-functions.d.ts.map +1 -1
  40. package/dist/rules/no-dead-files.d.ts +3 -1
  41. package/dist/rules/no-dead-files.d.ts.map +1 -1
  42. package/dist/rules/no-deprecated-workflow-functions.d.ts +3 -1
  43. package/dist/rules/no-deprecated-workflow-functions.d.ts.map +1 -1
  44. package/dist/rules/no-emoji-in-options.d.ts +3 -1
  45. package/dist/rules/no-emoji-in-options.d.ts.map +1 -1
  46. package/dist/rules/no-emoji-in-options.js +4 -4
  47. package/dist/rules/no-forbidden-lifecycle-scripts.d.ts +3 -1
  48. package/dist/rules/no-forbidden-lifecycle-scripts.d.ts.map +1 -1
  49. package/dist/rules/no-forbidden-lifecycle-scripts.js +1 -1
  50. package/dist/rules/no-hardcoded-secrets.d.ts +3 -1
  51. package/dist/rules/no-hardcoded-secrets.d.ts.map +1 -1
  52. package/dist/rules/no-http-request-with-manual-auth.d.ts +3 -1
  53. package/dist/rules/no-http-request-with-manual-auth.d.ts.map +1 -1
  54. package/dist/rules/no-overrides-field.d.ts +3 -1
  55. package/dist/rules/no-overrides-field.d.ts.map +1 -1
  56. package/dist/rules/no-restricted-globals.d.ts +3 -1
  57. package/dist/rules/no-restricted-globals.d.ts.map +1 -1
  58. package/dist/rules/no-restricted-imports.d.ts +3 -1
  59. package/dist/rules/no-restricted-imports.d.ts.map +1 -1
  60. package/dist/rules/no-runtime-dependencies.d.ts +3 -1
  61. package/dist/rules/no-runtime-dependencies.d.ts.map +1 -1
  62. package/dist/rules/no-silent-error-swallowing.d.ts +3 -1
  63. package/dist/rules/no-silent-error-swallowing.d.ts.map +1 -1
  64. package/dist/rules/no-template-placeholders.d.ts +3 -1
  65. package/dist/rules/no-template-placeholders.d.ts.map +1 -1
  66. package/dist/rules/no-unsafe-connection-type-cast.d.ts +5 -0
  67. package/dist/rules/no-unsafe-connection-type-cast.d.ts.map +1 -0
  68. package/dist/rules/no-unsafe-connection-type-cast.js +104 -0
  69. package/dist/rules/node-class-description-icon-missing.d.ts +3 -1
  70. package/dist/rules/node-class-description-icon-missing.d.ts.map +1 -1
  71. package/dist/rules/node-class-description-icon-missing.js +1 -2
  72. package/dist/rules/node-connection-type-literal.d.ts +3 -1
  73. package/dist/rules/node-connection-type-literal.d.ts.map +1 -1
  74. package/dist/rules/node-registration-complete.d.ts +3 -1
  75. package/dist/rules/node-registration-complete.d.ts.map +1 -1
  76. package/dist/rules/node-usable-as-tool.d.ts +3 -1
  77. package/dist/rules/node-usable-as-tool.d.ts.map +1 -1
  78. package/dist/rules/package-name-convention.d.ts +3 -1
  79. package/dist/rules/package-name-convention.d.ts.map +1 -1
  80. package/dist/rules/require-files-array.d.ts +3 -1
  81. package/dist/rules/require-files-array.d.ts.map +1 -1
  82. package/dist/rules/require-homepage.d.ts +3 -1
  83. package/dist/rules/require-homepage.d.ts.map +1 -1
  84. package/dist/rules/require-node-api-error.d.ts +3 -1
  85. package/dist/rules/require-node-api-error.d.ts.map +1 -1
  86. package/dist/rules/require-node-description-fields.d.ts +3 -1
  87. package/dist/rules/require-node-description-fields.d.ts.map +1 -1
  88. package/dist/rules/require-node-description-fields.js +1 -2
  89. package/dist/rules/require-version.d.ts +3 -1
  90. package/dist/rules/require-version.d.ts.map +1 -1
  91. package/dist/rules/resource-operation-pattern.d.ts +3 -1
  92. package/dist/rules/resource-operation-pattern.d.ts.map +1 -1
  93. package/dist/rules/resource-operation-pattern.js +3 -4
  94. package/dist/rules/trigger-node-conventions.d.ts +3 -1
  95. package/dist/rules/trigger-node-conventions.d.ts.map +1 -1
  96. package/dist/rules/valid-author.d.ts +3 -1
  97. package/dist/rules/valid-author.d.ts.map +1 -1
  98. package/dist/rules/valid-author.js +1 -1
  99. package/dist/rules/valid-credential-references.d.ts +3 -1
  100. package/dist/rules/valid-credential-references.d.ts.map +1 -1
  101. package/dist/rules/valid-credential-references.js +4 -6
  102. package/dist/rules/valid-description.d.ts +3 -1
  103. package/dist/rules/valid-description.d.ts.map +1 -1
  104. package/dist/rules/valid-peer-dependencies.d.ts +3 -1
  105. package/dist/rules/valid-peer-dependencies.d.ts.map +1 -1
  106. package/dist/rules/webhook-lifecycle-complete.d.ts +3 -1
  107. package/dist/rules/webhook-lifecycle-complete.d.ts.map +1 -1
  108. package/dist/utils/ast-utils.d.ts +6 -0
  109. package/dist/utils/ast-utils.d.ts.map +1 -1
  110. package/dist/utils/ast-utils.js +50 -0
  111. package/dist/utils/file-utils.d.ts.map +1 -1
  112. package/dist/utils/file-utils.js +1 -2
  113. package/dist/utils/rule-creator.d.ts +3 -1
  114. package/dist/utils/rule-creator.d.ts.map +1 -1
  115. package/docs/rules/icon-prefer-themed-variants.md +2 -2
  116. package/docs/rules/icon-validation.md +11 -11
  117. package/docs/rules/no-unsafe-connection-type-cast.md +58 -0
  118. package/eslint.config.mjs +2 -3
  119. package/package.json +8 -8
  120. package/src/plugin.ts +2 -0
  121. package/src/rules/credential-password-field.ts +1 -4
  122. package/src/rules/credential-unnecessary-password.ts +1 -4
  123. package/src/rules/icon-validation.test.ts +19 -21
  124. package/src/rules/icon-validation.ts +5 -20
  125. package/src/rules/index.ts +2 -0
  126. package/src/rules/no-credential-reuse.test.ts +54 -0
  127. package/src/rules/no-credential-reuse.ts +4 -8
  128. package/src/rules/no-emoji-in-options.test.ts +77 -0
  129. package/src/rules/no-emoji-in-options.ts +4 -4
  130. package/src/rules/no-forbidden-lifecycle-scripts.ts +1 -1
  131. package/src/rules/no-unsafe-connection-type-cast.test.ts +306 -0
  132. package/src/rules/no-unsafe-connection-type-cast.ts +125 -0
  133. package/src/rules/node-class-description-icon-missing.ts +1 -4
  134. package/src/rules/require-node-description-fields.ts +1 -4
  135. package/src/rules/resource-operation-pattern.ts +3 -6
  136. package/src/rules/valid-author.ts +1 -1
  137. package/src/rules/valid-credential-references.test.ts +75 -0
  138. package/src/rules/valid-credential-references.ts +4 -8
  139. package/src/utils/ast-utils.ts +67 -0
  140. package/src/utils/file-utils.ts +3 -6
  141. package/dist/plugin.js.map +0 -1
  142. package/dist/rules/ai-node-package-json.js.map +0 -1
  143. package/dist/rules/cred-class-field-icon-missing.js.map +0 -1
  144. package/dist/rules/cred-class-oauth2-naming.js.map +0 -1
  145. package/dist/rules/credential-documentation-url.js.map +0 -1
  146. package/dist/rules/credential-password-field.js.map +0 -1
  147. package/dist/rules/credential-test-required.js.map +0 -1
  148. package/dist/rules/credential-unnecessary-password.js.map +0 -1
  149. package/dist/rules/icon-prefer-themed-variants.js.map +0 -1
  150. package/dist/rules/icon-validation.js.map +0 -1
  151. package/dist/rules/index.js.map +0 -1
  152. package/dist/rules/missing-paired-item.js.map +0 -1
  153. package/dist/rules/n8n-object-validation.js.map +0 -1
  154. package/dist/rules/no-builder-hint-leakage.js.map +0 -1
  155. package/dist/rules/no-credential-reuse.js.map +0 -1
  156. package/dist/rules/no-dangerous-functions.js.map +0 -1
  157. package/dist/rules/no-dead-files.js.map +0 -1
  158. package/dist/rules/no-deprecated-workflow-functions.js.map +0 -1
  159. package/dist/rules/no-emoji-in-options.js.map +0 -1
  160. package/dist/rules/no-forbidden-lifecycle-scripts.js.map +0 -1
  161. package/dist/rules/no-hardcoded-secrets.js.map +0 -1
  162. package/dist/rules/no-http-request-with-manual-auth.js.map +0 -1
  163. package/dist/rules/no-overrides-field.js.map +0 -1
  164. package/dist/rules/no-restricted-globals.js.map +0 -1
  165. package/dist/rules/no-restricted-imports.js.map +0 -1
  166. package/dist/rules/no-runtime-dependencies.js.map +0 -1
  167. package/dist/rules/no-silent-error-swallowing.js.map +0 -1
  168. package/dist/rules/no-template-placeholders.js.map +0 -1
  169. package/dist/rules/node-class-description-icon-missing.js.map +0 -1
  170. package/dist/rules/node-connection-type-literal.js.map +0 -1
  171. package/dist/rules/node-registration-complete.js.map +0 -1
  172. package/dist/rules/node-usable-as-tool.js.map +0 -1
  173. package/dist/rules/package-name-convention.js.map +0 -1
  174. package/dist/rules/require-files-array.js.map +0 -1
  175. package/dist/rules/require-homepage.js.map +0 -1
  176. package/dist/rules/require-node-api-error.js.map +0 -1
  177. package/dist/rules/require-node-description-fields.js.map +0 -1
  178. package/dist/rules/require-version.js.map +0 -1
  179. package/dist/rules/resource-operation-pattern.js.map +0 -1
  180. package/dist/rules/trigger-node-conventions.js.map +0 -1
  181. package/dist/rules/valid-author.js.map +0 -1
  182. package/dist/rules/valid-credential-references.js.map +0 -1
  183. package/dist/rules/valid-description.js.map +0 -1
  184. package/dist/rules/valid-peer-dependencies.js.map +0 -1
  185. package/dist/rules/webhook-lifecycle-complete.js.map +0 -1
  186. package/dist/utils/ast-utils.js.map +0 -1
  187. package/dist/utils/constants.js.map +0 -1
  188. package/dist/utils/credential-fields.js.map +0 -1
  189. package/dist/utils/file-utils.js.map +0 -1
  190. package/dist/utils/index.js.map +0 -1
  191. package/dist/utils/rule-creator.js.map +0 -1
@@ -1,5 +1,4 @@
1
- import { TSESTree } from '@typescript-eslint/utils';
2
- import { isNodeTypeClass, findClassProperty, findArrayLiteralProperty, extractCredentialNameFromArray, findPackageJson, readPackageJsonCredentials, isFileType, findSimilarStrings, createRule, } from '../utils/index.js';
1
+ import { isNodeTypeClass, findNodeDescriptionObject, findArrayLiteralProperty, extractCredentialNameFromArray, findPackageJson, readPackageJsonCredentials, isFileType, findSimilarStrings, createRule, } from '../utils/index.js';
3
2
  export const ValidCredentialReferencesRule = createRule({
4
3
  name: 'valid-credential-references',
5
4
  meta: {
@@ -37,12 +36,11 @@ export const ValidCredentialReferencesRule = createRule({
37
36
  if (!isNodeTypeClass(node)) {
38
37
  return;
39
38
  }
40
- const descriptionProperty = findClassProperty(node, 'description');
41
- if (!descriptionProperty?.value ||
42
- descriptionProperty.value.type !== TSESTree.AST_NODE_TYPES.ObjectExpression) {
39
+ const description = findNodeDescriptionObject(node);
40
+ if (!description) {
43
41
  return;
44
42
  }
45
- const credentialsArray = findArrayLiteralProperty(descriptionProperty.value, 'credentials');
43
+ const credentialsArray = findArrayLiteralProperty(description, 'credentials');
46
44
  if (!credentialsArray) {
47
45
  return;
48
46
  }
@@ -1,2 +1,4 @@
1
- export declare const ValidDescriptionRule: import("@typescript-eslint/utils/ts-eslint").RuleModule<"emptyDescription" | "missingDescription", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
1
+ export declare const ValidDescriptionRule: import("@typescript-eslint/utils/ts-eslint").RuleModule<"emptyDescription" | "missingDescription", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
2
+ name: string;
3
+ };
2
4
  //# sourceMappingURL=valid-description.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"valid-description.d.ts","sourceRoot":"","sources":["../../src/rules/valid-description.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,oBAAoB,4KAmD/B,CAAC"}
1
+ {"version":3,"file":"valid-description.d.ts","sourceRoot":"","sources":["../../src/rules/valid-description.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,oBAAoB;;CAmD/B,CAAC"}
@@ -1,2 +1,4 @@
1
- export declare const ValidPeerDependenciesRule: import("@typescript-eslint/utils/ts-eslint").RuleModule<"forbiddenPeerDependency" | "invalidPeerDependenciesType" | "missingN8nWorkflow" | "missingPeerDependencies" | "pinnedN8nWorkflow", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
1
+ export declare const ValidPeerDependenciesRule: import("@typescript-eslint/utils/ts-eslint").RuleModule<"forbiddenPeerDependency" | "invalidPeerDependenciesType" | "missingN8nWorkflow" | "missingPeerDependencies" | "pinnedN8nWorkflow", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
2
+ name: string;
3
+ };
2
4
  //# sourceMappingURL=valid-peer-dependencies.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"valid-peer-dependencies.d.ts","sourceRoot":"","sources":["../../src/rules/valid-peer-dependencies.ts"],"names":[],"mappings":"AAWA,eAAO,MAAM,yBAAyB,qQA0GpC,CAAC"}
1
+ {"version":3,"file":"valid-peer-dependencies.d.ts","sourceRoot":"","sources":["../../src/rules/valid-peer-dependencies.ts"],"names":[],"mappings":"AAWA,eAAO,MAAM,yBAAyB;;CA0GpC,CAAC"}
@@ -1,2 +1,4 @@
1
- export declare const WebhookLifecycleCompleteRule: import("@typescript-eslint/utils/ts-eslint").RuleModule<"emptyWebhookMethods" | "missingLifecycleMethod" | "missingWebhookMethods", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
1
+ export declare const WebhookLifecycleCompleteRule: import("@typescript-eslint/utils/ts-eslint").RuleModule<"emptyWebhookMethods" | "missingLifecycleMethod" | "missingWebhookMethods", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
2
+ name: string;
3
+ };
2
4
  //# sourceMappingURL=webhook-lifecycle-complete.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"webhook-lifecycle-complete.d.ts","sourceRoot":"","sources":["../../src/rules/webhook-lifecycle-complete.ts"],"names":[],"mappings":"AA8CA,eAAO,MAAM,4BAA4B,6MAkFvC,CAAC"}
1
+ {"version":3,"file":"webhook-lifecycle-complete.d.ts","sourceRoot":"","sources":["../../src/rules/webhook-lifecycle-complete.ts"],"names":[],"mappings":"AA8CA,eAAO,MAAM,4BAA4B;;CAkFvC,CAAC"}
@@ -10,6 +10,12 @@ export declare function isTriggerNodeClass(node: TSESTree.ClassDeclaration): boo
10
10
  */
11
11
  export declare function isTriggerNode(node: TSESTree.ClassDeclaration, descriptionValue: TSESTree.ObjectExpression): boolean;
12
12
  export declare function findClassProperty(node: TSESTree.ClassDeclaration, propertyName: string): TSESTree.PropertyDefinition | null;
13
+ /**
14
+ * Returns the `description` object literal of a node class. Versioned nodes
15
+ * follow the layout in the node-building docs and assign `this.description` in
16
+ * their constructor, so look there when the class property has no initializer.
17
+ */
18
+ export declare function findNodeDescriptionObject(node: TSESTree.ClassDeclaration): TSESTree.ObjectExpression | null;
13
19
  export declare function findObjectProperty(obj: TSESTree.ObjectExpression, propertyName: string): TSESTree.Property | null;
14
20
  export declare function getLiteralValue(node: TSESTree.Node | null): string | boolean | number | null;
15
21
  export declare function getStringLiteralValue(node: TSESTree.Node | null): string | null;
@@ -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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"}
@@ -39,6 +39,56 @@ export function findClassProperty(node, propertyName) {
39
39
  member.key.name === propertyName);
40
40
  return property?.type === AST_NODE_TYPES.PropertyDefinition ? property : null;
41
41
  }
42
+ /**
43
+ * Returns the object literal behind an expression, and looks through the type
44
+ * assertions node authors write, e.g. `{ … } as INodeTypeDescription`.
45
+ */
46
+ function asObjectExpression(node) {
47
+ let current = node;
48
+ while (current?.type === AST_NODE_TYPES.TSAsExpression ||
49
+ current?.type === AST_NODE_TYPES.TSSatisfiesExpression ||
50
+ current?.type === AST_NODE_TYPES.TSTypeAssertion ||
51
+ current?.type === AST_NODE_TYPES.TSNonNullExpression) {
52
+ current = current.expression;
53
+ }
54
+ return current?.type === AST_NODE_TYPES.ObjectExpression ? current : null;
55
+ }
56
+ function findConstructorAssignedDescription(node) {
57
+ const constructor = node.body.body.find((member) => member.type === AST_NODE_TYPES.MethodDefinition && member.kind === 'constructor');
58
+ if (constructor?.type !== AST_NODE_TYPES.MethodDefinition) {
59
+ return null;
60
+ }
61
+ for (const statement of constructor.value.body?.body ?? []) {
62
+ if (statement.type !== AST_NODE_TYPES.ExpressionStatement)
63
+ continue;
64
+ const { expression } = statement;
65
+ if (expression.type === AST_NODE_TYPES.AssignmentExpression &&
66
+ expression.operator === '=' &&
67
+ expression.left.type === AST_NODE_TYPES.MemberExpression &&
68
+ expression.left.object.type === AST_NODE_TYPES.ThisExpression &&
69
+ expression.left.property.type === AST_NODE_TYPES.Identifier &&
70
+ expression.left.property.name === 'description') {
71
+ const description = asObjectExpression(expression.right);
72
+ if (description) {
73
+ return description;
74
+ }
75
+ }
76
+ }
77
+ return null;
78
+ }
79
+ /**
80
+ * Returns the `description` object literal of a node class. Versioned nodes
81
+ * follow the layout in the node-building docs and assign `this.description` in
82
+ * their constructor, so look there when the class property has no initializer.
83
+ */
84
+ export function findNodeDescriptionObject(node) {
85
+ const property = findClassProperty(node, 'description');
86
+ const propertyValue = asObjectExpression(property?.value ?? null);
87
+ if (propertyValue) {
88
+ return propertyValue;
89
+ }
90
+ return findConstructorAssignedDescription(node);
91
+ }
42
92
  export function findObjectProperty(obj, propertyName) {
43
93
  const property = obj.properties.find((prop) => prop.type === AST_NODE_TYPES.Property &&
44
94
  prop.key.type === AST_NODE_TYPES.Identifier &&
@@ -1 +1 @@
1
- {"version":3,"file":"file-utils.d.ts","sourceRoot":"","sources":["../../src/utils/file-utils.ts"],"names":[],"mappings":"AAgBA;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAShF;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,CAU3E;AAED,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAahE;AA+BD;;;;;GAKG;AACH,wBAAgB,8BAA8B,CAAC,eAAe,EAAE,MAAM,GAAG,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,CAO1F;AAkBD,wBAAgB,0BAA0B,CAAC,eAAe,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAkB/E;AAED,wBAAgB,6BAA6B,CAAC,kBAAkB,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CA4BvF;AAED,wBAAgB,gBAAgB,CAC/B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,GACb;IACF,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,EAAE,OAAO,CAAC;CAChB,CAYA;AAED,wBAAgB,oBAAoB,CAAC,eAAe,EAAE,MAAM,GAAG,MAAM,EAAE,CAItE;AAwBD;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,eAAe,EAAE,MAAM,GAAG,MAAM,EAAE,CAS3E;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CACrC,eAAe,EAAE,MAAM,EACvB,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,GACpC,MAAM,EAAE,CAYV;AAED,wBAAgB,mCAAmC,CAClD,cAAc,EAAE,MAAM,EACtB,UAAU,EAAE,MAAM,GAChB,OAAO,CAoBT;AAiED,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAiClF"}
1
+ {"version":3,"file":"file-utils.d.ts","sourceRoot":"","sources":["../../src/utils/file-utils.ts"],"names":[],"mappings":"AAgBA;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAShF;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,CAU3E;AAED,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAahE;AA+BD;;;;;GAKG;AACH,wBAAgB,8BAA8B,CAAC,eAAe,EAAE,MAAM,GAAG,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,CAO1F;AAkBD,wBAAgB,0BAA0B,CAAC,eAAe,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAkB/E;AAED,wBAAgB,6BAA6B,CAAC,kBAAkB,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CA4BvF;AAED,wBAAgB,gBAAgB,CAC/B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,GACb;IACF,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,EAAE,OAAO,CAAC;CAChB,CAYA;AAED,wBAAgB,oBAAoB,CAAC,eAAe,EAAE,MAAM,GAAG,MAAM,EAAE,CAItE;AAwBD;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,eAAe,EAAE,MAAM,GAAG,MAAM,EAAE,CAS3E;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CACrC,eAAe,EAAE,MAAM,EACvB,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,GACpC,MAAM,EAAE,CAYV;AAED,wBAAgB,mCAAmC,CAClD,cAAc,EAAE,MAAM,EACtB,UAAU,EAAE,MAAM,GAChB,OAAO,CAoBT;AA8DD,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAiClF"}
@@ -229,8 +229,7 @@ function checkCredentialUsageInFile(nodeFile, credentialName) {
229
229
  enter(node) {
230
230
  if (node.type === AST_NODE_TYPES.ClassDeclaration && isNodeTypeClass(node)) {
231
231
  const descriptionProperty = findClassProperty(node, 'description');
232
- if (!descriptionProperty?.value ||
233
- descriptionProperty.value.type !== AST_NODE_TYPES.ObjectExpression) {
232
+ if (descriptionProperty?.value?.type !== AST_NODE_TYPES.ObjectExpression) {
234
233
  return;
235
234
  }
236
235
  const credentialsArray = findArrayLiteralProperty(descriptionProperty.value, 'credentials');
@@ -1,3 +1,5 @@
1
1
  import { ESLintUtils } from '@typescript-eslint/utils';
2
- export declare const createRule: <Options extends readonly unknown[], MessageIds extends string>({ meta, name, ...rule }: Readonly<ESLintUtils.RuleWithMetaAndName<Options, MessageIds, unknown>>) => ESLintUtils.RuleModule<MessageIds, Options, unknown, ESLintUtils.RuleListener>;
2
+ export declare const createRule: <Options extends readonly unknown[], MessageIds extends string>({ meta, name, ...rule }: Readonly<ESLintUtils.RuleWithMetaAndName<Options, MessageIds, unknown>>) => ESLintUtils.RuleModule<MessageIds, Options, unknown, ESLintUtils.RuleListener> & {
3
+ name: string;
4
+ };
3
5
  //# sourceMappingURL=rule-creator.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"rule-creator.d.ts","sourceRoot":"","sources":["../../src/utils/rule-creator.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAKvD,eAAO,MAAM,UAAU,qPAA2E,CAAC"}
1
+ {"version":3,"file":"rule-creator.d.ts","sourceRoot":"","sources":["../../src/utils/rule-creator.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAKvD,eAAO,MAAM,UAAU;;CAA2E,CAAC"}
@@ -17,8 +17,8 @@ an error: most existing nodes ship a single icon, so this nudges authors toward
17
17
  themed variants without failing verification.
18
18
 
19
19
  The companion `icon-validation` rule still validates that the referenced files
20
- exist, use the `file:` protocol, and that the light and dark variants point to
21
- different files.
20
+ exist and use the `file:` protocol. Both slots can point to the same file if one
21
+ icon design works on both themes.
22
22
 
23
23
  ## Examples
24
24
 
@@ -1,4 +1,4 @@
1
- # Validate node and credential icon files exist, use the file: protocol, and that light/dark icons are different (`@n8n/community-nodes/icon-validation`)
1
+ # Validate node and credential icon files exist and use the file: protocol (`@n8n/community-nodes/icon-validation`)
2
2
 
3
3
  💼 This rule is enabled in the following configs: ✅ `recommended`, ☑️ `recommendedWithoutN8nCloudSupport`.
4
4
 
@@ -8,7 +8,7 @@
8
8
 
9
9
  ## Rule Details
10
10
 
11
- Validates that your node and credential icon files exist and use the correct `file:` protocol. Icons must be different files when providing light/dark theme variants.
11
+ Validates that your node and credential icon files exist and use the correct `file:` protocol. The `light` and `dark` slots can point to the same file: one theme-agnostic icon is a valid choice, and you do not have to copy it under a second filename.
12
12
 
13
13
  Both SVG and PNG icons are accepted. SVG is recommended (it scales cleanly and supports theming), but PNG is allowed — see the `icon-prefer-themed-variants` rule for the related light/dark nudge.
14
14
 
@@ -27,28 +27,28 @@ export class MyNode implements INodeType {
27
27
  }
28
28
  ```
29
29
 
30
+ ### ✅ Correct
31
+
30
32
  ```typescript
31
33
  export class MyNode implements INodeType {
32
34
  description: INodeTypeDescription = {
33
35
  displayName: 'My Node',
34
36
  name: 'myNode',
35
- icon: {
36
- light: 'file:icons/my-icon.svg',
37
- dark: 'file:icons/my-icon.svg', // Same file for both themes
38
- },
37
+ icon: 'file:icons/my-service.svg', // Correct format
39
38
  // ...
40
39
  };
41
40
  }
42
41
  ```
43
42
 
44
- ### ✅ Correct
45
-
46
43
  ```typescript
47
44
  export class MyNode implements INodeType {
48
45
  description: INodeTypeDescription = {
49
46
  displayName: 'My Node',
50
47
  name: 'myNode',
51
- icon: 'file:icons/my-service.svg', // Correct format
48
+ icon: {
49
+ light: 'file:icons/my-service-light.svg',
50
+ dark: 'file:icons/my-service-dark.svg', // Different files
51
+ },
52
52
  // ...
53
53
  };
54
54
  }
@@ -60,8 +60,8 @@ export class MyNode implements INodeType {
60
60
  displayName: 'My Node',
61
61
  name: 'myNode',
62
62
  icon: {
63
- light: 'file:icons/my-service-light.svg',
64
- dark: 'file:icons/my-service-dark.svg', // Different files
63
+ light: 'file:icons/my-service.svg',
64
+ dark: 'file:icons/my-service.svg', // One file that works on both themes
65
65
  },
66
66
  // ...
67
67
  };
@@ -0,0 +1,58 @@
1
+ # Disallow type-erasing casts (`as never`, `as any`, `as unknown`) on node description `inputs`/`outputs` (`@n8n/community-nodes/no-unsafe-connection-type-cast`)
2
+
3
+ 💼 This rule is enabled in the following configs: ✅ `recommended`, ☑️ `recommendedWithoutN8nCloudSupport`.
4
+
5
+ 💡 This rule is manually fixable by [editor suggestions](https://eslint.org/docs/latest/use/core-concepts#rule-suggestions).
6
+
7
+ <!-- end auto-generated rule header -->
8
+
9
+ ## Rule Details
10
+
11
+ A cast such as `inputs: ['main'] as never` compiles, so nothing in CI objects to it. It also switches off the check that makes `inputs` and `outputs` trustworthy. TypeScript stops verifying the value against `INodeTypeDescription`, and the [`node-connection-type-literal`](node-connection-type-literal.md) rule stops seeing the array, because the cast wraps it. A malformed connection declaration then reaches review and build with no automated signal at all. Removing the cast restores both checks.
12
+
13
+ There is no legitimate reason to erase the type here. Dynamic connection lists are already allowed as expression strings, and correct static lists type-check on their own:
14
+
15
+ ```typescript
16
+ inputs: `={{ (${configureInputs})($parameter) }}`,
17
+ ```
18
+
19
+ The rule flags `as never`, `as any` and `as unknown`, including inside a chain like `as unknown as NodeConnectionType[]`, where the intermediate `unknown` is what defeats the checker. Expressions that keep the value structurally checked are left alone: `as const`, `satisfies T` and a plain `as NodeConnectionType[]`.
20
+
21
+ A `satisfies` around a cast does not restore the check, so `['main'] as never satisfies NodeConnectionType[]` is still reported. The suggestion removes the cast and keeps the `satisfies`.
22
+
23
+ Removing the cast is offered as a suggestion rather than an autofix, because it may surface a real type error that was being suppressed. That error is the point: it is the bug the cast was hiding.
24
+
25
+ ## Examples
26
+
27
+ ### ❌ Incorrect
28
+
29
+ ```typescript
30
+ import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
31
+
32
+ export class MyNode implements INodeType {
33
+ description: INodeTypeDescription = {
34
+ displayName: 'My Node',
35
+ name: 'myNode',
36
+ inputs: ['main'] as never,
37
+ outputs: ['main'] as never,
38
+ properties: [],
39
+ };
40
+ }
41
+ ```
42
+
43
+ ### ✅ Correct
44
+
45
+ ```typescript
46
+ import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
47
+ import { NodeConnectionTypes } from 'n8n-workflow';
48
+
49
+ export class MyNode implements INodeType {
50
+ description: INodeTypeDescription = {
51
+ displayName: 'My Node',
52
+ name: 'myNode',
53
+ inputs: [NodeConnectionTypes.Main],
54
+ outputs: [NodeConnectionTypes.Main],
55
+ properties: [],
56
+ };
57
+ }
58
+ ```
package/eslint.config.mjs CHANGED
@@ -1,15 +1,14 @@
1
1
  import { defineConfig } from 'eslint/config';
2
- import { nodeConfig } from '@n8n/eslint-config/node';
2
+ import { backendConfig } from '@n8n/eslint-config/backend';
3
3
  import eslintPlugin from 'eslint-plugin-eslint-plugin';
4
4
 
5
5
  export default defineConfig([
6
- nodeConfig,
6
+ backendConfig,
7
7
  eslintPlugin.configs.recommended,
8
8
  {
9
9
  files: ['src/**/*.ts'],
10
10
  languageOptions: {
11
11
  parserOptions: {
12
- project: './tsconfig.json',
13
12
  allowDefaultProject: true,
14
13
  },
15
14
  },
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.31.0",
4
+ "version": "0.33.0",
5
5
  "main": "./dist/plugin.js",
6
6
  "types": "./dist/plugin.d.ts",
7
7
  "exports": {
@@ -11,23 +11,23 @@
11
11
  }
12
12
  },
13
13
  "dependencies": {
14
- "@typescript-eslint/typescript-estree": "^8.35.0",
15
- "@typescript-eslint/utils": "^8.35.0",
14
+ "@typescript-eslint/typescript-estree": "8.70.0",
15
+ "@typescript-eslint/utils": "8.70.0",
16
16
  "fastest-levenshtein": "1.0.16"
17
17
  },
18
18
  "devDependencies": {
19
+ "@n8n/typescript-config": "1.12.0",
20
+ "@n8n/vitest-config": "1.22.0",
19
21
  "@types/node": "24.10.1",
20
- "@typescript-eslint/rule-tester": "^8.35.0",
22
+ "n8n-workflow": "2.40.0",
23
+ "@typescript-eslint/rule-tester": "8.70.0",
21
24
  "eslint-doc-generator": "^2.2.2",
22
25
  "eslint-plugin-eslint-plugin": "^7.0.0",
23
26
  "rimraf": "6.0.1",
24
27
  "@typescript/native": "npm:typescript@7.0.2",
25
28
  "typescript": "npm:@typescript/typescript6@6.0.2",
26
29
  "vite": "^8.0.2",
27
- "vitest": "^4.1.9",
28
- "@n8n/typescript-config": "1.11.0",
29
- "n8n-workflow": "2.38.0",
30
- "@n8n/vitest-config": "1.21.0"
30
+ "vitest": "^4.1.9"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "eslint": "9.29.0",
package/src/plugin.ts CHANGED
@@ -41,6 +41,7 @@ const configs = {
41
41
  '@n8n/community-nodes/no-runtime-dependencies': 'error',
42
42
  '@n8n/community-nodes/no-silent-error-swallowing': 'error',
43
43
  '@n8n/community-nodes/no-template-placeholders': 'error',
44
+ '@n8n/community-nodes/no-unsafe-connection-type-cast': 'error',
44
45
  '@n8n/community-nodes/icon-validation': 'error',
45
46
  '@n8n/community-nodes/icon-prefer-themed-variants': 'warn',
46
47
  '@n8n/community-nodes/resource-operation-pattern': 'warn',
@@ -89,6 +90,7 @@ const configs = {
89
90
  '@n8n/community-nodes/no-runtime-dependencies': 'error',
90
91
  '@n8n/community-nodes/no-silent-error-swallowing': 'error',
91
92
  '@n8n/community-nodes/no-template-placeholders': 'error',
93
+ '@n8n/community-nodes/no-unsafe-connection-type-cast': 'error',
92
94
  '@n8n/community-nodes/icon-validation': 'error',
93
95
  '@n8n/community-nodes/icon-prefer-themed-variants': 'warn',
94
96
  '@n8n/community-nodes/credential-documentation-url': 'error',
@@ -69,10 +69,7 @@ export const CredentialPasswordFieldRule = createRule({
69
69
  }
70
70
 
71
71
  const propertiesProperty = findClassProperty(node, 'properties');
72
- if (
73
- !propertiesProperty?.value ||
74
- propertiesProperty.value.type !== TSESTree.AST_NODE_TYPES.ArrayExpression
75
- ) {
72
+ if (propertiesProperty?.value?.type !== TSESTree.AST_NODE_TYPES.ArrayExpression) {
76
73
  return;
77
74
  }
78
75
 
@@ -33,10 +33,7 @@ export const CredentialUnnecessaryPasswordRule = createRule({
33
33
  }
34
34
 
35
35
  const propertiesProperty = findClassProperty(node, 'properties');
36
- if (
37
- !propertiesProperty?.value ||
38
- propertiesProperty.value.type !== TSESTree.AST_NODE_TYPES.ArrayExpression
39
- ) {
36
+ if (propertiesProperty?.value?.type !== TSESTree.AST_NODE_TYPES.ArrayExpression) {
40
37
  return;
41
38
  }
42
39
 
@@ -162,6 +162,17 @@ ruleTester.run('icon-validation', IconValidationRule, {
162
162
  true,
163
163
  ),
164
164
  },
165
+ {
166
+ name: 'node with the same file for light and dark icons',
167
+ filename: nodeFilePath,
168
+ code: createNodeCode(
169
+ {
170
+ light: 'file:icons/SameIcon.svg',
171
+ dark: 'file:icons/SameIcon.svg',
172
+ },
173
+ true,
174
+ ),
175
+ },
165
176
  {
166
177
  name: 'credential with valid string icon',
167
178
  filename: credentialFilePath,
@@ -180,6 +191,14 @@ ruleTester.run('icon-validation', IconValidationRule, {
180
191
  dark: 'file:icons/ValidIcon.dark.svg',
181
192
  }),
182
193
  },
194
+ {
195
+ name: 'credential with the same file for light and dark icons',
196
+ filename: credentialFilePath,
197
+ code: createCredentialCode({
198
+ light: 'file:icons/SameIcon.svg',
199
+ dark: 'file:icons/SameIcon.svg',
200
+ }),
201
+ },
183
202
  ],
184
203
  invalid: [
185
204
  {
@@ -205,18 +224,6 @@ ruleTester.run('icon-validation', IconValidationRule, {
205
224
  code: createNodeCode('file:icons/NonExistent.svg', true),
206
225
  errors: [{ messageId: 'iconFileNotFound', data: { iconPath: 'icons/NonExistent.svg' } }],
207
226
  },
208
- {
209
- name: 'light and dark icons are the same file in description',
210
- filename: nodeFilePath,
211
- code: createNodeCode(
212
- {
213
- light: 'file:icons/SameIcon.svg',
214
- dark: 'file:icons/SameIcon.svg',
215
- },
216
- true,
217
- ),
218
- errors: [{ messageId: 'lightDarkSame', data: { iconPath: 'icons/SameIcon.svg' } }],
219
- },
220
227
  {
221
228
  name: 'credential missing icon property',
222
229
  filename: credentialFilePath,
@@ -240,15 +247,6 @@ ruleTester.run('icon-validation', IconValidationRule, {
240
247
  code: createCredentialCode('file:icons/NonExistent.svg'),
241
248
  errors: [{ messageId: 'iconFileNotFound', data: { iconPath: 'icons/NonExistent.svg' } }],
242
249
  },
243
- {
244
- name: 'credential light and dark icons are the same file',
245
- filename: credentialFilePath,
246
- code: createCredentialCode({
247
- light: 'file:icons/SameIcon.svg',
248
- dark: 'file:icons/SameIcon.svg',
249
- }),
250
- errors: [{ messageId: 'lightDarkSame', data: { iconPath: 'icons/SameIcon.svg' } }],
251
- },
252
250
  {
253
251
  name: 'node icon file does not exist but similar file exists - should suggest similar file',
254
252
  filename: nodeFilePath,
@@ -16,7 +16,6 @@ import {
16
16
 
17
17
  const messages = {
18
18
  iconFileNotFound: 'Icon file "{{ iconPath }}" does not exist',
19
- lightDarkSame: 'Light and dark icons cannot be the same file. Both point to "{{ iconPath }}"',
20
19
  invalidIconPath: 'Icon path "{{ iconPath }}" must use file: protocol and be a string',
21
20
  missingIcon: 'Node/Credential class must have an icon property defined',
22
21
  addPlaceholder: 'Add icon property with placeholder',
@@ -29,8 +28,7 @@ export const IconValidationRule = createRule({
29
28
  meta: {
30
29
  type: 'problem',
31
30
  docs: {
32
- description:
33
- 'Validate node and credential icon files exist, use the file: protocol, and that light/dark icons are different',
31
+ description: 'Validate node and credential icon files exist and use the file: protocol',
34
32
  },
35
33
  messages,
36
34
  schema: [],
@@ -114,22 +112,12 @@ export const IconValidationRule = createRule({
114
112
  const lightProperty = findObjectProperty(iconValue, 'light');
115
113
  const darkProperty = findObjectProperty(iconValue, 'dark');
116
114
 
117
- const lightPath = lightProperty ? getStringLiteralValue(lightProperty.value) : null;
118
- const darkPath = darkProperty ? getStringLiteralValue(darkProperty.value) : null;
119
-
115
+ // A theme-agnostic file can fill both slots, so only each path is checked
120
116
  if (lightProperty) {
121
- validateIcon(lightPath, lightProperty.value);
117
+ validateIcon(getStringLiteralValue(lightProperty.value), lightProperty.value);
122
118
  }
123
119
  if (darkProperty) {
124
- validateIcon(darkPath, darkProperty.value);
125
- }
126
-
127
- if (lightPath && darkPath && lightPath === darkPath && lightProperty) {
128
- context.report({
129
- node: lightProperty.value,
130
- messageId: 'lightDarkSame',
131
- data: { iconPath: lightPath.replace(/^file:/, '') },
132
- });
120
+ validateIcon(getStringLiteralValue(darkProperty.value), darkProperty.value);
133
121
  }
134
122
  }
135
123
  };
@@ -145,10 +133,7 @@ export const IconValidationRule = createRule({
145
133
 
146
134
  if (isNodeClass) {
147
135
  const descriptionProperty = findClassProperty(node, 'description');
148
- if (
149
- !descriptionProperty?.value ||
150
- descriptionProperty.value.type !== TSESTree.AST_NODE_TYPES.ObjectExpression
151
- ) {
136
+ if (descriptionProperty?.value?.type !== TSESTree.AST_NODE_TYPES.ObjectExpression) {
152
137
  context.report({
153
138
  node,
154
139
  messageId: 'missingIcon',
@@ -26,6 +26,7 @@ import { NoRestrictedImportsRule } from './no-restricted-imports.js';
26
26
  import { NoRuntimeDependenciesRule } from './no-runtime-dependencies.js';
27
27
  import { NoSilentErrorSwallowingRule } from './no-silent-error-swallowing.js';
28
28
  import { NoTemplatePlaceholdersRule } from './no-template-placeholders.js';
29
+ import { NoUnsafeConnectionTypeCastRule } from './no-unsafe-connection-type-cast.js';
29
30
  import { NodeClassDescriptionIconMissingRule } from './node-class-description-icon-missing.js';
30
31
  import { NodeConnectionTypeLiteralRule } from './node-connection-type-literal.js';
31
32
  import { NodeRegistrationCompleteRule } from './node-registration-complete.js';
@@ -65,6 +66,7 @@ export const rules = {
65
66
  'no-runtime-dependencies': NoRuntimeDependenciesRule,
66
67
  'no-silent-error-swallowing': NoSilentErrorSwallowingRule,
67
68
  'no-template-placeholders': NoTemplatePlaceholdersRule,
69
+ 'no-unsafe-connection-type-cast': NoUnsafeConnectionTypeCastRule,
68
70
  'icon-validation': IconValidationRule,
69
71
  'icon-prefer-themed-variants': IconPreferThemedVariantsRule,
70
72
  'resource-operation-pattern': ResourceOperationPatternRule,
@@ -19,6 +19,7 @@ const mockFindPackageJson = vi.mocked(fileUtils.findPackageJson);
19
19
  const ruleTester = new RuleTester();
20
20
 
21
21
  const nodeFilePath = '/tmp/TestNode.node.ts';
22
+ const versionedNodeFilePath = '/tmp/v1/TestNodeV1.node.ts';
22
23
 
23
24
  function createNodeCode(
24
25
  credentials: Array<string | { name: string; required?: boolean }> = [],
@@ -97,6 +98,36 @@ export class TestNode implements INodeType {
97
98
  }`;
98
99
  }
99
100
 
101
+ /**
102
+ * Versioned node layout from the node-building docs: each version class
103
+ * implements `INodeType` and assigns `description` in its constructor, merging
104
+ * the `baseDescription` that the `VersionedNodeType` entry file passes in.
105
+ */
106
+ function createVersionedNodeCode(credentialNameLiteral: string): string {
107
+ return `
108
+ import type { INodeType, INodeTypeBaseDescription, INodeTypeDescription } from 'n8n-workflow';
109
+
110
+ export class TestNodeV1 implements INodeType {
111
+ description: INodeTypeDescription;
112
+
113
+ constructor(baseDescription: INodeTypeBaseDescription) {
114
+ this.description = {
115
+ ...baseDescription,
116
+ version: 1,
117
+ inputs: ['main'],
118
+ outputs: ['main'],
119
+ credentials: [
120
+ {
121
+ name: ${credentialNameLiteral},
122
+ required: true,
123
+ },
124
+ ],
125
+ properties: [],
126
+ };
127
+ }
128
+ }`;
129
+ }
130
+
100
131
  // Helper function to create non-node class
101
132
  function createNonNodeClass(): string {
102
133
  return `
@@ -163,6 +194,29 @@ ruleTester.run('no-credential-reuse', NoCredentialReuseRule, {
163
194
  },
164
195
  ],
165
196
  invalid: [
197
+ {
198
+ name: 'SECURITY: versioned node assigning description in its constructor uses credential not in package',
199
+ filename: versionedNodeFilePath,
200
+ code: createVersionedNodeCode("'ExternalApi'"),
201
+ errors: [
202
+ {
203
+ messageId: 'credentialNotInPackage',
204
+ data: { credentialName: 'ExternalApi' },
205
+ suggestions: [
206
+ {
207
+ messageId: 'useAvailable',
208
+ data: { suggestedName: 'myApiCredential' },
209
+ output: createVersionedNodeCode('"myApiCredential"'),
210
+ },
211
+ {
212
+ messageId: 'useAvailable',
213
+ data: { suggestedName: 'anotherApiCredential' },
214
+ output: createVersionedNodeCode('"anotherApiCredential"'),
215
+ },
216
+ ],
217
+ },
218
+ ],
219
+ },
166
220
  {
167
221
  name: 'SECURITY: node using credential not in package (object form)',
168
222
  filename: nodeFilePath,