@starci/eslint-canon-be 3.2.18 → 3.3.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 +46 -4
- package/app-composition.mjs +176 -0
- package/config-owner.mjs +34 -19
- package/feature-thin.mjs +59 -17
- package/index.mjs +2 -0
- package/lib/app-roles.mjs +48 -0
- package/lib/webhook-ack.mjs +87 -0
- package/lib/webhook-handoff.mjs +138 -0
- package/lib/webhook-verifiers.mjs +147 -0
- package/module-layering.mjs +2 -2
- package/module-shape.mjs +6 -5
- package/package.json +1 -1
- package/runtime/knowledge/hfs/canon-pins.yaml +4 -4
- package/runtime/knowledge/hfs/rules.yaml +41 -13
- package/runtime/knowledge/hfs/slots.yaml +99 -43
- package/runtime/knowledge/patterns/fe/folder.yaml +3 -0
- package/runtime/scripts/api/git/cat-file.mjs +5 -0
- package/runtime/scripts/api/git/diff.mjs +5 -0
- package/runtime/scripts/hfs/architecture/backend-app-files.mjs +110 -0
- package/runtime/scripts/hfs/architecture/backend.mjs +20 -3
- package/runtime/scripts/hfs/architecture/background-unowned.mjs +157 -41
- package/runtime/scripts/hfs/architecture/canon-clones.mjs +240 -0
- package/runtime/scripts/hfs/architecture/canon-jsx-clones.mjs +143 -0
- package/runtime/scripts/hfs/architecture/clones.mjs +17 -8
- package/runtime/scripts/hfs/architecture/connection-map.mjs +14 -12
- package/runtime/scripts/hfs/architecture/context-map.mjs +25 -14
- package/runtime/scripts/hfs/architecture/context-owner.mjs +1 -2
- package/runtime/scripts/hfs/architecture/dead-exports.mjs +14 -9
- package/runtime/scripts/hfs/architecture/frontend-block-render.mjs +104 -0
- package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +30 -11
- package/runtime/scripts/hfs/architecture/i18n-keys.mjs +16 -3
- package/runtime/scripts/hfs/architecture/machine-ast.mjs +17 -0
- package/runtime/scripts/hfs/architecture/module-per-transport.mjs +21 -12
- package/runtime/scripts/hfs/architecture/register-once.mjs +13 -5
- package/runtime/scripts/hfs/architecture/required-files.mjs +2 -1
- package/runtime/scripts/hfs/architecture/staged-consumers.mjs +37 -0
- package/runtime/scripts/hfs/architecture/supabase-ast.mjs +3 -6
- package/runtime/scripts/hfs/architecture/supabase-be.mjs +1 -6
- package/runtime/scripts/hfs/architecture/tiers.mjs +8 -1
- package/runtime/scripts/hfs/architecture/typescript.mjs +1 -8
- package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +1 -11
- package/runtime/scripts/hfs/manifest-shape.mjs +27 -2
- package/runtime/scripts/hfs/path-findings.mjs +32 -2
- package/runtime/scripts/hfs/rules/secrets.mjs +13 -0
- package/runtime/scripts/hfs/slot-imports.mjs +3 -1
- package/runtime/scripts/hfs/slot-required.mjs +13 -0
- package/runtime/scripts/hfs/slot-semantic-problems.mjs +2 -1
- package/runtime/scripts/lib/ts-analysis.mjs +20 -0
- package/webhooks.mjs +141 -69
package/webhooks.mjs
CHANGED
|
@@ -3,14 +3,15 @@
|
|
|
3
3
|
*
|
|
4
4
|
* A provider webhook (`src/features/webhooks/<provider>/transport/http/<provider>.webhook.ts`, slot `be.feature.webhooks.http`) is a signed intake door. It
|
|
5
5
|
* proves the delivery came from the provider, then hands it to exactly ONE domain intake method, which records it and
|
|
6
|
-
*
|
|
6
|
+
* records the inbox delivery inside its own transaction. A reactor or job consumes it and dispatches the command. The door
|
|
7
7
|
* writes nothing and decides nothing:
|
|
8
8
|
*
|
|
9
9
|
* - `webhook-shape` (R182): the door is one `@Controller` with one `@Post` handler, `@Public({ reason: PublicReason.SignedWebhook })`;
|
|
10
10
|
* it injects only the `WebhookSignatureService` of `platform/http-security` and services of `modules/domain`; its handler has no
|
|
11
|
-
* branch, loop or `try`, makes
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* branch, loop or `try`, makes one domain intake call, optionally preceded by a declared domain verifier, and returns nothing or
|
|
12
|
+
* its own exported literal `PROVIDER_ACK`. An optional signed `@Get` verifies and echoes only the domain verifier result.
|
|
13
|
+
* A single declared domain method may verify and intake internally, using per-connection secrets.
|
|
14
|
+
* - `webhook-verify-first` (R183): the first statement is the unconditional shared or declared domain proof call, so no path reaches the
|
|
14
15
|
* intake with an unproven delivery.
|
|
15
16
|
*
|
|
16
17
|
* What a receiver is comes from its TYPE (where the class is declared); what a decorator is comes from the import that binds it.
|
|
@@ -19,26 +20,21 @@
|
|
|
19
20
|
import { walk } from "./lib/ast.mjs"
|
|
20
21
|
import { callsOf, decoratorsFrom, hasClassDecorator, injectedMembers, methodsDecoratedBy } from "./lib/doors.mjs"
|
|
21
22
|
import { hfsOf } from "./lib/hfs.mjs"
|
|
22
|
-
import { decoratorArguments, decoratorCallee, importOf } from "./lib/import-source.mjs"
|
|
23
|
-
import { baseName, enumMemberOf
|
|
24
|
-
import {
|
|
23
|
+
import { decoratorArguments, decoratorCallee, importOf, isImportedFrom } from "./lib/import-source.mjs"
|
|
24
|
+
import { baseName, enumMemberOf } from "./lib/ports.mjs"
|
|
25
|
+
import { isProviderAcknowledgement } from "./lib/webhook-ack.mjs"
|
|
26
|
+
import { webhookForbiddenReferences, webhookIntakeDispatchesCommand } from "./lib/webhook-handoff.mjs"
|
|
27
|
+
import { firstWebhookChallenge, firstWebhookVerification, isWebhookChallengeReturn, isWebhookDomainServiceType as isDomainServiceType, isWebhookSignatureType as isSignatureType } from "./lib/webhook-verifiers.mjs"
|
|
25
28
|
|
|
26
29
|
/** The slot of provider webhook doors. */
|
|
27
30
|
const SLOT = "be.feature.webhooks.http"
|
|
28
31
|
|
|
29
|
-
/**
|
|
32
|
+
/** One POST intake and, optionally, one GET subscription challenge are the only webhook routes. */
|
|
30
33
|
const HTTP_VERBS = ["Get", "Post", "Put", "Patch", "Delete", "All", "Options", "Head"]
|
|
31
34
|
|
|
32
35
|
/** Statements and expressions that are a decision or a repetition: none of them belongs in a door. */
|
|
33
36
|
const DECISION_NODES = new Set(["IfStatement", "SwitchStatement", "ConditionalExpression", "LogicalExpression", "ForStatement", "ForInStatement", "ForOfStatement", "WhileStatement", "DoWhileStatement", "TryStatement", "ThrowStatement"])
|
|
34
37
|
|
|
35
|
-
/** The `WebhookSignatureService` of `platform/http-security`. */
|
|
36
|
-
const isSignatureType = (context, node) => isOwnedType(context, node, { name: "WebhookSignatureService", capability: "http-security", tier: "platform" })
|
|
37
|
-
|
|
38
|
-
/** A service of `modules/domain`: a class declared in a `<name>.service.ts` of the domain tier. */
|
|
39
|
-
const isDomainServiceType = (context, hfs, node) =>
|
|
40
|
-
typeOrigins(context, node).some((origin) => origin.module === null && hfs.tierOf(origin.file) === "domain" && baseName(origin.file).endsWith(".service.ts"))
|
|
41
|
-
|
|
42
38
|
/** The `@Controller` classes of a webhook file, with their handler methods. */
|
|
43
39
|
const doorsOf = (context, program) => {
|
|
44
40
|
const doors = []
|
|
@@ -55,6 +51,19 @@ const routeMethods = (context, controller) => methodsDecoratedBy(context, contro
|
|
|
55
51
|
/** The handler of a webhook door: its `@Post` method, or null when it has none. */
|
|
56
52
|
const handlerOf = (context, controller) => methodsDecoratedBy(context, controller, "@nestjs/common", "Post")[0] ?? null
|
|
57
53
|
|
|
54
|
+
/** Aliased decorators retain the HTTP verb exported by Nest, rather than their local spelling. */
|
|
55
|
+
const routeVerbs = (context, route) => decoratorsFrom(context, route, "@nestjs/common", HTTP_VERBS).map((decorator) => HTTP_VERBS.find((verb) => isImportedFrom(context, decoratorCallee(decorator), "@nestjs/common", verb)))
|
|
56
|
+
|
|
57
|
+
/** One unambiguous POST plus at most one unambiguous GET; stacked verb decorators grant no extra routes. */
|
|
58
|
+
const validRoutes = (context, routes) => {
|
|
59
|
+
const verbs = routes.flatMap((route) => routeVerbs(context, route))
|
|
60
|
+
return verbs.length === routes.length && verbs.filter((verb) => verb === "Post").length === 1
|
|
61
|
+
&& verbs.filter((verb) => verb === "Get").length <= 1 && verbs.every((verb) => verb === "Post" || verb === "Get")
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** A route's name is only presentation, never its identity. */
|
|
65
|
+
const methodName = (method) => method.key.type === "Identifier" ? method.key.name : "the handler"
|
|
66
|
+
|
|
58
67
|
/** True when the method carries `@Public({ reason: PublicReason.SignedWebhook })`: the reason is read from the enum member the argument is. */
|
|
59
68
|
const isSignedWebhookDoor = (context, method) =>
|
|
60
69
|
(method.decorators ?? []).some((decorator) => {
|
|
@@ -67,60 +76,130 @@ const isSignedWebhookDoor = (context, method) =>
|
|
|
67
76
|
return member !== null && member.enumName === "PublicReason" && member.member === "SignedWebhook"
|
|
68
77
|
})
|
|
69
78
|
|
|
79
|
+
/** Decisions are equally forbidden in the delivery and subscription doors. */
|
|
80
|
+
const reportDecisions = (context, method) => {
|
|
81
|
+
walk(method.value.body, (child) => {
|
|
82
|
+
if (DECISION_NODES.has(child.type)) context.report({ node: child, messageId: "branch", data: { what: child.type === "LogicalExpression" ? child.operator : child.type.replace(/(Statement|Expression)$/, "").toLowerCase() } })
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** POST returns are restricted to the immutable literal acknowledgement of this webhook feature. */
|
|
87
|
+
const reportPostReturns = (context, hfs, handler) => {
|
|
88
|
+
walk(handler.value.body, (child) => {
|
|
89
|
+
if (child.type === "ReturnStatement" && child.argument && !isProviderAcknowledgement(context, hfs, child.argument)) context.report({ node: child, messageId: "returns", data: { name: methodName(handler) } })
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Delivery calls consist only of the completed proof and one domain inbox intake. */
|
|
94
|
+
const deliveryCallKind = (context, hfs, entry, separateProof, proof) => {
|
|
95
|
+
if (entry.receiver === null) return "call"
|
|
96
|
+
if (isSignatureType(context, entry.receiver) && entry.method === "verify") return "proof"
|
|
97
|
+
if (!isDomainServiceType(context, hfs, entry.receiver)) return "call"
|
|
98
|
+
if (separateProof && entry.call === proof) return "proof"
|
|
99
|
+
return "intake"
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** A declared proof is no escape hatch for dispatching commands behind the intake door. */
|
|
103
|
+
const reportDomainDispatch = (context, hfs, call, receiver) => {
|
|
104
|
+
if (receiver !== null && isDomainServiceType(context, hfs, receiver) && webhookIntakeDispatchesCommand(context, hfs, call)) context.report({ node: call, messageId: "handoff" })
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Delivery calls consist only of the completed proof and one domain inbox intake. */
|
|
108
|
+
const reportIntakeCalls = (context, hfs, handler) => {
|
|
109
|
+
const proof = firstWebhookVerification(context, hfs, handler)
|
|
110
|
+
const calls = callsOf(handler)
|
|
111
|
+
const domainCalls = calls.filter(({ receiver }) => receiver !== null && isDomainServiceType(context, hfs, receiver))
|
|
112
|
+
const separateProof = domainCalls.length > 1 && domainCalls.some(({ call }) => call === proof)
|
|
113
|
+
let intake = 0
|
|
114
|
+
for (const entry of calls) {
|
|
115
|
+
const { call } = entry
|
|
116
|
+
reportDomainDispatch(context, hfs, call, entry.receiver)
|
|
117
|
+
const kind = deliveryCallKind(context, hfs, entry, separateProof, proof)
|
|
118
|
+
if (kind === "proof") continue
|
|
119
|
+
if (kind === "intake") {
|
|
120
|
+
intake += 1
|
|
121
|
+
continue
|
|
122
|
+
}
|
|
123
|
+
context.report({ node: call, messageId: "call", data: { what: context.sourceCode.getText(call.callee) } })
|
|
124
|
+
}
|
|
125
|
+
if (intake !== 1) context.report({ node: handler.key, messageId: "intakeCalls", data: { name: methodName(handler), count: String(intake) } })
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** A challenge calls its domain verifier once and echoes exactly the returned value. */
|
|
129
|
+
const reportChallenge = (context, hfs, handler) => {
|
|
130
|
+
const challenge = firstWebhookChallenge(context, hfs, handler)
|
|
131
|
+
let echoes = 0
|
|
132
|
+
walk(handler.value.body, (child) => {
|
|
133
|
+
if (child.type !== "ReturnStatement") return
|
|
134
|
+
echoes += 1
|
|
135
|
+
if (!isWebhookChallengeReturn(context, child, challenge)) context.report({ node: child, messageId: "returns", data: { name: methodName(handler) } })
|
|
136
|
+
})
|
|
137
|
+
if (echoes === 0) context.report({ node: handler.key, messageId: "returns", data: { name: methodName(handler) } })
|
|
138
|
+
for (const { call, receiver } of callsOf(handler)) {
|
|
139
|
+
reportDomainDispatch(context, hfs, call, receiver)
|
|
140
|
+
if (call !== challenge?.call) context.report({ node: call, messageId: "call", data: { what: context.sourceCode.getText(call.callee) } })
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Public declaration and branch restrictions apply to both routes. */
|
|
145
|
+
const reportRoute = (context, hfs, handler, challenge = false) => {
|
|
146
|
+
if (!isSignedWebhookDoor(context, handler)) context.report({ node: handler.key, messageId: "notPublic", data: { name: methodName(handler) } })
|
|
147
|
+
reportDecisions(context, handler)
|
|
148
|
+
if (challenge) reportChallenge(context, hfs, handler)
|
|
149
|
+
else {
|
|
150
|
+
reportPostReturns(context, hfs, handler)
|
|
151
|
+
reportIntakeCalls(context, hfs, handler)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** The provider file contains one controller; an exported PROVIDER_ACK declaration may accompany it. */
|
|
156
|
+
const reportDoor = (context, hfs, program) => {
|
|
157
|
+
const doors = doorsOf(context, program)
|
|
158
|
+
if (doors.length !== 1) {
|
|
159
|
+
context.report({ node: program, messageId: "noDoor" })
|
|
160
|
+
return
|
|
161
|
+
}
|
|
162
|
+
const door = doors[0]
|
|
163
|
+
for (const { node, annotation } of injectedMembers(context, door)) {
|
|
164
|
+
if (!annotation || !(isSignatureType(context, annotation) || isDomainServiceType(context, hfs, annotation))) context.report({ node, messageId: "injects", data: { what: context.sourceCode.getText(node) } })
|
|
165
|
+
}
|
|
166
|
+
const routes = routeMethods(context, door)
|
|
167
|
+
if (!validRoutes(context, routes)) {
|
|
168
|
+
const verbs = routes.length > 0 ? ` (${routes.map((route) => routeVerbs(context, route).join("+")).join(", ")})` : ""
|
|
169
|
+
context.report({ node: door, messageId: "route", data: { count: String(routes.length), verbs } })
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
reportRoute(context, hfs, handlerOf(context, door))
|
|
173
|
+
for (const challenge of methodsDecoratedBy(context, door, "@nestjs/common", "Get")) reportRoute(context, hfs, challenge, true)
|
|
174
|
+
}
|
|
175
|
+
|
|
70
176
|
/** A provider webhook door: one signed `@Post`, the signature proof, one domain intake call, nothing else. */
|
|
71
177
|
export const webhookShape = {
|
|
72
178
|
meta: {
|
|
73
179
|
type: "problem",
|
|
74
|
-
docs: { description: "A webhook door
|
|
180
|
+
docs: { description: "A webhook door has one signed POST that verifies first, hands off once to a domain inbox intake and returns empty or its own literal PROVIDER_ACK, plus an optional signed GET that echoes only its domain verifier result. No command bus reaches the door or intake." },
|
|
75
181
|
schema: [],
|
|
76
182
|
messages: {
|
|
77
|
-
noDoor: "A webhook file declares one `@Controller` class: the door
|
|
78
|
-
injects: "`{{what}}` is injected into a webhook door. A webhook injects only the `WebhookSignatureService` of `platform/http-security` and services of `modules/domain`; a bus, a repository, an `EntityManager` or a queue never reaches it. The intake
|
|
79
|
-
route: "A webhook door has exactly one `@Post` handler (this class has {{count}} route methods{{verbs}}).
|
|
183
|
+
noDoor: "A webhook file declares one `@Controller` class: the provider door. Its own exported literal `PROVIDER_ACK` may accompany it in `<provider>.webhook.ts`.",
|
|
184
|
+
injects: "`{{what}}` is injected into a webhook door. A webhook injects only the `WebhookSignatureService` of `platform/http-security` and services of `modules/domain`; a bus, a repository, an `EntityManager` or a queue never reaches it. The intake records the inbox delivery inside its own transaction; a reactor or job dispatches the command afterward.",
|
|
185
|
+
route: "A webhook door has exactly one `@Post` delivery handler and at most one `@Get` subscription challenge (this class has {{count}} route methods{{verbs}}). The GET calls its declared domain verifier first and echoes only that result; other routes belong in their own provider folder or an api feature.",
|
|
80
186
|
notPublic: "`{{name}}` is not `@Public({ reason: PublicReason.SignedWebhook })`. The signature is the authentication of a webhook: it is public by that declaration and no other, so the open doors stay readable and the strict rate tier applies.",
|
|
81
187
|
branch: "`{{what}}` is a decision or a loop in a webhook door. The door proves the delivery and hands it over: a refusal is the thrown error of `verify`, and every other decision belongs in the domain intake method.",
|
|
82
|
-
call: "`{{what}}` is a call a webhook door does not make. The handler
|
|
83
|
-
intakeCalls: "`{{name}}`
|
|
84
|
-
returns: "`{{name}}` returns
|
|
188
|
+
call: "`{{what}}` is a call a webhook door does not make. The handler starts with the shared signature proof or a declared domain verifier and hands the delivery to one domain intake; a declared domain method may do both internally. Parsing, mapping and publishing belong in the domain service.",
|
|
189
|
+
intakeCalls: "`{{name}}` makes {{count}} domain intake calls. A webhook hands the verified delivery to exactly one domain method that records its inbox delivery in a transaction; a separate first domain proof is allowed, and a second intake is orchestration that belongs in that method.",
|
|
190
|
+
returns: "`{{name}}` returns an undeclared or computed body. POST returns empty or the exported `PROVIDER_ACK` const of this webhook feature, initialized only with literal object/string data and declared `as const` or with a readonly literal type. GET echoes exactly its first declared domain verifier call's result. Domain data, inline literals and every other computed body are refused.",
|
|
191
|
+
handoff: "A webhook door, its declaring HTTP module or its called domain method reaches the command/query bus or a feature command handler. A delivery or challenge verifier cannot conceal command dispatch. Verify, record the inbox delivery, and let a reactor or job dispatch the command afterward.",
|
|
85
192
|
},
|
|
86
193
|
},
|
|
87
194
|
create(context) {
|
|
88
195
|
const hfs = hfsOf(context)
|
|
89
|
-
|
|
196
|
+
const name = baseName(context.filename)
|
|
197
|
+
const isDoor = name.endsWith(".webhook.ts")
|
|
198
|
+
if (hfs.slotOf(context.filename) !== SLOT || !(isDoor || name.endsWith(".module.ts"))) return {}
|
|
90
199
|
return {
|
|
91
200
|
"Program:exit"(program) {
|
|
92
|
-
const
|
|
93
|
-
if (
|
|
94
|
-
context.report({ node: program, messageId: "noDoor" })
|
|
95
|
-
return
|
|
96
|
-
}
|
|
97
|
-
const door = doors[0]
|
|
98
|
-
for (const { node, annotation } of injectedMembers(context, door)) {
|
|
99
|
-
if (!annotation || !(isSignatureType(context, annotation) || isDomainServiceType(context, hfs, annotation))) context.report({ node, messageId: "injects", data: { what: context.sourceCode.getText(node) } })
|
|
100
|
-
}
|
|
101
|
-
const routes = routeMethods(context, door)
|
|
102
|
-
const handler = handlerOf(context, door)
|
|
103
|
-
if (routes.length !== 1 || handler === null) {
|
|
104
|
-
const verbs = routes.length > 0 ? ` (${routes.map((route) => decoratorsFrom(context, route, "@nestjs/common", HTTP_VERBS).map((decorator) => decoratorCallee(decorator).name ?? "route").join("+")).join(", ")})` : ""
|
|
105
|
-
context.report({ node: door, messageId: "route", data: { count: String(routes.length), verbs } })
|
|
106
|
-
return
|
|
107
|
-
}
|
|
108
|
-
const name = handler.key.type === "Identifier" ? handler.key.name : "the handler"
|
|
109
|
-
if (!isSignedWebhookDoor(context, handler)) context.report({ node: handler.key, messageId: "notPublic", data: { name } })
|
|
110
|
-
walk(handler.value.body, (child) => {
|
|
111
|
-
if (DECISION_NODES.has(child.type)) context.report({ node: child, messageId: "branch", data: { what: child.type === "LogicalExpression" ? child.operator : child.type.replace(/(Statement|Expression)$/, "").toLowerCase() } })
|
|
112
|
-
else if (child.type === "ReturnStatement" && child.argument) context.report({ node: child, messageId: "returns", data: { name } })
|
|
113
|
-
})
|
|
114
|
-
let intake = 0
|
|
115
|
-
for (const { call, receiver, method } of callsOf(handler)) {
|
|
116
|
-
if (receiver !== null && isSignatureType(context, receiver) && method === "verify") continue
|
|
117
|
-
if (receiver !== null && isDomainServiceType(context, hfs, receiver)) {
|
|
118
|
-
intake += 1
|
|
119
|
-
continue
|
|
120
|
-
}
|
|
121
|
-
context.report({ node: call, messageId: "call", data: { what: context.sourceCode.getText(call.callee) } })
|
|
122
|
-
}
|
|
123
|
-
if (intake !== 1) context.report({ node: handler.key, messageId: "intakeCalls", data: { name, count: String(intake) } })
|
|
201
|
+
for (const node of webhookForbiddenReferences(context, hfs, program)) context.report({ node, messageId: "handoff" })
|
|
202
|
+
if (isDoor) reportDoor(context, hfs, program)
|
|
124
203
|
},
|
|
125
204
|
}
|
|
126
205
|
},
|
|
@@ -130,10 +209,10 @@ export const webhookShape = {
|
|
|
130
209
|
export const webhookVerifyFirst = {
|
|
131
210
|
meta: {
|
|
132
211
|
type: "problem",
|
|
133
|
-
docs: { description: "
|
|
212
|
+
docs: { description: "POST starts with an unconditional shared signature or declared domain proof. An optional GET starts with its domain challenge verifier, binding or returning only that result. Async proofs are awaited before any other work." },
|
|
134
213
|
schema: [],
|
|
135
214
|
messages: {
|
|
136
|
-
first: "`{{name}}` does not start with `verify`
|
|
215
|
+
first: "`{{name}}` does not start with an unconditional webhook proof. POST starts with `WebhookSignatureService.verify` or a declared domain verifier. GET first binds or returns its declared domain verifier's challenge result. Await an async proof. Domain verifier service names end in `Verifier` or `verify`, or their methods appear in the adjacent HTTP module's exported `webhookVerifiers` array. No data call or other work may run before verification, including inside its arguments.",
|
|
137
216
|
},
|
|
138
217
|
},
|
|
139
218
|
create(context) {
|
|
@@ -142,18 +221,11 @@ export const webhookVerifyFirst = {
|
|
|
142
221
|
return {
|
|
143
222
|
"Program:exit"(program) {
|
|
144
223
|
for (const door of doorsOf(context, program)) {
|
|
145
|
-
const handler
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const proven = expression?.type === "CallExpression"
|
|
151
|
-
&& expression.callee.type === "MemberExpression"
|
|
152
|
-
&& !expression.callee.computed
|
|
153
|
-
&& expression.callee.property.type === "Identifier"
|
|
154
|
-
&& expression.callee.property.name === "verify"
|
|
155
|
-
&& isSignatureType(context, expression.callee.object)
|
|
156
|
-
if (!proven) context.report({ node: handler.key, messageId: "first", data: { name: handler.key.type === "Identifier" ? handler.key.name : "the handler" } })
|
|
224
|
+
for (const handler of methodsDecoratedBy(context, door, "@nestjs/common", ["Post", "Get"])) {
|
|
225
|
+
const challenge = decoratorsFrom(context, handler, "@nestjs/common", "Get").length > 0
|
|
226
|
+
const proven = challenge ? firstWebhookChallenge(context, hfs, handler) : firstWebhookVerification(context, hfs, handler)
|
|
227
|
+
if (!proven) context.report({ node: handler.key, messageId: "first", data: { name: methodName(handler) } })
|
|
228
|
+
}
|
|
157
229
|
}
|
|
158
230
|
},
|
|
159
231
|
}
|