@starci/eslint-canon-be 3.0.8 → 3.0.10

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 (151) hide show
  1. package/CHANGELOG.md +13 -1
  2. package/LICENSE +21 -0
  3. package/README.md +12 -6
  4. package/ambient-id.mjs +90 -0
  5. package/cli.mjs +181 -0
  6. package/comments.mjs +24 -36
  7. package/config-owner.mjs +20 -4
  8. package/connections.mjs +12 -12
  9. package/cookies.mjs +106 -0
  10. package/cqrs.mjs +22 -26
  11. package/data-access.mjs +9 -28
  12. package/default-deny.mjs +2 -1
  13. package/e2e-flow.mjs +4 -4
  14. package/event-bus.mjs +237 -0
  15. package/feature-thin.mjs +237 -0
  16. package/idempotency.mjs +57 -6
  17. package/index.mjs +22 -1
  18. package/infra-owner.mjs +1 -1
  19. package/injection.mjs +67 -22
  20. package/input-bounds.mjs +2 -1
  21. package/jobs.mjs +214 -0
  22. package/lib/ast.mjs +7 -0
  23. package/lib/config.mjs +1 -1
  24. package/lib/db-writes.mjs +1 -1
  25. package/lib/doors.mjs +81 -0
  26. package/lib/edition.mjs +45 -0
  27. package/lib/entity-writes.mjs +35 -0
  28. package/lib/path.mjs +0 -6
  29. package/lib/persistence.mjs +11 -18
  30. package/lib/ports.mjs +5 -2
  31. package/lib/specifier.mjs +27 -0
  32. package/lib/transactions.mjs +37 -0
  33. package/lib/transport-slots.mjs +8 -3
  34. package/lib/unit-spec.mjs +63 -2
  35. package/module-layering.mjs +3 -24
  36. package/module-shape.mjs +2 -2
  37. package/naming.mjs +2 -2
  38. package/package.json +5 -2
  39. package/project-graph.mjs +5 -2
  40. package/projections.mjs +117 -0
  41. package/query-safety.mjs +1 -1
  42. package/realtime.mjs +181 -0
  43. package/runtime/engine/config.mjs +486 -0
  44. package/runtime/engine/invalid-config.mjs +15 -0
  45. package/runtime/engine/orca-config.mjs +27 -0
  46. package/runtime/engine/secrets.mjs +130 -0
  47. package/runtime/knowledge/hfs/canon-pins.yaml +162 -31
  48. package/runtime/knowledge/hfs/rules.yaml +2059 -0
  49. package/runtime/knowledge/hfs/slots.yaml +912 -120
  50. package/runtime/knowledge/patterns/fe/folder.yaml +131 -25
  51. package/runtime/knowledge/sonar-gate.yaml +52 -18
  52. package/runtime/modules/models/registry.yaml +553 -0
  53. package/runtime/modules/models/runtimes.yaml +695 -0
  54. package/runtime/packages/hfs/templates/app/package-scripts/package.json +3 -10
  55. package/runtime/scripts/api/git/config-get.mjs +10 -0
  56. package/runtime/scripts/api/git/lib.mjs +18 -26
  57. package/runtime/scripts/api/git/ls-files.mjs +6 -0
  58. package/runtime/scripts/api/git/remote.mjs +5 -0
  59. package/runtime/scripts/api/git/rev-parse-query.mjs +5 -0
  60. package/runtime/scripts/hfs/architecture/ast-walks.mjs +220 -0
  61. package/runtime/scripts/hfs/architecture/backend.mjs +16 -23
  62. package/runtime/scripts/hfs/architecture/background-unowned.mjs +16 -34
  63. package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +3 -1
  64. package/runtime/scripts/hfs/architecture/clones.mjs +4 -4
  65. package/runtime/scripts/hfs/architecture/config.mjs +9 -6
  66. package/runtime/scripts/hfs/architecture/connection-map.mjs +20 -8
  67. package/runtime/scripts/hfs/architecture/context-coupling.mjs +88 -0
  68. package/runtime/scripts/hfs/architecture/context-map.mjs +237 -0
  69. package/runtime/scripts/hfs/architecture/context-owner.mjs +82 -0
  70. package/runtime/scripts/hfs/architecture/context-platform-tables.mjs +47 -0
  71. package/runtime/scripts/hfs/architecture/context-transaction.mjs +60 -0
  72. package/runtime/scripts/hfs/architecture/contract-fixture-guard.mjs +4 -10
  73. package/runtime/scripts/hfs/architecture/contracts.mjs +33 -162
  74. package/runtime/scripts/hfs/architecture/dead-exports.mjs +58 -29
  75. package/runtime/scripts/hfs/architecture/doc-language.mjs +1 -1
  76. package/runtime/scripts/hfs/architecture/entrypoint.mjs +2 -1
  77. package/runtime/scripts/hfs/architecture/fe-slot-allows.mjs +2 -2
  78. package/runtime/scripts/hfs/architecture/feature-shape.mjs +5 -2
  79. package/runtime/scripts/hfs/architecture/framework-pinned.mjs +1 -1
  80. package/runtime/scripts/hfs/architecture/frontend.mjs +17 -43
  81. package/runtime/scripts/hfs/architecture/hfs.mjs +24 -39
  82. package/runtime/scripts/hfs/architecture/i18n-keys.mjs +1 -1
  83. package/runtime/scripts/hfs/architecture/index.mjs +17 -4
  84. package/runtime/scripts/hfs/architecture/machine-ast.mjs +44 -11
  85. package/runtime/scripts/hfs/architecture/managed-scripts.mjs +27 -0
  86. package/runtime/scripts/hfs/architecture/module-per-transport.mjs +30 -6
  87. package/runtime/scripts/hfs/architecture/next-data.mjs +22 -63
  88. package/runtime/scripts/hfs/architecture/owners.mjs +35 -15
  89. package/runtime/scripts/hfs/architecture/register-once.mjs +2 -1
  90. package/runtime/scripts/hfs/architecture/registration.mjs +23 -81
  91. package/runtime/scripts/hfs/architecture/required-files.mjs +20 -12
  92. package/runtime/scripts/hfs/architecture/schema-owner.mjs +17 -114
  93. package/runtime/scripts/hfs/architecture/server-action.mjs +24 -0
  94. package/runtime/scripts/hfs/architecture/side-boundary.mjs +16 -0
  95. package/runtime/scripts/hfs/architecture/source-names.mjs +27 -147
  96. package/runtime/scripts/hfs/architecture/sql-owner.mjs +59 -18
  97. package/runtime/scripts/hfs/architecture/sql-returning.mjs +64 -0
  98. package/runtime/scripts/hfs/architecture/supabase-ast.mjs +111 -0
  99. package/runtime/scripts/hfs/architecture/supabase-be.mjs +131 -0
  100. package/runtime/scripts/hfs/architecture/supabase-tables.mjs +42 -0
  101. package/runtime/scripts/hfs/architecture/supabase.mjs +434 -0
  102. package/runtime/scripts/hfs/architecture/surface.mjs +9 -3
  103. package/runtime/scripts/hfs/architecture/test-world-files.mjs +9 -19
  104. package/runtime/scripts/hfs/architecture/tiers.mjs +47 -5
  105. package/runtime/scripts/hfs/architecture/transport-owner.mjs +3 -3
  106. package/runtime/scripts/hfs/architecture/typescript.mjs +12 -26
  107. package/runtime/scripts/hfs/declaration-shape.mjs +62 -0
  108. package/runtime/scripts/hfs/declaration-slots.mjs +75 -0
  109. package/runtime/scripts/hfs/edition-slots.mjs +66 -0
  110. package/runtime/scripts/hfs/manifest-shape.mjs +109 -32
  111. package/runtime/scripts/hfs/param-names.mjs +18 -0
  112. package/runtime/scripts/hfs/path-findings.mjs +1 -1
  113. package/runtime/scripts/hfs/project-graph.mjs +3 -3
  114. package/runtime/scripts/hfs/repo-identity.mjs +9 -6
  115. package/runtime/scripts/hfs/rule-params-shape.mjs +50 -0
  116. package/runtime/scripts/hfs/rules/fe-no-tests.mjs +6 -5
  117. package/runtime/scripts/hfs/rules/read.mjs +1 -1
  118. package/runtime/scripts/hfs/rules/secrets.mjs +3 -3
  119. package/runtime/scripts/hfs/rules/spec-placement.mjs +1 -1
  120. package/runtime/scripts/hfs/slots.mjs +78 -108
  121. package/runtime/scripts/hfs/test-world-slot.mjs +6 -0
  122. package/runtime/scripts/hfs/typescript-programs.mjs +0 -5
  123. package/runtime/scripts/hfs/view.mjs +3 -2
  124. package/runtime/scripts/lib/dockerfile.mjs +124 -0
  125. package/runtime/scripts/lib/git.mjs +36 -0
  126. package/runtime/scripts/lib/i18n.mjs +86 -0
  127. package/runtime/scripts/lib/jsdoc.mjs +35 -0
  128. package/runtime/scripts/lib/json.mjs +13 -3
  129. package/runtime/scripts/lib/language.mjs +24 -5
  130. package/runtime/scripts/lib/list.mjs +20 -0
  131. package/runtime/scripts/lib/mutation-fence.mjs +15 -0
  132. package/runtime/scripts/lib/path-key.mjs +57 -2
  133. package/runtime/scripts/lib/read-text.mjs +34 -0
  134. package/runtime/scripts/lib/regex.mjs +4 -0
  135. package/runtime/scripts/lib/secret-patterns.mjs +4 -3
  136. package/runtime/scripts/lib/sops-envelope.mjs +112 -0
  137. package/runtime/scripts/lib/stack-services.mjs +7 -31
  138. package/runtime/scripts/lib/ts-ast.mjs +35 -0
  139. package/runtime/scripts/lib/walk.mjs +4 -3
  140. package/schema-authority.mjs +14 -12
  141. package/service-deps.mjs +7 -13
  142. package/service-isolation.mjs +72 -0
  143. package/size-budget.mjs +1 -1
  144. package/test-world.mjs +5 -4
  145. package/testing.mjs +27 -24
  146. package/transport.mjs +12 -9
  147. package/unit-spec.mjs +1 -1
  148. package/user-copy.mjs +5 -4
  149. package/webhooks.mjs +173 -0
  150. package/runtime/scripts/api/sops/decrypt.mjs +0 -16
  151. package/runtime/scripts/lib/test-secrets.mjs +0 -116
package/realtime.mjs ADDED
@@ -0,0 +1,181 @@
1
+ /**
2
+ * The rules that hold the realtime kind (R184 `BE_REALTIME_SHAPE`, R185 `BE_REALTIME_WRITES`, R186 `BE_REALTIME_TOPIC_SCOPE`).
3
+ *
4
+ * A realtime door (`src/features/realtime/<channel>/transport/websocket/<channel>.gateway.ts` for a socket, `transport/graphql/<x>.subscription.ts` for a GraphQL
5
+ * subscription, slot `be.feature.realtime`) READS and PUSHES. It never writes: the push is fed by a reactor that calls the `RealtimeHub`
6
+ * of `platform/realtime`, and the door only subscribes a client to a topic of that hub.
7
+ *
8
+ * - `realtime-shape` (R184): a `.gateway.ts` holds one `@WebSocketGateway` class whose handlers are `@SubscribeMessage`; a
9
+ * `.subscription.ts` holds one `@Resolver` class whose operations are `@Subscription`. Each is the door its file role names, and
10
+ * neither carries a query, a mutation or a REST route.
11
+ * - `realtime-read-only` (R185): the only value injected into a realtime door is the `RealtimeHub` of `platform/realtime`. No
12
+ * `EntityManager`, bus, queue, event bus or domain service reaches it.
13
+ * - `realtime-topic-scope` (R186): the topic passed to `hub.subscribe` is built from the principal parameter of the handler
14
+ * (`@CurrentPrincipal()` of `domain/identity`), so a client can only listen to what the principal owns.
15
+ *
16
+ * What a receiver is comes from its TYPE, what a decorator is from the import that binds it, and where a parameter comes
17
+ * from from the declaration its name resolves to. No rule here tests a path pattern or a variable name.
18
+ */
19
+ import { walk } from "./lib/ast.mjs"
20
+ import { aliasTarget, callsOf, hasClassDecorator, injectedMembers, methodsDecoratedBy, parametersOf } from "./lib/doors.mjs"
21
+ import { hfsOf } from "./lib/hfs.mjs"
22
+ import { decoratorCallee } from "./lib/import-source.mjs"
23
+ import { baseName, isOwnedType, ownerNameOf } from "./lib/ports.mjs"
24
+
25
+ /** The slot of realtime doors. */
26
+ const DOOR_SLOTS = new Set(["be.feature.realtime.graphql", "be.feature.realtime.websocket"])
27
+
28
+ /** The `RealtimeHub` port of `platform/realtime`. */
29
+ const isHubType = (context, node) => isOwnedType(context, node, { name: "RealtimeHub", capability: "realtime", tier: "platform" })
30
+
31
+ /** The decorators that make a class a door of one kind of realtime file, by file role. */
32
+ const ROLES = {
33
+ gateway: { classDecorator: ["@nestjs/websockets", "WebSocketGateway"], operation: ["@nestjs/websockets", "SubscribeMessage"], foreign: [["@nestjs/graphql", ["Query", "Mutation", "ResolveField", "Subscription"]], ["@nestjs/common", ["Controller"]]] },
34
+ subscription: { classDecorator: ["@nestjs/graphql", "Resolver"], operation: ["@nestjs/graphql", "Subscription"], foreign: [["@nestjs/graphql", ["Query", "Mutation", "ResolveField"]], ["@nestjs/websockets", ["WebSocketGateway", "SubscribeMessage"]], ["@nestjs/common", ["Controller"]]] },
35
+ }
36
+
37
+ /** The role (`gateway` or `subscription`) a realtime file name carries, or null. */
38
+ const roleOf = (filename) => /\.(gateway|subscription)\.ts$/.exec(baseName(filename))?.[1] ?? null
39
+
40
+ /** The door classes of a file: every class with a class-level door decorator of the role. */
41
+ const doorClasses = (context, program, role) => {
42
+ const found = []
43
+ walk(program, (node) => {
44
+ if (node.type === "ClassDeclaration" && hasClassDecorator(context, node, ...ROLES[role].classDecorator)) found.push(node)
45
+ })
46
+ return found
47
+ }
48
+
49
+ /** A realtime file is the one door its role names, and nothing else. */
50
+ export const realtimeShape = {
51
+ meta: {
52
+ type: "problem",
53
+ docs: { description: "A realtime file is one door: a `.gateway.ts` holds one `@WebSocketGateway` class with `@SubscribeMessage` handlers, a `.subscription.ts` holds one `@Resolver` class with `@Subscription` operations, and neither carries a route, query or mutation." },
54
+ schema: [],
55
+ messages: {
56
+ door: "A `<channel>.{{role}}.ts` file declares exactly one `{{decorator}}` class (this one declares {{count}}). One file is one door.",
57
+ operations: "This {{role}} door has no `@{{operation}}` handler. A realtime door exists to serve a client one: it subscribes the client to a topic of the `RealtimeHub`.",
58
+ foreign: "`@{{name}}` has no place in a realtime {{role}} door. A realtime door only subscribes and pushes; a query, a mutation, a field resolver or a REST route belongs in an api feature, and the other realtime protocol in its own file.",
59
+ },
60
+ },
61
+ create(context) {
62
+ const hfs = hfsOf(context)
63
+ const role = roleOf(context.filename)
64
+ if (!DOOR_SLOTS.has(hfs.slotOf(context.filename) ?? "") || role === null) return {}
65
+ const spec = ROLES[role]
66
+ return {
67
+ "Program:exit"(program) {
68
+ const doors = doorClasses(context, program, role)
69
+ if (doors.length !== 1) {
70
+ context.report({ node: program, messageId: "door", data: { role, decorator: `@${spec.classDecorator[1]}`, count: String(doors.length) } })
71
+ return
72
+ }
73
+ if (methodsDecoratedBy(context, doors[0], ...spec.operation).length === 0) context.report({ node: doors[0], messageId: "operations", data: { role, operation: spec.operation[1] } })
74
+ walk(program, (node) => {
75
+ if (node.type !== "Decorator") return
76
+ for (const [source, names] of spec.foreign) {
77
+ const hit = names.find((name) => hasDecorator(context, node, source, name))
78
+ if (hit !== undefined) context.report({ node, messageId: "foreign", data: { name: hit, role } })
79
+ }
80
+ })
81
+ },
82
+ }
83
+ },
84
+ }
85
+
86
+ /** True when one decorator node is `name` exported by `source`. */
87
+ const hasDecorator = (context, decorator, source, name) => hasClassDecorator(context, { decorators: [decorator] }, source, name)
88
+
89
+ /** A realtime door injects the hub and nothing else: it reads and pushes, it never writes. */
90
+ export const realtimeReadOnly = {
91
+ meta: {
92
+ type: "problem",
93
+ docs: { description: "The only value a realtime door injects is the `RealtimeHub` of `platform/realtime`: no `EntityManager`, bus, queue, event bus or domain service." },
94
+ schema: [],
95
+ messages: {
96
+ injects: "`{{what}}` is injected into a realtime door. A realtime door reads and pushes only: it injects the `RealtimeHub` of `platform/realtime` and nothing that can write (an `EntityManager`, a `CommandBus`, an event bus, a queue, a domain service). The push is fed by a reactor that calls the hub; a write belongs in an api feature.",
97
+ },
98
+ },
99
+ create(context) {
100
+ const hfs = hfsOf(context)
101
+ if (!DOOR_SLOTS.has(hfs.slotOf(context.filename) ?? "") || roleOf(context.filename) === null) return {}
102
+ return {
103
+ ClassDeclaration(node) {
104
+ for (const { node: member, annotation } of injectedMembers(context, node)) {
105
+ if (!annotation || !isHubType(context, annotation)) context.report({ node: member, messageId: "injects", data: { what: context.sourceCode.getText(member) } })
106
+ }
107
+ },
108
+ }
109
+ },
110
+ }
111
+
112
+ /** True when the parameter carries `@CurrentPrincipal()` declared by `domain/identity`, judged by the declaration the decorator resolves to. */
113
+ const isPrincipalParameter = (context, hfs, param) =>
114
+ (param.decorators ?? []).some((decorator) => {
115
+ const callee = decoratorCallee(decorator)
116
+ if (callee?.type !== "Identifier") return false
117
+ const symbol = aliasTarget(context, callee)
118
+ return symbol?.getName() === "CurrentPrincipal" && (symbol.getDeclarations() ?? []).some((declaration) => ownerNameOf(hfs, declaration.getSourceFile().fileName) === "identity" && hfs.tierOf(declaration.getSourceFile().fileName) === "domain")
119
+ })
120
+
121
+ /** True when the identifier resolves to one of the `names` (parameters of the handler). */
122
+ const resolvesTo = (context, identifier, parameters) => {
123
+ for (let scope = context.sourceCode.getScope(identifier); scope; scope = scope.upper) {
124
+ const variable = scope.set.get(identifier.name)
125
+ if (!variable) continue
126
+ return variable.defs.some((definition) => definition.type === "Parameter" && parameters.has(definition.name))
127
+ }
128
+ return false
129
+ }
130
+
131
+ /** The topic a client subscribes to is built from the principal the handler receives. */
132
+ export const realtimeTopicScope = {
133
+ meta: {
134
+ type: "problem",
135
+ docs: { description: "The topic passed to `RealtimeHub.subscribe` is built from the handler's `@CurrentPrincipal()` parameter." },
136
+ schema: [],
137
+ messages: {
138
+ scope: "The topic of this `subscribe` is not built from the principal. A realtime handler takes `@CurrentPrincipal() principal` and subscribes to a topic made from `principal.id` (plus any verified id), so one client cannot listen to another's channel. A topic taken only from client input is a leak.",
139
+ },
140
+ },
141
+ create(context) {
142
+ const hfs = hfsOf(context)
143
+ const role = roleOf(context.filename)
144
+ if (!DOOR_SLOTS.has(hfs.slotOf(context.filename) ?? "") || role === null) return {}
145
+ const [source, name] = ROLES[role].operation
146
+ return {
147
+ ClassDeclaration(node) {
148
+ for (const method of methodsDecoratedBy(context, node, source, name)) {
149
+ const principals = new Set()
150
+ for (const param of parametersOf(method)) {
151
+ if (!isPrincipalParameter(context, hfs, param)) continue
152
+ const target = param.type === "AssignmentPattern" ? param.left : param
153
+ if (target.type === "Identifier") principals.add(target)
154
+ }
155
+ for (const { call, receiver, method: called } of callsOf(method)) {
156
+ if (called !== "subscribe" || receiver === null || !isHubType(context, receiver)) continue
157
+ let scoped = false
158
+ walk(call.arguments[0], (child) => {
159
+ if (child.type === "Identifier" && resolvesTo(context, child, principals)) scoped = true
160
+ })
161
+ if (!scoped) context.report({ node: call, messageId: "scope" })
162
+ }
163
+ }
164
+ },
165
+ }
166
+ },
167
+ }
168
+
169
+ /** The rules this law contributes to the plugin. */
170
+ export const rules = {
171
+ "realtime-shape": realtimeShape,
172
+ "realtime-read-only": realtimeReadOnly,
173
+ "realtime-topic-scope": realtimeTopicScope,
174
+ }
175
+
176
+ /** Every rule of this law at `error`. */
177
+ export const recommended = {
178
+ "starci-be/realtime-shape": "error",
179
+ "starci-be/realtime-read-only": "error",
180
+ "starci-be/realtime-topic-scope": "error",
181
+ }