@fjall/eslint-plugin 2.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +50 -0
- package/README.md +31 -0
- package/constructor-validates-public-construct.js +351 -0
- package/human-readable-durations.js +103 -0
- package/iam-secrets-arn-suffix.js +79 -0
- package/index.js +61 -0
- package/mask-before-truncate.js +154 -0
- package/mask-error-message-at-boundary.js +673 -0
- package/no-bare-sdk-abort-timeout.js +79 -0
- package/no-classic-connected-account-assume.js +62 -0
- package/no-clickhouse-internal-reexport.js +99 -0
- package/no-duplicate-fjall-util-helper.js +275 -0
- package/no-empty-string-env-fallthrough.js +117 -0
- package/no-l2-asg-lifecycle-hook.js +61 -0
- package/no-mask-identity-mock.js +339 -0
- package/no-raw-block-device-volume.js +75 -0
- package/no-raw-cdk-properties-on-public-constructs.js +80 -0
- package/no-raw-db-transaction.js +112 -0
- package/no-throw-in-services.js +63 -0
- package/no-tier-stage-conflation.js +423 -0
- package/no-undefined-prop-in-construct-id.js +119 -0
- package/no-vacuous-cdk-synth-regex.js +109 -0
- package/no-zod-enum-redeclaring-named-constant.js +159 -0
- package/no-zod-optional-string-empty-trap.js +193 -0
- package/package.json +29 -0
- package/paired-ecs-validation.js +208 -0
- package/prefer-set-has.js +84 -0
- package/prefer-with-agent-flags.js +64 -0
- package/require-abort-precheck-in-sdk-loop.js +169 -0
- package/zod-companion-type.js +159 -0
- package/zod-strict-required.js +153 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
Fjall Proprietary Software Licence
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Fjall. All rights reserved.
|
|
4
|
+
|
|
5
|
+
This software, including all source, object, bundled, and minified forms
|
|
6
|
+
("the Software"), is the proprietary and confidential property of Fjall.
|
|
7
|
+
|
|
8
|
+
1. Permitted Use. Subject to the terms of this Licence, Fjall grants you
|
|
9
|
+
a non-exclusive, non-transferable, revocable licence to install the
|
|
10
|
+
Software via the npm registry and to execute it solely for the purpose
|
|
11
|
+
of deploying, operating, and managing your own applications and
|
|
12
|
+
infrastructure on cloud providers.
|
|
13
|
+
|
|
14
|
+
2. Restrictions. You may NOT, and may not permit any third party to:
|
|
15
|
+
(a) copy, redistribute, sublicense, sell, rent, lease, or otherwise
|
|
16
|
+
transfer the Software;
|
|
17
|
+
(b) modify, adapt, translate, or create derivative works of the Software;
|
|
18
|
+
(c) reverse engineer, decompile, disassemble, deminify, or otherwise
|
|
19
|
+
attempt to derive the source code, structure, or organisation of
|
|
20
|
+
the Software, except to the minimum extent expressly permitted by
|
|
21
|
+
applicable mandatory law;
|
|
22
|
+
(d) use the Software, or any portion of it, to develop, train, or
|
|
23
|
+
improve any product or service that competes with Fjall;
|
|
24
|
+
(e) remove, alter, or obscure any proprietary notices contained in
|
|
25
|
+
the Software;
|
|
26
|
+
(f) publish, share, or otherwise disclose the Software or its contents
|
|
27
|
+
to any third party.
|
|
28
|
+
|
|
29
|
+
3. Ownership. All right, title, and interest in and to the Software,
|
|
30
|
+
including all intellectual property rights, remain with Fjall. No
|
|
31
|
+
rights are granted except as expressly set out in this Licence.
|
|
32
|
+
|
|
33
|
+
4. Termination. This Licence terminates automatically if you breach any
|
|
34
|
+
of its terms. Upon termination you must cease all use of the Software
|
|
35
|
+
and destroy all copies in your possession.
|
|
36
|
+
|
|
37
|
+
5. Disclaimer of Warranty. THE SOFTWARE IS PROVIDED "AS IS" WITHOUT
|
|
38
|
+
WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION
|
|
39
|
+
THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE,
|
|
40
|
+
AND NON-INFRINGEMENT.
|
|
41
|
+
|
|
42
|
+
6. Limitation of Liability. IN NO EVENT SHALL FJALL BE LIABLE FOR ANY
|
|
43
|
+
INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR PUNITIVE DAMAGES
|
|
44
|
+
ARISING OUT OF OR RELATED TO THE SOFTWARE, EVEN IF ADVISED OF THE
|
|
45
|
+
POSSIBILITY OF SUCH DAMAGES.
|
|
46
|
+
|
|
47
|
+
7. Governing Law. This Licence is governed by the laws of England and
|
|
48
|
+
Wales, without regard to conflict of laws principles.
|
|
49
|
+
|
|
50
|
+
For commercial licensing enquiries, contact: contact@fjall.io
|
package/README.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# @fjall/eslint-plugin
|
|
2
|
+
|
|
3
|
+
Flat-config ESLint plugin holding Fjall's internal coding-standard rules
|
|
4
|
+
(credential masking, Zod strictness, tenant-isolation, AWS-SDK abort hygiene,
|
|
5
|
+
CDK construct invariants, and more).
|
|
6
|
+
|
|
7
|
+
It is the single source of truth shared between the Fjall monorepo and the
|
|
8
|
+
Fjall web app — the web app consumes it from npm because it builds without a
|
|
9
|
+
monorepo checkout. The rules encode Fjall-specific conventions and are not
|
|
10
|
+
intended for use outside Fjall projects.
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
```js
|
|
15
|
+
// eslint.config.mjs (flat config)
|
|
16
|
+
import fjall from "@fjall/eslint-plugin";
|
|
17
|
+
|
|
18
|
+
export default [
|
|
19
|
+
{
|
|
20
|
+
plugins: { fjall },
|
|
21
|
+
rules: {
|
|
22
|
+
"fjall/zod-strict-required": "error",
|
|
23
|
+
"fjall/mask-error-message-at-boundary": "error"
|
|
24
|
+
// … enable the rules you need
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
];
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Every rule lives in its own file (`<rule-name>.js`); `index.js` is the barrel
|
|
31
|
+
exposing them all under the `rules` map.
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint Rule: constructor-validates-public-construct
|
|
3
|
+
*
|
|
4
|
+
* Enforces `.claude/rules/generator-standards.md § "Public-Construct Validation
|
|
5
|
+
* Discipline"`: a public construct that is both direct-instantiable
|
|
6
|
+
* (`new EcsCompute(stack, id, props)`) AND reachable through a factory
|
|
7
|
+
* (`ComputeFactory.build(...)`) MUST self-validate inside its constructor.
|
|
8
|
+
* Placing the call only in the factory's pre-construction hook leaves every
|
|
9
|
+
* direct-instantiation consumer (other patterns, generator-emitted code,
|
|
10
|
+
* tests) bypassing the check.
|
|
11
|
+
*
|
|
12
|
+
* Detection shape (intra-file only):
|
|
13
|
+
* 1. Collect every top-level `function validate<Anything>(props: T)` or
|
|
14
|
+
* `function validate<Anything>Props(props: T)` declaration. Map the
|
|
15
|
+
* param type name `T` -> validator function name(s).
|
|
16
|
+
* 2. For each exported `class X extends Y { constructor(_, _, props: T) { ... } }`
|
|
17
|
+
* whose third constructor parameter is typed `T` and where the file
|
|
18
|
+
* defines a validator for `T`, the constructor body MUST contain a
|
|
19
|
+
* `CallExpression` invoking one of the validators with `props`.
|
|
20
|
+
* 3. Report on the constructor when the call is missing (messageId:
|
|
21
|
+
* `constructorMissingValidatorCall`).
|
|
22
|
+
* 4. When the call IS present but is NOT the first statement after
|
|
23
|
+
* `super(scope, id)`, report (messageId:
|
|
24
|
+
* `constructorValidatorNotFirstStatement`). A `this.* = …` assignment
|
|
25
|
+
* or `const x = …` declaration before the validator means props were
|
|
26
|
+
* partially trusted before being validated.
|
|
27
|
+
*
|
|
28
|
+
* Detection limitations (acknowledged ceiling):
|
|
29
|
+
* - Intra-file only. The cross-file layered-validation case (resources-vs-
|
|
30
|
+
* patterns mirroring) is covered by the sibling `paired-ecs-validation`
|
|
31
|
+
* rule.
|
|
32
|
+
* - Match-by-param-type-name. A validator with `props: IRelationalDatabaseProps`
|
|
33
|
+
* pairs with a class whose constructor takes `props: IRelationalDatabaseProps`.
|
|
34
|
+
* If the class's prop type is a union or intersection that resolves to
|
|
35
|
+
* the same shape but uses a different alias, the rule will not pair them
|
|
36
|
+
* and the runtime-test discipline in `code-quality.md § "ESLint-Mandated
|
|
37
|
+
* Validations Need Direct Runtime Tests"` is the backstop.
|
|
38
|
+
* - Only checks the constructor body. Calls inside private helper methods
|
|
39
|
+
* invoked from the constructor (`this.validateProps(props)`) do not
|
|
40
|
+
* count — the discipline says the call lives at the top of the
|
|
41
|
+
* constructor, immediately after `super(scope, id)`, where it is
|
|
42
|
+
* unambiguously reached for every code path.
|
|
43
|
+
* - Argument identity check is by name only (`props`). A validator called
|
|
44
|
+
* with a derived value (`validateProps({...props, vpc: injected})`)
|
|
45
|
+
* still satisfies the rule textually; the constructor's typed parameter
|
|
46
|
+
* remains the canonical input.
|
|
47
|
+
* - Direct-export shape only: `export class X {}` / `export default class X`.
|
|
48
|
+
* A class declared without inline export and re-exported via
|
|
49
|
+
* `export { X }` is not currently detected — `isExported()` walks
|
|
50
|
+
* `classNode.parent` only. Widen this if a real consumer ships the
|
|
51
|
+
* sibling-re-export shape; current `lib/{patterns,resources}/` callers
|
|
52
|
+
* all use the inline form.
|
|
53
|
+
* - Position check tolerates exactly one leading `super(scope, id)` call.
|
|
54
|
+
* TypeScript-only declarations (parameter-property decorators, type-only
|
|
55
|
+
* declarations) are stripped by the TS parser before the visitor sees the
|
|
56
|
+
* body, so they do not count toward statement ordering. The check does NOT
|
|
57
|
+
* tolerate `this.* = …` assignments or `const x = …` declarations before
|
|
58
|
+
* the validator call — those mean state was mutated on partially-trusted
|
|
59
|
+
* props.
|
|
60
|
+
*
|
|
61
|
+
* Pair with runtime tests (MANDATORY per `code-quality.md § "ESLint-Mandated
|
|
62
|
+
* Validations Need Direct Runtime Tests"`): every public construct this rule
|
|
63
|
+
* covers needs a direct-instantiation test that asserts the throw fires on
|
|
64
|
+
* bad input. The rule covers existence; the runtime test covers behaviour.
|
|
65
|
+
*/
|
|
66
|
+
|
|
67
|
+
const VALIDATOR_NAME_RE = /^validate[A-Z][A-Za-z0-9_]*$/;
|
|
68
|
+
|
|
69
|
+
function extractTypeName(typeAnnotation) {
|
|
70
|
+
if (!typeAnnotation || typeAnnotation.type !== "TSTypeAnnotation")
|
|
71
|
+
return null;
|
|
72
|
+
const inner = typeAnnotation.typeAnnotation;
|
|
73
|
+
if (!inner) return null;
|
|
74
|
+
if (inner.type === "TSTypeReference") {
|
|
75
|
+
const tn = inner.typeName;
|
|
76
|
+
if (tn && tn.type === "Identifier") return tn.name;
|
|
77
|
+
if (
|
|
78
|
+
tn &&
|
|
79
|
+
tn.type === "TSQualifiedName" &&
|
|
80
|
+
tn.right &&
|
|
81
|
+
tn.right.type === "Identifier"
|
|
82
|
+
) {
|
|
83
|
+
return tn.right.name;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function getParamTypeName(param) {
|
|
90
|
+
if (!param) return null;
|
|
91
|
+
if (param.type === "Identifier") return extractTypeName(param.typeAnnotation);
|
|
92
|
+
if (
|
|
93
|
+
param.type === "AssignmentPattern" &&
|
|
94
|
+
param.left &&
|
|
95
|
+
param.left.type === "Identifier"
|
|
96
|
+
) {
|
|
97
|
+
return extractTypeName(param.left.typeAnnotation);
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getParamName(param) {
|
|
103
|
+
if (!param) return null;
|
|
104
|
+
if (param.type === "Identifier") return param.name;
|
|
105
|
+
if (
|
|
106
|
+
param.type === "AssignmentPattern" &&
|
|
107
|
+
param.left &&
|
|
108
|
+
param.left.type === "Identifier"
|
|
109
|
+
) {
|
|
110
|
+
return param.left.name;
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Walk the constructor body's statements and any nested BlockStatement to
|
|
116
|
+
* find a CallExpression whose callee.name is one of `validatorNames` and
|
|
117
|
+
* whose first argument is the identifier `propsName`. Searches the whole
|
|
118
|
+
* body — the discipline prefers first-line-after-super, but call from any
|
|
119
|
+
* branch satisfies the structural existence check. The runtime-test rule
|
|
120
|
+
* backstops "called but on the wrong path". */
|
|
121
|
+
function constructorCallsAnyValidator(
|
|
122
|
+
constructorBody,
|
|
123
|
+
validatorNames,
|
|
124
|
+
propsName
|
|
125
|
+
) {
|
|
126
|
+
if (!constructorBody || constructorBody.type !== "BlockStatement")
|
|
127
|
+
return false;
|
|
128
|
+
const stack = [...constructorBody.body];
|
|
129
|
+
while (stack.length > 0) {
|
|
130
|
+
const node = stack.pop();
|
|
131
|
+
if (!node || typeof node !== "object") continue;
|
|
132
|
+
if (node.type === "ExpressionStatement" && node.expression) {
|
|
133
|
+
stack.push(node.expression);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (node.type === "CallExpression") {
|
|
137
|
+
const callee = node.callee;
|
|
138
|
+
if (
|
|
139
|
+
callee &&
|
|
140
|
+
callee.type === "Identifier" &&
|
|
141
|
+
validatorNames.has(callee.name) &&
|
|
142
|
+
node.arguments.length >= 1 &&
|
|
143
|
+
node.arguments[0].type === "Identifier" &&
|
|
144
|
+
node.arguments[0].name === propsName
|
|
145
|
+
) {
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
stack.push(callee);
|
|
149
|
+
for (const arg of node.arguments) stack.push(arg);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (node.type === "BlockStatement") {
|
|
153
|
+
for (const stmt of node.body) stack.push(stmt);
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (node.type === "IfStatement") {
|
|
157
|
+
stack.push(node.consequent);
|
|
158
|
+
if (node.alternate) stack.push(node.alternate);
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (node.type === "SwitchStatement") {
|
|
162
|
+
for (const c of node.cases) for (const s of c.consequent) stack.push(s);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (node.type === "TryStatement") {
|
|
166
|
+
stack.push(node.block);
|
|
167
|
+
if (node.handler && node.handler.body) stack.push(node.handler.body);
|
|
168
|
+
if (node.finalizer) stack.push(node.finalizer);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
for (const key of Object.keys(node)) {
|
|
172
|
+
const child = node[key];
|
|
173
|
+
if (key === "parent" || child === null || typeof child !== "object")
|
|
174
|
+
continue;
|
|
175
|
+
if (Array.isArray(child)) {
|
|
176
|
+
for (const c of child) if (c && typeof c === "object") stack.push(c);
|
|
177
|
+
} else {
|
|
178
|
+
stack.push(child);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function isSuperCallStatement(stmt) {
|
|
186
|
+
return (
|
|
187
|
+
stmt &&
|
|
188
|
+
stmt.type === "ExpressionStatement" &&
|
|
189
|
+
stmt.expression &&
|
|
190
|
+
stmt.expression.type === "CallExpression" &&
|
|
191
|
+
stmt.expression.callee &&
|
|
192
|
+
stmt.expression.callee.type === "Super"
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function isValidatorCallStatement(stmt, validatorNames, propsName) {
|
|
197
|
+
return (
|
|
198
|
+
stmt &&
|
|
199
|
+
stmt.type === "ExpressionStatement" &&
|
|
200
|
+
stmt.expression &&
|
|
201
|
+
stmt.expression.type === "CallExpression" &&
|
|
202
|
+
stmt.expression.callee &&
|
|
203
|
+
stmt.expression.callee.type === "Identifier" &&
|
|
204
|
+
validatorNames.has(stmt.expression.callee.name) &&
|
|
205
|
+
stmt.expression.arguments.length >= 1 &&
|
|
206
|
+
stmt.expression.arguments[0].type === "Identifier" &&
|
|
207
|
+
stmt.expression.arguments[0].name === propsName
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Anything between super() and the validator (this.* assignment, const
|
|
212
|
+
* decl, if statement, helper call) reads or mutates partially-trusted
|
|
213
|
+
* props — rejected. When there is no super() call, the validator MUST be
|
|
214
|
+
* the first statement in the constructor body. */
|
|
215
|
+
function constructorValidatorIsFirstAfterSuper(
|
|
216
|
+
constructorBody,
|
|
217
|
+
validatorNames,
|
|
218
|
+
propsName
|
|
219
|
+
) {
|
|
220
|
+
if (!constructorBody || constructorBody.type !== "BlockStatement")
|
|
221
|
+
return false;
|
|
222
|
+
const body = constructorBody.body;
|
|
223
|
+
if (body.length === 0) return false;
|
|
224
|
+
const firstAfterSuperIdx = isSuperCallStatement(body[0]) ? 1 : 0;
|
|
225
|
+
return isValidatorCallStatement(
|
|
226
|
+
body[firstAfterSuperIdx],
|
|
227
|
+
validatorNames,
|
|
228
|
+
propsName
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function isExported(classNode) {
|
|
233
|
+
const parent = classNode.parent;
|
|
234
|
+
if (!parent) return false;
|
|
235
|
+
return (
|
|
236
|
+
parent.type === "ExportNamedDeclaration" ||
|
|
237
|
+
parent.type === "ExportDefaultDeclaration"
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
242
|
+
export default {
|
|
243
|
+
meta: {
|
|
244
|
+
type: "problem",
|
|
245
|
+
docs: {
|
|
246
|
+
description:
|
|
247
|
+
"Public CDK constructs in lib/{patterns,resources} that have a sibling `validate*Props` function in the same file MUST call that validator inside their constructor body. Otherwise direct `new X(...)` consumers bypass validation that the factory hook performs.",
|
|
248
|
+
category: "Best Practices",
|
|
249
|
+
recommended: true
|
|
250
|
+
},
|
|
251
|
+
messages: {
|
|
252
|
+
constructorMissingValidatorCall:
|
|
253
|
+
'Public construct `{{className}}` takes `props: {{propType}}` but its constructor does not call `{{validators}}` — the validator(s) defined in this file for `{{propType}}`. Add the call as the first statement after `super(scope, id)` so direct `new {{className}}(...)` consumers do not bypass validation. See .claude/rules/generator-standards.md § "Public-Construct Validation Discipline".',
|
|
254
|
+
constructorValidatorNotFirstStatement:
|
|
255
|
+
'Public construct `{{className}}` calls `{{validators}}` but not as the first statement after `super(scope, id)`. A `this.* = …` assignment or `const x = …` declaration before the validator means partially-trusted props were already read or stored. Move the validator call to immediately after `super(scope, id)`. See .claude/rules/generator-standards.md § "Public-Construct Validation Discipline".'
|
|
256
|
+
},
|
|
257
|
+
schema: []
|
|
258
|
+
},
|
|
259
|
+
|
|
260
|
+
create(context) {
|
|
261
|
+
/** Map<paramTypeName, Set<validatorFunctionName>> */
|
|
262
|
+
const validatorsByPropType = new Map();
|
|
263
|
+
/** Array of {classNode, className, propType, propsName, constructorNode} */
|
|
264
|
+
const candidateClasses = [];
|
|
265
|
+
|
|
266
|
+
function recordValidator(node) {
|
|
267
|
+
if (!node.id || !VALIDATOR_NAME_RE.test(node.id.name)) return;
|
|
268
|
+
if (!node.params || node.params.length === 0) return;
|
|
269
|
+
const typeName = getParamTypeName(node.params[0]);
|
|
270
|
+
if (!typeName) return;
|
|
271
|
+
const set = validatorsByPropType.get(typeName) ?? new Set();
|
|
272
|
+
set.add(node.id.name);
|
|
273
|
+
validatorsByPropType.set(typeName, set);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
"Program > FunctionDeclaration"(node) {
|
|
278
|
+
recordValidator(node);
|
|
279
|
+
},
|
|
280
|
+
"Program > ExportNamedDeclaration > FunctionDeclaration"(node) {
|
|
281
|
+
recordValidator(node);
|
|
282
|
+
},
|
|
283
|
+
|
|
284
|
+
ClassDeclaration(node) {
|
|
285
|
+
if (!node.id) return;
|
|
286
|
+
if (!isExported(node)) return;
|
|
287
|
+
const ctor = node.body.body.find(
|
|
288
|
+
(m) =>
|
|
289
|
+
m.type === "MethodDefinition" &&
|
|
290
|
+
m.kind === "constructor" &&
|
|
291
|
+
m.value &&
|
|
292
|
+
m.value.params
|
|
293
|
+
);
|
|
294
|
+
if (!ctor) return;
|
|
295
|
+
// CDK construct signature is `(scope, id, props)` — validators target props at index 2.
|
|
296
|
+
if (ctor.value.params.length < 3) return;
|
|
297
|
+
const propsParam = ctor.value.params[2];
|
|
298
|
+
const propType = getParamTypeName(propsParam);
|
|
299
|
+
const propsName = getParamName(propsParam);
|
|
300
|
+
if (!propType || !propsName) return;
|
|
301
|
+
candidateClasses.push({
|
|
302
|
+
classNode: node,
|
|
303
|
+
className: node.id.name,
|
|
304
|
+
propType,
|
|
305
|
+
propsName,
|
|
306
|
+
constructorNode: ctor
|
|
307
|
+
});
|
|
308
|
+
},
|
|
309
|
+
|
|
310
|
+
// Run at Program:exit so validator declarations below the class are still in scope.
|
|
311
|
+
"Program:exit"() {
|
|
312
|
+
for (const c of candidateClasses) {
|
|
313
|
+
const validators = validatorsByPropType.get(c.propType);
|
|
314
|
+
if (!validators || validators.size === 0) continue;
|
|
315
|
+
const found = constructorCallsAnyValidator(
|
|
316
|
+
c.constructorNode.value.body,
|
|
317
|
+
validators,
|
|
318
|
+
c.propsName
|
|
319
|
+
);
|
|
320
|
+
if (!found) {
|
|
321
|
+
context.report({
|
|
322
|
+
node: c.constructorNode,
|
|
323
|
+
messageId: "constructorMissingValidatorCall",
|
|
324
|
+
data: {
|
|
325
|
+
className: c.className,
|
|
326
|
+
propType: c.propType,
|
|
327
|
+
validators: [...validators].sort().join(", ")
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
const isFirst = constructorValidatorIsFirstAfterSuper(
|
|
333
|
+
c.constructorNode.value.body,
|
|
334
|
+
validators,
|
|
335
|
+
c.propsName
|
|
336
|
+
);
|
|
337
|
+
if (!isFirst) {
|
|
338
|
+
context.report({
|
|
339
|
+
node: c.constructorNode,
|
|
340
|
+
messageId: "constructorValidatorNotFirstStatement",
|
|
341
|
+
data: {
|
|
342
|
+
className: c.className,
|
|
343
|
+
validators: [...validators].sort().join(", ")
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint Rule: human-readable-durations
|
|
3
|
+
*
|
|
4
|
+
* User-facing messages should format durations in human-readable form.
|
|
5
|
+
* Users shouldn't see raw milliseconds like "1800000ms" - show "30 minutes" instead.
|
|
6
|
+
*
|
|
7
|
+
* Detects patterns like:
|
|
8
|
+
* - `${timeout}ms`
|
|
9
|
+
* - `${duration}ms`
|
|
10
|
+
* - `after ${timeoutMs}ms`
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
14
|
+
export default {
|
|
15
|
+
meta: {
|
|
16
|
+
type: "suggestion",
|
|
17
|
+
docs: {
|
|
18
|
+
description:
|
|
19
|
+
"Require human-readable duration formatting in user messages",
|
|
20
|
+
category: "User Experience",
|
|
21
|
+
recommended: false
|
|
22
|
+
},
|
|
23
|
+
messages: {
|
|
24
|
+
rawMs:
|
|
25
|
+
"Format durations for humans. Instead of '${...}ms', use: const minutes = Math.floor({{variable}} / MS_PER_MINUTE); `${minutes} minutes`",
|
|
26
|
+
rawSeconds:
|
|
27
|
+
"Consider formatting seconds for humans when > 60. Use minutes for longer durations."
|
|
28
|
+
},
|
|
29
|
+
schema: []
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
create(context) {
|
|
33
|
+
return {
|
|
34
|
+
TemplateLiteral(node) {
|
|
35
|
+
const reported = new Set(); // Track reported expressions to avoid duplicates
|
|
36
|
+
|
|
37
|
+
// Check each quasi (string part) and expression pair
|
|
38
|
+
// Pattern: `... ${someVar}ms ...` where the next quasi starts with "ms"
|
|
39
|
+
for (let i = 0; i < node.expressions.length; i++) {
|
|
40
|
+
const nextQuasi = node.quasis[i + 1];
|
|
41
|
+
if (!nextQuasi) continue;
|
|
42
|
+
|
|
43
|
+
const nextQuasiValue =
|
|
44
|
+
nextQuasi.value.raw || nextQuasi.value.cooked || "";
|
|
45
|
+
|
|
46
|
+
// Check if the quasi immediately after this expression starts with "ms"
|
|
47
|
+
if (/^ms\b/.test(nextQuasiValue)) {
|
|
48
|
+
const expr = node.expressions[i];
|
|
49
|
+
const exprKey = `${expr.range[0]}-${expr.range[1]}`;
|
|
50
|
+
|
|
51
|
+
// Skip if already reported or if it's a formatted duration
|
|
52
|
+
if (reported.has(exprKey) || isFormattedDuration(expr, context)) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
reported.add(exprKey);
|
|
57
|
+
const variableName = getVariableName(expr);
|
|
58
|
+
|
|
59
|
+
context.report({
|
|
60
|
+
node: expr,
|
|
61
|
+
messageId: "rawMs",
|
|
62
|
+
data: { variable: variableName }
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Get a variable name from an expression for error messages
|
|
73
|
+
*/
|
|
74
|
+
function getVariableName(expr) {
|
|
75
|
+
if (expr.type === "Identifier") {
|
|
76
|
+
return expr.name;
|
|
77
|
+
}
|
|
78
|
+
if (expr.type === "MemberExpression" && expr.property.type === "Identifier") {
|
|
79
|
+
return expr.property.name;
|
|
80
|
+
}
|
|
81
|
+
return "duration";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Check if expression is already a formatted duration (e.g., minutes calculation)
|
|
86
|
+
*/
|
|
87
|
+
function isFormattedDuration(expr, context) {
|
|
88
|
+
const sourceCode = context.sourceCode;
|
|
89
|
+
const exprText = sourceCode.getText(expr);
|
|
90
|
+
|
|
91
|
+
// Already formatted patterns
|
|
92
|
+
const formattedPatterns = [
|
|
93
|
+
/minute/i,
|
|
94
|
+
/second/i,
|
|
95
|
+
/hour/i,
|
|
96
|
+
/MS_PER_/,
|
|
97
|
+
/\.toFixed\(/,
|
|
98
|
+
/Math\.floor\(/,
|
|
99
|
+
/Math\.round\(/
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
return formattedPatterns.some((pattern) => pattern.test(exprText));
|
|
103
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint Rule: iam-secrets-arn-suffix
|
|
3
|
+
*
|
|
4
|
+
* Flags IAM resource ARN strings scoping a Secrets Manager secret with a bare
|
|
5
|
+
* `*` suffix instead of `-*`. AWS Secrets Manager appends a 6-character random
|
|
6
|
+
* ID after a `-` separator (e.g. `my-secret-AbCdEf`), so a bare `*` widens to
|
|
7
|
+
* sibling secrets:
|
|
8
|
+
*
|
|
9
|
+
* `:secret:my-secret*` matches `my-secret-AbCdEf` AND `my-secret2-XyZw1A`
|
|
10
|
+
* `:secret:my-secret-*` matches only `my-secret-<6char>` siblings
|
|
11
|
+
*
|
|
12
|
+
* No autofix — the right repair depends on whether the upstream is a name
|
|
13
|
+
* (append `-`) or a pre-formed ARN (re-architect).
|
|
14
|
+
*
|
|
15
|
+
* Per `.claude/rules/security-standards.md`.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const SECRET_SEGMENT = /:secret:([^"`\s]+?)\*(?!\*)/g;
|
|
19
|
+
|
|
20
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
21
|
+
export default {
|
|
22
|
+
meta: {
|
|
23
|
+
type: "problem",
|
|
24
|
+
docs: {
|
|
25
|
+
description:
|
|
26
|
+
"Disallow Secrets Manager ARN scoping with a bare `*` suffix; require `-*` to delimit the AWS-generated 6-char suffix and avoid sibling-secret access.",
|
|
27
|
+
category: "Possible Errors",
|
|
28
|
+
recommended: true
|
|
29
|
+
},
|
|
30
|
+
messages: {
|
|
31
|
+
bareWildcard:
|
|
32
|
+
"IAM resource `:secret:{{name}}*` matches sibling secrets (e.g. `{{name}}2-AbCdEf`). Use `:secret:{{name}}-*` to scope to the AWS-generated 6-char suffix only. See .claude/rules/security-standards.md."
|
|
33
|
+
},
|
|
34
|
+
schema: []
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
create(context) {
|
|
38
|
+
function reconstructTemplate(node) {
|
|
39
|
+
let out = "";
|
|
40
|
+
const quasis = node.quasis;
|
|
41
|
+
const expressions = node.expressions;
|
|
42
|
+
for (let i = 0; i < quasis.length; i++) {
|
|
43
|
+
out += quasis[i].value.cooked ?? quasis[i].value.raw;
|
|
44
|
+
if (i < expressions.length) {
|
|
45
|
+
out += "${_}";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function checkString(node, value) {
|
|
52
|
+
if (!value.includes(":secret:")) return;
|
|
53
|
+
SECRET_SEGMENT.lastIndex = 0;
|
|
54
|
+
let match;
|
|
55
|
+
while ((match = SECRET_SEGMENT.exec(value)) !== null) {
|
|
56
|
+
const name = match[1];
|
|
57
|
+
if (name === "") continue;
|
|
58
|
+
const lastChar = name[name.length - 1];
|
|
59
|
+
if (lastChar === "-") continue;
|
|
60
|
+
if (lastChar === "}" && name.endsWith("-}")) continue;
|
|
61
|
+
context.report({
|
|
62
|
+
node,
|
|
63
|
+
messageId: "bareWildcard",
|
|
64
|
+
data: { name }
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
TemplateLiteral(node) {
|
|
71
|
+
checkString(node, reconstructTemplate(node));
|
|
72
|
+
},
|
|
73
|
+
Literal(node) {
|
|
74
|
+
if (typeof node.value !== "string") return;
|
|
75
|
+
checkString(node, node.value);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
};
|
package/index.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import noThrowInServices from "./no-throw-in-services.js";
|
|
2
|
+
import zodStrictRequired from "./zod-strict-required.js";
|
|
3
|
+
import zodCompanionType from "./zod-companion-type.js";
|
|
4
|
+
import preferSetHas from "./prefer-set-has.js";
|
|
5
|
+
import humanReadableDurations from "./human-readable-durations.js";
|
|
6
|
+
import preferWithAgentFlags from "./prefer-with-agent-flags.js";
|
|
7
|
+
import noEmptyStringEnvFallthrough from "./no-empty-string-env-fallthrough.js";
|
|
8
|
+
import maskErrorMessageAtBoundary from "./mask-error-message-at-boundary.js";
|
|
9
|
+
import maskBeforeTruncate from "./mask-before-truncate.js";
|
|
10
|
+
import noZodOptionalStringEmptyTrap from "./no-zod-optional-string-empty-trap.js";
|
|
11
|
+
import noZodEnumRedeclaringNamedConstant from "./no-zod-enum-redeclaring-named-constant.js";
|
|
12
|
+
import noRawCdkPropertiesOnPublicConstructs from "./no-raw-cdk-properties-on-public-constructs.js";
|
|
13
|
+
import noClickhouseInternalReexport from "./no-clickhouse-internal-reexport.js";
|
|
14
|
+
import noVacuousCdkSynthRegex from "./no-vacuous-cdk-synth-regex.js";
|
|
15
|
+
import pairedEcsValidation from "./paired-ecs-validation.js";
|
|
16
|
+
import iamSecretsArnSuffix from "./iam-secrets-arn-suffix.js";
|
|
17
|
+
import noRawBlockDeviceVolume from "./no-raw-block-device-volume.js";
|
|
18
|
+
import constructorValidatesPublicConstruct from "./constructor-validates-public-construct.js";
|
|
19
|
+
import noUndefinedPropInConstructId from "./no-undefined-prop-in-construct-id.js";
|
|
20
|
+
import noMaskIdentityMock from "./no-mask-identity-mock.js";
|
|
21
|
+
import noDuplicateFjallUtilHelper from "./no-duplicate-fjall-util-helper.js";
|
|
22
|
+
import noL2AsgLifecycleHook from "./no-l2-asg-lifecycle-hook.js";
|
|
23
|
+
import noTierStageConflation from "./no-tier-stage-conflation.js";
|
|
24
|
+
import noBareSdkAbortTimeout from "./no-bare-sdk-abort-timeout.js";
|
|
25
|
+
import requireAbortPrecheckInSdkLoop from "./require-abort-precheck-in-sdk-loop.js";
|
|
26
|
+
import noClassicConnectedAccountAssume from "./no-classic-connected-account-assume.js";
|
|
27
|
+
import noRawDbTransaction from "./no-raw-db-transaction.js";
|
|
28
|
+
|
|
29
|
+
export default {
|
|
30
|
+
rules: {
|
|
31
|
+
"no-throw-in-services": noThrowInServices,
|
|
32
|
+
"zod-strict-required": zodStrictRequired,
|
|
33
|
+
"zod-companion-type": zodCompanionType,
|
|
34
|
+
"prefer-set-has": preferSetHas,
|
|
35
|
+
"human-readable-durations": humanReadableDurations,
|
|
36
|
+
"prefer-with-agent-flags": preferWithAgentFlags,
|
|
37
|
+
"no-empty-string-env-fallthrough": noEmptyStringEnvFallthrough,
|
|
38
|
+
"mask-error-message-at-boundary": maskErrorMessageAtBoundary,
|
|
39
|
+
"mask-before-truncate": maskBeforeTruncate,
|
|
40
|
+
"no-zod-optional-string-empty-trap": noZodOptionalStringEmptyTrap,
|
|
41
|
+
"no-zod-enum-redeclaring-named-constant": noZodEnumRedeclaringNamedConstant,
|
|
42
|
+
"no-raw-cdk-properties-on-public-constructs":
|
|
43
|
+
noRawCdkPropertiesOnPublicConstructs,
|
|
44
|
+
"no-clickhouse-internal-reexport": noClickhouseInternalReexport,
|
|
45
|
+
"no-vacuous-cdk-synth-regex": noVacuousCdkSynthRegex,
|
|
46
|
+
"paired-ecs-validation": pairedEcsValidation,
|
|
47
|
+
"iam-secrets-arn-suffix": iamSecretsArnSuffix,
|
|
48
|
+
"no-raw-block-device-volume": noRawBlockDeviceVolume,
|
|
49
|
+
"constructor-validates-public-construct":
|
|
50
|
+
constructorValidatesPublicConstruct,
|
|
51
|
+
"no-undefined-prop-in-construct-id": noUndefinedPropInConstructId,
|
|
52
|
+
"no-mask-identity-mock": noMaskIdentityMock,
|
|
53
|
+
"no-duplicate-fjall-util-helper": noDuplicateFjallUtilHelper,
|
|
54
|
+
"no-l2-asg-lifecycle-hook": noL2AsgLifecycleHook,
|
|
55
|
+
"no-tier-stage-conflation": noTierStageConflation,
|
|
56
|
+
"no-bare-sdk-abort-timeout": noBareSdkAbortTimeout,
|
|
57
|
+
"require-abort-precheck-in-sdk-loop": requireAbortPrecheckInSdkLoop,
|
|
58
|
+
"no-classic-connected-account-assume": noClassicConnectedAccountAssume,
|
|
59
|
+
"no-raw-db-transaction": noRawDbTransaction
|
|
60
|
+
}
|
|
61
|
+
};
|