@crowdsource.you/core 1.2.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/README.md +571 -0
- package/dist/cases.d.ts +66 -0
- package/dist/cases.d.ts.map +1 -0
- package/dist/cases.js +79 -0
- package/dist/cases.js.map +1 -0
- package/dist/client.d.ts +90 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +125 -0
- package/dist/client.js.map +1 -0
- package/dist/communityNotes.d.ts +49 -0
- package/dist/communityNotes.d.ts.map +1 -0
- package/dist/communityNotes.js +104 -0
- package/dist/communityNotes.js.map +1 -0
- package/dist/credential.d.ts +58 -0
- package/dist/credential.d.ts.map +1 -0
- package/dist/credential.js +71 -0
- package/dist/credential.js.map +1 -0
- package/dist/defaults.d.ts +61 -0
- package/dist/defaults.d.ts.map +1 -0
- package/dist/defaults.js +82 -0
- package/dist/defaults.js.map +1 -0
- package/dist/digest.d.ts +44 -0
- package/dist/digest.d.ts.map +1 -0
- package/dist/digest.js +79 -0
- package/dist/digest.js.map +1 -0
- package/dist/envelope.d.ts +143 -0
- package/dist/envelope.d.ts.map +1 -0
- package/dist/envelope.js +258 -0
- package/dist/envelope.js.map +1 -0
- package/dist/errors.d.ts +90 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +151 -0
- package/dist/errors.js.map +1 -0
- package/dist/esm/cases.js +74 -0
- package/dist/esm/cases.js.map +1 -0
- package/dist/esm/client.js +121 -0
- package/dist/esm/client.js.map +1 -0
- package/dist/esm/communityNotes.js +100 -0
- package/dist/esm/communityNotes.js.map +1 -0
- package/dist/esm/credential.js +67 -0
- package/dist/esm/credential.js.map +1 -0
- package/dist/esm/defaults.js +77 -0
- package/dist/esm/defaults.js.map +1 -0
- package/dist/esm/digest.js +74 -0
- package/dist/esm/digest.js.map +1 -0
- package/dist/esm/envelope.js +252 -0
- package/dist/esm/envelope.js.map +1 -0
- package/dist/esm/errors.js +140 -0
- package/dist/esm/errors.js.map +1 -0
- package/dist/esm/express/index.js +29 -0
- package/dist/esm/express/index.js.map +1 -0
- package/dist/esm/express/middleware.js +293 -0
- package/dist/esm/express/middleware.js.map +1 -0
- package/dist/esm/express/store.js +69 -0
- package/dist/esm/express/store.js.map +1 -0
- package/dist/esm/express/verify.js +133 -0
- package/dist/esm/express/verify.js.map +1 -0
- package/dist/esm/index.js +57 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/outbox/client.js +43 -0
- package/dist/esm/outbox/client.js.map +1 -0
- package/dist/esm/outbox/decision.js +153 -0
- package/dist/esm/outbox/decision.js.map +1 -0
- package/dist/esm/outbox/delivery.js +129 -0
- package/dist/esm/outbox/delivery.js.map +1 -0
- package/dist/esm/outbox/enforcement/executor.js +219 -0
- package/dist/esm/outbox/enforcement/executor.js.map +1 -0
- package/dist/esm/outbox/enforcement/planner.js +326 -0
- package/dist/esm/outbox/enforcement/planner.js.map +1 -0
- package/dist/esm/outbox/evidence.js +173 -0
- package/dist/esm/outbox/evidence.js.map +1 -0
- package/dist/esm/outbox/inbound.js +87 -0
- package/dist/esm/outbox/inbound.js.map +1 -0
- package/dist/esm/outbox/index.js +71 -0
- package/dist/esm/outbox/index.js.map +1 -0
- package/dist/esm/outbox/intake.js +140 -0
- package/dist/esm/outbox/intake.js.map +1 -0
- package/dist/esm/outbox/integration.js +116 -0
- package/dist/esm/outbox/integration.js.map +1 -0
- package/dist/esm/outbox/outbox/dispatcher.js +114 -0
- package/dist/esm/outbox/outbox/dispatcher.js.map +1 -0
- package/dist/esm/outbox/outbox/service.js +271 -0
- package/dist/esm/outbox/outbox/service.js.map +1 -0
- package/dist/esm/outbox/postgres/index.js +41 -0
- package/dist/esm/outbox/postgres/index.js.map +1 -0
- package/dist/esm/outbox/postgres/registries.js +155 -0
- package/dist/esm/outbox/postgres/registries.js.map +1 -0
- package/dist/esm/outbox/postgres/reportColumns.js +191 -0
- package/dist/esm/outbox/postgres/reportColumns.js.map +1 -0
- package/dist/esm/outbox/postgres/store/enforcement.js +158 -0
- package/dist/esm/outbox/postgres/store/enforcement.js.map +1 -0
- package/dist/esm/outbox/postgres/store/events.js +101 -0
- package/dist/esm/outbox/postgres/store/events.js.map +1 -0
- package/dist/esm/outbox/postgres/store/index.js +59 -0
- package/dist/esm/outbox/postgres/store/index.js.map +1 -0
- package/dist/esm/outbox/postgres/store/outbox.js +207 -0
- package/dist/esm/outbox/postgres/store/outbox.js.map +1 -0
- package/dist/esm/outbox/postgres/store/reports.js +231 -0
- package/dist/esm/outbox/postgres/store/reports.js.map +1 -0
- package/dist/esm/outbox/postgres/store/transaction.js +29 -0
- package/dist/esm/outbox/postgres/store/transaction.js.map +1 -0
- package/dist/esm/outbox/postgres/tables.js +270 -0
- package/dist/esm/outbox/postgres/tables.js.map +1 -0
- package/dist/esm/outbox/reconciliation.js +157 -0
- package/dist/esm/outbox/reconciliation.js.map +1 -0
- package/dist/esm/outbox/reportStatus.js +22 -0
- package/dist/esm/outbox/reportStatus.js.map +1 -0
- package/dist/esm/outbox/retention.js +33 -0
- package/dist/esm/outbox/retention.js.map +1 -0
- package/dist/esm/outbox/store/types.js +30 -0
- package/dist/esm/outbox/store/types.js.map +1 -0
- package/dist/esm/outbox/types.js +23 -0
- package/dist/esm/outbox/types.js.map +1 -0
- package/dist/esm/outbox/webhook.js +145 -0
- package/dist/esm/outbox/webhook.js.map +1 -0
- package/dist/esm/package.json +1 -0
- package/dist/esm/reports.js +74 -0
- package/dist/esm/reports.js.map +1 -0
- package/dist/esm/testing/fixtures.js +132 -0
- package/dist/esm/testing/fixtures.js.map +1 -0
- package/dist/esm/testing/index.js +33 -0
- package/dist/esm/testing/index.js.map +1 -0
- package/dist/esm/testing/sandbox.js +337 -0
- package/dist/esm/testing/sandbox.js.map +1 -0
- package/dist/esm/testing/webhook-simulator.js +93 -0
- package/dist/esm/testing/webhook-simulator.js.map +1 -0
- package/dist/esm/transport.js +177 -0
- package/dist/esm/transport.js.map +1 -0
- package/dist/esm/webhookEndpoints.js +139 -0
- package/dist/esm/webhookEndpoints.js.map +1 -0
- package/dist/express/index.d.ts +32 -0
- package/dist/express/index.d.ts.map +1 -0
- package/dist/express/index.js +40 -0
- package/dist/express/index.js.map +1 -0
- package/dist/express/middleware.d.ts +97 -0
- package/dist/express/middleware.d.ts.map +1 -0
- package/dist/express/middleware.js +298 -0
- package/dist/express/middleware.js.map +1 -0
- package/dist/express/store.d.ts +45 -0
- package/dist/express/store.d.ts.map +1 -0
- package/dist/express/store.js +72 -0
- package/dist/express/store.js.map +1 -0
- package/dist/express/verify.d.ts +86 -0
- package/dist/express/verify.d.ts.map +1 -0
- package/dist/express/verify.js +138 -0
- package/dist/express/verify.js.map +1 -0
- package/dist/index.d.ts +67 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +89 -0
- package/dist/index.js.map +1 -0
- package/dist/outbox/client.d.ts +35 -0
- package/dist/outbox/client.d.ts.map +1 -0
- package/dist/outbox/client.js +46 -0
- package/dist/outbox/client.js.map +1 -0
- package/dist/outbox/decision.d.ts +36 -0
- package/dist/outbox/decision.d.ts.map +1 -0
- package/dist/outbox/decision.js +159 -0
- package/dist/outbox/decision.js.map +1 -0
- package/dist/outbox/delivery.d.ts +50 -0
- package/dist/outbox/delivery.d.ts.map +1 -0
- package/dist/outbox/delivery.js +135 -0
- package/dist/outbox/delivery.js.map +1 -0
- package/dist/outbox/enforcement/executor.d.ts +26 -0
- package/dist/outbox/enforcement/executor.d.ts.map +1 -0
- package/dist/outbox/enforcement/executor.js +222 -0
- package/dist/outbox/enforcement/executor.js.map +1 -0
- package/dist/outbox/enforcement/planner.d.ts +50 -0
- package/dist/outbox/enforcement/planner.d.ts.map +1 -0
- package/dist/outbox/enforcement/planner.js +333 -0
- package/dist/outbox/enforcement/planner.js.map +1 -0
- package/dist/outbox/evidence.d.ts +114 -0
- package/dist/outbox/evidence.d.ts.map +1 -0
- package/dist/outbox/evidence.js +181 -0
- package/dist/outbox/evidence.js.map +1 -0
- package/dist/outbox/inbound.d.ts +78 -0
- package/dist/outbox/inbound.d.ts.map +1 -0
- package/dist/outbox/inbound.js +91 -0
- package/dist/outbox/inbound.js.map +1 -0
- package/dist/outbox/index.d.ts +79 -0
- package/dist/outbox/index.d.ts.map +1 -0
- package/dist/outbox/index.js +98 -0
- package/dist/outbox/index.js.map +1 -0
- package/dist/outbox/intake.d.ts +62 -0
- package/dist/outbox/intake.d.ts.map +1 -0
- package/dist/outbox/intake.js +145 -0
- package/dist/outbox/intake.js.map +1 -0
- package/dist/outbox/integration.d.ts +62 -0
- package/dist/outbox/integration.d.ts.map +1 -0
- package/dist/outbox/integration.js +119 -0
- package/dist/outbox/integration.js.map +1 -0
- package/dist/outbox/outbox/dispatcher.d.ts +26 -0
- package/dist/outbox/outbox/dispatcher.d.ts.map +1 -0
- package/dist/outbox/outbox/dispatcher.js +119 -0
- package/dist/outbox/outbox/dispatcher.js.map +1 -0
- package/dist/outbox/outbox/service.d.ts +124 -0
- package/dist/outbox/outbox/service.d.ts.map +1 -0
- package/dist/outbox/outbox/service.js +279 -0
- package/dist/outbox/outbox/service.js.map +1 -0
- package/dist/outbox/postgres/index.d.ts +44 -0
- package/dist/outbox/postgres/index.d.ts.map +1 -0
- package/dist/outbox/postgres/index.js +51 -0
- package/dist/outbox/postgres/index.js.map +1 -0
- package/dist/outbox/postgres/registries.d.ts +69 -0
- package/dist/outbox/postgres/registries.d.ts.map +1 -0
- package/dist/outbox/postgres/registries.js +159 -0
- package/dist/outbox/postgres/registries.js.map +1 -0
- package/dist/outbox/postgres/reportColumns.d.ts +154 -0
- package/dist/outbox/postgres/reportColumns.d.ts.map +1 -0
- package/dist/outbox/postgres/reportColumns.js +195 -0
- package/dist/outbox/postgres/reportColumns.js.map +1 -0
- package/dist/outbox/postgres/store/enforcement.d.ts +33 -0
- package/dist/outbox/postgres/store/enforcement.d.ts.map +1 -0
- package/dist/outbox/postgres/store/enforcement.js +161 -0
- package/dist/outbox/postgres/store/enforcement.js.map +1 -0
- package/dist/outbox/postgres/store/events.d.ts +46 -0
- package/dist/outbox/postgres/store/events.d.ts.map +1 -0
- package/dist/outbox/postgres/store/events.js +104 -0
- package/dist/outbox/postgres/store/events.js.map +1 -0
- package/dist/outbox/postgres/store/index.d.ts +27 -0
- package/dist/outbox/postgres/store/index.d.ts.map +1 -0
- package/dist/outbox/postgres/store/index.js +62 -0
- package/dist/outbox/postgres/store/index.js.map +1 -0
- package/dist/outbox/postgres/store/outbox.d.ts +8 -0
- package/dist/outbox/postgres/store/outbox.d.ts.map +1 -0
- package/dist/outbox/postgres/store/outbox.js +210 -0
- package/dist/outbox/postgres/store/outbox.js.map +1 -0
- package/dist/outbox/postgres/store/reports.d.ts +69 -0
- package/dist/outbox/postgres/store/reports.d.ts.map +1 -0
- package/dist/outbox/postgres/store/reports.js +234 -0
- package/dist/outbox/postgres/store/reports.js.map +1 -0
- package/dist/outbox/postgres/store/transaction.d.ts +49 -0
- package/dist/outbox/postgres/store/transaction.d.ts.map +1 -0
- package/dist/outbox/postgres/store/transaction.js +32 -0
- package/dist/outbox/postgres/store/transaction.js.map +1 -0
- package/dist/outbox/postgres/tables.d.ts +737 -0
- package/dist/outbox/postgres/tables.d.ts.map +1 -0
- package/dist/outbox/postgres/tables.js +273 -0
- package/dist/outbox/postgres/tables.js.map +1 -0
- package/dist/outbox/reconciliation.d.ts +49 -0
- package/dist/outbox/reconciliation.d.ts.map +1 -0
- package/dist/outbox/reconciliation.js +162 -0
- package/dist/outbox/reconciliation.js.map +1 -0
- package/dist/outbox/reportStatus.d.ts +12 -0
- package/dist/outbox/reportStatus.d.ts.map +1 -0
- package/dist/outbox/reportStatus.js +25 -0
- package/dist/outbox/reportStatus.js.map +1 -0
- package/dist/outbox/retention.d.ts +33 -0
- package/dist/outbox/retention.d.ts.map +1 -0
- package/dist/outbox/retention.js +36 -0
- package/dist/outbox/retention.js.map +1 -0
- package/dist/outbox/store/types.d.ts +466 -0
- package/dist/outbox/store/types.d.ts.map +1 -0
- package/dist/outbox/store/types.js +31 -0
- package/dist/outbox/store/types.js.map +1 -0
- package/dist/outbox/types.d.ts +660 -0
- package/dist/outbox/types.d.ts.map +1 -0
- package/dist/outbox/types.js +24 -0
- package/dist/outbox/types.js.map +1 -0
- package/dist/outbox/webhook.d.ts +15 -0
- package/dist/outbox/webhook.d.ts.map +1 -0
- package/dist/outbox/webhook.js +148 -0
- package/dist/outbox/webhook.js.map +1 -0
- package/dist/reports.d.ts +73 -0
- package/dist/reports.d.ts.map +1 -0
- package/dist/reports.js +78 -0
- package/dist/reports.js.map +1 -0
- package/dist/testing/fixtures.d.ts +62 -0
- package/dist/testing/fixtures.d.ts.map +1 -0
- package/dist/testing/fixtures.js +137 -0
- package/dist/testing/fixtures.js.map +1 -0
- package/dist/testing/index.d.ts +36 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +43 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/sandbox.d.ts +109 -0
- package/dist/testing/sandbox.d.ts.map +1 -0
- package/dist/testing/sandbox.js +342 -0
- package/dist/testing/sandbox.js.map +1 -0
- package/dist/testing/webhook-simulator.d.ts +78 -0
- package/dist/testing/webhook-simulator.d.ts.map +1 -0
- package/dist/testing/webhook-simulator.js +98 -0
- package/dist/testing/webhook-simulator.js.map +1 -0
- package/dist/transport.d.ts +59 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +181 -0
- package/dist/transport.js.map +1 -0
- package/dist/webhookEndpoints.d.ts +147 -0
- package/dist/webhookEndpoints.d.ts.map +1 -0
- package/dist/webhookEndpoints.js +143 -0
- package/dist/webhookEndpoints.js.map +1 -0
- package/package.json +127 -0
- package/src/cases.ts +119 -0
- package/src/client.ts +202 -0
- package/src/communityNotes.ts +194 -0
- package/src/credential.ts +100 -0
- package/src/defaults.ts +87 -0
- package/src/digest.ts +91 -0
- package/src/envelope.ts +432 -0
- package/src/errors.ts +165 -0
- package/src/express/index.ts +43 -0
- package/src/express/middleware.ts +387 -0
- package/src/express/store.ts +85 -0
- package/src/express/verify.ts +166 -0
- package/src/index.ts +107 -0
- package/src/outbox/client.ts +75 -0
- package/src/outbox/decision.ts +209 -0
- package/src/outbox/delivery.ts +163 -0
- package/src/outbox/enforcement/executor.ts +288 -0
- package/src/outbox/enforcement/planner.ts +377 -0
- package/src/outbox/evidence.ts +231 -0
- package/src/outbox/inbound.ts +140 -0
- package/src/outbox/index.ts +160 -0
- package/src/outbox/intake.ts +175 -0
- package/src/outbox/integration.ts +205 -0
- package/src/outbox/outbox/dispatcher.ts +131 -0
- package/src/outbox/outbox/service.ts +444 -0
- package/src/outbox/postgres/index.ts +59 -0
- package/src/outbox/postgres/registries.ts +177 -0
- package/src/outbox/postgres/reportColumns.ts +281 -0
- package/src/outbox/postgres/store/enforcement.ts +186 -0
- package/src/outbox/postgres/store/events.ts +112 -0
- package/src/outbox/postgres/store/index.ts +72 -0
- package/src/outbox/postgres/store/outbox.ts +273 -0
- package/src/outbox/postgres/store/reports.ts +349 -0
- package/src/outbox/postgres/store/transaction.ts +64 -0
- package/src/outbox/postgres/tables.ts +339 -0
- package/src/outbox/reconciliation.ts +199 -0
- package/src/outbox/reportStatus.ts +24 -0
- package/src/outbox/retention.ts +34 -0
- package/src/outbox/store/types.ts +528 -0
- package/src/outbox/types.ts +753 -0
- package/src/outbox/webhook.ts +165 -0
- package/src/reports.ts +138 -0
- package/src/testing/fixtures.ts +183 -0
- package/src/testing/index.ts +57 -0
- package/src/testing/sandbox.ts +469 -0
- package/src/testing/webhook-simulator.ts +166 -0
- package/src/transport.ts +250 -0
- package/src/webhookEndpoints.ts +256 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telling CrowdSource where to deliver decisions, and holding the secret it
|
|
3
|
+
* mints once.
|
|
4
|
+
*
|
|
5
|
+
* This is the half of the webhook integration that used to have no client at
|
|
6
|
+
* all: `@crowdsource.you/core/express` verifies deliveries against
|
|
7
|
+
* `CROWDSOURCE_WEBHOOK_SECRET`, and there was no supported way to obtain that
|
|
8
|
+
* value except hand-rolling an HTTP call with the bearer half of the service
|
|
9
|
+
* key. "Near-zero configuration" cannot mean an integrator writes their own
|
|
10
|
+
* fetch for a mandatory step, so §10.2's two management routes are here.
|
|
11
|
+
*
|
|
12
|
+
* Two properties of the secret shape everything below, and both are the server's
|
|
13
|
+
* design rather than this client's choice:
|
|
14
|
+
*
|
|
15
|
+
* * **It is returned once and never again.** A webhook secret is decryptable
|
|
16
|
+
* by CrowdSource because deliveries have to be signed with it, but re-serving
|
|
17
|
+
* it would turn any credential holding `webhooks:manage` into a way to read
|
|
18
|
+
* the signing key of an endpoint somebody else configured. So `secret` on a
|
|
19
|
+
* registration is the only time that value exists outside the service.
|
|
20
|
+
* * **Re-registering an existing URL mints nothing.** That is what makes a
|
|
21
|
+
* deploy script safe to run on every boot — it will not invalidate the
|
|
22
|
+
* secret the running process is verifying with. The corollary is that
|
|
23
|
+
* re-registering cannot RECOVER a secret you failed to store; that is what
|
|
24
|
+
* `rotateSecret` is for.
|
|
25
|
+
*
|
|
26
|
+
* Which is why `secret` is optional on the result. Its presence is not a detail:
|
|
27
|
+
* it means a value has just come into existence that nothing can produce again,
|
|
28
|
+
* and the integration is responsible for persisting it before the process exits.
|
|
29
|
+
*/
|
|
30
|
+
import { z } from 'zod';
|
|
31
|
+
import { sha256Digest } from './digest.js';
|
|
32
|
+
import { CrowdSourceTransportError } from './errors.js';
|
|
33
|
+
/**
|
|
34
|
+
* Appendix D's key for a registration.
|
|
35
|
+
*
|
|
36
|
+
* Derived from the URL because the URL is the endpoint's identity as far as the
|
|
37
|
+
* server is concerned — the same URL registered twice is one logical operation,
|
|
38
|
+
* which is exactly what makes a deploy-time `register` safe to retry. Hashed
|
|
39
|
+
* rather than interpolated because a URL carries `:` and `/`, and a composite
|
|
40
|
+
* value with separators in it is how an id grammar gets ambiguous.
|
|
41
|
+
*
|
|
42
|
+
* Note that the two webhook routes do NOT currently read `Idempotency-Key`
|
|
43
|
+
* server-side; only `POST /v1/reports` does. This satisfies the transport's
|
|
44
|
+
* Appendix D requirement and is inert at the service until that changes.
|
|
45
|
+
*/
|
|
46
|
+
function registrationIdempotencyKey(url) {
|
|
47
|
+
return `webhook-endpoint.${sha256Digest(url).replace('sha256:', '')}`;
|
|
48
|
+
}
|
|
49
|
+
const WebhookSecretSchema = z.looseObject({
|
|
50
|
+
version: z.number(),
|
|
51
|
+
value: z.string(),
|
|
52
|
+
signingStartsAt: z.string(),
|
|
53
|
+
});
|
|
54
|
+
const WebhookEndpointSchema = z.looseObject({
|
|
55
|
+
webhookEndpointId: z.string(),
|
|
56
|
+
url: z.string(),
|
|
57
|
+
eventTypes: z.array(z.string()),
|
|
58
|
+
status: z.string(),
|
|
59
|
+
disabledReason: z.string().nullish(),
|
|
60
|
+
createdAt: z.string(),
|
|
61
|
+
updatedAt: z.string(),
|
|
62
|
+
secret: WebhookSecretSchema.optional(),
|
|
63
|
+
});
|
|
64
|
+
const RotatedWebhookSecretSchema = z.looseObject({
|
|
65
|
+
webhookEndpointId: z.string(),
|
|
66
|
+
secret: WebhookSecretSchema,
|
|
67
|
+
previousSecret: z
|
|
68
|
+
.looseObject({ version: z.number(), expiresAt: z.string() })
|
|
69
|
+
.nullish(),
|
|
70
|
+
});
|
|
71
|
+
/**
|
|
72
|
+
* §10.2's two webhook-management routes.
|
|
73
|
+
*
|
|
74
|
+
* There is no list, no read-back and no delete, because the API serves none —
|
|
75
|
+
* §10.2 defines exactly two routes. An integrator therefore cannot enumerate
|
|
76
|
+
* what it has registered, which is a real gap in the service rather than
|
|
77
|
+
* something this client can paper over; re-registering a URL is the documented
|
|
78
|
+
* way to change a subscription or revive a disabled endpoint.
|
|
79
|
+
*
|
|
80
|
+
* Both routes need a credential with the `crowdsource:webhooks:manage` scope,
|
|
81
|
+
* and both derive the application from that credential. There is no
|
|
82
|
+
* `applicationId` in any request or response here.
|
|
83
|
+
*/
|
|
84
|
+
export class WebhookEndpoints {
|
|
85
|
+
constructor(transport) {
|
|
86
|
+
this.transport = transport;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Registers, or updates, the endpoint CrowdSource delivers to.
|
|
90
|
+
*
|
|
91
|
+
* Safe to call on every deploy: an existing URL is updated and no new secret
|
|
92
|
+
* is minted, so the secret the running process verifies with keeps working.
|
|
93
|
+
*/
|
|
94
|
+
async register(input, options = {}) {
|
|
95
|
+
const response = await this.transport.request({
|
|
96
|
+
method: 'POST',
|
|
97
|
+
path: '/v1/webhook-endpoints',
|
|
98
|
+
body: { url: input.url, eventTypes: [...input.eventTypes] },
|
|
99
|
+
idempotencyKey: options.idempotencyKey ?? registrationIdempotencyKey(input.url),
|
|
100
|
+
signal: options.signal,
|
|
101
|
+
});
|
|
102
|
+
const parsed = WebhookEndpointSchema.safeParse(response);
|
|
103
|
+
if (!parsed.success) {
|
|
104
|
+
throw new CrowdSourceTransportError('CrowdSource registered the webhook endpoint but answered with a body this client does not recognise.', { retryable: false, cause: parsed.error });
|
|
105
|
+
}
|
|
106
|
+
const { disabledReason, secret, ...rest } = parsed.data;
|
|
107
|
+
return {
|
|
108
|
+
...rest,
|
|
109
|
+
disabledReason: disabledReason ?? null,
|
|
110
|
+
...(secret === undefined ? {} : { secret }),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Mints a new signing secret, retiring the current one after an overlap.
|
|
115
|
+
*
|
|
116
|
+
* The only way to recover from a secret that was never stored, and the correct
|
|
117
|
+
* response to one that leaked.
|
|
118
|
+
*/
|
|
119
|
+
async rotateSecret(webhookEndpointId, options = {}) {
|
|
120
|
+
const response = await this.transport.request({
|
|
121
|
+
method: 'POST',
|
|
122
|
+
path: `/v1/webhook-endpoints/${encodeURIComponent(webhookEndpointId)}/rotate-secret`,
|
|
123
|
+
body: options.overlapSeconds === undefined ? {} : { overlapSeconds: options.overlapSeconds },
|
|
124
|
+
idempotencyKey: options.idempotencyKey ??
|
|
125
|
+
`webhook-endpoint.${sha256Digest(webhookEndpointId).replace('sha256:', '')}.rotate`,
|
|
126
|
+
signal: options.signal,
|
|
127
|
+
});
|
|
128
|
+
const parsed = RotatedWebhookSecretSchema.safeParse(response);
|
|
129
|
+
if (!parsed.success) {
|
|
130
|
+
throw new CrowdSourceTransportError('CrowdSource rotated the webhook secret but answered with a body this client does not recognise.', { retryable: false, cause: parsed.error });
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
webhookEndpointId: parsed.data.webhookEndpointId,
|
|
134
|
+
secret: parsed.data.secret,
|
|
135
|
+
previousSecret: parsed.data.previousSecret ?? null,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
//# sourceMappingURL=webhookEndpoints.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhookEndpoints.js","sourceRoot":"","sources":["../../src/webhookEndpoints.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@crowdsource.you/core/express` — receiving CrowdSource webhooks safely.
|
|
3
|
+
*
|
|
4
|
+
* The whole integration:
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* import { crowdsourceWebhooks } from '@crowdsource.you/core/express';
|
|
8
|
+
*
|
|
9
|
+
* app.post('/webhooks/crowdsource', crowdsourceWebhooks({
|
|
10
|
+
* on: {
|
|
11
|
+
* 'case.decided': async (event) => {
|
|
12
|
+
* await moderationQueue.add(event.id, event.data);
|
|
13
|
+
* },
|
|
14
|
+
* },
|
|
15
|
+
* }));
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* The secret comes from `CROWDSOURCE_WEBHOOK_SECRET`, the raw body is read by
|
|
19
|
+
* the handler itself, the signature is verified in constant time over the bytes
|
|
20
|
+
* that arrived, a stale or replayed delivery is refused, and an event type this
|
|
21
|
+
* integration does not handle is acknowledged and ignored. None of that is
|
|
22
|
+
* optional and none of it can be got wrong by mounting things in the wrong
|
|
23
|
+
* order — see `middleware.ts` for what happens when a body parser gets there
|
|
24
|
+
* first.
|
|
25
|
+
*/
|
|
26
|
+
export { crowdsourceWebhooks, CrowdSourceWebhookConfigurationError, WEBHOOK_PREVIOUS_SECRET_ENV_VAR, WEBHOOK_SECRET_ENV_VAR, } from './middleware.js';
|
|
27
|
+
export type { CrowdSourceWebhooksOptions, WebhookEventHandlers } from './middleware.js';
|
|
28
|
+
export { memoryProcessedEventStore } from './store.js';
|
|
29
|
+
export type { MemoryProcessedEventStoreOptions, ProcessedEventStore } from './store.js';
|
|
30
|
+
export { signedPayloadBytes, verifyWebhookDelivery, WEBHOOK_REJECTIONS, } from './verify.js';
|
|
31
|
+
export type { WebhookRejection, WebhookVerification, WebhookVerificationInput } from './verify.js';
|
|
32
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/express/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EACL,mBAAmB,EACnB,oCAAoC,EACpC,+BAA+B,EAC/B,sBAAsB,GACvB,MAAM,iBAAiB,CAAC;AACzB,YAAY,EAAE,0BAA0B,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAExF,OAAO,EAAE,yBAAyB,EAAE,MAAM,YAAY,CAAC;AACvD,YAAY,EAAE,gCAAgC,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAExF,OAAO,EACL,kBAAkB,EAClB,qBAAqB,EACrB,kBAAkB,GACnB,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAC"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* `@crowdsource.you/core/express` — receiving CrowdSource webhooks safely.
|
|
4
|
+
*
|
|
5
|
+
* The whole integration:
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { crowdsourceWebhooks } from '@crowdsource.you/core/express';
|
|
9
|
+
*
|
|
10
|
+
* app.post('/webhooks/crowdsource', crowdsourceWebhooks({
|
|
11
|
+
* on: {
|
|
12
|
+
* 'case.decided': async (event) => {
|
|
13
|
+
* await moderationQueue.add(event.id, event.data);
|
|
14
|
+
* },
|
|
15
|
+
* },
|
|
16
|
+
* }));
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* The secret comes from `CROWDSOURCE_WEBHOOK_SECRET`, the raw body is read by
|
|
20
|
+
* the handler itself, the signature is verified in constant time over the bytes
|
|
21
|
+
* that arrived, a stale or replayed delivery is refused, and an event type this
|
|
22
|
+
* integration does not handle is acknowledged and ignored. None of that is
|
|
23
|
+
* optional and none of it can be got wrong by mounting things in the wrong
|
|
24
|
+
* order — see `middleware.ts` for what happens when a body parser gets there
|
|
25
|
+
* first.
|
|
26
|
+
*/
|
|
27
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
28
|
+
exports.WEBHOOK_REJECTIONS = exports.verifyWebhookDelivery = exports.signedPayloadBytes = exports.memoryProcessedEventStore = exports.WEBHOOK_SECRET_ENV_VAR = exports.WEBHOOK_PREVIOUS_SECRET_ENV_VAR = exports.CrowdSourceWebhookConfigurationError = exports.crowdsourceWebhooks = void 0;
|
|
29
|
+
var middleware_js_1 = require("./middleware.js");
|
|
30
|
+
Object.defineProperty(exports, "crowdsourceWebhooks", { enumerable: true, get: function () { return middleware_js_1.crowdsourceWebhooks; } });
|
|
31
|
+
Object.defineProperty(exports, "CrowdSourceWebhookConfigurationError", { enumerable: true, get: function () { return middleware_js_1.CrowdSourceWebhookConfigurationError; } });
|
|
32
|
+
Object.defineProperty(exports, "WEBHOOK_PREVIOUS_SECRET_ENV_VAR", { enumerable: true, get: function () { return middleware_js_1.WEBHOOK_PREVIOUS_SECRET_ENV_VAR; } });
|
|
33
|
+
Object.defineProperty(exports, "WEBHOOK_SECRET_ENV_VAR", { enumerable: true, get: function () { return middleware_js_1.WEBHOOK_SECRET_ENV_VAR; } });
|
|
34
|
+
var store_js_1 = require("./store.js");
|
|
35
|
+
Object.defineProperty(exports, "memoryProcessedEventStore", { enumerable: true, get: function () { return store_js_1.memoryProcessedEventStore; } });
|
|
36
|
+
var verify_js_1 = require("./verify.js");
|
|
37
|
+
Object.defineProperty(exports, "signedPayloadBytes", { enumerable: true, get: function () { return verify_js_1.signedPayloadBytes; } });
|
|
38
|
+
Object.defineProperty(exports, "verifyWebhookDelivery", { enumerable: true, get: function () { return verify_js_1.verifyWebhookDelivery; } });
|
|
39
|
+
Object.defineProperty(exports, "WEBHOOK_REJECTIONS", { enumerable: true, get: function () { return verify_js_1.WEBHOOK_REJECTIONS; } });
|
|
40
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/express/index.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;;;AAEH,iDAKyB;AAJvB,oHAAA,mBAAmB,OAAA;AACnB,qIAAA,oCAAoC,OAAA;AACpC,gIAAA,+BAA+B,OAAA;AAC/B,uHAAA,sBAAsB,OAAA;AAIxB,uCAAuD;AAA9C,qHAAA,yBAAyB,OAAA;AAGlC,yCAIqB;AAHnB,+GAAA,kBAAkB,OAAA;AAClB,kHAAA,qBAAqB,OAAA;AACrB,+GAAA,kBAAkB,OAAA"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The webhook receiver.
|
|
3
|
+
*
|
|
4
|
+
* ```ts
|
|
5
|
+
* app.post('/webhooks/crowdsource', crowdsourceWebhooks({
|
|
6
|
+
* on: {
|
|
7
|
+
* 'case.decided': async (event) => { await moderationQueue.add(event.id, event.data); },
|
|
8
|
+
* },
|
|
9
|
+
* }));
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* No `express.raw`, no body-parser ordering to get right, no secret to plumb
|
|
13
|
+
* through — the secret comes from the environment and the handler reads the
|
|
14
|
+
* request stream itself. That is not convenience for its own sake. The single
|
|
15
|
+
* most likely way to ship a broken CrowdSource integration is to verify a
|
|
16
|
+
* signature over `JSON.stringify(req.body)`, which succeeds on every test
|
|
17
|
+
* payload a developer writes by hand and fails on the first real delivery whose
|
|
18
|
+
* key order or unicode escaping differs — or, far worse, succeeds on a forged
|
|
19
|
+
* body that happens to re-serialise identically. This middleware makes that
|
|
20
|
+
* unreachable: it verifies the bytes it read, and if something upstream already
|
|
21
|
+
* consumed them it REFUSES rather than reconstructing them.
|
|
22
|
+
*
|
|
23
|
+
* What it guarantees, in order:
|
|
24
|
+
*
|
|
25
|
+
* 1. The signature is checked over the bytes that arrived (§10.8).
|
|
26
|
+
* 2. A timestamp outside the five-minute window is rejected before anything
|
|
27
|
+
* is dispatched, in both directions.
|
|
28
|
+
* 3. An event id already claimed is acknowledged without running the handler
|
|
29
|
+
* again (§10.8), and a handler that throws releases the claim so the
|
|
30
|
+
* retry in §10.9 still works.
|
|
31
|
+
* 4. An event type this integration does not handle — including one that did
|
|
32
|
+
* not exist when it was written — is acknowledged and ignored (§10.11). A
|
|
33
|
+
* new event type never breaks an existing integration.
|
|
34
|
+
* 5. Nothing sensitive is logged. The `onRejected` hook is given the reason
|
|
35
|
+
* and never the body (§10.8's last line).
|
|
36
|
+
*/
|
|
37
|
+
import type { RequestHandler } from 'express';
|
|
38
|
+
import { type KnownWebhookEvent, type WebhookEventEnvelope, type WebhookEventType } from '@crowdsource.you/contracts';
|
|
39
|
+
import { type ProcessedEventStore } from './store.js';
|
|
40
|
+
import { type WebhookRejection } from './verify.js';
|
|
41
|
+
/** The active signing secret. */
|
|
42
|
+
export declare const WEBHOOK_SECRET_ENV_VAR = "CROWDSOURCE_WEBHOOK_SECRET";
|
|
43
|
+
/** The secret being retired, during the overlap §10.8 requires for rotation. */
|
|
44
|
+
export declare const WEBHOOK_PREVIOUS_SECRET_ENV_VAR = "CROWDSOURCE_WEBHOOK_SECRET_PREVIOUS";
|
|
45
|
+
/**
|
|
46
|
+
* A handler per event type, each receiving the event with its payload narrowed.
|
|
47
|
+
*
|
|
48
|
+
* Every entry is optional and an absent one is not an error: an integration that
|
|
49
|
+
* only cares about `case.decided` registers only that.
|
|
50
|
+
*/
|
|
51
|
+
export type WebhookEventHandlers = {
|
|
52
|
+
readonly [T in WebhookEventType]?: (event: Extract<KnownWebhookEvent, {
|
|
53
|
+
type: T;
|
|
54
|
+
}>) => void | Promise<void>;
|
|
55
|
+
};
|
|
56
|
+
export interface CrowdSourceWebhooksOptions {
|
|
57
|
+
/**
|
|
58
|
+
* The active secret. Defaults to `process.env.CROWDSOURCE_WEBHOOK_SECRET`.
|
|
59
|
+
*/
|
|
60
|
+
readonly secret?: string;
|
|
61
|
+
/**
|
|
62
|
+
* The secret being retired. Defaults to
|
|
63
|
+
* `process.env.CROWDSOURCE_WEBHOOK_SECRET_PREVIOUS`. Both are accepted while
|
|
64
|
+
* it is set, which is what makes a rotation lossless (§10.8).
|
|
65
|
+
*/
|
|
66
|
+
readonly previousSecret?: string;
|
|
67
|
+
readonly on?: WebhookEventHandlers;
|
|
68
|
+
/**
|
|
69
|
+
* Called for a verified event no handler in `on` covers, including a type this
|
|
70
|
+
* version of the contracts package does not know. Optional: without it, an
|
|
71
|
+
* unhandled event is acknowledged and dropped, which is what §10.11 requires.
|
|
72
|
+
*/
|
|
73
|
+
readonly onUnhandled?: (event: WebhookEventEnvelope) => void | Promise<void>;
|
|
74
|
+
/** Defaults to an in-process store. See `store.ts` for when that is not enough. */
|
|
75
|
+
readonly store?: ProcessedEventStore;
|
|
76
|
+
/** §10.8's five minutes. Lower it in tests, never raise it in production. */
|
|
77
|
+
readonly toleranceSeconds?: number;
|
|
78
|
+
readonly maxBodyBytes?: number;
|
|
79
|
+
/**
|
|
80
|
+
* Observability for a refused delivery. Receives the reason and nothing else
|
|
81
|
+
* — never the body, never the signature, never a header.
|
|
82
|
+
*/
|
|
83
|
+
readonly onRejected?: (rejection: WebhookRejection) => void;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Thrown when the request cannot be verified because of how the application is
|
|
87
|
+
* assembled, not because of anything the sender did.
|
|
88
|
+
*
|
|
89
|
+
* Passed to `next()` so it reaches the application's error handler and is
|
|
90
|
+
* impossible to miss. The alternative — falling back to `req.body` — is the bug
|
|
91
|
+
* this whole package exists to prevent.
|
|
92
|
+
*/
|
|
93
|
+
export declare class CrowdSourceWebhookConfigurationError extends Error {
|
|
94
|
+
constructor(message: string);
|
|
95
|
+
}
|
|
96
|
+
export declare function crowdsourceWebhooks(options?: CrowdSourceWebhooksOptions): RequestHandler;
|
|
97
|
+
//# sourceMappingURL=middleware.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../../src/express/middleware.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAmC,MAAM,SAAS,CAAC;AAE/E,OAAO,EAML,KAAK,iBAAiB,EACtB,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACtB,MAAM,4BAA4B,CAAC;AAEpC,OAAO,EAA6B,KAAK,mBAAmB,EAAE,MAAM,YAAY,CAAC;AACjF,OAAO,EAAyB,KAAK,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAE3E,iCAAiC;AACjC,eAAO,MAAM,sBAAsB,+BAA+B,CAAC;AAEnE,gFAAgF;AAChF,eAAO,MAAM,+BAA+B,wCAAwC,CAAC;AAKrF;;;;;GAKG;AACH,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,EAAE,CAAC,IAAI,gBAAgB,CAAC,CAAC,EAAE,CACjC,KAAK,EAAE,OAAO,CAAC,iBAAiB,EAAE;QAAE,IAAI,EAAE,CAAC,CAAA;KAAE,CAAC,KAC3C,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;CAC1B,CAAC;AAEF,MAAM,WAAW,0BAA0B;IACzC;;OAEG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB;;;;OAIG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,EAAE,CAAC,EAAE,oBAAoB,CAAC;IACnC;;;;OAIG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7E,mFAAmF;IACnF,QAAQ,CAAC,KAAK,CAAC,EAAE,mBAAmB,CAAC;IACrC,6EAA6E;IAC7E,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B;;;OAGG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC,SAAS,EAAE,gBAAgB,KAAK,IAAI,CAAC;CAC7D;AAED;;;;;;;GAOG;AACH,qBAAa,oCAAqC,SAAQ,KAAK;gBACjD,OAAO,EAAE,MAAM;CAI5B;AAED,wBAAgB,mBAAmB,CAAC,OAAO,GAAE,0BAA+B,GAAG,cAAc,CAgB5F"}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The webhook receiver.
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* app.post('/webhooks/crowdsource', crowdsourceWebhooks({
|
|
7
|
+
* on: {
|
|
8
|
+
* 'case.decided': async (event) => { await moderationQueue.add(event.id, event.data); },
|
|
9
|
+
* },
|
|
10
|
+
* }));
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* No `express.raw`, no body-parser ordering to get right, no secret to plumb
|
|
14
|
+
* through — the secret comes from the environment and the handler reads the
|
|
15
|
+
* request stream itself. That is not convenience for its own sake. The single
|
|
16
|
+
* most likely way to ship a broken CrowdSource integration is to verify a
|
|
17
|
+
* signature over `JSON.stringify(req.body)`, which succeeds on every test
|
|
18
|
+
* payload a developer writes by hand and fails on the first real delivery whose
|
|
19
|
+
* key order or unicode escaping differs — or, far worse, succeeds on a forged
|
|
20
|
+
* body that happens to re-serialise identically. This middleware makes that
|
|
21
|
+
* unreachable: it verifies the bytes it read, and if something upstream already
|
|
22
|
+
* consumed them it REFUSES rather than reconstructing them.
|
|
23
|
+
*
|
|
24
|
+
* What it guarantees, in order:
|
|
25
|
+
*
|
|
26
|
+
* 1. The signature is checked over the bytes that arrived (§10.8).
|
|
27
|
+
* 2. A timestamp outside the five-minute window is rejected before anything
|
|
28
|
+
* is dispatched, in both directions.
|
|
29
|
+
* 3. An event id already claimed is acknowledged without running the handler
|
|
30
|
+
* again (§10.8), and a handler that throws releases the claim so the
|
|
31
|
+
* retry in §10.9 still works.
|
|
32
|
+
* 4. An event type this integration does not handle — including one that did
|
|
33
|
+
* not exist when it was written — is acknowledged and ignored (§10.11). A
|
|
34
|
+
* new event type never breaks an existing integration.
|
|
35
|
+
* 5. Nothing sensitive is logged. The `onRejected` hook is given the reason
|
|
36
|
+
* and never the body (§10.8's last line).
|
|
37
|
+
*/
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.CrowdSourceWebhookConfigurationError = exports.WEBHOOK_PREVIOUS_SECRET_ENV_VAR = exports.WEBHOOK_SECRET_ENV_VAR = void 0;
|
|
40
|
+
exports.crowdsourceWebhooks = crowdsourceWebhooks;
|
|
41
|
+
const contracts_1 = require("@crowdsource.you/contracts");
|
|
42
|
+
const store_js_1 = require("./store.js");
|
|
43
|
+
const verify_js_1 = require("./verify.js");
|
|
44
|
+
/** The active signing secret. */
|
|
45
|
+
exports.WEBHOOK_SECRET_ENV_VAR = 'CROWDSOURCE_WEBHOOK_SECRET';
|
|
46
|
+
/** The secret being retired, during the overlap §10.8 requires for rotation. */
|
|
47
|
+
exports.WEBHOOK_PREVIOUS_SECRET_ENV_VAR = 'CROWDSOURCE_WEBHOOK_SECRET_PREVIOUS';
|
|
48
|
+
/** Matches the backend's own `express.json` limit. */
|
|
49
|
+
const DEFAULT_MAX_BODY_BYTES = 1048576;
|
|
50
|
+
/**
|
|
51
|
+
* Thrown when the request cannot be verified because of how the application is
|
|
52
|
+
* assembled, not because of anything the sender did.
|
|
53
|
+
*
|
|
54
|
+
* Passed to `next()` so it reaches the application's error handler and is
|
|
55
|
+
* impossible to miss. The alternative — falling back to `req.body` — is the bug
|
|
56
|
+
* this whole package exists to prevent.
|
|
57
|
+
*/
|
|
58
|
+
class CrowdSourceWebhookConfigurationError extends Error {
|
|
59
|
+
constructor(message) {
|
|
60
|
+
super(message);
|
|
61
|
+
this.name = 'CrowdSourceWebhookConfigurationError';
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
exports.CrowdSourceWebhookConfigurationError = CrowdSourceWebhookConfigurationError;
|
|
65
|
+
function crowdsourceWebhooks(options = {}) {
|
|
66
|
+
const store = options.store ?? (0, store_js_1.memoryProcessedEventStore)();
|
|
67
|
+
const handlers = options.on ?? {};
|
|
68
|
+
const maxBodyBytes = options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;
|
|
69
|
+
return (request, response, next) => {
|
|
70
|
+
void handleDelivery({
|
|
71
|
+
request,
|
|
72
|
+
response,
|
|
73
|
+
next,
|
|
74
|
+
store,
|
|
75
|
+
handlers,
|
|
76
|
+
maxBodyBytes,
|
|
77
|
+
options,
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
async function handleDelivery(context) {
|
|
82
|
+
const { request, response, next, options } = context;
|
|
83
|
+
let rawBody;
|
|
84
|
+
try {
|
|
85
|
+
rawBody = await readRawBody(request, context.maxBodyBytes);
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
if (error instanceof BodyTooLargeError) {
|
|
89
|
+
options.onRejected?.('payload_too_large');
|
|
90
|
+
response.status(413).json({ received: false, rejection: 'payload_too_large' });
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
next(error);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const verification = (0, verify_js_1.verifyWebhookDelivery)({
|
|
97
|
+
eventIdHeader: request.get(contracts_1.WEBHOOK_EVENT_ID_HEADER),
|
|
98
|
+
timestampHeader: request.get(contracts_1.WEBHOOK_TIMESTAMP_HEADER),
|
|
99
|
+
signatureHeader: request.get(contracts_1.WEBHOOK_SIGNATURE_HEADER),
|
|
100
|
+
rawBody,
|
|
101
|
+
secrets: configuredSecrets(options),
|
|
102
|
+
nowMs: Date.now(),
|
|
103
|
+
...(options.toleranceSeconds === undefined
|
|
104
|
+
? {}
|
|
105
|
+
: { toleranceSeconds: options.toleranceSeconds }),
|
|
106
|
+
});
|
|
107
|
+
if (!verification.ok) {
|
|
108
|
+
options.onRejected?.(verification.rejection);
|
|
109
|
+
/**
|
|
110
|
+
* 401, not 400. A refused delivery is an authentication failure from the
|
|
111
|
+
* sender's point of view, and §10.9 keeps retrying 4xx a bounded number of
|
|
112
|
+
* times either way — what matters is that it is never a 2xx, because a 2xx
|
|
113
|
+
* would retire the delivery as successfully processed.
|
|
114
|
+
*
|
|
115
|
+
* `no_secret_configured` is the one case that is OUR fault, and it still
|
|
116
|
+
* answers 401 rather than 500: a receiver with no secret must not tell the
|
|
117
|
+
* world it is running unconfigured, and answering non-2xx keeps the event on
|
|
118
|
+
* the sender's retry schedule until somebody fixes it.
|
|
119
|
+
*/
|
|
120
|
+
response.status(401).json({ received: false, rejection: verification.rejection });
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const parsed = contracts_1.WebhookEventEnvelopeSchema.safeParse(parseJson(rawBody));
|
|
124
|
+
if (!parsed.success) {
|
|
125
|
+
options.onRejected?.('malformed_event');
|
|
126
|
+
response.status(400).json({ received: false, rejection: 'malformed_event' });
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const envelope = parsed.data;
|
|
130
|
+
/**
|
|
131
|
+
* The signed header and the signed body must name the same event. They are
|
|
132
|
+
* both inside the signature, so a disagreement is not an attack — it is a
|
|
133
|
+
* sender defect — but a receiver that deduped on the header while handling the
|
|
134
|
+
* body would process one event under another's id.
|
|
135
|
+
*/
|
|
136
|
+
if (envelope.id !== verification.eventId) {
|
|
137
|
+
options.onRejected?.('event_id_mismatch');
|
|
138
|
+
response.status(400).json({ received: false, rejection: 'event_id_mismatch' });
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const claimed = await context.store.claim(envelope.id);
|
|
142
|
+
if (!claimed) {
|
|
143
|
+
response.status(200).json({ received: true, eventId: envelope.id, duplicate: true });
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
const handled = await dispatch(context.handlers, envelope, options.onUnhandled);
|
|
148
|
+
response.status(200).json({ received: true, eventId: envelope.id, duplicate: false, handled });
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
// The claim goes back so §10.9's retry schedule can deliver it again. Losing
|
|
152
|
+
// a decision because a queue was briefly down is not an acceptable outcome.
|
|
153
|
+
await context.store.release(envelope.id);
|
|
154
|
+
next(error);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Dispatches a verified event, and returns whether anything handled it.
|
|
159
|
+
*
|
|
160
|
+
* The switch is exhaustive over the contract's event types on purpose: adding an
|
|
161
|
+
* event to `WEBHOOK_EVENT_TYPES` stops this file compiling until it is handled
|
|
162
|
+
* here, which is the right failure for the SDK. For an INTEGRATION it is the
|
|
163
|
+
* opposite — the `default` branch means an unrecognised type is acknowledged and
|
|
164
|
+
* ignored, so a newer CrowdSource never breaks an older receiver (§10.11).
|
|
165
|
+
*
|
|
166
|
+
* The payload is only parsed against the known-event schema when a handler is
|
|
167
|
+
* actually registered for that type. An integration handling `case.decided` is
|
|
168
|
+
* therefore unaffected by a later change to what `appeal.created` carries.
|
|
169
|
+
*/
|
|
170
|
+
async function dispatch(handlers, envelope, onUnhandled) {
|
|
171
|
+
const known = contracts_1.KnownWebhookEventSchema.safeParse(envelope);
|
|
172
|
+
if (known.success && (await invoke(handlers, known.data)))
|
|
173
|
+
return true;
|
|
174
|
+
if (onUnhandled) {
|
|
175
|
+
await onUnhandled(envelope);
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
/** Calls `handler` with `event` if there is one, and says whether there was. */
|
|
181
|
+
async function call(handler, event) {
|
|
182
|
+
if (handler === undefined)
|
|
183
|
+
return false;
|
|
184
|
+
await handler(event);
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* The exhaustive half of `dispatch`, written out rather than looked up.
|
|
189
|
+
*
|
|
190
|
+
* Indexing the handler map with `event.type` while `event.type` is still a union
|
|
191
|
+
* would need a cast to call the result, and a cast is exactly what must not sit
|
|
192
|
+
* between a signed event and the code that acts on it. Narrowing per case is
|
|
193
|
+
* what makes each call provably well-typed, and it means adding an event to the
|
|
194
|
+
* contract stops this file compiling until it is handled.
|
|
195
|
+
*/
|
|
196
|
+
async function invoke(handlers, event) {
|
|
197
|
+
switch (event.type) {
|
|
198
|
+
case 'report.received':
|
|
199
|
+
return await call(handlers['report.received'], event);
|
|
200
|
+
case 'case.created':
|
|
201
|
+
return await call(handlers['case.created'], event);
|
|
202
|
+
case 'case.escalated':
|
|
203
|
+
return await call(handlers['case.escalated'], event);
|
|
204
|
+
case 'case.decided':
|
|
205
|
+
return await call(handlers['case.decided'], event);
|
|
206
|
+
case 'decision.corrected':
|
|
207
|
+
return await call(handlers['decision.corrected'], event);
|
|
208
|
+
case 'appeal.created':
|
|
209
|
+
return await call(handlers['appeal.created'], event);
|
|
210
|
+
case 'appeal.decided':
|
|
211
|
+
return await call(handlers['appeal.decided'], event);
|
|
212
|
+
case 'case.closed':
|
|
213
|
+
return await call(handlers['case.closed'], event);
|
|
214
|
+
case 'community_note.status_changed':
|
|
215
|
+
return await call(handlers['community_note.status_changed'], event);
|
|
216
|
+
default:
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function configuredSecrets(options) {
|
|
221
|
+
const active = options.secret ?? process.env[exports.WEBHOOK_SECRET_ENV_VAR];
|
|
222
|
+
const previous = options.previousSecret ?? process.env[exports.WEBHOOK_PREVIOUS_SECRET_ENV_VAR];
|
|
223
|
+
return [active, previous].filter((secret) => typeof secret === 'string' && secret.length > 0);
|
|
224
|
+
}
|
|
225
|
+
function parseJson(rawBody) {
|
|
226
|
+
try {
|
|
227
|
+
return JSON.parse(rawBody.toString('utf8'));
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
class BodyTooLargeError extends Error {
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* The bytes as they arrived.
|
|
237
|
+
*
|
|
238
|
+
* Three sources, in the order that keeps the guarantee:
|
|
239
|
+
*
|
|
240
|
+
* 1. `req.body` is already a Buffer — `express.raw()` is mounted ahead of this
|
|
241
|
+
* handler, which is correct and supported.
|
|
242
|
+
* 2. `req.rawBody` is a Buffer — the `express.json({ verify })` idiom, also
|
|
243
|
+
* correct.
|
|
244
|
+
* 3. Nothing has touched the request — read the stream.
|
|
245
|
+
*
|
|
246
|
+
* Anything else REFUSES. A parsed `req.body` means a JSON parser ran first and
|
|
247
|
+
* the original bytes are gone; re-serialising them would produce a different
|
|
248
|
+
* value from the one that was signed, and the only way to make that "work"
|
|
249
|
+
* would be to verify a signature over bytes CrowdSource never sent.
|
|
250
|
+
*/
|
|
251
|
+
async function readRawBody(request, maxBodyBytes) {
|
|
252
|
+
const carried = Reflect.get(request, 'rawBody');
|
|
253
|
+
if (Buffer.isBuffer(carried))
|
|
254
|
+
return carried;
|
|
255
|
+
const parsedBody = request.body;
|
|
256
|
+
if (Buffer.isBuffer(parsedBody))
|
|
257
|
+
return parsedBody;
|
|
258
|
+
if (parsedBody !== undefined && parsedBody !== null) {
|
|
259
|
+
throw new CrowdSourceWebhookConfigurationError('A body parser ran before the CrowdSource webhook handler, so the signed bytes no longer exist. Mount this handler before express.json(), scope express.json() away from the webhook route, or use express.json({ verify }) to keep the raw body.');
|
|
260
|
+
}
|
|
261
|
+
if (request.readableEnded) {
|
|
262
|
+
throw new CrowdSourceWebhookConfigurationError('The request body was already consumed before the CrowdSource webhook handler ran, so the signed bytes no longer exist.');
|
|
263
|
+
}
|
|
264
|
+
return await new Promise((resolve, reject) => {
|
|
265
|
+
const chunks = [];
|
|
266
|
+
let received = 0;
|
|
267
|
+
let settled = false;
|
|
268
|
+
request.on('data', (chunk) => {
|
|
269
|
+
if (settled)
|
|
270
|
+
return;
|
|
271
|
+
received += chunk.length;
|
|
272
|
+
if (received > maxBodyBytes) {
|
|
273
|
+
settled = true;
|
|
274
|
+
// Paused rather than destroyed: destroying the request takes the socket
|
|
275
|
+
// with it, and the 413 the caller is about to write would never reach
|
|
276
|
+
// the sender — which would turn a clear refusal into a connection reset
|
|
277
|
+
// that a delivery worker can only report as "unknown".
|
|
278
|
+
request.pause();
|
|
279
|
+
reject(new BodyTooLargeError());
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
chunks.push(chunk);
|
|
283
|
+
});
|
|
284
|
+
request.on('end', () => {
|
|
285
|
+
if (settled)
|
|
286
|
+
return;
|
|
287
|
+
settled = true;
|
|
288
|
+
resolve(Buffer.concat(chunks));
|
|
289
|
+
});
|
|
290
|
+
request.on('error', (error) => {
|
|
291
|
+
if (settled)
|
|
292
|
+
return;
|
|
293
|
+
settled = true;
|
|
294
|
+
reject(error);
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
//# sourceMappingURL=middleware.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"middleware.js","sourceRoot":"","sources":["../../src/express/middleware.ts"],"names":[],"mappings":"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"}
|