@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,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The webhook simulator — the producer side of §10.8.
|
|
4
|
+
*
|
|
5
|
+
* An integrator can point this at their own receiver and watch a real delivery
|
|
6
|
+
* arrive, signed exactly as CrowdSource signs one, without a jury having sat and
|
|
7
|
+
* without anything being enforced against a real user.
|
|
8
|
+
*
|
|
9
|
+
* It deliberately also makes it easy to send a delivery that is WRONG:
|
|
10
|
+
* `expired`, `wrongSecret`, `tampered` and a raw `signature` override are here
|
|
11
|
+
* so an integration test can assert that the receiver REFUSES those. A test
|
|
12
|
+
* suite that only ever sends valid deliveries proves the receiver can say yes.
|
|
13
|
+
* Proving it can say no is the part that matters, and it is the part nobody
|
|
14
|
+
* writes unless the tooling makes it a one-liner.
|
|
15
|
+
*
|
|
16
|
+
* Signing lives here rather than in `@crowdsource.you/core/express` because the two
|
|
17
|
+
* are opposite sides of the contract: the verifier must never import a signer
|
|
18
|
+
* that could be "corrected" until they agree with each other and both disagree
|
|
19
|
+
* with the service. What they share instead is
|
|
20
|
+
* `buildWebhookSignedPayload` from the contracts package, which is the one
|
|
21
|
+
* definition of what gets signed.
|
|
22
|
+
*/
|
|
23
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
+
exports.WebhookSimulator = void 0;
|
|
25
|
+
exports.signWebhookDelivery = signWebhookDelivery;
|
|
26
|
+
const node_crypto_1 = require("node:crypto");
|
|
27
|
+
const contracts_1 = require("@crowdsource.you/contracts");
|
|
28
|
+
function eventIdOf(event) {
|
|
29
|
+
if (typeof event === 'object' && event !== null && 'id' in event) {
|
|
30
|
+
const id = event.id;
|
|
31
|
+
if (typeof id === 'string')
|
|
32
|
+
return id;
|
|
33
|
+
}
|
|
34
|
+
throw new TypeError('A webhook event must carry a string `id` (§10.7).');
|
|
35
|
+
}
|
|
36
|
+
/** Signs one delivery. */
|
|
37
|
+
function signWebhookDelivery(input) {
|
|
38
|
+
const signedBody = JSON.stringify(input.event);
|
|
39
|
+
const timestamp = String(input.timestampSeconds ?? Math.floor(Date.now() / 1000));
|
|
40
|
+
const signature = input.signature ??
|
|
41
|
+
`${contracts_1.WEBHOOK_SIGNATURE_VERSION}=${(0, node_crypto_1.createHmac)('sha256', input.secret)
|
|
42
|
+
.update((0, contracts_1.buildWebhookSignedPayload)(timestamp, signedBody), 'utf8')
|
|
43
|
+
.digest('hex')}`;
|
|
44
|
+
return {
|
|
45
|
+
headers: {
|
|
46
|
+
'content-type': 'application/json',
|
|
47
|
+
[contracts_1.WEBHOOK_EVENT_ID_HEADER]: input.eventId ?? eventIdOf(input.event),
|
|
48
|
+
[contracts_1.WEBHOOK_TIMESTAMP_HEADER]: timestamp,
|
|
49
|
+
[contracts_1.WEBHOOK_SIGNATURE_HEADER]: signature,
|
|
50
|
+
},
|
|
51
|
+
// The tampered body is sent WITHOUT re-signing, which is the whole point:
|
|
52
|
+
// the signature above covers `signedBody` and the receiver is handed
|
|
53
|
+
// something else.
|
|
54
|
+
body: input.tamperedBody ?? signedBody,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/** Comfortably outside the five-minute window, in either direction. */
|
|
58
|
+
const EXPIRED_SKEW_SECONDS = 3600;
|
|
59
|
+
class WebhookSimulator {
|
|
60
|
+
constructor(options) {
|
|
61
|
+
this.options = options;
|
|
62
|
+
}
|
|
63
|
+
/** Signs a delivery without sending it. */
|
|
64
|
+
sign(event, overrides = {}) {
|
|
65
|
+
return signWebhookDelivery({
|
|
66
|
+
secret: overrides.wrongSecret ?? this.options.secret,
|
|
67
|
+
event,
|
|
68
|
+
timestampSeconds: overrides.timestampSeconds ??
|
|
69
|
+
Math.floor(Date.now() / 1000) - (overrides.expired === true ? EXPIRED_SKEW_SECONDS : 0),
|
|
70
|
+
...(overrides.tamperedBody === undefined ? {} : { tamperedBody: overrides.tamperedBody }),
|
|
71
|
+
...(overrides.signature === undefined ? {} : { signature: overrides.signature }),
|
|
72
|
+
...(overrides.eventId === undefined ? {} : { eventId: overrides.eventId }),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
/** Signs and POSTs a delivery to the receiver. */
|
|
76
|
+
async deliver(event, overrides = {}) {
|
|
77
|
+
const delivery = this.sign(event, overrides);
|
|
78
|
+
const fetchImpl = this.options.fetch ?? globalThis.fetch;
|
|
79
|
+
const response = await fetchImpl(this.options.url, {
|
|
80
|
+
method: 'POST',
|
|
81
|
+
headers: delivery.headers,
|
|
82
|
+
body: delivery.body,
|
|
83
|
+
});
|
|
84
|
+
const text = await response.text();
|
|
85
|
+
let body = null;
|
|
86
|
+
if (text.length > 0) {
|
|
87
|
+
try {
|
|
88
|
+
body = JSON.parse(text);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
body = text;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return { status: response.status, body };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
exports.WebhookSimulator = WebhookSimulator;
|
|
98
|
+
//# sourceMappingURL=webhook-simulator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhook-simulator.js","sourceRoot":"","sources":["../../src/testing/webhook-simulator.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The HTTP transport: one authenticated call, and what happens when it fails.
|
|
3
|
+
*
|
|
4
|
+
* Two things here are worth reading before changing anything.
|
|
5
|
+
*
|
|
6
|
+
* **Retries do not replace the integrator's outbox.** §7.1 is explicit that the
|
|
7
|
+
* durable retry path belongs to the application: it stores the report and an
|
|
8
|
+
* outbox row in one operation, and a worker re-delivers until CrowdSource
|
|
9
|
+
* accepts. The retries below only smooth over a blip inside a single delivery
|
|
10
|
+
* attempt — they are bounded, they only fire for the statuses §10.5 marks as
|
|
11
|
+
* "come back later", and when they give up the error they throw still says
|
|
12
|
+
* `retryable: true` so the outbox keeps its job.
|
|
13
|
+
*
|
|
14
|
+
* **A retry is only safe because the write is idempotent.** Every write this
|
|
15
|
+
* client makes carries an `Idempotency-Key` (Appendix D), so a request that was
|
|
16
|
+
* received but whose response was lost returns the same `reportId` on the next
|
|
17
|
+
* attempt rather than creating a second report. A write without one must not be
|
|
18
|
+
* retried, and `request` refuses to retry a mutation that has no key rather than
|
|
19
|
+
* trusting the caller to have thought about it.
|
|
20
|
+
*/
|
|
21
|
+
export type FetchLike = typeof globalThis.fetch;
|
|
22
|
+
/** How long a single attempt may take before it is abandoned. */
|
|
23
|
+
export declare const DEFAULT_TIMEOUT_MS = 10000;
|
|
24
|
+
/** Attempts per call, including the first. */
|
|
25
|
+
export declare const DEFAULT_MAX_ATTEMPTS = 3;
|
|
26
|
+
export interface TransportConfig {
|
|
27
|
+
readonly baseUrl: string;
|
|
28
|
+
/**
|
|
29
|
+
* What goes in `Authorization`, resolved per attempt.
|
|
30
|
+
*
|
|
31
|
+
* A function and not a string because a first-party service authenticates with
|
|
32
|
+
* an Oxy service token that EXPIRES (oxy ADR 0026) — a value captured once at
|
|
33
|
+
* construction would work for an hour and then fail forever. A CrowdSource
|
|
34
|
+
* service key never changes, so its provider simply returns the same string.
|
|
35
|
+
*/
|
|
36
|
+
readonly bearerToken: () => string | Promise<string>;
|
|
37
|
+
readonly timeoutMs: number;
|
|
38
|
+
readonly maxAttempts: number;
|
|
39
|
+
readonly fetch: FetchLike;
|
|
40
|
+
}
|
|
41
|
+
export interface TransportRequest {
|
|
42
|
+
readonly method: 'GET' | 'POST';
|
|
43
|
+
/** Absolute path from the service root, e.g. `/v1/reports`. */
|
|
44
|
+
readonly path: string;
|
|
45
|
+
readonly body?: unknown;
|
|
46
|
+
/** Appendix D. Required for every mutation; a mutation without one is a defect. */
|
|
47
|
+
readonly idempotencyKey?: string;
|
|
48
|
+
readonly signal?: AbortSignal;
|
|
49
|
+
}
|
|
50
|
+
export declare class Transport {
|
|
51
|
+
private readonly config;
|
|
52
|
+
constructor(config: TransportConfig);
|
|
53
|
+
request<T>(request: TransportRequest): Promise<T>;
|
|
54
|
+
private attempt;
|
|
55
|
+
/** The caller's cancellation and this attempt's timeout, as one signal. */
|
|
56
|
+
private attemptSignal;
|
|
57
|
+
private readJson;
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=transport.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AASH,MAAM,MAAM,SAAS,GAAG,OAAO,UAAU,CAAC,KAAK,CAAC;AAEhD,iEAAiE;AACjE,eAAO,MAAM,kBAAkB,QAAS,CAAC;AAEzC,8CAA8C;AAC9C,eAAO,MAAM,oBAAoB,IAAI,CAAC;AAQtC,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB;;;;;;;OAOG;IACH,QAAQ,CAAC,WAAW,EAAE,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACrD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC;CAC3B;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,MAAM,EAAE,KAAK,GAAG,MAAM,CAAC;IAChC,+DAA+D;IAC/D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IACxB,mFAAmF;IACnF,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;CAC/B;AAgED,qBAAa,SAAS;IACpB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAkB;gBAE7B,MAAM,EAAE,eAAe;IAI7B,OAAO,CAAC,CAAC,EAAE,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,CAAC,CAAC;YA4BzC,OAAO;IAkErB,2EAA2E;IAC3E,OAAO,CAAC,aAAa;YAKP,QAAQ;CAYvB"}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The HTTP transport: one authenticated call, and what happens when it fails.
|
|
4
|
+
*
|
|
5
|
+
* Two things here are worth reading before changing anything.
|
|
6
|
+
*
|
|
7
|
+
* **Retries do not replace the integrator's outbox.** §7.1 is explicit that the
|
|
8
|
+
* durable retry path belongs to the application: it stores the report and an
|
|
9
|
+
* outbox row in one operation, and a worker re-delivers until CrowdSource
|
|
10
|
+
* accepts. The retries below only smooth over a blip inside a single delivery
|
|
11
|
+
* attempt — they are bounded, they only fire for the statuses §10.5 marks as
|
|
12
|
+
* "come back later", and when they give up the error they throw still says
|
|
13
|
+
* `retryable: true` so the outbox keeps its job.
|
|
14
|
+
*
|
|
15
|
+
* **A retry is only safe because the write is idempotent.** Every write this
|
|
16
|
+
* client makes carries an `Idempotency-Key` (Appendix D), so a request that was
|
|
17
|
+
* received but whose response was lost returns the same `reportId` on the next
|
|
18
|
+
* attempt rather than creating a second report. A write without one must not be
|
|
19
|
+
* retried, and `request` refuses to retry a mutation that has no key rather than
|
|
20
|
+
* trusting the caller to have thought about it.
|
|
21
|
+
*/
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.Transport = exports.DEFAULT_MAX_ATTEMPTS = exports.DEFAULT_TIMEOUT_MS = void 0;
|
|
24
|
+
const errors_js_1 = require("./errors.js");
|
|
25
|
+
/** How long a single attempt may take before it is abandoned. */
|
|
26
|
+
exports.DEFAULT_TIMEOUT_MS = 10000;
|
|
27
|
+
/** Attempts per call, including the first. */
|
|
28
|
+
exports.DEFAULT_MAX_ATTEMPTS = 3;
|
|
29
|
+
/** The base of the exponential backoff between attempts. */
|
|
30
|
+
const RETRY_BASE_DELAY_MS = 250;
|
|
31
|
+
/** A cap so a `Retry-After` of an hour cannot hang a delivery worker. */
|
|
32
|
+
const RETRY_DELAY_CEILING_MS = 5000;
|
|
33
|
+
function parsedErrorBody(payload) {
|
|
34
|
+
if (typeof payload !== 'object' || payload === null || !('error' in payload))
|
|
35
|
+
return null;
|
|
36
|
+
const error = payload.error;
|
|
37
|
+
if (typeof error !== 'object' || error === null)
|
|
38
|
+
return null;
|
|
39
|
+
const record = { ...error };
|
|
40
|
+
if (typeof record.code !== 'string' || typeof record.message !== 'string')
|
|
41
|
+
return null;
|
|
42
|
+
const details = typeof record.details === 'object' && record.details !== null
|
|
43
|
+
? record.details
|
|
44
|
+
: undefined;
|
|
45
|
+
return { code: record.code, message: record.message, details };
|
|
46
|
+
}
|
|
47
|
+
/** `Retry-After` in either of its two legal forms, in milliseconds. */
|
|
48
|
+
function retryAfterMs(header) {
|
|
49
|
+
if (header === null)
|
|
50
|
+
return null;
|
|
51
|
+
const seconds = Number(header);
|
|
52
|
+
if (Number.isFinite(seconds) && seconds >= 0)
|
|
53
|
+
return seconds * 1000;
|
|
54
|
+
const date = Date.parse(header);
|
|
55
|
+
if (Number.isNaN(date))
|
|
56
|
+
return null;
|
|
57
|
+
return Math.max(0, date - Date.now());
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Full jitter over an exponential base. Without jitter, every worker that hit
|
|
61
|
+
* the same 503 comes back at the same millisecond and reproduces it.
|
|
62
|
+
*/
|
|
63
|
+
function backoffMs(attempt) {
|
|
64
|
+
const ceiling = Math.min(RETRY_BASE_DELAY_MS * 2 ** (attempt - 1), RETRY_DELAY_CEILING_MS);
|
|
65
|
+
return Math.random() * ceiling;
|
|
66
|
+
}
|
|
67
|
+
function delay(ms, signal) {
|
|
68
|
+
return new Promise((resolve, reject) => {
|
|
69
|
+
const timer = setTimeout(() => {
|
|
70
|
+
signal?.removeEventListener('abort', onAbort);
|
|
71
|
+
resolve();
|
|
72
|
+
}, ms);
|
|
73
|
+
const onAbort = () => {
|
|
74
|
+
clearTimeout(timer);
|
|
75
|
+
reject(new errors_js_1.CrowdSourceTransportError('The request was aborted.', { retryable: false }));
|
|
76
|
+
};
|
|
77
|
+
if (signal?.aborted === true) {
|
|
78
|
+
onAbort();
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
class Transport {
|
|
85
|
+
constructor(config) {
|
|
86
|
+
this.config = config;
|
|
87
|
+
}
|
|
88
|
+
async request(request) {
|
|
89
|
+
if (request.method !== 'GET' && request.idempotencyKey === undefined) {
|
|
90
|
+
throw new Error(`A ${request.method} to ${request.path} was made without an idempotency key. Appendix D requires one on every write.`);
|
|
91
|
+
}
|
|
92
|
+
let lastError = null;
|
|
93
|
+
for (let attempt = 1; attempt <= this.config.maxAttempts; attempt += 1) {
|
|
94
|
+
const outcome = await this.attempt(request);
|
|
95
|
+
if (outcome.ok)
|
|
96
|
+
return outcome.value;
|
|
97
|
+
lastError = outcome.error;
|
|
98
|
+
if (!outcome.error.retryable || attempt === this.config.maxAttempts)
|
|
99
|
+
break;
|
|
100
|
+
await delay(outcome.retryAfterMs ?? backoffMs(attempt), request.signal);
|
|
101
|
+
}
|
|
102
|
+
// Unreachable with `maxAttempts >= 1`; the loop either returns or assigns.
|
|
103
|
+
if (lastError === null) {
|
|
104
|
+
throw new errors_js_1.CrowdSourceTransportError('The request was never attempted.', {
|
|
105
|
+
retryable: false,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
throw lastError;
|
|
109
|
+
}
|
|
110
|
+
async attempt(request) {
|
|
111
|
+
const headers = {
|
|
112
|
+
accept: 'application/json',
|
|
113
|
+
// Resolved per ATTEMPT, not per call: a retry that outlives a short-lived
|
|
114
|
+
// token must carry the new one, and this is the only place that knows an
|
|
115
|
+
// attempt is starting.
|
|
116
|
+
authorization: `Bearer ${await this.config.bearerToken()}`,
|
|
117
|
+
};
|
|
118
|
+
if (request.body !== undefined)
|
|
119
|
+
headers['content-type'] = 'application/json';
|
|
120
|
+
if (request.idempotencyKey !== undefined) {
|
|
121
|
+
headers['idempotency-key'] = request.idempotencyKey;
|
|
122
|
+
}
|
|
123
|
+
let response;
|
|
124
|
+
try {
|
|
125
|
+
response = await this.config.fetch(`${this.config.baseUrl}${request.path}`, {
|
|
126
|
+
method: request.method,
|
|
127
|
+
headers,
|
|
128
|
+
body: request.body === undefined ? undefined : JSON.stringify(request.body),
|
|
129
|
+
signal: this.attemptSignal(request.signal),
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
catch (cause) {
|
|
133
|
+
const callerAborted = request.signal?.aborted === true;
|
|
134
|
+
return {
|
|
135
|
+
ok: false,
|
|
136
|
+
error: new errors_js_1.CrowdSourceTransportError(callerAborted
|
|
137
|
+
? 'The request was aborted by the caller.'
|
|
138
|
+
: `The request to ${request.path} did not complete.`, { cause, retryable: !callerAborted }),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
if (response.ok) {
|
|
142
|
+
return { ok: true, value: (await this.readJson(response)) };
|
|
143
|
+
}
|
|
144
|
+
const payload = await this.readJson(response).catch(() => null);
|
|
145
|
+
const body = parsedErrorBody(payload);
|
|
146
|
+
const code = body !== null && (0, errors_js_1.isCrowdSourceApiErrorCode)(body.code)
|
|
147
|
+
? body.code
|
|
148
|
+
: (0, errors_js_1.apiErrorCodeForStatus)(response.status);
|
|
149
|
+
return {
|
|
150
|
+
ok: false,
|
|
151
|
+
error: new errors_js_1.CrowdSourceApiError({
|
|
152
|
+
status: response.status,
|
|
153
|
+
code,
|
|
154
|
+
message: body?.message ?? `CrowdSource answered ${response.status} for ${request.path}.`,
|
|
155
|
+
details: body?.details,
|
|
156
|
+
}),
|
|
157
|
+
retryAfterMs: retryAfterMs(response.headers.get('retry-after')) ?? undefined,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/** The caller's cancellation and this attempt's timeout, as one signal. */
|
|
161
|
+
attemptSignal(callerSignal) {
|
|
162
|
+
const timeout = AbortSignal.timeout(this.config.timeoutMs);
|
|
163
|
+
return callerSignal === undefined ? timeout : AbortSignal.any([callerSignal, timeout]);
|
|
164
|
+
}
|
|
165
|
+
async readJson(response) {
|
|
166
|
+
const text = await response.text();
|
|
167
|
+
if (text.length === 0)
|
|
168
|
+
return null;
|
|
169
|
+
try {
|
|
170
|
+
return JSON.parse(text);
|
|
171
|
+
}
|
|
172
|
+
catch (cause) {
|
|
173
|
+
throw new errors_js_1.CrowdSourceTransportError('CrowdSource answered with a body that is not JSON.', {
|
|
174
|
+
cause,
|
|
175
|
+
retryable: false,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
exports.Transport = Transport;
|
|
181
|
+
//# sourceMappingURL=transport.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport.js","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,147 @@
|
|
|
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 type { Transport } from './transport.js';
|
|
31
|
+
/**
|
|
32
|
+
* A signing secret, at the one moment it is visible.
|
|
33
|
+
*
|
|
34
|
+
* `signingStartsAt` is what makes a rotation overlap followable rather than
|
|
35
|
+
* guesswork: deliveries begin carrying this signature at that instant, so
|
|
36
|
+
* "install both, retire the old one after this time" is a procedure instead of
|
|
37
|
+
* an estimate.
|
|
38
|
+
*/
|
|
39
|
+
export interface WebhookSecret {
|
|
40
|
+
readonly version: number;
|
|
41
|
+
/** The value to configure as `CROWDSOURCE_WEBHOOK_SECRET`. Shown once. */
|
|
42
|
+
readonly value: string;
|
|
43
|
+
/** ISO-8601 UTC. When deliveries start being signed with this version. */
|
|
44
|
+
readonly signingStartsAt: string;
|
|
45
|
+
}
|
|
46
|
+
/** A registered endpoint, and the secret if this call is what created it. */
|
|
47
|
+
export interface WebhookEndpoint {
|
|
48
|
+
readonly webhookEndpointId: string;
|
|
49
|
+
readonly url: string;
|
|
50
|
+
readonly eventTypes: readonly string[];
|
|
51
|
+
/**
|
|
52
|
+
* Kept open deliberately. §10.11 requires a newer server not to break an older
|
|
53
|
+
* client, and a status added later is exactly that case.
|
|
54
|
+
*/
|
|
55
|
+
readonly status: string;
|
|
56
|
+
readonly disabledReason: string | null;
|
|
57
|
+
/** ISO-8601 UTC, as the API sends it. */
|
|
58
|
+
readonly createdAt: string;
|
|
59
|
+
readonly updatedAt: string;
|
|
60
|
+
/**
|
|
61
|
+
* Present ONLY when this call minted one — a newly created endpoint, or one
|
|
62
|
+
* whose subscription revived it. Absent when an existing URL was updated,
|
|
63
|
+
* because an update deliberately leaves the running secret alone.
|
|
64
|
+
*
|
|
65
|
+
* When it is present, store it before doing anything else. Nothing can return
|
|
66
|
+
* it again and `rotateSecret` is the only way to get a working secret back.
|
|
67
|
+
*/
|
|
68
|
+
readonly secret?: WebhookSecret;
|
|
69
|
+
}
|
|
70
|
+
/** The outcome of a rotation: the new secret, and when the old one dies. */
|
|
71
|
+
export interface RotatedWebhookSecret {
|
|
72
|
+
readonly webhookEndpointId: string;
|
|
73
|
+
readonly secret: WebhookSecret;
|
|
74
|
+
/**
|
|
75
|
+
* The secret being retired, and the instant it stops being accepted — set it
|
|
76
|
+
* as `CROWDSOURCE_WEBHOOK_SECRET_PREVIOUS` until then and a rotation drops no
|
|
77
|
+
* delivery. `null` when the cutover was immediate, which is what a leaked
|
|
78
|
+
* secret needs.
|
|
79
|
+
*/
|
|
80
|
+
readonly previousSecret: {
|
|
81
|
+
readonly version: number;
|
|
82
|
+
readonly expiresAt: string;
|
|
83
|
+
} | null;
|
|
84
|
+
}
|
|
85
|
+
export interface RegisterWebhookEndpointInput {
|
|
86
|
+
/** Where CrowdSource should POST. HTTPS in production. */
|
|
87
|
+
readonly url: string;
|
|
88
|
+
/**
|
|
89
|
+
* The event types to subscribe to. Import `WEBHOOK_EVENT_TYPES` from
|
|
90
|
+
* `@crowdsource.you/contracts` for the current set; unknown types are
|
|
91
|
+
* accepted so a newer contract does not need a new client.
|
|
92
|
+
*/
|
|
93
|
+
readonly eventTypes: readonly string[];
|
|
94
|
+
}
|
|
95
|
+
export interface RotateSecretOptions {
|
|
96
|
+
/**
|
|
97
|
+
* How long the outgoing secret keeps being accepted. `0` is an immediate
|
|
98
|
+
* cutover — the right choice for a leaked secret and the wrong one for routine
|
|
99
|
+
* hygiene, because in-flight deliveries signed with the old secret are refused.
|
|
100
|
+
* Omitted, the server applies its own default overlap.
|
|
101
|
+
*/
|
|
102
|
+
readonly overlapSeconds?: number;
|
|
103
|
+
/**
|
|
104
|
+
* Appendix D. Defaults to a key derived from the endpoint id, which makes a
|
|
105
|
+
* RETRY of one rotation safe. Two deliberate rotations are two different
|
|
106
|
+
* operations, so pass a distinct key for the second one.
|
|
107
|
+
*/
|
|
108
|
+
readonly idempotencyKey?: string;
|
|
109
|
+
readonly signal?: AbortSignal;
|
|
110
|
+
}
|
|
111
|
+
export interface WebhookEndpointRequestOptions {
|
|
112
|
+
/** Appendix D. Defaults to a key derived from the URL being registered. */
|
|
113
|
+
readonly idempotencyKey?: string;
|
|
114
|
+
readonly signal?: AbortSignal;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* §10.2's two webhook-management routes.
|
|
118
|
+
*
|
|
119
|
+
* There is no list, no read-back and no delete, because the API serves none —
|
|
120
|
+
* §10.2 defines exactly two routes. An integrator therefore cannot enumerate
|
|
121
|
+
* what it has registered, which is a real gap in the service rather than
|
|
122
|
+
* something this client can paper over; re-registering a URL is the documented
|
|
123
|
+
* way to change a subscription or revive a disabled endpoint.
|
|
124
|
+
*
|
|
125
|
+
* Both routes need a credential with the `crowdsource:webhooks:manage` scope,
|
|
126
|
+
* and both derive the application from that credential. There is no
|
|
127
|
+
* `applicationId` in any request or response here.
|
|
128
|
+
*/
|
|
129
|
+
export declare class WebhookEndpoints {
|
|
130
|
+
private readonly transport;
|
|
131
|
+
constructor(transport: Transport);
|
|
132
|
+
/**
|
|
133
|
+
* Registers, or updates, the endpoint CrowdSource delivers to.
|
|
134
|
+
*
|
|
135
|
+
* Safe to call on every deploy: an existing URL is updated and no new secret
|
|
136
|
+
* is minted, so the secret the running process verifies with keeps working.
|
|
137
|
+
*/
|
|
138
|
+
register(input: RegisterWebhookEndpointInput, options?: WebhookEndpointRequestOptions): Promise<WebhookEndpoint>;
|
|
139
|
+
/**
|
|
140
|
+
* Mints a new signing secret, retiring the current one after an overlap.
|
|
141
|
+
*
|
|
142
|
+
* The only way to recover from a secret that was never stored, and the correct
|
|
143
|
+
* response to one that leaked.
|
|
144
|
+
*/
|
|
145
|
+
rotateSecret(webhookEndpointId: string, options?: RotateSecretOptions): Promise<RotatedWebhookSecret>;
|
|
146
|
+
}
|
|
147
|
+
//# sourceMappingURL=webhookEndpoints.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhookEndpoints.d.ts","sourceRoot":"","sources":["../src/webhookEndpoints.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAMH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAEhD;;;;;;;GAOG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,0EAA0E;IAC1E,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,0EAA0E;IAC1E,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;CAClC;AAED,6EAA6E;AAC7E,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;IACvC;;;OAGG;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IACvC,yCAAyC;IACzC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;;;;;;;OAOG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,aAAa,CAAC;CACjC;AAED,4EAA4E;AAC5E,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B;;;;;OAKG;IACH,QAAQ,CAAC,cAAc,EAAE;QAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;CAC1F;AAED,MAAM,WAAW,4BAA4B;IAC3C,0DAA0D;IAC1D,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,QAAQ,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;CACxC;AAmBD,MAAM,WAAW,mBAAmB;IAClC;;;;;OAKG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC;;;;OAIG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;CAC/B;AAED,MAAM,WAAW,6BAA6B;IAC5C,2EAA2E;IAC3E,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;CAC/B;AA2BD;;;;;;;;;;;;GAYG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAY;gBAE1B,SAAS,EAAE,SAAS;IAIhC;;;;;OAKG;IACG,QAAQ,CACZ,KAAK,EAAE,4BAA4B,EACnC,OAAO,GAAE,6BAAkC,GAC1C,OAAO,CAAC,eAAe,CAAC;IAyB3B;;;;;OAKG;IACG,YAAY,CAChB,iBAAiB,EAAE,MAAM,EACzB,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC,oBAAoB,CAAC;CA0BjC"}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Telling CrowdSource where to deliver decisions, and holding the secret it
|
|
4
|
+
* mints once.
|
|
5
|
+
*
|
|
6
|
+
* This is the half of the webhook integration that used to have no client at
|
|
7
|
+
* all: `@crowdsource.you/core/express` verifies deliveries against
|
|
8
|
+
* `CROWDSOURCE_WEBHOOK_SECRET`, and there was no supported way to obtain that
|
|
9
|
+
* value except hand-rolling an HTTP call with the bearer half of the service
|
|
10
|
+
* key. "Near-zero configuration" cannot mean an integrator writes their own
|
|
11
|
+
* fetch for a mandatory step, so §10.2's two management routes are here.
|
|
12
|
+
*
|
|
13
|
+
* Two properties of the secret shape everything below, and both are the server's
|
|
14
|
+
* design rather than this client's choice:
|
|
15
|
+
*
|
|
16
|
+
* * **It is returned once and never again.** A webhook secret is decryptable
|
|
17
|
+
* by CrowdSource because deliveries have to be signed with it, but re-serving
|
|
18
|
+
* it would turn any credential holding `webhooks:manage` into a way to read
|
|
19
|
+
* the signing key of an endpoint somebody else configured. So `secret` on a
|
|
20
|
+
* registration is the only time that value exists outside the service.
|
|
21
|
+
* * **Re-registering an existing URL mints nothing.** That is what makes a
|
|
22
|
+
* deploy script safe to run on every boot — it will not invalidate the
|
|
23
|
+
* secret the running process is verifying with. The corollary is that
|
|
24
|
+
* re-registering cannot RECOVER a secret you failed to store; that is what
|
|
25
|
+
* `rotateSecret` is for.
|
|
26
|
+
*
|
|
27
|
+
* Which is why `secret` is optional on the result. Its presence is not a detail:
|
|
28
|
+
* it means a value has just come into existence that nothing can produce again,
|
|
29
|
+
* and the integration is responsible for persisting it before the process exits.
|
|
30
|
+
*/
|
|
31
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
32
|
+
exports.WebhookEndpoints = void 0;
|
|
33
|
+
const zod_1 = require("zod");
|
|
34
|
+
const digest_js_1 = require("./digest.js");
|
|
35
|
+
const errors_js_1 = require("./errors.js");
|
|
36
|
+
/**
|
|
37
|
+
* Appendix D's key for a registration.
|
|
38
|
+
*
|
|
39
|
+
* Derived from the URL because the URL is the endpoint's identity as far as the
|
|
40
|
+
* server is concerned — the same URL registered twice is one logical operation,
|
|
41
|
+
* which is exactly what makes a deploy-time `register` safe to retry. Hashed
|
|
42
|
+
* rather than interpolated because a URL carries `:` and `/`, and a composite
|
|
43
|
+
* value with separators in it is how an id grammar gets ambiguous.
|
|
44
|
+
*
|
|
45
|
+
* Note that the two webhook routes do NOT currently read `Idempotency-Key`
|
|
46
|
+
* server-side; only `POST /v1/reports` does. This satisfies the transport's
|
|
47
|
+
* Appendix D requirement and is inert at the service until that changes.
|
|
48
|
+
*/
|
|
49
|
+
function registrationIdempotencyKey(url) {
|
|
50
|
+
return `webhook-endpoint.${(0, digest_js_1.sha256Digest)(url).replace('sha256:', '')}`;
|
|
51
|
+
}
|
|
52
|
+
const WebhookSecretSchema = zod_1.z.looseObject({
|
|
53
|
+
version: zod_1.z.number(),
|
|
54
|
+
value: zod_1.z.string(),
|
|
55
|
+
signingStartsAt: zod_1.z.string(),
|
|
56
|
+
});
|
|
57
|
+
const WebhookEndpointSchema = zod_1.z.looseObject({
|
|
58
|
+
webhookEndpointId: zod_1.z.string(),
|
|
59
|
+
url: zod_1.z.string(),
|
|
60
|
+
eventTypes: zod_1.z.array(zod_1.z.string()),
|
|
61
|
+
status: zod_1.z.string(),
|
|
62
|
+
disabledReason: zod_1.z.string().nullish(),
|
|
63
|
+
createdAt: zod_1.z.string(),
|
|
64
|
+
updatedAt: zod_1.z.string(),
|
|
65
|
+
secret: WebhookSecretSchema.optional(),
|
|
66
|
+
});
|
|
67
|
+
const RotatedWebhookSecretSchema = zod_1.z.looseObject({
|
|
68
|
+
webhookEndpointId: zod_1.z.string(),
|
|
69
|
+
secret: WebhookSecretSchema,
|
|
70
|
+
previousSecret: zod_1.z
|
|
71
|
+
.looseObject({ version: zod_1.z.number(), expiresAt: zod_1.z.string() })
|
|
72
|
+
.nullish(),
|
|
73
|
+
});
|
|
74
|
+
/**
|
|
75
|
+
* §10.2's two webhook-management routes.
|
|
76
|
+
*
|
|
77
|
+
* There is no list, no read-back and no delete, because the API serves none —
|
|
78
|
+
* §10.2 defines exactly two routes. An integrator therefore cannot enumerate
|
|
79
|
+
* what it has registered, which is a real gap in the service rather than
|
|
80
|
+
* something this client can paper over; re-registering a URL is the documented
|
|
81
|
+
* way to change a subscription or revive a disabled endpoint.
|
|
82
|
+
*
|
|
83
|
+
* Both routes need a credential with the `crowdsource:webhooks:manage` scope,
|
|
84
|
+
* and both derive the application from that credential. There is no
|
|
85
|
+
* `applicationId` in any request or response here.
|
|
86
|
+
*/
|
|
87
|
+
class WebhookEndpoints {
|
|
88
|
+
constructor(transport) {
|
|
89
|
+
this.transport = transport;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Registers, or updates, the endpoint CrowdSource delivers to.
|
|
93
|
+
*
|
|
94
|
+
* Safe to call on every deploy: an existing URL is updated and no new secret
|
|
95
|
+
* is minted, so the secret the running process verifies with keeps working.
|
|
96
|
+
*/
|
|
97
|
+
async register(input, options = {}) {
|
|
98
|
+
const response = await this.transport.request({
|
|
99
|
+
method: 'POST',
|
|
100
|
+
path: '/v1/webhook-endpoints',
|
|
101
|
+
body: { url: input.url, eventTypes: [...input.eventTypes] },
|
|
102
|
+
idempotencyKey: options.idempotencyKey ?? registrationIdempotencyKey(input.url),
|
|
103
|
+
signal: options.signal,
|
|
104
|
+
});
|
|
105
|
+
const parsed = WebhookEndpointSchema.safeParse(response);
|
|
106
|
+
if (!parsed.success) {
|
|
107
|
+
throw new errors_js_1.CrowdSourceTransportError('CrowdSource registered the webhook endpoint but answered with a body this client does not recognise.', { retryable: false, cause: parsed.error });
|
|
108
|
+
}
|
|
109
|
+
const { disabledReason, secret, ...rest } = parsed.data;
|
|
110
|
+
return {
|
|
111
|
+
...rest,
|
|
112
|
+
disabledReason: disabledReason ?? null,
|
|
113
|
+
...(secret === undefined ? {} : { secret }),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Mints a new signing secret, retiring the current one after an overlap.
|
|
118
|
+
*
|
|
119
|
+
* The only way to recover from a secret that was never stored, and the correct
|
|
120
|
+
* response to one that leaked.
|
|
121
|
+
*/
|
|
122
|
+
async rotateSecret(webhookEndpointId, options = {}) {
|
|
123
|
+
const response = await this.transport.request({
|
|
124
|
+
method: 'POST',
|
|
125
|
+
path: `/v1/webhook-endpoints/${encodeURIComponent(webhookEndpointId)}/rotate-secret`,
|
|
126
|
+
body: options.overlapSeconds === undefined ? {} : { overlapSeconds: options.overlapSeconds },
|
|
127
|
+
idempotencyKey: options.idempotencyKey ??
|
|
128
|
+
`webhook-endpoint.${(0, digest_js_1.sha256Digest)(webhookEndpointId).replace('sha256:', '')}.rotate`,
|
|
129
|
+
signal: options.signal,
|
|
130
|
+
});
|
|
131
|
+
const parsed = RotatedWebhookSecretSchema.safeParse(response);
|
|
132
|
+
if (!parsed.success) {
|
|
133
|
+
throw new errors_js_1.CrowdSourceTransportError('CrowdSource rotated the webhook secret but answered with a body this client does not recognise.', { retryable: false, cause: parsed.error });
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
webhookEndpointId: parsed.data.webhookEndpointId,
|
|
137
|
+
secret: parsed.data.secret,
|
|
138
|
+
previousSecret: parsed.data.previousSecret ?? null,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
exports.WebhookEndpoints = WebhookEndpoints;
|
|
143
|
+
//# sourceMappingURL=webhookEndpoints.js.map
|