@n8n/eslint-plugin-community-nodes 0.26.0 → 0.27.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 (85) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/README.md +7 -1
  3. package/dist/plugin.d.ts +36 -0
  4. package/dist/plugin.d.ts.map +1 -1
  5. package/dist/plugin.js +12 -0
  6. package/dist/plugin.js.map +1 -1
  7. package/dist/rules/credential-password-field.d.ts.map +1 -1
  8. package/dist/rules/credential-password-field.js +3 -31
  9. package/dist/rules/credential-password-field.js.map +1 -1
  10. package/dist/rules/credential-unnecessary-password.d.ts +2 -0
  11. package/dist/rules/credential-unnecessary-password.d.ts.map +1 -0
  12. package/dist/rules/credential-unnecessary-password.js +54 -0
  13. package/dist/rules/credential-unnecessary-password.js.map +1 -0
  14. package/dist/rules/index.d.ts +6 -0
  15. package/dist/rules/index.d.ts.map +1 -1
  16. package/dist/rules/index.js +12 -0
  17. package/dist/rules/index.js.map +1 -1
  18. package/dist/rules/no-dead-files.d.ts +7 -0
  19. package/dist/rules/no-dead-files.d.ts.map +1 -0
  20. package/dist/rules/no-dead-files.js +57 -0
  21. package/dist/rules/no-dead-files.js.map +1 -0
  22. package/dist/rules/no-hardcoded-secrets.d.ts +2 -0
  23. package/dist/rules/no-hardcoded-secrets.d.ts.map +1 -0
  24. package/dist/rules/no-hardcoded-secrets.js +147 -0
  25. package/dist/rules/no-hardcoded-secrets.js.map +1 -0
  26. package/dist/rules/no-silent-error-swallowing.d.ts +2 -0
  27. package/dist/rules/no-silent-error-swallowing.d.ts.map +1 -0
  28. package/dist/rules/no-silent-error-swallowing.js +112 -0
  29. package/dist/rules/no-silent-error-swallowing.js.map +1 -0
  30. package/dist/rules/require-files-array.d.ts +2 -0
  31. package/dist/rules/require-files-array.d.ts.map +1 -0
  32. package/dist/rules/require-files-array.js +41 -0
  33. package/dist/rules/require-files-array.js.map +1 -0
  34. package/dist/rules/require-homepage.d.ts +2 -0
  35. package/dist/rules/require-homepage.d.ts.map +1 -0
  36. package/dist/rules/require-homepage.js +52 -0
  37. package/dist/rules/require-homepage.js.map +1 -0
  38. package/dist/rules/webhook-lifecycle-complete.d.ts.map +1 -1
  39. package/dist/rules/webhook-lifecycle-complete.js +2 -3
  40. package/dist/rules/webhook-lifecycle-complete.js.map +1 -1
  41. package/dist/typecheck.tsbuildinfo +1 -1
  42. package/dist/utils/constants.d.ts +5 -0
  43. package/dist/utils/constants.d.ts.map +1 -0
  44. package/dist/utils/constants.js +15 -0
  45. package/dist/utils/constants.js.map +1 -0
  46. package/dist/utils/credential-fields.d.ts +20 -0
  47. package/dist/utils/credential-fields.d.ts.map +1 -0
  48. package/dist/utils/credential-fields.js +45 -0
  49. package/dist/utils/credential-fields.js.map +1 -0
  50. package/dist/utils/file-utils.d.ts +6 -0
  51. package/dist/utils/file-utils.d.ts.map +1 -1
  52. package/dist/utils/file-utils.js +16 -0
  53. package/dist/utils/file-utils.js.map +1 -1
  54. package/dist/utils/index.d.ts +2 -0
  55. package/dist/utils/index.d.ts.map +1 -1
  56. package/dist/utils/index.js +2 -0
  57. package/dist/utils/index.js.map +1 -1
  58. package/docs/rules/credential-password-field.md +3 -1
  59. package/docs/rules/credential-unnecessary-password.md +46 -0
  60. package/docs/rules/no-dead-files.md +56 -0
  61. package/docs/rules/no-hardcoded-secrets.md +59 -0
  62. package/docs/rules/no-silent-error-swallowing.md +72 -0
  63. package/docs/rules/require-files-array.md +43 -0
  64. package/docs/rules/require-homepage.md +49 -0
  65. package/package.json +2 -2
  66. package/src/plugin.ts +12 -0
  67. package/src/rules/credential-password-field.ts +4 -40
  68. package/src/rules/credential-unnecessary-password.test.ts +105 -0
  69. package/src/rules/credential-unnecessary-password.ts +72 -0
  70. package/src/rules/index.ts +12 -0
  71. package/src/rules/no-dead-files.test.ts +128 -0
  72. package/src/rules/no-dead-files.ts +71 -0
  73. package/src/rules/no-hardcoded-secrets.test.ts +124 -0
  74. package/src/rules/no-hardcoded-secrets.ts +161 -0
  75. package/src/rules/no-silent-error-swallowing.test.ts +245 -0
  76. package/src/rules/no-silent-error-swallowing.ts +127 -0
  77. package/src/rules/require-files-array.test.ts +57 -0
  78. package/src/rules/require-files-array.ts +50 -0
  79. package/src/rules/require-homepage.test.ts +80 -0
  80. package/src/rules/require-homepage.ts +59 -0
  81. package/src/rules/webhook-lifecycle-complete.ts +4 -5
  82. package/src/utils/constants.ts +21 -0
  83. package/src/utils/credential-fields.ts +57 -0
  84. package/src/utils/file-utils.ts +23 -0
  85. package/src/utils/index.ts +2 -0
@@ -0,0 +1,80 @@
1
+ import { RuleTester } from '@typescript-eslint/rule-tester';
2
+
3
+ import { RequireHomepageRule } from './require-homepage.js';
4
+
5
+ const ruleTester = new RuleTester();
6
+
7
+ ruleTester.run('require-homepage', RequireHomepageRule, {
8
+ valid: [
9
+ {
10
+ name: 'homepage with an https url',
11
+ filename: 'package.json',
12
+ code: '{ "name": "n8n-nodes-example", "homepage": "https://example.com" }',
13
+ },
14
+ {
15
+ name: 'homepage with a repository url and path',
16
+ filename: 'package.json',
17
+ code: '{ "homepage": "https://github.com/acme/n8n-nodes-example#readme" }',
18
+ },
19
+ {
20
+ name: 'non-package.json file is ignored',
21
+ filename: 'some-config.json',
22
+ code: '{ "name": "n8n-nodes-example" }',
23
+ },
24
+ {
25
+ name: 'nested objects are not checked',
26
+ filename: 'package.json',
27
+ code: '{ "homepage": "https://example.com", "config": { "nested": "value" } }',
28
+ },
29
+ ],
30
+ invalid: [
31
+ {
32
+ name: 'missing homepage field entirely',
33
+ filename: 'package.json',
34
+ code: '{ "name": "n8n-nodes-example", "version": "1.0.0" }',
35
+ errors: [{ messageId: 'homepageMissing' }],
36
+ },
37
+ {
38
+ name: 'empty package.json object',
39
+ filename: 'package.json',
40
+ code: '{}',
41
+ errors: [{ messageId: 'homepageMissing' }],
42
+ },
43
+ {
44
+ name: 'empty homepage string',
45
+ filename: 'package.json',
46
+ code: '{ "homepage": "" }',
47
+ errors: [{ messageId: 'homepageInvalid' }],
48
+ },
49
+ {
50
+ name: 'whitespace-only homepage string',
51
+ filename: 'package.json',
52
+ code: '{ "homepage": " " }',
53
+ errors: [{ messageId: 'homepageInvalid' }],
54
+ },
55
+ {
56
+ name: 'homepage without a protocol',
57
+ filename: 'package.json',
58
+ code: '{ "homepage": "example.com" }',
59
+ errors: [{ messageId: 'homepageInvalid' }],
60
+ },
61
+ {
62
+ name: 'placeholder homepage value',
63
+ filename: 'package.json',
64
+ code: '{ "homepage": "TODO" }',
65
+ errors: [{ messageId: 'homepageInvalid' }],
66
+ },
67
+ {
68
+ name: 'non-string homepage value',
69
+ filename: 'package.json',
70
+ code: '{ "homepage": 123 }',
71
+ errors: [{ messageId: 'homepageInvalid' }],
72
+ },
73
+ {
74
+ name: 'null homepage value',
75
+ filename: 'package.json',
76
+ code: '{ "homepage": null }',
77
+ errors: [{ messageId: 'homepageInvalid' }],
78
+ },
79
+ ],
80
+ });
@@ -0,0 +1,59 @@
1
+ import type { TSESTree } from '@typescript-eslint/utils';
2
+ import { AST_NODE_TYPES } from '@typescript-eslint/utils';
3
+
4
+ import { createRule, findJsonProperty, getTopLevelObjectInJson } from '../utils/index.js';
5
+
6
+ function isValidUrl(value: string): boolean {
7
+ try {
8
+ new URL(value);
9
+ return true;
10
+ } catch {
11
+ return false;
12
+ }
13
+ }
14
+
15
+ export const RequireHomepageRule = createRule({
16
+ name: 'require-homepage',
17
+ meta: {
18
+ type: 'suggestion',
19
+ docs: {
20
+ description: 'Require a "homepage" field with a valid URL in package.json',
21
+ },
22
+ messages: {
23
+ homepageMissing: 'package.json must have a "homepage" field.',
24
+ homepageInvalid: 'The "homepage" field must be a valid URL (e.g. "https://example.com").',
25
+ },
26
+ schema: [],
27
+ },
28
+ defaultOptions: [],
29
+ create(context) {
30
+ if (!context.filename.endsWith('package.json')) {
31
+ return {};
32
+ }
33
+
34
+ return {
35
+ ObjectExpression(node: TSESTree.ObjectExpression) {
36
+ const root = getTopLevelObjectInJson(node);
37
+ if (!root) {
38
+ return;
39
+ }
40
+
41
+ const homepageProp = findJsonProperty(root, 'homepage');
42
+ if (!homepageProp) {
43
+ context.report({ node: root, messageId: 'homepageMissing' });
44
+ return;
45
+ }
46
+
47
+ const homepageValue = homepageProp.value;
48
+ const isValidHomepage =
49
+ homepageValue.type === AST_NODE_TYPES.Literal &&
50
+ typeof homepageValue.value === 'string' &&
51
+ isValidUrl(homepageValue.value);
52
+
53
+ if (!isValidHomepage) {
54
+ context.report({ node: homepageProp, messageId: 'homepageInvalid' });
55
+ }
56
+ },
57
+ };
58
+ },
59
+ });
@@ -5,11 +5,10 @@ import {
5
5
  findClassProperty,
6
6
  findObjectProperty,
7
7
  isNodeTypeClass,
8
+ WEBHOOK_LIFECYCLE_METHODS,
9
+ type WebhookLifecycleMethod,
8
10
  } from '../utils/index.js';
9
11
 
10
- const REQUIRED_METHODS = ['checkExists', 'create', 'delete'] as const;
11
- type RequiredMethod = (typeof REQUIRED_METHODS)[number];
12
-
13
12
  /**
14
13
  * Returns true if the description declares webhook endpoints, indicating the
15
14
  * node is a webhook-based trigger that needs a complete lifecycle.
@@ -39,8 +38,8 @@ function isMethodProperty(property: TSESTree.ObjectLiteralElement, name: string)
39
38
  );
40
39
  }
41
40
 
42
- function findMissingMethods(group: TSESTree.ObjectExpression): RequiredMethod[] {
43
- return REQUIRED_METHODS.filter(
41
+ function findMissingMethods(group: TSESTree.ObjectExpression): WebhookLifecycleMethod[] {
42
+ return WEBHOOK_LIFECYCLE_METHODS.filter(
44
43
  (method) => !group.properties.some((property) => isMethodProperty(property, method)),
45
44
  );
46
45
  }
@@ -0,0 +1,21 @@
1
+ import type { WebhookSetupMethodNames } from 'n8n-workflow';
2
+
3
+ export type WebhookLifecycleMethod = WebhookSetupMethodNames;
4
+
5
+ /**
6
+ * n8n's canonical webhook lifecycle method names (`WebhookSetupMethodNames`) are
7
+ * a type, so they can't be iterated at lint time. This object mirrors them as a
8
+ * runtime value; the `satisfies Record<WebhookSetupMethodNames, true>` ties it to
9
+ * the source of truth — adding, removing, or renaming a method upstream breaks
10
+ * type-checking here until this list is updated.
11
+ */
12
+ const LIFECYCLE_METHOD_SET = {
13
+ checkExists: true,
14
+ create: true,
15
+ delete: true,
16
+ } as const satisfies Record<WebhookSetupMethodNames, true>;
17
+
18
+ /** The webhook trigger lifecycle methods, in the order n8n invokes them. */
19
+ export const WEBHOOK_LIFECYCLE_METHODS = Object.keys(
20
+ LIFECYCLE_METHOD_SET,
21
+ ) as readonly WebhookLifecycleMethod[];
@@ -0,0 +1,57 @@
1
+ import { TSESTree } from '@typescript-eslint/utils';
2
+
3
+ import { findObjectProperty, getBooleanLiteralValue } from './ast-utils.js';
4
+
5
+ const SENSITIVE_PATTERNS = [
6
+ 'password',
7
+ 'secret',
8
+ 'token',
9
+ 'cert',
10
+ 'passphrase',
11
+ 'apikey',
12
+ 'secretkey',
13
+ 'privatekey',
14
+ 'authkey',
15
+ ];
16
+
17
+ const NON_SENSITIVE_PATTERNS = ['url', 'pub', 'id'];
18
+
19
+ /**
20
+ * Whether a name looks like it holds a secret. A name is sensitive when it
21
+ * contains a sensitive pattern (or a caller-supplied `extra` one) and none of
22
+ * the `exclusions`. Exclusions win, so under the default set `tokenUrl` and
23
+ * `secretId` are not sensitive even though they contain `token`/`secret`.
24
+ *
25
+ * - Credential-field masking uses the default exclusions (a field named
26
+ * `tokenUrl` is a URL, not a secret to mask).
27
+ * - Pass `exclusions: []` to match on the raw patterns alone, e.g. when a
28
+ * second gate (like a value heuristic) already guards against false positives.
29
+ * - Pass `extra` to add caller-specific patterns without widening the shared set.
30
+ */
31
+ export function isSensitiveName(
32
+ name: string,
33
+ options: { extra?: string[]; exclusions?: string[] } = {},
34
+ ): boolean {
35
+ const { extra = [], exclusions = NON_SENSITIVE_PATTERNS } = options;
36
+ const lowerName = name.toLowerCase();
37
+
38
+ if (exclusions.some((pattern) => lowerName.includes(pattern))) {
39
+ return false;
40
+ }
41
+
42
+ return [...SENSITIVE_PATTERNS, ...extra].some((pattern) => lowerName.includes(pattern));
43
+ }
44
+
45
+ /** Whether the credential field element sets `typeOptions: { password: true }`. */
46
+ export function hasPasswordTypeOption(element: TSESTree.ObjectExpression): boolean {
47
+ const typeOptionsProperty = findObjectProperty(element, 'typeOptions');
48
+
49
+ if (typeOptionsProperty?.value.type !== TSESTree.AST_NODE_TYPES.ObjectExpression) {
50
+ return false;
51
+ }
52
+
53
+ const passwordProperty = findObjectProperty(typeOptionsProperty.value, 'password');
54
+ const passwordValue = passwordProperty ? getBooleanLiteralValue(passwordProperty.value) : null;
55
+
56
+ return passwordValue === true;
57
+ }
@@ -238,6 +238,29 @@ export function findNodeSourceFilesOnDisk(packageJsonPath: string): string[] {
238
238
  return findFilesRecursively(nodesDir, (fileName) => fileName.endsWith('.node.ts'));
239
239
  }
240
240
 
241
+ /**
242
+ * Recursively finds files matching `matches` within the given subdirectories of
243
+ * the package (relative to its `package.json`). Missing subdirectories are
244
+ * skipped. Returns absolute paths.
245
+ */
246
+ export function findFilesInPackageDirs(
247
+ packageJsonPath: string,
248
+ dirs: string[],
249
+ matches: (fileName: string) => boolean,
250
+ ): string[] {
251
+ const packageDir = dirname(packageJsonPath);
252
+ const results: string[] = [];
253
+
254
+ for (const dir of dirs) {
255
+ const fullPath = safeJoinPath(packageDir, dir);
256
+ if (existsSync(fullPath)) {
257
+ results.push(...findFilesRecursively(fullPath, matches));
258
+ }
259
+ }
260
+
261
+ return results;
262
+ }
263
+
241
264
  export function areAllCredentialUsagesTestedByNodes(
242
265
  credentialName: string,
243
266
  packageDir: string,
@@ -1,3 +1,5 @@
1
1
  export * from './ast-utils.js';
2
+ export * from './credential-fields.js';
3
+ export * from './constants.js';
2
4
  export * from './file-utils.js';
3
5
  export * from './rule-creator.js';