@starci/eslint-canon-be 1.7.0 → 1.7.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/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.7.1 - 2026-09-30
4
+
5
+ - Fixed: eslint no longer fails to start in a product repository with `HFS slot manifest not found at <repo>/knowledge/hfs/slots.yaml`. `lib/slots.mjs` used to look for the manifest at a path above the package (the product repository, or a linked runtime copy that no longer exists). The package now depends on `@starci/hfs` and resolves the manifest and the YAML reader as `@starci/hfs/runtime/knowledge/hfs/slots.yaml` and `@starci/hfs/runtime/engine/yaml.mjs`, so what the canon reads travels with the installed packages. `STARCI_HFS_SLOTS` still overrides the manifest file.
6
+
3
7
  ## 1.7.0 - 2026-09-30
4
8
 
5
9
  - New: `named-entity-manager-only` (R83 `BE_UNNAMED_DATA_ACCESS`, in `data-access`): a bare `@InjectEntityManager()` with no connection argument, any `.getRepository(...)` call (on a DataSource, an EntityManager or a transaction manager) and an injected `DataSource` (a constructor parameter typed `DataSource`, `@InjectDataSource()`, `@InjectConnection()`) are refused. A `DataSource` is allowed only under `platform/database/` or `platform/databases/` (also `platform/db`, `platform/datasource`, `platform/typeorm`) and `apps/migrate/`; spec files are skipped. The message says to inject the shared EntityManager with the named injector and call it directly.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @starci/eslint-canon-be
2
2
 
3
- **78 ESLint rules, from 26 laws, that hold a NestJS-shaped back end to one way of being written.**
3
+ **ESLint rules, grouped by law (the count is `Object.keys(rules)`; knowledge/hfs/rules.yaml names the HFS rule each one enforces), that hold a NestJS-shaped back end to one way of being written.**
4
4
 
5
5
  Not a style pack. These rules enforce *architecture*: which layer may import which, whether a
6
6
  failure carries its own identity, where a query is allowed to be built, what an end-to-end test is
@@ -103,43 +103,7 @@ ESLint 9+ (flat config), Node 20.9+.
103
103
 
104
104
  ## HFS rules and their why codes
105
105
 
106
- Each rule below reports through the code-pattern gate with a catalogued Vietnamese reason (`modules/kernel/failure-codes.yaml`,
107
- mapped by `scripts/checks/lint-why.mjs`). The catalog id is the rule number in `knowledge/hfs/rules.yaml`.
108
-
109
- | Rule | Catalog | Why code |
110
- |---|---|---|
111
- | `catch-must-account` | R40 | `BE_LOGGER_REQUIRED` |
112
- | `error-home` | R38 | `BE_ERROR_HOME` |
113
- | `no-runtime-schema` | R34 | `BE_SCHEMA_AUTHORITY` |
114
- | `sql-only-in-repository` | R36 | `BE_SQL_OUTSIDE_REPOSITORY` |
115
- | `no-entity-in-contract` | R37 | `BE_ENTITY_IN_CONTRACT` |
116
- | `no-untyped-body`, `public-needs-reason`, `secret-compare-timing-safe` | R41 | `BE_DEFAULT_DENY` |
117
- | `no-direct-env-read` | R43 | `BE_CONFIG_OWNER` |
118
- | `no-secret-default` | R44 | `BE_SECRET_DEFAULT` |
119
- | `global-module-allowlist`, `typed-module-definition`, `static-module-register`, `no-new-injectable`, `no-module-let`, `one-module-per-file` | R45 | `BE_MODULE_SHAPE` |
120
- | `no-inline-suppression` | R18 | `HFS_INLINE_SUPPRESSION` |
121
- | `file-size-growth` | R20 | `HFS_SIZE_GROWTH` |
122
- | `spec-no-source-read`, `spec-typed-doubles` | R48 | `BE_SPEC_QUALITY` |
123
- | `must-deep-module-import`, `no-folder-reexport` | R30 | `BE_PUBLIC_SURFACE` |
124
- | `dto-needs-validator` | R42 | `BE_INPUT_BOUNDED` |
125
- | `no-interpolated-sql` | R68 | `BE_SQL_INTERPOLATED` |
126
- | `query-needs-limit` | R69 | `BE_QUERY_UNBOUNDED` |
127
- | `no-query-in-loop` | R77 | `BE_QUERY_IN_LOOP` |
128
- | `http-needs-timeout` | R70 | `BE_HTTP_TIMEOUT` |
129
- | `no-secret-in-log` | R71 | `BE_LOG_SECRET` |
130
- | `no-never-cast`, `no-non-null-assertion` | R72 | `BE_TYPE_ESCAPE` |
131
- | `async-needs-await` | R73 | `BE_ASYNC_NO_AWAIT` |
132
- | `migration-down-reversible` | R74 | `BE_MIGRATION_REVERSIBLE` |
133
- | `explicit-handler-return-type` | R75 | `BE_RETURN_TYPE` |
134
- | `json-parse-needs-guard` | R76 | `BE_JSON_PARSE_UNGUARDED` |
135
- | `user-copy-through-catalog` | R78 | `BE_USER_COPY_LITERAL` |
136
- | `no-ambient-clock` | R79 | `BE_AMBIENT_CLOCK` |
137
- | `inbox-dedupe-required` | R80 | `BE_INBOX_DEDUPE_MISSING` |
138
- | `no-hand-rolled-retry` | R81 | `BE_HAND_ROLLED_RETRY` |
139
- | `no-external-call-in-transaction` | R82 | `BE_TRANSACTION_EXTERNAL_CALL` |
140
- | `named-entity-manager-only`, `no-injected-repository` | R83 | `BE_UNNAMED_DATA_ACCESS` |
141
-
142
- There is no soft size-limit lint rule: a warning under a zero-warning gate is an exception in disguise. Listing files over the soft budget (`ruleParams.be.fileLines.soft`) is a report item of the hfs check or the architecture machine, as migration backlog, and never blocks.
143
-
144
- The tier direction matrix and the feature-imports-feature ban (R26, R28) are owned by the architecture machine
145
- (`@starci/hfs`), not by a lint rule: they need the whole import graph.
106
+ Every rule of this plugin is an enforcer of one HFS rule. The relation (rule id -> HFS rule -> catalogued Vietnamese why code)
107
+ lives in exactly one place, `knowledge/hfs/rules.yaml` (`enforcers`), and `scripts/checks/check-hfs-rules.mjs` refuses a
108
+ rule of this plugin that no catalog entry names. There is no second table here.
109
+
package/authorization.mjs CHANGED
@@ -1,106 +1,106 @@
1
- /**
2
- * The rule that holds `authorization.md`.
3
- *
4
- * ONE rule, and the honest reason is worth stating: authorization is decided against a row, and a
5
- * parser does not know which row a handler is reaching for or what owning it means. AUTHZ-3, -4 and
6
- * -5 all turn on that, so a rule aimed at them would fire on shape rather than on meaning - and a
7
- * rule that fires on shape is one authors learn to disable, which leaves the law worse off than when
8
- * nothing enforced it.
9
- *
10
- * AUTHZ-1 was measured and deliberately left alone. A handler checking `if (!user)` reads like
11
- * duplication of the door's guard and is not: the guard belongs to one door and the handler belongs
12
- * to every caller of the bus. A rule refusing it would fire on the majority of correct handlers in
13
- * this tree, which is a gate contradicting canon rather than holding it.
14
- *
15
- * What IS checkable in one file is the door: a resolver method that READS the authenticated user
16
- * while nothing on that method or its class establishes the identity. That failure looks like
17
- * nothing at all - the parameter still says `user`, the handler still receives one - and the only
18
- * missing piece is the line that proved it belonged to the caller.
19
- */
20
-
21
- /** Parameter decorators that read an already-authenticated identity off the request. */
22
- const IDENTITY_PARAM_DECORATORS = new Set([
23
- "KeycloakGraphQLUser",
24
- "KeycloakUser",
25
- "CurrentUser",
26
- ])
27
-
28
- /** The decorator that establishes the identity a door then reads. */
29
- const GUARD_DECORATOR = "UseGuards"
30
-
31
- /** Reads the callee name off a decorator expression, whether or not it is called. */
32
- const decoratorName = (decorator) => {
33
- const expression = decorator.expression
34
- if (!expression) return undefined
35
- if (expression.type === "CallExpression") {
36
- return expression.callee && expression.callee.type === "Identifier"
37
- ? expression.callee.name
38
- : undefined
39
- }
40
- return expression.type === "Identifier" ? expression.name : undefined
41
- }
42
-
43
- /** Whether any decorator in the list is the guard. */
44
- const hasGuard = (decorators) =>
45
- (decorators || []).some((decorator) => decoratorName(decorator) === GUARD_DECORATOR)
46
-
47
- /** The identity parameter on a method, when it has one. */
48
- const identityParameter = (node) => {
49
- for (const parameter of node.value && node.value.params ? node.value.params : []) {
50
- // a decorated parameter carries its decorators on the parameter node itself, and TS-ESTree
51
- // wraps a typed+decorated parameter in TSParameterProperty for constructor promotion
52
- const carrier = parameter.type === "TSParameterProperty" ? parameter.parameter : parameter
53
- const decorators = parameter.decorators || (carrier && carrier.decorators) || []
54
- for (const decorator of decorators) {
55
- if (IDENTITY_PARAM_DECORATORS.has(decoratorName(decorator))) return parameter
56
- }
57
- }
58
- return undefined
59
- }
60
-
61
- // -- AUTHZ-2 ---------------------------------------------------------------------------------------
62
-
63
- /** A door that reads an identity carries the guard that establishes it. */
64
- export const identityNeedsGuard = {
65
- meta: {
66
- type: "problem",
67
- docs: {
68
- description:
69
- "A resolver or controller method reading the authenticated user must carry a guard on that method or its class.",
70
- },
71
- schema: [],
72
- messages: {
73
- unguarded:
74
- "This door reads the authenticated user through `@{{decorator}}()` while neither the method nor its class carries `@UseGuards(...)`. Nothing established the identity, so what reaches the handler is whatever the pipeline left there - and the failure is silent, because the parameter still says `user` and the handler still receives one. Put the guard on the door that reads the identity.",
75
- },
76
- },
77
- create(context) {
78
- return {
79
- MethodDefinition(node) {
80
- const parameter = identityParameter(node)
81
- if (!parameter) return
82
- if (hasGuard(node.decorators)) return
83
-
84
- // the guard may sit on the class instead, covering every method on it
85
- const classBody = node.parent
86
- const classNode = classBody && classBody.parent
87
- if (classNode && hasGuard(classNode.decorators)) return
88
-
89
- const carrier = parameter.type === "TSParameterProperty" ? parameter.parameter : parameter
90
- const decorators = parameter.decorators || (carrier && carrier.decorators) || []
91
- const named = decorators.map(decoratorName).find((name) => IDENTITY_PARAM_DECORATORS.has(name))
92
- context.report({ node: parameter, messageId: "unguarded", data: { decorator: named } })
93
- },
94
- }
95
- },
96
- }
97
-
98
- /** Every rule this module ships, by the name a config switches it on under. */
99
- export const rules = {
100
- "identity-needs-guard": identityNeedsGuard,
101
- }
102
-
103
- /** The level a consuming repository switches this on at. */
104
- export const recommended = {
105
- "starci-be/identity-needs-guard": "error",
106
- }
1
+ /**
2
+ * The rule that holds `authorization.md`.
3
+ *
4
+ * ONE rule, and the honest reason is worth stating: authorization is decided against a row, and a
5
+ * parser does not know which row a handler is reaching for or what owning it means. AUTHZ-3, -4 and
6
+ * -5 all turn on that, so a rule aimed at them would fire on shape rather than on meaning - and a
7
+ * rule that fires on shape is one authors learn to disable, which leaves the law worse off than when
8
+ * nothing enforced it.
9
+ *
10
+ * AUTHZ-1 was measured and deliberately left alone. A handler checking `if (!user)` reads like
11
+ * duplication of the door's guard and is not: the guard belongs to one door and the handler belongs
12
+ * to every caller of the bus. A rule refusing it would fire on the majority of correct handlers in
13
+ * this tree, which is a gate contradicting canon rather than holding it.
14
+ *
15
+ * What IS checkable in one file is the door: a resolver method that READS the authenticated user
16
+ * while nothing on that method or its class establishes the identity. That failure looks like
17
+ * nothing at all - the parameter still says `user`, the handler still receives one - and the only
18
+ * missing piece is the line that proved it belonged to the caller.
19
+ */
20
+
21
+ /** Parameter decorators that read an already-authenticated identity off the request. */
22
+ const IDENTITY_PARAM_DECORATORS = new Set([
23
+ "KeycloakGraphQLUser",
24
+ "KeycloakUser",
25
+ "CurrentUser",
26
+ ])
27
+
28
+ /** The decorator that establishes the identity a door then reads. */
29
+ const GUARD_DECORATOR = "UseGuards"
30
+
31
+ /** Reads the callee name off a decorator expression, whether or not it is called. */
32
+ const decoratorName = (decorator) => {
33
+ const expression = decorator.expression
34
+ if (!expression) return undefined
35
+ if (expression.type === "CallExpression") {
36
+ return expression.callee && expression.callee.type === "Identifier"
37
+ ? expression.callee.name
38
+ : undefined
39
+ }
40
+ return expression.type === "Identifier" ? expression.name : undefined
41
+ }
42
+
43
+ /** Whether any decorator in the list is the guard. */
44
+ const hasGuard = (decorators) =>
45
+ (decorators || []).some((decorator) => decoratorName(decorator) === GUARD_DECORATOR)
46
+
47
+ /** The identity parameter on a method, when it has one. */
48
+ const identityParameter = (node) => {
49
+ for (const parameter of node.value && node.value.params ? node.value.params : []) {
50
+ // a decorated parameter carries its decorators on the parameter node itself, and TS-ESTree
51
+ // wraps a typed+decorated parameter in TSParameterProperty for constructor promotion
52
+ const carrier = parameter.type === "TSParameterProperty" ? parameter.parameter : parameter
53
+ const decorators = parameter.decorators || (carrier && carrier.decorators) || []
54
+ for (const decorator of decorators) {
55
+ if (IDENTITY_PARAM_DECORATORS.has(decoratorName(decorator))) return parameter
56
+ }
57
+ }
58
+ return undefined
59
+ }
60
+
61
+ // -- AUTHZ-2 ---------------------------------------------------------------------------------------
62
+
63
+ /** A door that reads an identity carries the guard that establishes it. */
64
+ export const identityNeedsGuard = {
65
+ meta: {
66
+ type: "problem",
67
+ docs: {
68
+ description:
69
+ "A resolver or controller method reading the authenticated user must carry a guard on that method or its class.",
70
+ },
71
+ schema: [],
72
+ messages: {
73
+ unguarded:
74
+ "This door reads the authenticated user through `@{{decorator}}()` while neither the method nor its class carries `@UseGuards(...)`. Nothing established the identity, so what reaches the handler is whatever the pipeline left there - and the failure is silent, because the parameter still says `user` and the handler still receives one. Put the guard on the door that reads the identity.",
75
+ },
76
+ },
77
+ create(context) {
78
+ return {
79
+ MethodDefinition(node) {
80
+ const parameter = identityParameter(node)
81
+ if (!parameter) return
82
+ if (hasGuard(node.decorators)) return
83
+
84
+ // the guard may sit on the class instead, covering every method on it
85
+ const classBody = node.parent
86
+ const classNode = classBody && classBody.parent
87
+ if (classNode && hasGuard(classNode.decorators)) return
88
+
89
+ const carrier = parameter.type === "TSParameterProperty" ? parameter.parameter : parameter
90
+ const decorators = parameter.decorators || (carrier && carrier.decorators) || []
91
+ const named = decorators.map(decoratorName).find((name) => IDENTITY_PARAM_DECORATORS.has(name))
92
+ context.report({ node: parameter, messageId: "unguarded", data: { decorator: named } })
93
+ },
94
+ }
95
+ },
96
+ }
97
+
98
+ /** Every rule this module ships, by the name a config switches it on under. */
99
+ export const rules = {
100
+ "identity-needs-guard": identityNeedsGuard,
101
+ }
102
+
103
+ /** The level a consuming repository switches this on at. */
104
+ export const recommended = {
105
+ "starci-be/identity-needs-guard": "error",
106
+ }