@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.
- package/CHANGELOG.md +13 -1
- package/LICENSE +21 -0
- package/README.md +12 -6
- package/ambient-id.mjs +90 -0
- package/cli.mjs +181 -0
- package/comments.mjs +24 -36
- package/config-owner.mjs +20 -4
- package/connections.mjs +12 -12
- package/cookies.mjs +106 -0
- package/cqrs.mjs +22 -26
- package/data-access.mjs +9 -28
- package/default-deny.mjs +2 -1
- package/e2e-flow.mjs +4 -4
- package/event-bus.mjs +237 -0
- package/feature-thin.mjs +237 -0
- package/idempotency.mjs +57 -6
- package/index.mjs +22 -1
- package/infra-owner.mjs +1 -1
- package/injection.mjs +67 -22
- package/input-bounds.mjs +2 -1
- package/jobs.mjs +214 -0
- package/lib/ast.mjs +7 -0
- package/lib/config.mjs +1 -1
- package/lib/db-writes.mjs +1 -1
- package/lib/doors.mjs +81 -0
- package/lib/edition.mjs +45 -0
- package/lib/entity-writes.mjs +35 -0
- package/lib/path.mjs +0 -6
- package/lib/persistence.mjs +11 -18
- package/lib/ports.mjs +5 -2
- package/lib/specifier.mjs +27 -0
- package/lib/transactions.mjs +37 -0
- package/lib/transport-slots.mjs +8 -3
- package/lib/unit-spec.mjs +63 -2
- package/module-layering.mjs +3 -24
- package/module-shape.mjs +2 -2
- package/naming.mjs +2 -2
- package/package.json +5 -2
- package/project-graph.mjs +5 -2
- package/projections.mjs +117 -0
- package/query-safety.mjs +1 -1
- package/realtime.mjs +181 -0
- package/runtime/engine/config.mjs +486 -0
- package/runtime/engine/invalid-config.mjs +15 -0
- package/runtime/engine/orca-config.mjs +27 -0
- package/runtime/engine/secrets.mjs +130 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +162 -31
- package/runtime/knowledge/hfs/rules.yaml +2059 -0
- package/runtime/knowledge/hfs/slots.yaml +912 -120
- package/runtime/knowledge/patterns/fe/folder.yaml +131 -25
- package/runtime/knowledge/sonar-gate.yaml +52 -18
- package/runtime/modules/models/registry.yaml +553 -0
- package/runtime/modules/models/runtimes.yaml +695 -0
- package/runtime/packages/hfs/templates/app/package-scripts/package.json +3 -10
- package/runtime/scripts/api/git/config-get.mjs +10 -0
- package/runtime/scripts/api/git/lib.mjs +18 -26
- package/runtime/scripts/api/git/ls-files.mjs +6 -0
- package/runtime/scripts/api/git/remote.mjs +5 -0
- package/runtime/scripts/api/git/rev-parse-query.mjs +5 -0
- package/runtime/scripts/hfs/architecture/ast-walks.mjs +220 -0
- package/runtime/scripts/hfs/architecture/backend.mjs +16 -23
- package/runtime/scripts/hfs/architecture/background-unowned.mjs +16 -34
- package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +3 -1
- package/runtime/scripts/hfs/architecture/clones.mjs +4 -4
- package/runtime/scripts/hfs/architecture/config.mjs +9 -6
- package/runtime/scripts/hfs/architecture/connection-map.mjs +20 -8
- package/runtime/scripts/hfs/architecture/context-coupling.mjs +88 -0
- package/runtime/scripts/hfs/architecture/context-map.mjs +237 -0
- package/runtime/scripts/hfs/architecture/context-owner.mjs +82 -0
- package/runtime/scripts/hfs/architecture/context-platform-tables.mjs +47 -0
- package/runtime/scripts/hfs/architecture/context-transaction.mjs +60 -0
- package/runtime/scripts/hfs/architecture/contract-fixture-guard.mjs +4 -10
- package/runtime/scripts/hfs/architecture/contracts.mjs +33 -162
- package/runtime/scripts/hfs/architecture/dead-exports.mjs +58 -29
- package/runtime/scripts/hfs/architecture/doc-language.mjs +1 -1
- package/runtime/scripts/hfs/architecture/entrypoint.mjs +2 -1
- package/runtime/scripts/hfs/architecture/fe-slot-allows.mjs +2 -2
- package/runtime/scripts/hfs/architecture/feature-shape.mjs +5 -2
- package/runtime/scripts/hfs/architecture/framework-pinned.mjs +1 -1
- package/runtime/scripts/hfs/architecture/frontend.mjs +17 -43
- package/runtime/scripts/hfs/architecture/hfs.mjs +24 -39
- package/runtime/scripts/hfs/architecture/i18n-keys.mjs +1 -1
- package/runtime/scripts/hfs/architecture/index.mjs +17 -4
- package/runtime/scripts/hfs/architecture/machine-ast.mjs +44 -11
- package/runtime/scripts/hfs/architecture/managed-scripts.mjs +27 -0
- package/runtime/scripts/hfs/architecture/module-per-transport.mjs +30 -6
- package/runtime/scripts/hfs/architecture/next-data.mjs +22 -63
- package/runtime/scripts/hfs/architecture/owners.mjs +35 -15
- package/runtime/scripts/hfs/architecture/register-once.mjs +2 -1
- package/runtime/scripts/hfs/architecture/registration.mjs +23 -81
- package/runtime/scripts/hfs/architecture/required-files.mjs +20 -12
- package/runtime/scripts/hfs/architecture/schema-owner.mjs +17 -114
- package/runtime/scripts/hfs/architecture/server-action.mjs +24 -0
- package/runtime/scripts/hfs/architecture/side-boundary.mjs +16 -0
- package/runtime/scripts/hfs/architecture/source-names.mjs +27 -147
- package/runtime/scripts/hfs/architecture/sql-owner.mjs +59 -18
- package/runtime/scripts/hfs/architecture/sql-returning.mjs +64 -0
- package/runtime/scripts/hfs/architecture/supabase-ast.mjs +111 -0
- package/runtime/scripts/hfs/architecture/supabase-be.mjs +131 -0
- package/runtime/scripts/hfs/architecture/supabase-tables.mjs +42 -0
- package/runtime/scripts/hfs/architecture/supabase.mjs +434 -0
- package/runtime/scripts/hfs/architecture/surface.mjs +9 -3
- package/runtime/scripts/hfs/architecture/test-world-files.mjs +9 -19
- package/runtime/scripts/hfs/architecture/tiers.mjs +47 -5
- package/runtime/scripts/hfs/architecture/transport-owner.mjs +3 -3
- package/runtime/scripts/hfs/architecture/typescript.mjs +12 -26
- package/runtime/scripts/hfs/declaration-shape.mjs +62 -0
- package/runtime/scripts/hfs/declaration-slots.mjs +75 -0
- package/runtime/scripts/hfs/edition-slots.mjs +66 -0
- package/runtime/scripts/hfs/manifest-shape.mjs +109 -32
- package/runtime/scripts/hfs/param-names.mjs +18 -0
- package/runtime/scripts/hfs/path-findings.mjs +1 -1
- package/runtime/scripts/hfs/project-graph.mjs +3 -3
- package/runtime/scripts/hfs/repo-identity.mjs +9 -6
- package/runtime/scripts/hfs/rule-params-shape.mjs +50 -0
- package/runtime/scripts/hfs/rules/fe-no-tests.mjs +6 -5
- package/runtime/scripts/hfs/rules/read.mjs +1 -1
- package/runtime/scripts/hfs/rules/secrets.mjs +3 -3
- package/runtime/scripts/hfs/rules/spec-placement.mjs +1 -1
- package/runtime/scripts/hfs/slots.mjs +78 -108
- package/runtime/scripts/hfs/test-world-slot.mjs +6 -0
- package/runtime/scripts/hfs/typescript-programs.mjs +0 -5
- package/runtime/scripts/hfs/view.mjs +3 -2
- package/runtime/scripts/lib/dockerfile.mjs +124 -0
- package/runtime/scripts/lib/git.mjs +36 -0
- package/runtime/scripts/lib/i18n.mjs +86 -0
- package/runtime/scripts/lib/jsdoc.mjs +35 -0
- package/runtime/scripts/lib/json.mjs +13 -3
- package/runtime/scripts/lib/language.mjs +24 -5
- package/runtime/scripts/lib/list.mjs +20 -0
- package/runtime/scripts/lib/mutation-fence.mjs +15 -0
- package/runtime/scripts/lib/path-key.mjs +57 -2
- package/runtime/scripts/lib/read-text.mjs +34 -0
- package/runtime/scripts/lib/regex.mjs +4 -0
- package/runtime/scripts/lib/secret-patterns.mjs +4 -3
- package/runtime/scripts/lib/sops-envelope.mjs +112 -0
- package/runtime/scripts/lib/stack-services.mjs +7 -31
- package/runtime/scripts/lib/ts-ast.mjs +35 -0
- package/runtime/scripts/lib/walk.mjs +4 -3
- package/schema-authority.mjs +14 -12
- package/service-deps.mjs +7 -13
- package/service-isolation.mjs +72 -0
- package/size-budget.mjs +1 -1
- package/test-world.mjs +5 -4
- package/testing.mjs +27 -24
- package/transport.mjs +12 -9
- package/unit-spec.mjs +1 -1
- package/user-copy.mjs +5 -4
- package/webhooks.mjs +173 -0
- package/runtime/scripts/api/sops/decrypt.mjs +0 -16
- 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
|
+
}
|