@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,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* `@crowdsource.you/core/testing` — integrate against CrowdSource before a jury
|
|
4
|
+
* exists.
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* import { createCrowdSourceSandbox } from '@crowdsource.you/core/testing';
|
|
8
|
+
* import { CrowdSource } from '@crowdsource.you/core';
|
|
9
|
+
*
|
|
10
|
+
* const sandbox = createCrowdSourceSandbox();
|
|
11
|
+
* const crowdsource = new CrowdSource({
|
|
12
|
+
* serviceKey: sandbox.serviceKey,
|
|
13
|
+
* baseUrl: sandbox.baseUrl,
|
|
14
|
+
* fetch: sandbox.fetch,
|
|
15
|
+
* });
|
|
16
|
+
*
|
|
17
|
+
* const { caseId } = await crowdsource.reports.create({ ... });
|
|
18
|
+
* const decision = sandbox.decide(caseId, { outcome: 'violation' });
|
|
19
|
+
* await sandbox.deliver('http://localhost:3000/webhooks/crowdsource', sandbox.eventFor(decision));
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* The report goes through the real client, the sandbox applies the real rules
|
|
23
|
+
* (tenant from the credential, idempotency, 409 on a changed body, one case per
|
|
24
|
+
* reported version), and the webhook that comes back is genuinely signed — so
|
|
25
|
+
* the receiver being tested is the receiver that will run in production.
|
|
26
|
+
*
|
|
27
|
+
* The simulator can also deliver a stale, forged or tampered event on purpose.
|
|
28
|
+
* Asserting that a receiver REFUSES those is the half of a webhook test that
|
|
29
|
+
* actually proves something.
|
|
30
|
+
*/
|
|
31
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
32
|
+
exports.signWebhookDelivery = exports.WebhookSimulator = exports.createCrowdSourceSandbox = exports.CrowdSourceSandbox = exports.decisionFixture = exports.caseEnvelopeFixture = exports.caseDecidedEventFixture = void 0;
|
|
33
|
+
var fixtures_js_1 = require("./fixtures.js");
|
|
34
|
+
Object.defineProperty(exports, "caseDecidedEventFixture", { enumerable: true, get: function () { return fixtures_js_1.caseDecidedEventFixture; } });
|
|
35
|
+
Object.defineProperty(exports, "caseEnvelopeFixture", { enumerable: true, get: function () { return fixtures_js_1.caseEnvelopeFixture; } });
|
|
36
|
+
Object.defineProperty(exports, "decisionFixture", { enumerable: true, get: function () { return fixtures_js_1.decisionFixture; } });
|
|
37
|
+
var sandbox_js_1 = require("./sandbox.js");
|
|
38
|
+
Object.defineProperty(exports, "CrowdSourceSandbox", { enumerable: true, get: function () { return sandbox_js_1.CrowdSourceSandbox; } });
|
|
39
|
+
Object.defineProperty(exports, "createCrowdSourceSandbox", { enumerable: true, get: function () { return sandbox_js_1.createCrowdSourceSandbox; } });
|
|
40
|
+
var webhook_simulator_js_1 = require("./webhook-simulator.js");
|
|
41
|
+
Object.defineProperty(exports, "WebhookSimulator", { enumerable: true, get: function () { return webhook_simulator_js_1.WebhookSimulator; } });
|
|
42
|
+
Object.defineProperty(exports, "signWebhookDelivery", { enumerable: true, get: function () { return webhook_simulator_js_1.signWebhookDelivery; } });
|
|
43
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;;;AAEH,6CAIuB;AAHrB,sHAAA,uBAAuB,OAAA;AACvB,kHAAA,mBAAmB,OAAA;AACnB,8GAAA,eAAe,OAAA;AAQjB,2CAA4E;AAAnE,gHAAA,kBAAkB,OAAA;AAAE,sHAAA,wBAAwB,OAAA;AAQrD,+DAA+E;AAAtE,wHAAA,gBAAgB,OAAA;AAAE,2HAAA,mBAAmB,OAAA"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An in-process CrowdSource, so an integration can be exercised end to end
|
|
3
|
+
* before a jury exists.
|
|
4
|
+
*
|
|
5
|
+
* The path an application actually cares about is: deliver a report → get a
|
|
6
|
+
* case → a decision is published → a signed webhook arrives → enforcement runs.
|
|
7
|
+
* Today the middle of that does not exist anywhere — sortition, review and
|
|
8
|
+
* consensus are not built, so nothing publishes a decision. Waiting for them
|
|
9
|
+
* means an integrator cannot write, or test, the half of their code that
|
|
10
|
+
* matters most: what they DO when a decision says `violation`.
|
|
11
|
+
*
|
|
12
|
+
* So this sandbox implements the two ends and lets the test drive the middle.
|
|
13
|
+
* It accepts reports over a `fetch` the real `@crowdsource.you/core` client can be
|
|
14
|
+
* pointed at — same transport, same idempotency, same 409, same tenant check —
|
|
15
|
+
* and `decide()` publishes a decision the way consensus eventually will, which
|
|
16
|
+
* `deliver()` then sends as a genuinely signed `case.decided`.
|
|
17
|
+
*
|
|
18
|
+
* **What it is and is not.** It is a faithful implementation of the RULES an
|
|
19
|
+
* integrator's code depends on: `applicationId` from the credential, an
|
|
20
|
+
* idempotency key that returns the same `reportId`, a 409 for a reused
|
|
21
|
+
* `externalReportId` with a changed body, and §7.3's "two reports about the same
|
|
22
|
+
* version of the same content are one case". It is NOT the service, it holds no
|
|
23
|
+
* state between processes, and where it and the backend ever disagree the
|
|
24
|
+
* backend is right. The deduplication projection below mirrors the backend's
|
|
25
|
+
* `modules/evidence/contentSnapshot.ts`; that logic belongs in the contracts
|
|
26
|
+
* package so both sides share one implementation, and until it moves this file
|
|
27
|
+
* is the second copy — which is a thing to know rather than a thing to rely on.
|
|
28
|
+
*/
|
|
29
|
+
import { type CaseEnvelope, type Decision, type DecisionFinding, type DecisionOutcome, type DecisionRecommendedAction, type KnownWebhookEvent, type TaxonomyCode } from '@crowdsource.you/contracts';
|
|
30
|
+
import { type WebhookDeliveryResult } from './webhook-simulator.js';
|
|
31
|
+
export interface SandboxReport {
|
|
32
|
+
readonly reportId: string;
|
|
33
|
+
readonly externalReportId: string;
|
|
34
|
+
readonly caseId: string;
|
|
35
|
+
readonly idempotencyKey: string;
|
|
36
|
+
readonly envelope: CaseEnvelope;
|
|
37
|
+
/** `merged` when this report joined a case another report had already opened. */
|
|
38
|
+
readonly status: 'received' | 'merged';
|
|
39
|
+
readonly receivedAt: string;
|
|
40
|
+
/**
|
|
41
|
+
* The fingerprint of what was delivered — the backend calls it `payloadHash`
|
|
42
|
+
* and computes it over `{ externalReportId, envelope }`, deliberately without
|
|
43
|
+
* the idempotency key, so the same content re-sent under a fresh key is
|
|
44
|
+
* recognised as the same report rather than conflicting with it.
|
|
45
|
+
*/
|
|
46
|
+
readonly payloadHash: string;
|
|
47
|
+
}
|
|
48
|
+
export interface SandboxCase {
|
|
49
|
+
readonly caseId: string;
|
|
50
|
+
readonly externalSubjectId: string;
|
|
51
|
+
readonly dedupKey: string;
|
|
52
|
+
readonly policy: CaseEnvelope['policy'];
|
|
53
|
+
/** The union of what every merged report alleged (§6.2). */
|
|
54
|
+
readonly allegationCodes: readonly TaxonomyCode[];
|
|
55
|
+
readonly reportIds: readonly string[];
|
|
56
|
+
readonly decisions: readonly Decision[];
|
|
57
|
+
}
|
|
58
|
+
export interface CrowdSourceSandboxOptions {
|
|
59
|
+
readonly applicationId?: string;
|
|
60
|
+
readonly organizationId?: string;
|
|
61
|
+
readonly webhookSecret?: string;
|
|
62
|
+
/** The origin the client believes it is calling. Never reached over a socket. */
|
|
63
|
+
readonly baseUrl?: string;
|
|
64
|
+
}
|
|
65
|
+
export interface SandboxDecisionInput {
|
|
66
|
+
readonly outcome?: DecisionOutcome;
|
|
67
|
+
readonly status?: Decision['status'];
|
|
68
|
+
readonly findings?: readonly DecisionFinding[];
|
|
69
|
+
readonly recommendedActions?: readonly DecisionRecommendedAction[];
|
|
70
|
+
readonly confidence?: number;
|
|
71
|
+
}
|
|
72
|
+
export declare class CrowdSourceSandbox {
|
|
73
|
+
readonly applicationId: string;
|
|
74
|
+
readonly organizationId: string;
|
|
75
|
+
readonly baseUrl: string;
|
|
76
|
+
readonly webhookSecret: string;
|
|
77
|
+
/** The one opaque string an integration configures. */
|
|
78
|
+
readonly serviceKey: string;
|
|
79
|
+
private readonly bearerToken;
|
|
80
|
+
private readonly reportsById;
|
|
81
|
+
private readonly reportsByIdempotencyKey;
|
|
82
|
+
private readonly reportsByExternalId;
|
|
83
|
+
private readonly casesById;
|
|
84
|
+
private readonly caseIdByDedupKey;
|
|
85
|
+
private readonly decisionsById;
|
|
86
|
+
constructor(options?: CrowdSourceSandboxOptions);
|
|
87
|
+
get reports(): readonly SandboxReport[];
|
|
88
|
+
get cases(): readonly SandboxCase[];
|
|
89
|
+
/** The `fetch` to hand `new CrowdSource({ fetch, serviceKey, baseUrl })`. */
|
|
90
|
+
readonly fetch: typeof globalThis.fetch;
|
|
91
|
+
/**
|
|
92
|
+
* Publishes a decision for a case, the way consensus eventually will.
|
|
93
|
+
*
|
|
94
|
+
* A published revision is immutable (Appendix F), so calling this twice
|
|
95
|
+
* produces revision 2 superseding revision 1 rather than editing anything.
|
|
96
|
+
*/
|
|
97
|
+
decide(caseId: string, input?: SandboxDecisionInput): Decision;
|
|
98
|
+
/** The `case.decided` (or `decision.corrected`) event for a decision. */
|
|
99
|
+
eventFor(decision: Decision): KnownWebhookEvent;
|
|
100
|
+
/** Signs and POSTs an event to the integration's webhook endpoint. */
|
|
101
|
+
deliver(url: string, event: unknown): Promise<WebhookDeliveryResult>;
|
|
102
|
+
private createReport;
|
|
103
|
+
private receiptResponse;
|
|
104
|
+
private readReport;
|
|
105
|
+
private readCase;
|
|
106
|
+
private readDecision;
|
|
107
|
+
}
|
|
108
|
+
export declare function createCrowdSourceSandbox(options?: CrowdSourceSandboxOptions): CrowdSourceSandbox;
|
|
109
|
+
//# sourceMappingURL=sandbox.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sandbox.d.ts","sourceRoot":"","sources":["../../src/testing/sandbox.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* An in-process CrowdSource, so an integration can be exercised end to end
|
|
4
|
+
* before a jury exists.
|
|
5
|
+
*
|
|
6
|
+
* The path an application actually cares about is: deliver a report → get a
|
|
7
|
+
* case → a decision is published → a signed webhook arrives → enforcement runs.
|
|
8
|
+
* Today the middle of that does not exist anywhere — sortition, review and
|
|
9
|
+
* consensus are not built, so nothing publishes a decision. Waiting for them
|
|
10
|
+
* means an integrator cannot write, or test, the half of their code that
|
|
11
|
+
* matters most: what they DO when a decision says `violation`.
|
|
12
|
+
*
|
|
13
|
+
* So this sandbox implements the two ends and lets the test drive the middle.
|
|
14
|
+
* It accepts reports over a `fetch` the real `@crowdsource.you/core` client can be
|
|
15
|
+
* pointed at — same transport, same idempotency, same 409, same tenant check —
|
|
16
|
+
* and `decide()` publishes a decision the way consensus eventually will, which
|
|
17
|
+
* `deliver()` then sends as a genuinely signed `case.decided`.
|
|
18
|
+
*
|
|
19
|
+
* **What it is and is not.** It is a faithful implementation of the RULES an
|
|
20
|
+
* integrator's code depends on: `applicationId` from the credential, an
|
|
21
|
+
* idempotency key that returns the same `reportId`, a 409 for a reused
|
|
22
|
+
* `externalReportId` with a changed body, and §7.3's "two reports about the same
|
|
23
|
+
* version of the same content are one case". It is NOT the service, it holds no
|
|
24
|
+
* state between processes, and where it and the backend ever disagree the
|
|
25
|
+
* backend is right. The deduplication projection below mirrors the backend's
|
|
26
|
+
* `modules/evidence/contentSnapshot.ts`; that logic belongs in the contracts
|
|
27
|
+
* package so both sides share one implementation, and until it moves this file
|
|
28
|
+
* is the second copy — which is a thing to know rather than a thing to rely on.
|
|
29
|
+
*/
|
|
30
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
31
|
+
exports.CrowdSourceSandbox = void 0;
|
|
32
|
+
exports.createCrowdSourceSandbox = createCrowdSourceSandbox;
|
|
33
|
+
const node_crypto_1 = require("node:crypto");
|
|
34
|
+
const contracts_1 = require("@crowdsource.you/contracts");
|
|
35
|
+
const webhook_simulator_js_1 = require("./webhook-simulator.js");
|
|
36
|
+
const DEFAULT_BASE_URL = 'https://sandbox.crowdsource.test';
|
|
37
|
+
function publicId(prefix) {
|
|
38
|
+
return `${prefix}_${(0, node_crypto_1.randomUUID)().replace(/-/g, '')}`;
|
|
39
|
+
}
|
|
40
|
+
function sha256Hex(value) {
|
|
41
|
+
return (0, node_crypto_1.createHash)('sha256').update(value, 'utf8').digest('hex');
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* §7.3's `contentEnvelopeHash`, over the same projection the backend hashes.
|
|
45
|
+
*
|
|
46
|
+
* The three normalisations that make two reporters agree, restated so a reader
|
|
47
|
+
* does not have to open the backend to know what is excluded: order is removed
|
|
48
|
+
* where it is not meaning, proof-of-delivery fields are dropped from principal
|
|
49
|
+
* bindings, and nothing per-reporter is included at all — no
|
|
50
|
+
* `externalReportId`, no `source`, no `allegations`, no reporter binding, no
|
|
51
|
+
* `urgency`, no `metadata`.
|
|
52
|
+
*/
|
|
53
|
+
function contentHashOf(envelope) {
|
|
54
|
+
const referencedPrincipals = new Set();
|
|
55
|
+
for (const resource of envelope.resources) {
|
|
56
|
+
if (resource.authorPrincipalRef !== undefined) {
|
|
57
|
+
referencedPrincipals.add(resource.authorPrincipalRef);
|
|
58
|
+
}
|
|
59
|
+
if (resource.type === 'listing' && resource.data.sellerRef !== undefined) {
|
|
60
|
+
referencedPrincipals.add(resource.data.sellerRef);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
for (const relation of envelope.relations) {
|
|
64
|
+
if (contracts_1.PRINCIPAL_TARGETED_RELATION_TYPES.some((type) => type === relation.type)) {
|
|
65
|
+
referencedPrincipals.add(relation.to);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const snapshot = {
|
|
69
|
+
schemaVersion: envelope.schemaVersion,
|
|
70
|
+
subject: envelope.subject,
|
|
71
|
+
resources: [...envelope.resources].sort((left, right) => (left.id < right.id ? -1 : 1)),
|
|
72
|
+
relations: [...envelope.relations].sort((left, right) => `${left.from} ${left.type} ${left.to}` < `${right.from} ${right.type} ${right.to}` ? -1 : 1),
|
|
73
|
+
principals: envelope.principalBindings
|
|
74
|
+
.filter((binding) => referencedPrincipals.has(binding.principalRef))
|
|
75
|
+
.sort((left, right) => (left.principalRef < right.principalRef ? -1 : 1))
|
|
76
|
+
.map((binding) => ({
|
|
77
|
+
principalRef: binding.principalRef,
|
|
78
|
+
type: binding.type,
|
|
79
|
+
externalPrincipalId: binding.externalPrincipalId,
|
|
80
|
+
})),
|
|
81
|
+
};
|
|
82
|
+
return sha256Hex(JSON.stringify(snapshot));
|
|
83
|
+
}
|
|
84
|
+
function jsonResponse(status, body) {
|
|
85
|
+
return new Response(JSON.stringify(body), {
|
|
86
|
+
status,
|
|
87
|
+
headers: { 'content-type': 'application/json' },
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
function apiError(status, code, message) {
|
|
91
|
+
return jsonResponse(status, { error: { code, message } });
|
|
92
|
+
}
|
|
93
|
+
class CrowdSourceSandbox {
|
|
94
|
+
constructor(options = {}) {
|
|
95
|
+
this.reportsById = new Map();
|
|
96
|
+
this.reportsByIdempotencyKey = new Map();
|
|
97
|
+
this.reportsByExternalId = new Map();
|
|
98
|
+
this.casesById = new Map();
|
|
99
|
+
this.caseIdByDedupKey = new Map();
|
|
100
|
+
this.decisionsById = new Map();
|
|
101
|
+
/** The `fetch` to hand `new CrowdSource({ fetch, serviceKey, baseUrl })`. */
|
|
102
|
+
this.fetch = async (input, init) => {
|
|
103
|
+
const url = new URL(typeof input === 'string' ? input : input instanceof URL ? input.href : input.url);
|
|
104
|
+
const method = (init?.method ?? 'GET').toUpperCase();
|
|
105
|
+
const headers = new Headers(init?.headers);
|
|
106
|
+
if (headers.get('authorization') !== `Bearer ${this.bearerToken}`) {
|
|
107
|
+
return apiError(401, 'unauthorized', 'The service credential is missing or invalid.');
|
|
108
|
+
}
|
|
109
|
+
if (method === 'POST' && url.pathname === '/v1/reports') {
|
|
110
|
+
return this.createReport(headers, init?.body);
|
|
111
|
+
}
|
|
112
|
+
if (method === 'GET' && url.pathname.startsWith('/v1/reports/')) {
|
|
113
|
+
return this.readReport(decodeURIComponent(url.pathname.slice('/v1/reports/'.length)));
|
|
114
|
+
}
|
|
115
|
+
if (method === 'GET' && url.pathname.startsWith('/v1/cases/')) {
|
|
116
|
+
return this.readCase(decodeURIComponent(url.pathname.slice('/v1/cases/'.length)));
|
|
117
|
+
}
|
|
118
|
+
if (method === 'GET' && url.pathname.startsWith('/v1/decisions/')) {
|
|
119
|
+
return this.readDecision(decodeURIComponent(url.pathname.slice('/v1/decisions/'.length)));
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Everything else, including the upload routes, answers 404 — the same thing
|
|
123
|
+
* the deployed backend does today. A sandbox that faked an endpoint the
|
|
124
|
+
* service does not serve would let an integration pass its tests and fail on
|
|
125
|
+
* its first real call.
|
|
126
|
+
*/
|
|
127
|
+
return apiError(404, 'not_found', `The sandbox does not serve ${method} ${url.pathname}.`);
|
|
128
|
+
};
|
|
129
|
+
this.applicationId = options.applicationId ?? publicId('app');
|
|
130
|
+
this.organizationId = options.organizationId ?? publicId('org');
|
|
131
|
+
this.baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
|
|
132
|
+
this.webhookSecret = options.webhookSecret ?? (0, node_crypto_1.randomUUID)();
|
|
133
|
+
const credentialId = publicId('csk');
|
|
134
|
+
const secret = (0, node_crypto_1.randomUUID)().replace(/-/g, '');
|
|
135
|
+
this.bearerToken = `${credentialId}.${secret}`;
|
|
136
|
+
this.serviceKey = [this.applicationId, credentialId, secret].join(':');
|
|
137
|
+
}
|
|
138
|
+
get reports() {
|
|
139
|
+
return [...this.reportsById.values()];
|
|
140
|
+
}
|
|
141
|
+
get cases() {
|
|
142
|
+
return [...this.casesById.values()];
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Publishes a decision for a case, the way consensus eventually will.
|
|
146
|
+
*
|
|
147
|
+
* A published revision is immutable (Appendix F), so calling this twice
|
|
148
|
+
* produces revision 2 superseding revision 1 rather than editing anything.
|
|
149
|
+
*/
|
|
150
|
+
decide(caseId, input = {}) {
|
|
151
|
+
const stored = this.casesById.get(caseId);
|
|
152
|
+
if (!stored)
|
|
153
|
+
throw new Error(`The sandbox holds no case '${caseId}'.`);
|
|
154
|
+
const previous = stored.decisions.at(-1);
|
|
155
|
+
const revision = (previous?.revision ?? 0) + 1;
|
|
156
|
+
const outcome = input.outcome ?? 'violation';
|
|
157
|
+
const decision = contracts_1.DecisionSchema.parse({
|
|
158
|
+
id: publicId('dec'),
|
|
159
|
+
caseId,
|
|
160
|
+
revision,
|
|
161
|
+
status: input.status ?? 'final',
|
|
162
|
+
outcome,
|
|
163
|
+
contextSufficiency: outcome === 'insufficient_context' ? 'insufficient' : 'sufficient',
|
|
164
|
+
confidence: input.confidence ?? 1,
|
|
165
|
+
findings: input.findings ??
|
|
166
|
+
(outcome === 'violation'
|
|
167
|
+
? [
|
|
168
|
+
{
|
|
169
|
+
code: stored.allegationCodes[0] ?? 'other.unclassifiable',
|
|
170
|
+
resourceIds: ['res_subject'],
|
|
171
|
+
severity: 'medium',
|
|
172
|
+
scope: 'application_local',
|
|
173
|
+
attribution: 'author',
|
|
174
|
+
},
|
|
175
|
+
]
|
|
176
|
+
: []),
|
|
177
|
+
recommendedActions: input.recommendedActions ??
|
|
178
|
+
(outcome === 'violation' ? [{ action: 'remove_or_restrict' }] : [{ action: 'no_action' }]),
|
|
179
|
+
jury: { size: 3, decisiveVotes: 3, winningVotes: 3, agreement: 1, specialistPresent: false },
|
|
180
|
+
policyVersions: {
|
|
181
|
+
taxonomy: contracts_1.UNIVERSAL_TAXONOMY_VERSION,
|
|
182
|
+
application: stored.policy.version,
|
|
183
|
+
oxyConduct: stored.policy.version,
|
|
184
|
+
},
|
|
185
|
+
...(previous === undefined ? {} : { supersedesDecisionId: previous.id }),
|
|
186
|
+
publishedAt: new Date().toISOString(),
|
|
187
|
+
});
|
|
188
|
+
this.decisionsById.set(decision.id, decision);
|
|
189
|
+
this.casesById.set(caseId, { ...stored, decisions: [...stored.decisions, decision] });
|
|
190
|
+
return decision;
|
|
191
|
+
}
|
|
192
|
+
/** The `case.decided` (or `decision.corrected`) event for a decision. */
|
|
193
|
+
eventFor(decision) {
|
|
194
|
+
return contracts_1.KnownWebhookEventSchema.parse({
|
|
195
|
+
id: publicId('evt'),
|
|
196
|
+
type: decision.revision === 1 ? 'case.decided' : 'decision.corrected',
|
|
197
|
+
createdAt: new Date().toISOString(),
|
|
198
|
+
organizationId: this.organizationId,
|
|
199
|
+
applicationId: this.applicationId,
|
|
200
|
+
data: { caseId: decision.caseId, decision },
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
/** Signs and POSTs an event to the integration's webhook endpoint. */
|
|
204
|
+
async deliver(url, event) {
|
|
205
|
+
return await new webhook_simulator_js_1.WebhookSimulator({ secret: this.webhookSecret, url }).deliver(event);
|
|
206
|
+
}
|
|
207
|
+
createReport(headers, body) {
|
|
208
|
+
const idempotencyKey = headers.get('idempotency-key');
|
|
209
|
+
if (idempotencyKey === null || idempotencyKey.length === 0) {
|
|
210
|
+
return apiError(400, 'invalid_request', 'The Idempotency-Key header is required.');
|
|
211
|
+
}
|
|
212
|
+
if (typeof body !== 'string') {
|
|
213
|
+
return apiError(400, 'invalid_request', 'The request body must be JSON.');
|
|
214
|
+
}
|
|
215
|
+
let payload;
|
|
216
|
+
try {
|
|
217
|
+
payload = JSON.parse(body);
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
return apiError(400, 'invalid_request', 'The request body must be JSON.');
|
|
221
|
+
}
|
|
222
|
+
const parsed = contracts_1.CreateReportRequestSchema.safeParse(payload);
|
|
223
|
+
if (!parsed.success) {
|
|
224
|
+
return apiError(422, 'unprocessable_envelope', `The envelope cannot be processed — ${parsed.error.issues
|
|
225
|
+
.map((issue) => `${issue.path.join('.') || '(root)'}: ${issue.message}`)
|
|
226
|
+
.join('; ')}`);
|
|
227
|
+
}
|
|
228
|
+
const { externalReportId, envelope } = parsed.data;
|
|
229
|
+
if (envelope.applicationId !== this.applicationId) {
|
|
230
|
+
return apiError(403, 'forbidden', 'The envelope names a different applicationId than the credential it was delivered with.');
|
|
231
|
+
}
|
|
232
|
+
const payloadHash = sha256Hex(JSON.stringify(parsed.data));
|
|
233
|
+
/**
|
|
234
|
+
* Appendix D, both halves. The same key with the same payload is the SAME
|
|
235
|
+
* report and returns the same `reportId`; the same `externalReportId` with a
|
|
236
|
+
* DIFFERENT payload is §10.5's 409 — the two cannot both be that report, and
|
|
237
|
+
* no retry resolves it.
|
|
238
|
+
*/
|
|
239
|
+
const byKey = this.reportsByIdempotencyKey.get(idempotencyKey);
|
|
240
|
+
if (byKey !== undefined) {
|
|
241
|
+
if (byKey.payloadHash !== payloadHash) {
|
|
242
|
+
return apiError(409, 'conflict', 'This Idempotency-Key was already used with a different payload.');
|
|
243
|
+
}
|
|
244
|
+
return this.receiptResponse(byKey);
|
|
245
|
+
}
|
|
246
|
+
const byExternalId = this.reportsByExternalId.get(externalReportId);
|
|
247
|
+
if (byExternalId !== undefined) {
|
|
248
|
+
if (byExternalId.payloadHash !== payloadHash) {
|
|
249
|
+
return apiError(409, 'conflict', `externalReportId '${externalReportId}' was already delivered with different content.`);
|
|
250
|
+
}
|
|
251
|
+
return this.receiptResponse(byExternalId);
|
|
252
|
+
}
|
|
253
|
+
const dedupKey = sha256Hex([
|
|
254
|
+
this.applicationId,
|
|
255
|
+
envelope.subject.externalId,
|
|
256
|
+
contentHashOf(envelope),
|
|
257
|
+
`${envelope.policy.policySetId}@${envelope.policy.version}`,
|
|
258
|
+
].join(':'));
|
|
259
|
+
const existingCaseId = this.caseIdByDedupKey.get(dedupKey);
|
|
260
|
+
const merged = existingCaseId !== undefined;
|
|
261
|
+
const caseId = existingCaseId ?? publicId('case');
|
|
262
|
+
const report = {
|
|
263
|
+
reportId: publicId('rpt'),
|
|
264
|
+
externalReportId,
|
|
265
|
+
caseId,
|
|
266
|
+
idempotencyKey,
|
|
267
|
+
envelope,
|
|
268
|
+
status: merged ? 'merged' : 'received',
|
|
269
|
+
receivedAt: new Date().toISOString(),
|
|
270
|
+
payloadHash,
|
|
271
|
+
};
|
|
272
|
+
this.reportsById.set(report.reportId, report);
|
|
273
|
+
this.reportsByIdempotencyKey.set(idempotencyKey, report);
|
|
274
|
+
this.reportsByExternalId.set(externalReportId, report);
|
|
275
|
+
const existingCase = this.casesById.get(caseId);
|
|
276
|
+
this.casesById.set(caseId, {
|
|
277
|
+
caseId,
|
|
278
|
+
externalSubjectId: envelope.subject.externalId,
|
|
279
|
+
dedupKey,
|
|
280
|
+
policy: envelope.policy,
|
|
281
|
+
allegationCodes: [
|
|
282
|
+
...new Set([
|
|
283
|
+
...(existingCase?.allegationCodes ?? []),
|
|
284
|
+
...envelope.allegations.map((allegation) => allegation.code),
|
|
285
|
+
]),
|
|
286
|
+
],
|
|
287
|
+
reportIds: [...(existingCase?.reportIds ?? []), report.reportId],
|
|
288
|
+
decisions: existingCase?.decisions ?? [],
|
|
289
|
+
});
|
|
290
|
+
this.caseIdByDedupKey.set(dedupKey, caseId);
|
|
291
|
+
return this.receiptResponse(report);
|
|
292
|
+
}
|
|
293
|
+
receiptResponse(report) {
|
|
294
|
+
return jsonResponse(202, {
|
|
295
|
+
reportId: report.reportId,
|
|
296
|
+
caseId: report.caseId,
|
|
297
|
+
status: report.status,
|
|
298
|
+
merged: report.status === 'merged',
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
readReport(reportId) {
|
|
302
|
+
const report = this.reportsById.get(reportId);
|
|
303
|
+
if (!report)
|
|
304
|
+
return apiError(404, 'not_found', 'No such report.');
|
|
305
|
+
return jsonResponse(200, {
|
|
306
|
+
reportId: report.reportId,
|
|
307
|
+
externalReportId: report.externalReportId,
|
|
308
|
+
caseId: report.caseId,
|
|
309
|
+
status: report.status,
|
|
310
|
+
receivedAt: report.receivedAt,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
readCase(caseId) {
|
|
314
|
+
const stored = this.casesById.get(caseId);
|
|
315
|
+
if (!stored)
|
|
316
|
+
return apiError(404, 'not_found', 'No such case.');
|
|
317
|
+
return jsonResponse(200, {
|
|
318
|
+
caseId: stored.caseId,
|
|
319
|
+
status: stored.decisions.length > 0 ? 'decided' : 'awaiting_review',
|
|
320
|
+
subject: { externalId: stored.externalSubjectId, type: 'social.post' },
|
|
321
|
+
policy: stored.policy,
|
|
322
|
+
taxonomyVersion: contracts_1.UNIVERSAL_TAXONOMY_VERSION,
|
|
323
|
+
allegationCodes: stored.allegationCodes,
|
|
324
|
+
reportCount: stored.reportIds.length,
|
|
325
|
+
sensitivityClass: 'standard',
|
|
326
|
+
currentRevision: stored.decisions.length,
|
|
327
|
+
createdAt: new Date(0).toISOString(),
|
|
328
|
+
updatedAt: new Date().toISOString(),
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
readDecision(decisionId) {
|
|
332
|
+
const decision = this.decisionsById.get(decisionId);
|
|
333
|
+
if (!decision)
|
|
334
|
+
return apiError(404, 'not_found', 'No such decision.');
|
|
335
|
+
return jsonResponse(200, decision);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
exports.CrowdSourceSandbox = CrowdSourceSandbox;
|
|
339
|
+
function createCrowdSourceSandbox(options = {}) {
|
|
340
|
+
return new CrowdSourceSandbox(options);
|
|
341
|
+
}
|
|
342
|
+
//# sourceMappingURL=sandbox.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sandbox.js","sourceRoot":"","sources":["../../src/testing/sandbox.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The webhook simulator — the producer side of §10.8.
|
|
3
|
+
*
|
|
4
|
+
* An integrator can point this at their own receiver and watch a real delivery
|
|
5
|
+
* arrive, signed exactly as CrowdSource signs one, without a jury having sat and
|
|
6
|
+
* without anything being enforced against a real user.
|
|
7
|
+
*
|
|
8
|
+
* It deliberately also makes it easy to send a delivery that is WRONG:
|
|
9
|
+
* `expired`, `wrongSecret`, `tampered` and a raw `signature` override are here
|
|
10
|
+
* so an integration test can assert that the receiver REFUSES those. A test
|
|
11
|
+
* suite that only ever sends valid deliveries proves the receiver can say yes.
|
|
12
|
+
* Proving it can say no is the part that matters, and it is the part nobody
|
|
13
|
+
* writes unless the tooling makes it a one-liner.
|
|
14
|
+
*
|
|
15
|
+
* Signing lives here rather than in `@crowdsource.you/core/express` because the two
|
|
16
|
+
* are opposite sides of the contract: the verifier must never import a signer
|
|
17
|
+
* that could be "corrected" until they agree with each other and both disagree
|
|
18
|
+
* with the service. What they share instead is
|
|
19
|
+
* `buildWebhookSignedPayload` from the contracts package, which is the one
|
|
20
|
+
* definition of what gets signed.
|
|
21
|
+
*/
|
|
22
|
+
export interface SignedWebhookDelivery {
|
|
23
|
+
/** Headers as CrowdSource sends them, plus `content-type`. */
|
|
24
|
+
readonly headers: Readonly<Record<string, string>>;
|
|
25
|
+
/** The exact bytes that were signed. Send these, not a re-serialisation. */
|
|
26
|
+
readonly body: string;
|
|
27
|
+
}
|
|
28
|
+
export interface SignWebhookInput {
|
|
29
|
+
readonly secret: string;
|
|
30
|
+
/**
|
|
31
|
+
* The event to deliver. Typed as `unknown` on purpose: a simulator that only
|
|
32
|
+
* accepted `KnownWebhookEvent` could not send an event type from a future
|
|
33
|
+
* version of the service, which is precisely the case §10.11 asks every
|
|
34
|
+
* receiver to survive.
|
|
35
|
+
*/
|
|
36
|
+
readonly event: unknown;
|
|
37
|
+
/** Unix seconds. Defaults to now. */
|
|
38
|
+
readonly timestampSeconds?: number;
|
|
39
|
+
/** Overrides the body sent, WITHOUT re-signing it. For tamper tests. */
|
|
40
|
+
readonly tamperedBody?: string;
|
|
41
|
+
/** Overrides the signature header verbatim. For forgery tests. */
|
|
42
|
+
readonly signature?: string;
|
|
43
|
+
/** Overrides the event id header, to test header/body disagreement. */
|
|
44
|
+
readonly eventId?: string;
|
|
45
|
+
}
|
|
46
|
+
/** Signs one delivery. */
|
|
47
|
+
export declare function signWebhookDelivery(input: SignWebhookInput): SignedWebhookDelivery;
|
|
48
|
+
export interface WebhookDeliveryResult {
|
|
49
|
+
readonly status: number;
|
|
50
|
+
readonly body: unknown;
|
|
51
|
+
}
|
|
52
|
+
export interface WebhookSimulatorOptions {
|
|
53
|
+
readonly secret: string;
|
|
54
|
+
/** The receiver's webhook URL. */
|
|
55
|
+
readonly url: string;
|
|
56
|
+
readonly fetch?: typeof globalThis.fetch;
|
|
57
|
+
}
|
|
58
|
+
/** How a delivery should be wrong, for a test that asserts a refusal. */
|
|
59
|
+
export interface WebhookDeliveryOverrides {
|
|
60
|
+
/** Signs with a timestamp far outside §10.8's five-minute window. */
|
|
61
|
+
readonly expired?: boolean;
|
|
62
|
+
/** Signs with a secret the receiver does not hold. */
|
|
63
|
+
readonly wrongSecret?: string;
|
|
64
|
+
/** Sends a body different from the one that was signed. */
|
|
65
|
+
readonly tamperedBody?: string;
|
|
66
|
+
readonly signature?: string;
|
|
67
|
+
readonly eventId?: string;
|
|
68
|
+
readonly timestampSeconds?: number;
|
|
69
|
+
}
|
|
70
|
+
export declare class WebhookSimulator {
|
|
71
|
+
private readonly options;
|
|
72
|
+
constructor(options: WebhookSimulatorOptions);
|
|
73
|
+
/** Signs a delivery without sending it. */
|
|
74
|
+
sign(event: unknown, overrides?: WebhookDeliveryOverrides): SignedWebhookDelivery;
|
|
75
|
+
/** Signs and POSTs a delivery to the receiver. */
|
|
76
|
+
deliver(event: unknown, overrides?: WebhookDeliveryOverrides): Promise<WebhookDeliveryResult>;
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=webhook-simulator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhook-simulator.d.ts","sourceRoot":"","sources":["../../src/testing/webhook-simulator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAYH,MAAM,WAAW,qBAAqB;IACpC,8DAA8D;IAC9D,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACnD,4EAA4E;IAC5E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB;;;;;OAKG;IACH,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,qCAAqC;IACrC,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC,wEAAwE;IACxE,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,kEAAkE;IAClE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,uEAAuE;IACvE,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAUD,0BAA0B;AAC1B,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,gBAAgB,GAAG,qBAAqB,CAsBlF;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,kCAAkC;IAClC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;CAC1C;AAED,yEAAyE;AACzE,MAAM,WAAW,wBAAwB;IACvC,qEAAqE;IACrE,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,sDAAsD;IACtD,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,2DAA2D;IAC3D,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;CACpC;AAKD,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA0B;gBAEtC,OAAO,EAAE,uBAAuB;IAI5C,2CAA2C;IAC3C,IAAI,CAAC,KAAK,EAAE,OAAO,EAAE,SAAS,GAAE,wBAA6B,GAAG,qBAAqB;IAarF,kDAAkD;IAC5C,OAAO,CACX,KAAK,EAAE,OAAO,EACd,SAAS,GAAE,wBAA6B,GACvC,OAAO,CAAC,qBAAqB,CAAC;CAqBlC"}
|