@n8n/eslint-plugin-community-nodes 0.26.0 → 0.28.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 (92) 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 +7 -1
  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-restricted-globals.d.ts +1 -1
  27. package/dist/rules/no-restricted-globals.d.ts.map +1 -1
  28. package/dist/rules/no-restricted-globals.js +18 -5
  29. package/dist/rules/no-restricted-globals.js.map +1 -1
  30. package/dist/rules/no-silent-error-swallowing.d.ts +2 -0
  31. package/dist/rules/no-silent-error-swallowing.d.ts.map +1 -0
  32. package/dist/rules/no-silent-error-swallowing.js +112 -0
  33. package/dist/rules/no-silent-error-swallowing.js.map +1 -0
  34. package/dist/rules/require-files-array.d.ts +2 -0
  35. package/dist/rules/require-files-array.d.ts.map +1 -0
  36. package/dist/rules/require-files-array.js +41 -0
  37. package/dist/rules/require-files-array.js.map +1 -0
  38. package/dist/rules/require-homepage.d.ts +2 -0
  39. package/dist/rules/require-homepage.d.ts.map +1 -0
  40. package/dist/rules/require-homepage.js +52 -0
  41. package/dist/rules/require-homepage.js.map +1 -0
  42. package/dist/rules/webhook-lifecycle-complete.d.ts.map +1 -1
  43. package/dist/rules/webhook-lifecycle-complete.js +2 -3
  44. package/dist/rules/webhook-lifecycle-complete.js.map +1 -1
  45. package/dist/typecheck.tsbuildinfo +1 -1
  46. package/dist/utils/constants.d.ts +5 -0
  47. package/dist/utils/constants.d.ts.map +1 -0
  48. package/dist/utils/constants.js +15 -0
  49. package/dist/utils/constants.js.map +1 -0
  50. package/dist/utils/credential-fields.d.ts +20 -0
  51. package/dist/utils/credential-fields.d.ts.map +1 -0
  52. package/dist/utils/credential-fields.js +45 -0
  53. package/dist/utils/credential-fields.js.map +1 -0
  54. package/dist/utils/file-utils.d.ts +6 -0
  55. package/dist/utils/file-utils.d.ts.map +1 -1
  56. package/dist/utils/file-utils.js +16 -0
  57. package/dist/utils/file-utils.js.map +1 -1
  58. package/dist/utils/index.d.ts +2 -0
  59. package/dist/utils/index.d.ts.map +1 -1
  60. package/dist/utils/index.js +2 -0
  61. package/dist/utils/index.js.map +1 -1
  62. package/docs/rules/credential-password-field.md +3 -1
  63. package/docs/rules/credential-unnecessary-password.md +46 -0
  64. package/docs/rules/no-dead-files.md +56 -0
  65. package/docs/rules/no-hardcoded-secrets.md +59 -0
  66. package/docs/rules/no-restricted-globals.md +14 -0
  67. package/docs/rules/no-silent-error-swallowing.md +72 -0
  68. package/docs/rules/require-files-array.md +43 -0
  69. package/docs/rules/require-homepage.md +49 -0
  70. package/package.json +2 -2
  71. package/src/plugin.ts +12 -0
  72. package/src/rules/credential-password-field.ts +4 -40
  73. package/src/rules/credential-unnecessary-password.test.ts +105 -0
  74. package/src/rules/credential-unnecessary-password.ts +72 -0
  75. package/src/rules/index.ts +12 -0
  76. package/src/rules/no-dead-files.test.ts +128 -0
  77. package/src/rules/no-dead-files.ts +71 -0
  78. package/src/rules/no-hardcoded-secrets.test.ts +124 -0
  79. package/src/rules/no-hardcoded-secrets.ts +161 -0
  80. package/src/rules/no-restricted-globals.test.ts +26 -2
  81. package/src/rules/no-restricted-globals.ts +22 -5
  82. package/src/rules/no-silent-error-swallowing.test.ts +245 -0
  83. package/src/rules/no-silent-error-swallowing.ts +127 -0
  84. package/src/rules/require-files-array.test.ts +57 -0
  85. package/src/rules/require-files-array.ts +50 -0
  86. package/src/rules/require-homepage.test.ts +80 -0
  87. package/src/rules/require-homepage.ts +59 -0
  88. package/src/rules/webhook-lifecycle-complete.ts +4 -5
  89. package/src/utils/constants.ts +21 -0
  90. package/src/utils/credential-fields.ts +57 -0
  91. package/src/utils/file-utils.ts +23 -0
  92. package/src/utils/index.ts +2 -0
@@ -0,0 +1,127 @@
1
+ import { AST_NODE_TYPES, type TSESTree } from '@typescript-eslint/utils';
2
+
3
+ import {
4
+ createRule,
5
+ findClassProperty,
6
+ isNodeTypeClass,
7
+ WEBHOOK_LIFECYCLE_METHODS,
8
+ } from '../utils/index.js';
9
+
10
+ const LIFECYCLE_METHODS: readonly string[] = WEBHOOK_LIFECYCLE_METHODS;
11
+
12
+ /** Returns the static name of a non-computed object property key, or null. */
13
+ function getPropertyName(property: TSESTree.Property): string | null {
14
+ if (property.computed) return null;
15
+ if (property.key.type === AST_NODE_TYPES.Identifier) return property.key.name;
16
+ if (property.key.type === AST_NODE_TYPES.Literal) return String(property.key.value);
17
+ return null;
18
+ }
19
+
20
+ function isFunctionNode(node: TSESTree.Node): boolean {
21
+ return (
22
+ node.type === AST_NODE_TYPES.FunctionDeclaration ||
23
+ node.type === AST_NODE_TYPES.FunctionExpression ||
24
+ node.type === AST_NODE_TYPES.ArrowFunctionExpression
25
+ );
26
+ }
27
+
28
+ /** Walks up to the function that lexically encloses `node`, or null at module scope. */
29
+ function getEnclosingFunction(node: TSESTree.Node): TSESTree.Node | null {
30
+ let current = node.parent;
31
+ while (current) {
32
+ if (isFunctionNode(current)) return current;
33
+ current = current.parent;
34
+ }
35
+ return null;
36
+ }
37
+
38
+ /** A bare `return;` or `return true/false;` with no other work in the block. */
39
+ function isSilentReturn(statement: TSESTree.Statement): boolean {
40
+ if (statement.type !== AST_NODE_TYPES.ReturnStatement) return false;
41
+ const argument = statement.argument;
42
+ if (argument === null) return true;
43
+ return argument.type === AST_NODE_TYPES.Literal && typeof argument.value === 'boolean';
44
+ }
45
+
46
+ export const NoSilentErrorSwallowingRule = createRule({
47
+ name: 'no-silent-error-swallowing',
48
+ meta: {
49
+ type: 'problem',
50
+ docs: {
51
+ description:
52
+ 'Disallow webhook lifecycle methods (checkExists, create, delete) from silently swallowing errors in catch blocks',
53
+ },
54
+ messages: {
55
+ emptyCatch:
56
+ 'Empty catch block in webhook lifecycle method `{{method}}` silently swallows the error. Log the error or rethrow it so failures surface instead of being hidden.',
57
+ silentReturn:
58
+ 'Catch block in webhook lifecycle method `{{method}}` swallows the error and only returns without logging. Log the error (or rethrow) so failures surface instead of being hidden.',
59
+ },
60
+ schema: [],
61
+ },
62
+ defaultOptions: [],
63
+ create(context) {
64
+ // Maps each lifecycle method's function node to its method name. Populated
65
+ // when the enclosing class is visited (before its `catch` clauses), so a
66
+ // `catch` is only flagged when its *nearest* enclosing function is a
67
+ // lifecycle method — catches inside nested callbacks are left alone.
68
+ const lifecycleMethodByFn = new WeakMap<TSESTree.Node, string>();
69
+
70
+ return {
71
+ ClassDeclaration(node) {
72
+ if (!isNodeTypeClass(node)) return;
73
+
74
+ const webhookMethodsProperty = findClassProperty(node, 'webhookMethods');
75
+ if (webhookMethodsProperty?.value?.type !== AST_NODE_TYPES.ObjectExpression) return;
76
+
77
+ for (const groupProperty of webhookMethodsProperty.value.properties) {
78
+ if (groupProperty.type !== AST_NODE_TYPES.Property) continue;
79
+ if (groupProperty.value.type !== AST_NODE_TYPES.ObjectExpression) continue;
80
+
81
+ for (const methodProperty of groupProperty.value.properties) {
82
+ if (methodProperty.type !== AST_NODE_TYPES.Property) continue;
83
+
84
+ const methodName = getPropertyName(methodProperty);
85
+ if (methodName === null || !LIFECYCLE_METHODS.includes(methodName)) continue;
86
+
87
+ const fn = methodProperty.value;
88
+ if (
89
+ fn.type === AST_NODE_TYPES.FunctionExpression ||
90
+ fn.type === AST_NODE_TYPES.ArrowFunctionExpression
91
+ ) {
92
+ lifecycleMethodByFn.set(fn, methodName);
93
+ }
94
+ }
95
+ }
96
+ },
97
+
98
+ CatchClause(node) {
99
+ const enclosingFn = getEnclosingFunction(node);
100
+ if (enclosingFn === null) return;
101
+
102
+ const methodName = lifecycleMethodByFn.get(enclosingFn);
103
+ if (methodName === undefined) return;
104
+
105
+ const statements = node.body.body;
106
+
107
+ if (statements.length === 0) {
108
+ context.report({
109
+ node: node.body,
110
+ messageId: 'emptyCatch',
111
+ data: { method: methodName },
112
+ });
113
+ return;
114
+ }
115
+
116
+ const onlyStatement = statements.length === 1 ? statements[0] : undefined;
117
+ if (onlyStatement && isSilentReturn(onlyStatement)) {
118
+ context.report({
119
+ node: onlyStatement,
120
+ messageId: 'silentReturn',
121
+ data: { method: methodName },
122
+ });
123
+ }
124
+ },
125
+ };
126
+ },
127
+ });
@@ -0,0 +1,57 @@
1
+ import { RuleTester } from '@typescript-eslint/rule-tester';
2
+
3
+ import { RequireFilesArrayRule } from './require-files-array.js';
4
+
5
+ const ruleTester = new RuleTester();
6
+
7
+ ruleTester.run('require-files-array', RequireFilesArrayRule, {
8
+ valid: [
9
+ {
10
+ name: 'files array with entries',
11
+ filename: 'package.json',
12
+ code: '{ "name": "n8n-nodes-example", "files": ["dist"] }',
13
+ },
14
+ {
15
+ name: 'files array with multiple entries',
16
+ filename: 'package.json',
17
+ code: '{ "files": ["dist", "credentials"] }',
18
+ },
19
+ {
20
+ name: 'non-package.json file is ignored',
21
+ filename: 'some-config.json',
22
+ code: '{ "name": "n8n-nodes-example" }',
23
+ },
24
+ ],
25
+ invalid: [
26
+ {
27
+ name: 'missing files field entirely',
28
+ filename: 'package.json',
29
+ code: '{ "name": "n8n-nodes-example", "version": "1.0.0" }',
30
+ errors: [{ messageId: 'filesMissing' }],
31
+ },
32
+ {
33
+ name: 'empty package.json object',
34
+ filename: 'package.json',
35
+ code: '{}',
36
+ errors: [{ messageId: 'filesMissing' }],
37
+ },
38
+ {
39
+ name: 'files as empty array',
40
+ filename: 'package.json',
41
+ code: '{ "files": [] }',
42
+ errors: [{ messageId: 'filesEmpty' }],
43
+ },
44
+ {
45
+ name: 'files as string instead of array',
46
+ filename: 'package.json',
47
+ code: '{ "files": "dist" }',
48
+ errors: [{ messageId: 'filesEmpty' }],
49
+ },
50
+ {
51
+ name: 'files as non-array value',
52
+ filename: 'package.json',
53
+ code: '{ "files": null }',
54
+ errors: [{ messageId: 'filesEmpty' }],
55
+ },
56
+ ],
57
+ });
@@ -0,0 +1,50 @@
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
+ export const RequireFilesArrayRule = createRule({
7
+ name: 'require-files-array',
8
+ meta: {
9
+ type: 'problem',
10
+ docs: {
11
+ description: 'Require a non-empty "files" array in package.json',
12
+ },
13
+ messages: {
14
+ filesMissing:
15
+ 'package.json must have a "files" field with a non-empty array of files to publish.',
16
+ filesEmpty: 'The "files" field must be a non-empty array.',
17
+ },
18
+ schema: [],
19
+ },
20
+ defaultOptions: [],
21
+ create(context) {
22
+ if (!context.filename.endsWith('package.json')) {
23
+ return {};
24
+ }
25
+
26
+ return {
27
+ ObjectExpression(node: TSESTree.ObjectExpression) {
28
+ const root = getTopLevelObjectInJson(node);
29
+ if (!root) {
30
+ return;
31
+ }
32
+
33
+ const filesProp = findJsonProperty(root, 'files');
34
+ if (!filesProp) {
35
+ context.report({ node: root, messageId: 'filesMissing' });
36
+ return;
37
+ }
38
+
39
+ const filesValue = filesProp.value;
40
+
41
+ if (
42
+ filesValue.type !== AST_NODE_TYPES.ArrayExpression ||
43
+ filesValue.elements.length === 0
44
+ ) {
45
+ context.report({ node: filesProp, messageId: 'filesEmpty' });
46
+ }
47
+ },
48
+ };
49
+ },
50
+ });
@@ -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';