@starci/eslint-canon-be 2.1.2 → 3.0.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.
- package/CHANGELOG.md +15 -0
- package/README.md +8 -7
- package/comments.mjs +104 -1
- package/config-owner.mjs +113 -13
- package/connections.mjs +1 -12
- package/cqrs.mjs +68 -0
- package/data-access.mjs +100 -4
- package/error-handling.mjs +107 -1
- package/index.mjs +2 -0
- package/infra-owner.mjs +39 -0
- package/input-bounds.mjs +64 -2
- package/lib/config.mjs +1 -1
- package/lib/hfs.mjs +7 -6
- package/lib/persistence.mjs +29 -0
- package/log-safety.mjs +89 -2
- package/package.json +1 -1
- package/runtime/knowledge/hfs/canon-pins.yaml +28 -9
- package/runtime/knowledge/hfs/slots.yaml +193 -128
- package/runtime/knowledge/patterns/fe/folder.yaml +36 -36
- package/runtime/packages/hfs/templates/app/package-scripts/package.json +22 -0
- package/runtime/scripts/checks/architecture/config.mjs +31 -11
- package/runtime/scripts/checks/architecture/contracts.mjs +4 -4
- package/runtime/scripts/checks/architecture/fe-slot-allows.mjs +7 -3
- package/runtime/scripts/checks/architecture/framework-pinned.mjs +5 -47
- package/runtime/scripts/checks/architecture/frontend.mjs +6 -4
- package/runtime/scripts/checks/architecture/hfs.mjs +104 -66
- package/runtime/scripts/checks/architecture/next-data.mjs +3 -2
- package/runtime/scripts/checks/architecture/registration.mjs +1 -1
- package/runtime/scripts/checks/architecture/symbols.mjs +13 -2
- package/runtime/scripts/checks/architecture/test-world-files.mjs +83 -45
- package/runtime/scripts/checks/architecture/typescript.mjs +45 -20
- package/runtime/scripts/checks/typescript-programs.mjs +2 -2
- package/runtime/scripts/lib/hfs-path-findings.mjs +13 -2
- package/runtime/scripts/lib/hfs-slots.mjs +244 -61
- package/runtime/scripts/lib/hfs-view.mjs +9 -7
- package/runtime/scripts/lib/language.mjs +11 -1
- package/service-deps.mjs +249 -0
- package/runtime/packages/hfs/templates/be/package-scripts/package.json +0 -19
- package/runtime/packages/hfs/templates/fe/package-scripts/package.json +0 -13
package/log-safety.mjs
CHANGED
|
@@ -14,8 +14,14 @@
|
|
|
14
14
|
*
|
|
15
15
|
* A log line outlives the request, is copied to a vendor, and is read by people who never had the credential or the
|
|
16
16
|
* consent. Log an id, a count, or a masked form instead.
|
|
17
|
+
*
|
|
18
|
+
* `no-secret-in-error` (R71) holds the same line for an error: a constructed error (its TYPE derives from `Error`) carries
|
|
19
|
+
* no `Secret` value and no text value named like a credential, because its message and params reach the same logs.
|
|
17
20
|
*/
|
|
18
|
-
import
|
|
21
|
+
import ts from "typescript"
|
|
22
|
+
import { isCredentialName } from "./config-owner.mjs"
|
|
23
|
+
import { keyName, wordsOf } from "./lib/ast.mjs"
|
|
24
|
+
import { derivesFromBuiltinError, presentParts, typeOf } from "./lib/declared.mjs"
|
|
19
25
|
import { isLoggerCall, isOwnedType } from "./lib/ports.mjs"
|
|
20
26
|
import { isDeclarationFile } from "./lib/path.mjs"
|
|
21
27
|
|
|
@@ -205,12 +211,93 @@ export const noSecretInLog = {
|
|
|
205
211
|
},
|
|
206
212
|
}
|
|
207
213
|
|
|
214
|
+
const STRINGY = ts.TypeFlags.String | ts.TypeFlags.StringLiteral | ts.TypeFlags.TemplateLiteral
|
|
215
|
+
|
|
216
|
+
/** Whether a node's type is text (a string, with `null`/`undefined` removed): the shape a raw credential travels in. */
|
|
217
|
+
const isText = (context, node) => {
|
|
218
|
+
const parts = presentParts(typeOf(context, node))
|
|
219
|
+
return parts.length > 0 && parts.every((part) => (part.flags & STRINGY) && !(part.flags & ts.TypeFlags.EnumLiteral))
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** The last name a value expression ends in (`token`, `request.headers.authorization`), or null. */
|
|
223
|
+
const ownNameOf = (node) => {
|
|
224
|
+
if (node.type === "Identifier") return node.name
|
|
225
|
+
if (node.type === "MemberExpression" && !node.computed && node.property.type === "Identifier") return node.property.name
|
|
226
|
+
return null
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** The `key: value` pairs of an argument tree whose value is text, so a key can name what the value is (`{ token: raw }`). */
|
|
230
|
+
const textPropertiesIn = (context, node, found = []) => {
|
|
231
|
+
if (node?.type === "ObjectExpression") {
|
|
232
|
+
for (const property of node.properties) {
|
|
233
|
+
if (property.type !== "Property" || property.computed || isHidingCall(property.value)) continue
|
|
234
|
+
const key = keyName(property.key)
|
|
235
|
+
if (key && isText(context, property.value)) found.push({ name: key, node: property.key })
|
|
236
|
+
textPropertiesIn(context, property.value, found)
|
|
237
|
+
}
|
|
238
|
+
} else if (node?.type === "ArrayExpression") node.elements.forEach((element) => textPropertiesIn(context, element, found))
|
|
239
|
+
return found
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* An error carries no secret. `no-secret-in-error` (R71) judges every `new X(...)` whose constructed TYPE derives from the
|
|
244
|
+
* language `Error` (a `DomainError` family, a library error, `Error` itself): no argument holds a value typed by the
|
|
245
|
+
* `Secret` brand of `platform/config` or read out of one, and no text value (a `string`) whose own name, or the key it is
|
|
246
|
+
* given under, says it is a credential (the same last-word test `secret-compare-timing-safe` uses: `password`,
|
|
247
|
+
* `accessToken`, `clientSecret`, `signingKey`). An error's message and params reach the error filter, the log line of the
|
|
248
|
+
* failure, the tracing vendor and, for a declared error, the client. Personal identifiers are not judged here: an error
|
|
249
|
+
* may name the account it refused.
|
|
250
|
+
*/
|
|
251
|
+
export const noSecretInError = {
|
|
252
|
+
meta: {
|
|
253
|
+
type: "problem",
|
|
254
|
+
docs: { description: "A constructed error carries no `Secret` value and no text value named like a credential." },
|
|
255
|
+
schema: [],
|
|
256
|
+
messages: {
|
|
257
|
+
secretType:
|
|
258
|
+
"`{{name}}` is a `Secret` (or is read out of one) and is put into an error. An error's message and params reach logs, tracing and sometimes the client. Name the failure with its code and pass an id or a masked form (`mask(...)`), never the value.",
|
|
259
|
+
secret:
|
|
260
|
+
"`{{name}}` reads as a credential and is put into an error. An error's message and params reach logs, tracing and sometimes the client. Name the failure with its code and pass an id or a masked form (`mask(...)`), never the value.",
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
create(context) {
|
|
264
|
+
const filename = context.filename || context.getFilename()
|
|
265
|
+
if (isDeclarationFile(filename)) return {}
|
|
266
|
+
const sourceCode = context.sourceCode || context.getSourceCode()
|
|
267
|
+
return {
|
|
268
|
+
NewExpression(node) {
|
|
269
|
+
if (node.arguments.length === 0 || !derivesFromBuiltinError(context, typeOf(context, node))) return
|
|
270
|
+
const reported = new Set()
|
|
271
|
+
const report = (at, messageId, name) => {
|
|
272
|
+
if (reported.has(at)) return
|
|
273
|
+
reported.add(at)
|
|
274
|
+
context.report({ node: at, messageId, data: { name } })
|
|
275
|
+
}
|
|
276
|
+
for (const argument of node.arguments) {
|
|
277
|
+
for (const value of valuesIn(argument)) {
|
|
278
|
+
if (isSecretType(context, value)) report(value, "secretType", sourceCode.getText(value))
|
|
279
|
+
else {
|
|
280
|
+
const name = ownNameOf(value)
|
|
281
|
+
if (name !== null && isCredentialName(name) && isText(context, value)) report(value, "secret", name)
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
for (const { name, node: at } of textPropertiesIn(context, argument)) {
|
|
285
|
+
if (isCredentialName(name)) report(at, "secret", name)
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
}
|
|
292
|
+
|
|
208
293
|
/** The rules this law contributes to the plugin. */
|
|
209
294
|
export const rules = {
|
|
210
295
|
"no-secret-in-log": noSecretInLog,
|
|
296
|
+
"no-secret-in-error": noSecretInError,
|
|
211
297
|
}
|
|
212
298
|
|
|
213
|
-
/**
|
|
299
|
+
/** Both start at error: no baseline exists, and the repositories' fix lanes clear the debt. */
|
|
214
300
|
export const recommended = {
|
|
215
301
|
"starci-be/no-secret-in-log": "error",
|
|
302
|
+
"starci-be/no-secret-in-error": "error",
|
|
216
303
|
}
|
package/package.json
CHANGED
|
@@ -5,7 +5,14 @@ purpose: >-
|
|
|
5
5
|
then the repositories' own upgrade lane (each sets the exact version and reinstalls; nothing else installs a @starci package). Read on 2026-09-29 from the packages under
|
|
6
6
|
.claude/packages and from the installed node_modules of nivo-backend, nivo-fe, starci-next, starci-next-fe,
|
|
7
7
|
mia-mia-backend and miamia-fe; where they disagreed the newest version installed in more than one repository
|
|
8
|
-
won, and the runtime's own version won for @starci packages.
|
|
8
|
+
won, and the runtime's own version won for @starci packages. An app (hfs 4) has ONE package.json for both sides, so a
|
|
9
|
+
dependency both sides declared at different versions takes one: its pin where it has one, otherwise the higher of the
|
|
10
|
+
two, pinned here with a why (2026-10-01, from nivo-backend/nivo-fe, starci-next/starci-next-fe and
|
|
11
|
+
mia-mia-backend/miamia-fe: every such conflict but graphql was already pinned). The runtime's example apps merged the
|
|
12
|
+
same way on 2026-10-01 (examples/todo-app from todo-app-backend/todo-app-frontend, examples/ecommerce-app from
|
|
13
|
+
ecommerce-app-be/ecommerce-app-fe): no dependency outside this file was declared at two versions across their sides, so
|
|
14
|
+
every choice is a pin (each side's older @starci pins and graphql moved to the version here). `side` names the side whose code uses
|
|
15
|
+
the dependency; the app root package.json carries the pins of both.
|
|
9
16
|
pins:
|
|
10
17
|
# --- @starci packages. Every @starci package is a PUBLISHED npm package (install: registry): a product repository
|
|
11
18
|
# installs the exact pinned version from the npm registry in the root and every workspace package.json, never `file:`.
|
|
@@ -17,19 +24,19 @@ pins:
|
|
|
17
24
|
source: packages/grammar/package.json
|
|
18
25
|
why: nivo-fe pins 0.4.11 and 0.5.0 in one workspace, starci-next-fe 0.5.1, miamia-fe 0.5.0; the runtime source is 0.8.0 (the brand layer sets `--font-sans` and `--font-mono`; the grammar reads them; 0.7.2 added the Input tel kind and IconButton disclosure props).
|
|
19
26
|
'@starci/eslint-canon-be':
|
|
20
|
-
version:
|
|
27
|
+
version: 3.0.0
|
|
21
28
|
group: starci
|
|
22
29
|
install: registry
|
|
23
30
|
side: be
|
|
24
31
|
source: packages/eslint/be/package.json
|
|
25
|
-
why: '2.0.0 (C0 release): starciBeConfig({ hfs: loadHfs(import.meta.url) }) typed factory and the BE-CONVENTION laws
|
|
32
|
+
why: '3.0.0: `loadHfs(import.meta.url)` of be/eslint.config.mjs finds the app-root hfs.json (kind app) and gives every linted file the view of the be side; the project graph is built per side. 2.0.0 (C0 release): starciBeConfig({ hfs: loadHfs(import.meta.url) }) typed factory and the BE-CONVENTION laws.'
|
|
26
33
|
'@starci/eslint-canon-fe':
|
|
27
|
-
version:
|
|
34
|
+
version: 8.0.0
|
|
28
35
|
group: starci
|
|
29
36
|
install: registry
|
|
30
37
|
side: fe
|
|
31
38
|
source: packages/eslint/fe/package.json
|
|
32
|
-
why: '7.0.0: the front end has no tests (
|
|
39
|
+
why: '8.0.0: `loadHfs(import.meta.url)` of fe/eslint.config.mjs finds the app-root hfs.json (kind app) and gives every linted file the view of the fe side; the project graph is built per side. 7.0.0: the front end has no tests (FE_NO_TESTS R97 in hfs); no-vietnamese-in-source (R91).'
|
|
33
40
|
'@starci/stylelint-canon':
|
|
34
41
|
version: 2.0.1
|
|
35
42
|
group: starci
|
|
@@ -50,19 +57,26 @@ pins:
|
|
|
50
57
|
side: both
|
|
51
58
|
source: packages/prettier-config/package.json
|
|
52
59
|
'@starci/jest-preset':
|
|
53
|
-
version: 2.
|
|
60
|
+
version: 2.2.0
|
|
54
61
|
group: starci
|
|
55
62
|
install: registry
|
|
56
63
|
side: be
|
|
57
64
|
source: packages/jest-preset/package.json
|
|
58
|
-
why: '2.1.0: four projects on the test world, the unit kit (mockEntityManager, fakeTransaction, fakeIds, FakeClock, Outcome matchers) and the recordingOutbox claim side.'
|
|
65
|
+
why: '2.2.0: the integration, e2e and contract projects run every spec file in a worker process of its own (world-runner.cjs), so a process-global framework registry (@nestjs/graphql type metadata) never leaks between e2e files; 2.1.0: four projects on the test world, the unit kit (mockEntityManager, fakeTransaction, fakeIds, FakeClock, Outcome matchers) and the recordingOutbox claim side.'
|
|
66
|
+
'@starci/test-world':
|
|
67
|
+
version: 1.0.0
|
|
68
|
+
group: starci
|
|
69
|
+
install: registry
|
|
70
|
+
side: be
|
|
71
|
+
source: packages/test-world/package.json
|
|
72
|
+
why: 'the shared e2e library of every back end (R47, R48): the warm stack behind toxiproxy, the network-edge fakes, the Nest boot, the typed useTestWorld handle, useSandbox for contract specs, the outage lock and the starci-test-stack bin behind the managed test:stack script; a devDependency of every back end.'
|
|
59
73
|
'@starci/hfs':
|
|
60
|
-
version:
|
|
74
|
+
version: 4.0.0
|
|
61
75
|
group: starci
|
|
62
76
|
install: registry
|
|
63
77
|
side: both
|
|
64
78
|
source: packages/hfs/package.json
|
|
65
|
-
why: '
|
|
79
|
+
why: '4.0.0: the app monorepo standard: one app repository with the root package.json, lockfile and hfs.json of kind app, and the be/ and fe/ sides; `hfs scaffold app <name>` makes it, `hfs lint` at the root lints be/** with the BE canon and fe/** with the FE canon; the standalone be/fe repository kinds and `hfs init` are deleted.'
|
|
66
80
|
# --- tooling
|
|
67
81
|
typescript:
|
|
68
82
|
version: 5.9.3
|
|
@@ -152,6 +166,11 @@ pins:
|
|
|
152
166
|
group: framework
|
|
153
167
|
side: fe
|
|
154
168
|
why: every HFS front end uses next-intl with the [locale] segment (hfs sync --init writes it); nivo-fe resolves 4.13.6.
|
|
169
|
+
graphql:
|
|
170
|
+
version: 16.14.2
|
|
171
|
+
group: framework
|
|
172
|
+
side: both
|
|
173
|
+
why: 'one app package.json for both sides (hfs 4): nivo-backend declares ^16.14.1 and nivo-fe ^16.14.2, the higher wins; all four product installs resolve 16.14.2. The example apps: todo-app-backend declared ^16.14.2 (devDependencies) and ecommerce-app-be ^16.14.2 (dependencies and devDependencies); each app root now has one dependencies entry at the pin.'
|
|
155
174
|
supertest:
|
|
156
175
|
version: 7.3.0
|
|
157
176
|
group: tooling
|