@n8n/eslint-plugin-community-nodes 0.3.0 → 0.5.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 (114) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/README.md +60 -0
  3. package/dist/plugin.d.ts +144 -28
  4. package/dist/plugin.d.ts.map +1 -1
  5. package/dist/plugin.js +28 -25
  6. package/dist/plugin.js.map +1 -1
  7. package/dist/rules/credential-documentation-url.d.ts +7 -0
  8. package/dist/rules/credential-documentation-url.d.ts.map +1 -0
  9. package/dist/rules/credential-documentation-url.js +100 -0
  10. package/dist/rules/credential-documentation-url.js.map +1 -0
  11. package/dist/rules/credential-password-field.d.ts +1 -2
  12. package/dist/rules/credential-password-field.d.ts.map +1 -1
  13. package/dist/rules/credential-password-field.js +25 -14
  14. package/dist/rules/credential-password-field.js.map +1 -1
  15. package/dist/rules/credential-test-required.d.ts +1 -2
  16. package/dist/rules/credential-test-required.d.ts.map +1 -1
  17. package/dist/rules/credential-test-required.js +43 -5
  18. package/dist/rules/credential-test-required.js.map +1 -1
  19. package/dist/rules/icon-validation.d.ts +1 -2
  20. package/dist/rules/icon-validation.d.ts.map +1 -1
  21. package/dist/rules/icon-validation.js +81 -15
  22. package/dist/rules/icon-validation.js.map +1 -1
  23. package/dist/rules/index.d.ts +9 -5
  24. package/dist/rules/index.d.ts.map +1 -1
  25. package/dist/rules/index.js +7 -5
  26. package/dist/rules/index.js.map +1 -1
  27. package/dist/rules/no-credential-reuse.d.ts +1 -2
  28. package/dist/rules/no-credential-reuse.d.ts.map +1 -1
  29. package/dist/rules/no-credential-reuse.js +33 -4
  30. package/dist/rules/no-credential-reuse.js.map +1 -1
  31. package/dist/rules/no-deprecated-workflow-functions.d.ts +1 -2
  32. package/dist/rules/no-deprecated-workflow-functions.d.ts.map +1 -1
  33. package/dist/rules/no-deprecated-workflow-functions.js +38 -10
  34. package/dist/rules/no-deprecated-workflow-functions.js.map +1 -1
  35. package/dist/rules/no-restricted-globals.d.ts +2 -2
  36. package/dist/rules/no-restricted-globals.d.ts.map +1 -1
  37. package/dist/rules/no-restricted-globals.js +5 -3
  38. package/dist/rules/no-restricted-globals.js.map +1 -1
  39. package/dist/rules/no-restricted-imports.d.ts +1 -2
  40. package/dist/rules/no-restricted-imports.d.ts.map +1 -1
  41. package/dist/rules/no-restricted-imports.js +3 -3
  42. package/dist/rules/no-restricted-imports.js.map +1 -1
  43. package/dist/rules/node-usable-as-tool.d.ts +1 -2
  44. package/dist/rules/node-usable-as-tool.d.ts.map +1 -1
  45. package/dist/rules/node-usable-as-tool.js +6 -5
  46. package/dist/rules/node-usable-as-tool.js.map +1 -1
  47. package/dist/rules/package-name-convention.d.ts +1 -2
  48. package/dist/rules/package-name-convention.d.ts.map +1 -1
  49. package/dist/rules/package-name-convention.js +38 -2
  50. package/dist/rules/package-name-convention.js.map +1 -1
  51. package/dist/rules/resource-operation-pattern.d.ts +1 -2
  52. package/dist/rules/resource-operation-pattern.d.ts.map +1 -1
  53. package/dist/rules/resource-operation-pattern.js +9 -7
  54. package/dist/rules/resource-operation-pattern.js.map +1 -1
  55. package/dist/utils/ast-utils.d.ts +2 -1
  56. package/dist/utils/ast-utils.d.ts.map +1 -1
  57. package/dist/utils/ast-utils.js +37 -19
  58. package/dist/utils/ast-utils.js.map +1 -1
  59. package/dist/utils/file-utils.d.ts +14 -0
  60. package/dist/utils/file-utils.d.ts.map +1 -1
  61. package/dist/utils/file-utils.js +85 -18
  62. package/dist/utils/file-utils.js.map +1 -1
  63. package/dist/utils/index.d.ts +1 -0
  64. package/dist/utils/index.d.ts.map +1 -1
  65. package/dist/utils/index.js +1 -0
  66. package/dist/utils/index.js.map +1 -1
  67. package/dist/utils/rule-creator.d.ts +3 -0
  68. package/dist/utils/rule-creator.d.ts.map +1 -0
  69. package/dist/utils/rule-creator.js +5 -0
  70. package/dist/utils/rule-creator.js.map +1 -0
  71. package/docs/rules/credential-documentation-url.md +94 -0
  72. package/docs/rules/credential-password-field.md +45 -0
  73. package/docs/rules/credential-test-required.md +58 -0
  74. package/docs/rules/icon-validation.md +67 -0
  75. package/docs/rules/no-credential-reuse.md +82 -0
  76. package/docs/rules/no-deprecated-workflow-functions.md +61 -0
  77. package/docs/rules/no-restricted-globals.md +44 -0
  78. package/docs/rules/no-restricted-imports.md +47 -0
  79. package/docs/rules/node-usable-as-tool.md +43 -0
  80. package/docs/rules/package-name-convention.md +52 -0
  81. package/docs/rules/resource-operation-pattern.md +84 -0
  82. package/eslint.config.mjs +27 -0
  83. package/package.json +25 -4
  84. package/src/plugin.ts +30 -26
  85. package/src/rules/credential-documentation-url.test.ts +306 -0
  86. package/src/rules/credential-documentation-url.ts +129 -0
  87. package/src/rules/credential-password-field.test.ts +1 -0
  88. package/src/rules/credential-password-field.ts +34 -16
  89. package/src/rules/credential-test-required.test.ts +84 -57
  90. package/src/rules/credential-test-required.ts +51 -5
  91. package/src/rules/icon-validation.test.ts +97 -14
  92. package/src/rules/icon-validation.ts +95 -14
  93. package/src/rules/index.ts +8 -5
  94. package/src/rules/no-credential-reuse.test.ts +306 -58
  95. package/src/rules/no-credential-reuse.ts +43 -3
  96. package/src/rules/no-deprecated-workflow-functions.test.ts +70 -0
  97. package/src/rules/no-deprecated-workflow-functions.ts +44 -10
  98. package/src/rules/no-restricted-globals.test.ts +1 -0
  99. package/src/rules/no-restricted-globals.ts +6 -3
  100. package/src/rules/no-restricted-imports.test.ts +1 -0
  101. package/src/rules/no-restricted-imports.ts +8 -3
  102. package/src/rules/node-usable-as-tool.test.ts +1 -0
  103. package/src/rules/node-usable-as-tool.ts +8 -6
  104. package/src/rules/package-name-convention.test.ts +82 -5
  105. package/src/rules/package-name-convention.ts +46 -2
  106. package/src/rules/resource-operation-pattern.test.ts +1 -0
  107. package/src/rules/resource-operation-pattern.ts +13 -6
  108. package/src/utils/ast-utils.ts +47 -19
  109. package/src/utils/file-utils.ts +108 -18
  110. package/src/utils/index.ts +1 -0
  111. package/src/utils/rule-creator.ts +6 -0
  112. package/tsconfig.build.json +4 -0
  113. package/tsconfig.eslint.json +5 -0
  114. package/tsconfig.json +1 -2
@@ -1,6 +1,9 @@
1
- import { readFileSync, existsSync } from 'node:fs';
2
- import { join, dirname, parse as parsePath } from 'node:path';
3
- import { parse, simpleTraverse, TSESTree } from '@typescript-eslint/typescript-estree';
1
+ import type { TSESTree } from '@typescript-eslint/typescript-estree';
2
+ import { parse, simpleTraverse, AST_NODE_TYPES } from '@typescript-eslint/typescript-estree';
3
+ import { readFileSync, existsSync, readdirSync } from 'node:fs';
4
+ import * as path from 'node:path';
5
+ import { dirname, parse as parsePath } from 'node:path';
6
+
4
7
  import {
5
8
  isCredentialTypeClass,
6
9
  isNodeTypeClass,
@@ -8,14 +11,49 @@ import {
8
11
  getStringLiteralValue,
9
12
  findArrayLiteralProperty,
10
13
  extractCredentialInfoFromArray,
14
+ findSimilarStrings,
11
15
  } from './ast-utils.js';
12
16
 
17
+ /**
18
+ * Checks if the given childPath is contained within the parentPath. Resolves
19
+ * the paths before comparing them, so that relative paths are also supported.
20
+ */
21
+ export function isContainedWithin(parentPath: string, childPath: string): boolean {
22
+ parentPath = path.resolve(parentPath);
23
+ childPath = path.resolve(childPath);
24
+
25
+ if (parentPath === childPath) {
26
+ return true;
27
+ }
28
+
29
+ return childPath.startsWith(parentPath + path.sep);
30
+ }
31
+
32
+ /**
33
+ * Joins the given paths to the parentPath, ensuring that the resulting path
34
+ * is still contained within the parentPath. If not, it throws an error to
35
+ * prevent path traversal vulnerabilities.
36
+ *
37
+ * @throws {UnexpectedError} If the resulting path is not contained within the parentPath.
38
+ */
39
+ export function safeJoinPath(parentPath: string, ...paths: string[]): string {
40
+ const candidate = path.join(parentPath, ...paths);
41
+
42
+ if (!isContainedWithin(parentPath, candidate)) {
43
+ throw new Error(
44
+ `Path traversal detected, refusing to join paths: ${parentPath} and ${JSON.stringify(paths)}`,
45
+ );
46
+ }
47
+
48
+ return candidate;
49
+ }
50
+
13
51
  export function findPackageJson(startPath: string): string | null {
14
- let currentDir = startPath;
52
+ let currentDir = path.dirname(startPath);
15
53
 
16
54
  while (parsePath(currentDir).dir !== parsePath(currentDir).root) {
17
- const testPath = join(currentDir, 'package.json');
18
- if (existsSync(testPath)) {
55
+ const testPath = safeJoinPath(currentDir, 'package.json');
56
+ if (fileExistsWithCaseSync(testPath)) {
19
57
  return testPath;
20
58
  }
21
59
 
@@ -25,10 +63,24 @@ export function findPackageJson(startPath: string): string | null {
25
63
  return null;
26
64
  }
27
65
 
28
- function readPackageJsonN8n(packageJsonPath: string): any {
66
+ interface PackageJsonN8n {
67
+ credentials?: string[];
68
+ nodes?: string[];
69
+ [key: string]: unknown;
70
+ }
71
+
72
+ function isValidPackageJson(obj: unknown): obj is { n8n?: PackageJsonN8n } {
73
+ return typeof obj === 'object' && obj !== null;
74
+ }
75
+
76
+ function readPackageJsonN8n(packageJsonPath: string): PackageJsonN8n {
29
77
  try {
30
- const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
31
- return packageJson.n8n || {};
78
+ const content = readFileSync(packageJsonPath, 'utf8');
79
+ const parsed: unknown = JSON.parse(content);
80
+ if (isValidPackageJson(parsed)) {
81
+ return parsed.n8n ?? {};
82
+ }
83
+ return {};
32
84
  } catch {
33
85
  return {};
34
86
  }
@@ -40,7 +92,7 @@ function resolveN8nFilePaths(packageJsonPath: string, filePaths: string[]): stri
40
92
 
41
93
  for (const filePath of filePaths) {
42
94
  const sourcePath = filePath.replace(/^dist\//, '').replace(/\.js$/, '.ts');
43
- const fullSourcePath = join(packageDir, sourcePath);
95
+ const fullSourcePath = safeJoinPath(packageDir, sourcePath);
44
96
 
45
97
  if (existsSync(fullSourcePath)) {
46
98
  resolvedFiles.push(fullSourcePath);
@@ -52,7 +104,7 @@ function resolveN8nFilePaths(packageJsonPath: string, filePaths: string[]): stri
52
104
 
53
105
  export function readPackageJsonCredentials(packageJsonPath: string): Set<string> {
54
106
  const n8nConfig = readPackageJsonN8n(packageJsonPath);
55
- const credentialPaths = n8nConfig.credentials || [];
107
+ const credentialPaths = n8nConfig.credentials ?? [];
56
108
  const credentialFiles = resolveN8nFilePaths(packageJsonPath, credentialPaths);
57
109
  const credentialNames: string[] = [];
58
110
 
@@ -82,7 +134,7 @@ export function extractCredentialNameFromFile(credentialFilePath: string): strin
82
134
 
83
135
  simpleTraverse(ast, {
84
136
  enter(node: TSESTree.Node) {
85
- if (node.type === 'ClassDeclaration' && isCredentialTypeClass(node)) {
137
+ if (node.type === AST_NODE_TYPES.ClassDeclaration && isCredentialTypeClass(node)) {
86
138
  const nameProperty = findClassProperty(node, 'name');
87
139
  if (nameProperty) {
88
140
  const nameValue = getStringLiteralValue(nameProperty.value);
@@ -112,8 +164,9 @@ export function validateIconPath(
112
164
  const isFile = iconPath.startsWith('file:');
113
165
  const relativePath = iconPath.replace(/^file:/, '');
114
166
  const isSvg = relativePath.endsWith('.svg');
115
- const fullPath = join(baseDir, relativePath);
116
- const exists = existsSync(fullPath);
167
+ // Should not use safeJoinPath here because iconPath can be outside of the node class folder
168
+ const fullPath = path.join(baseDir, relativePath);
169
+ const exists = fileExistsWithCaseSync(fullPath);
117
170
 
118
171
  return {
119
172
  isValid: isFile && isSvg && exists,
@@ -125,7 +178,7 @@ export function validateIconPath(
125
178
 
126
179
  export function readPackageJsonNodes(packageJsonPath: string): string[] {
127
180
  const n8nConfig = readPackageJsonN8n(packageJsonPath);
128
- const nodePaths = n8nConfig.nodes || [];
181
+ const nodePaths = n8nConfig.nodes ?? [];
129
182
  return resolveN8nFilePaths(packageJsonPath, nodePaths);
130
183
  }
131
184
 
@@ -133,7 +186,7 @@ export function areAllCredentialUsagesTestedByNodes(
133
186
  credentialName: string,
134
187
  packageDir: string,
135
188
  ): boolean {
136
- const packageJsonPath = join(packageDir, 'package.json');
189
+ const packageJsonPath = safeJoinPath(packageDir, 'package.json');
137
190
  if (!existsSync(packageJsonPath)) {
138
191
  return false;
139
192
  }
@@ -167,11 +220,11 @@ function checkCredentialUsageInFile(
167
220
 
168
221
  simpleTraverse(ast, {
169
222
  enter(node: TSESTree.Node) {
170
- if (node.type === 'ClassDeclaration' && isNodeTypeClass(node)) {
223
+ if (node.type === AST_NODE_TYPES.ClassDeclaration && isNodeTypeClass(node)) {
171
224
  const descriptionProperty = findClassProperty(node, 'description');
172
225
  if (
173
226
  !descriptionProperty?.value ||
174
- descriptionProperty.value.type !== 'ObjectExpression'
227
+ descriptionProperty.value.type !== AST_NODE_TYPES.ObjectExpression
175
228
  ) {
176
229
  return;
177
230
  }
@@ -202,3 +255,40 @@ function checkCredentialUsageInFile(
202
255
  return { hasUsage: false, allTestedBy: true };
203
256
  }
204
257
  }
258
+
259
+ function fileExistsWithCaseSync(filePath: string): boolean {
260
+ try {
261
+ const dir = path.dirname(filePath);
262
+ const file = path.basename(filePath);
263
+ const files = new Set(readdirSync(dir));
264
+
265
+ return files.has(file);
266
+ } catch {
267
+ return false;
268
+ }
269
+ }
270
+
271
+ export function findSimilarSvgFiles(targetPath: string, baseDir: string): string[] {
272
+ try {
273
+ const targetFileName = path.basename(targetPath, path.extname(targetPath));
274
+ const targetDir = path.dirname(targetPath);
275
+ // Should not use safeJoinPath here because iconPath can be outside of the node class folder
276
+ const searchDir = path.join(baseDir, targetDir);
277
+
278
+ if (!existsSync(searchDir)) {
279
+ return [];
280
+ }
281
+
282
+ const files = readdirSync(searchDir);
283
+ const svgFileNames = files
284
+ .filter((file) => file.endsWith('.svg'))
285
+ .map((file) => path.basename(file, '.svg'));
286
+
287
+ const candidateNames = new Set(svgFileNames);
288
+ const similarNames = findSimilarStrings(targetFileName, candidateNames);
289
+
290
+ return similarNames.map((name) => path.join(targetDir, `${name}.svg`));
291
+ } catch {
292
+ return [];
293
+ }
294
+ }
@@ -1,2 +1,3 @@
1
1
  export * from './ast-utils.js';
2
2
  export * from './file-utils.js';
3
+ export * from './rule-creator.js';
@@ -0,0 +1,6 @@
1
+ import { ESLintUtils } from '@typescript-eslint/utils';
2
+
3
+ const REPO_URL = 'https://github.com/n8n-io/n8n';
4
+ const DOCS_PATH = 'blob/master/packages/@n8n/eslint-plugin-community-nodes/docs/rules';
5
+
6
+ export const createRule = ESLintUtils.RuleCreator((name) => `${REPO_URL}/${DOCS_PATH}/${name}.md`);
@@ -0,0 +1,4 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "exclude": ["**/*.test.ts", "**/*.spec.ts"]
4
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "include": ["src/**/*.ts"],
4
+ "exclude": []
5
+ }
package/tsconfig.json CHANGED
@@ -6,6 +6,5 @@
6
6
  "outDir": "dist",
7
7
  "types": ["vitest/globals"]
8
8
  },
9
- "include": ["src/**/*.ts"],
10
- "exclude": ["**/*.test.ts"]
9
+ "include": ["src/**/*.ts"]
11
10
  }