@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
package/src/index.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@crowdsource.you/core` — the TypeScript client for the CrowdSource moderation
|
|
3
|
+
* API.
|
|
4
|
+
*
|
|
5
|
+
* The smallest integration this package supports, in full:
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { CrowdSource } from '@crowdsource.you/core';
|
|
9
|
+
*
|
|
10
|
+
* const crowdsource = new CrowdSource();
|
|
11
|
+
*
|
|
12
|
+
* await crowdsource.reports.create({
|
|
13
|
+
* externalReportId: report.id,
|
|
14
|
+
* reportedBy: { oxyUserId: session.sub },
|
|
15
|
+
* subject: {
|
|
16
|
+
* externalId: post.id,
|
|
17
|
+
* type: 'social.post',
|
|
18
|
+
* author: { oxyUserId: post.authorId },
|
|
19
|
+
* },
|
|
20
|
+
* content: post.text,
|
|
21
|
+
* allegations: ['harassment.targeted_abuse'],
|
|
22
|
+
* });
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* One environment variable (`CROWDSOURCE_SERVICE_KEY`) and the object being
|
|
26
|
+
* reported. The Case Envelope, its resource ids, its digests, its relations, its
|
|
27
|
+
* principal bindings, the identity binding proof, the policy version, the
|
|
28
|
+
* retention terms and the idempotency key are all composed from that — see
|
|
29
|
+
* `envelope.ts` for what each one is derived from and why it cannot be left to
|
|
30
|
+
* the caller.
|
|
31
|
+
*
|
|
32
|
+
* Types come from `@crowdsource.you/contracts`. This package re-exports none
|
|
33
|
+
* of them: an integrator imports the contract from the contract package, so a
|
|
34
|
+
* `Decision` or a `TaxonomyCode` has exactly one definition.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
export { CrowdSource, SERVICE_KEY_ENV_VAR, BASE_URL_ENV_VAR } from './client.js';
|
|
38
|
+
export type { CrowdSourceOptions } from './client.js';
|
|
39
|
+
|
|
40
|
+
export { formatServiceKey, parseServiceKey } from './credential.js';
|
|
41
|
+
export type { ServiceCredential } from './credential.js';
|
|
42
|
+
|
|
43
|
+
export {
|
|
44
|
+
COMMUNITY_REVIEW_FORBIDDEN_ALLEGATIONS,
|
|
45
|
+
DEFAULT_BASE_URL,
|
|
46
|
+
DEFAULT_POLICY,
|
|
47
|
+
DEFAULT_RETENTION_DAYS,
|
|
48
|
+
} from './defaults.js';
|
|
49
|
+
|
|
50
|
+
export { canonicalJson, resourceDigest, sha256Digest } from './digest.js';
|
|
51
|
+
export type { CanonicalValue } from './digest.js';
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* `composeCaseEnvelope` is deliberately NOT exported.
|
|
55
|
+
*
|
|
56
|
+
* It is the one function in this package that takes an `applicationId`, and it
|
|
57
|
+
* takes it from `Reports`, which got it from the credential. Exporting it would
|
|
58
|
+
* put an `applicationId` parameter on the public surface — which is the thing
|
|
59
|
+
* Appendix F says must not exist, however carefully the doc comment above it
|
|
60
|
+
* were worded. The envelope a report produces is reachable by reading what
|
|
61
|
+
* `reports.create` sends, not by building one yourself.
|
|
62
|
+
*/
|
|
63
|
+
export { CrowdSourceReportInputError, defaultIdempotencyKey } from './envelope.js';
|
|
64
|
+
export type {
|
|
65
|
+
AllegationInput,
|
|
66
|
+
ContextInput,
|
|
67
|
+
ContextRole,
|
|
68
|
+
PrincipalInput,
|
|
69
|
+
ReportInput,
|
|
70
|
+
ReportSubjectInput,
|
|
71
|
+
ResourceInput,
|
|
72
|
+
} from './envelope.js';
|
|
73
|
+
|
|
74
|
+
export {
|
|
75
|
+
CROWDSOURCE_API_ERROR_CODES,
|
|
76
|
+
CrowdSourceApiError,
|
|
77
|
+
CrowdSourceConfigurationError,
|
|
78
|
+
CrowdSourceError,
|
|
79
|
+
CrowdSourceTransportError,
|
|
80
|
+
isCrowdSourceApiError,
|
|
81
|
+
isCrowdSourceApiErrorCode,
|
|
82
|
+
isCrowdSourceError,
|
|
83
|
+
} from './errors.js';
|
|
84
|
+
export type { CrowdSourceApiErrorCode } from './errors.js';
|
|
85
|
+
|
|
86
|
+
export { Reports } from './reports.js';
|
|
87
|
+
export type { ReportReceipt, ReportRequestOptions } from './reports.js';
|
|
88
|
+
|
|
89
|
+
export { Cases, Decisions } from './cases.js';
|
|
90
|
+
export type { CaseView, ReadOptions } from './cases.js';
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
export { CommunityNotes } from './communityNotes.js';
|
|
94
|
+
export type { CommunityNoteReadOptions, CommunityNoteRequestOptions } from './communityNotes.js';
|
|
95
|
+
|
|
96
|
+
export { WebhookEndpoints } from './webhookEndpoints.js';
|
|
97
|
+
export type {
|
|
98
|
+
RegisterWebhookEndpointInput,
|
|
99
|
+
RotateSecretOptions,
|
|
100
|
+
RotatedWebhookSecret,
|
|
101
|
+
WebhookEndpoint,
|
|
102
|
+
WebhookEndpointRequestOptions,
|
|
103
|
+
WebhookSecret,
|
|
104
|
+
} from './webhookEndpoints.js';
|
|
105
|
+
|
|
106
|
+
export { DEFAULT_MAX_ATTEMPTS, DEFAULT_TIMEOUT_MS } from './transport.js';
|
|
107
|
+
export type { FetchLike } from './transport.js';
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { CrowdSource } from '../index.js';
|
|
2
|
+
import type { CrowdSourceConnectionConfig, ModerationLogger } from './types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The CrowdSource client, built once and only when configured.
|
|
6
|
+
*
|
|
7
|
+
* There is deliberately almost nothing here. The SDK already owns the base URL,
|
|
8
|
+
* the timeouts, the bounded per-attempt retries, the idempotency key and the
|
|
9
|
+
* error classification, and a wrapper that re-implemented any of them would be a
|
|
10
|
+
* second answer to a question that has one. What this adds is exactly two
|
|
11
|
+
* things: the client is absent until the integration is switched on, and it is
|
|
12
|
+
* built once rather than per delivery.
|
|
13
|
+
*
|
|
14
|
+
* `applicationId` appears nowhere — the client reads it off the service key, and
|
|
15
|
+
* there is no option, field or parameter through which one could be passed.
|
|
16
|
+
*/
|
|
17
|
+
export interface CrowdSourceClientProvider {
|
|
18
|
+
/**
|
|
19
|
+
* The client, or `undefined` when the integration is not configured.
|
|
20
|
+
*
|
|
21
|
+
* `undefined` rather than a throw: a disabled integration is the normal state
|
|
22
|
+
* of a local checkout and of every deployment before rollout, and a report
|
|
23
|
+
* filed there must still be stored. The delivery worker is what notices there
|
|
24
|
+
* is nowhere to send it.
|
|
25
|
+
*
|
|
26
|
+
* A MISCONFIGURED client — a malformed service key — is a different thing and
|
|
27
|
+
* is logged once at error level. Once, because the alternative is one line per
|
|
28
|
+
* delivery attempt per report, which buries the cause it is meant to reveal.
|
|
29
|
+
*/
|
|
30
|
+
get(): CrowdSource | undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function createClientProvider(input: {
|
|
34
|
+
config: CrowdSourceConnectionConfig;
|
|
35
|
+
logger: ModerationLogger;
|
|
36
|
+
}): CrowdSourceClientProvider {
|
|
37
|
+
let client: CrowdSource | null = null;
|
|
38
|
+
let configurationError: string | null = null;
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
get(): CrowdSource | undefined {
|
|
42
|
+
if (!input.config.enabled) return undefined;
|
|
43
|
+
if (client) return client;
|
|
44
|
+
if (configurationError !== null) return undefined;
|
|
45
|
+
|
|
46
|
+
const serviceKey = input.config.serviceKey;
|
|
47
|
+
if (!serviceKey) {
|
|
48
|
+
configurationError = 'no CrowdSource service key is configured';
|
|
49
|
+
input.logger.error('[CrowdSource] enabled but not configured', {
|
|
50
|
+
reason: configurationError,
|
|
51
|
+
});
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
client = new CrowdSource({
|
|
57
|
+
serviceKey,
|
|
58
|
+
...(input.config.baseUrl === undefined ? {} : { baseUrl: input.config.baseUrl }),
|
|
59
|
+
});
|
|
60
|
+
input.logger.info('[CrowdSource] client ready', {
|
|
61
|
+
applicationId: client.applicationId,
|
|
62
|
+
});
|
|
63
|
+
return client;
|
|
64
|
+
} catch (error: unknown) {
|
|
65
|
+
// The SDK's configuration errors name which part of the key is wrong and
|
|
66
|
+
// never echo the secret, so the message is safe to log.
|
|
67
|
+
configurationError = error instanceof Error ? error.message : String(error);
|
|
68
|
+
input.logger.error('[CrowdSource] service key rejected', {
|
|
69
|
+
reason: configurationError,
|
|
70
|
+
});
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { DecisionSchema, type Decision } from '@crowdsource.you/contracts';
|
|
2
|
+
import type { EnforcementExecutor } from './enforcement/executor.js';
|
|
3
|
+
import { primaryAction } from './enforcement/planner.js';
|
|
4
|
+
import { localStatusForDecision } from './reportStatus.js';
|
|
5
|
+
import type { ModerationReportStore } from './store/types.js';
|
|
6
|
+
import type {
|
|
7
|
+
EnforcementOutcome,
|
|
8
|
+
ModerationEnforcementConfig,
|
|
9
|
+
ModerationLogger,
|
|
10
|
+
ModerationOutboxEvent,
|
|
11
|
+
ModerationReportFields,
|
|
12
|
+
ReportDecisionExtraFields,
|
|
13
|
+
} from './types.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Applying a decision that has already been received and recorded.
|
|
17
|
+
*
|
|
18
|
+
* The webhook answered 2xx long before this runs. What is left is the part that
|
|
19
|
+
* touches several collections: write the decision onto every report that opened
|
|
20
|
+
* or joined the case, and enforce it once.
|
|
21
|
+
*
|
|
22
|
+
* "Once" is the invariant that shapes this file. A hundred reports about the
|
|
23
|
+
* same material produce ONE case and ONE consequence, so enforcement is keyed on
|
|
24
|
+
* the decision and not on the reports — the executor is called a single time no
|
|
25
|
+
* matter how many reports are updated, and its own unique index makes even that
|
|
26
|
+
* call safe to repeat.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/** A failure that will not be fixed by trying again. */
|
|
30
|
+
export class ModerationDecisionRejectedError extends Error {
|
|
31
|
+
/** Read by the outbox: `false` dead-letters instead of retrying. */
|
|
32
|
+
readonly retryable = false;
|
|
33
|
+
|
|
34
|
+
constructor(message: string) {
|
|
35
|
+
super(message);
|
|
36
|
+
this.name = 'ModerationDecisionRejectedError';
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** A failure that a later attempt can still resolve. */
|
|
41
|
+
export class ModerationDecisionDeferredError extends Error {
|
|
42
|
+
readonly retryable = true;
|
|
43
|
+
|
|
44
|
+
constructor(message: string) {
|
|
45
|
+
super(message);
|
|
46
|
+
this.name = 'ModerationDecisionDeferredError';
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function createDecisionWorker<
|
|
51
|
+
TReport extends ModerationReportFields,
|
|
52
|
+
TAction extends string,
|
|
53
|
+
TTx,
|
|
54
|
+
>(input: {
|
|
55
|
+
reports: ModerationReportStore<TReport, TTx>;
|
|
56
|
+
executor: EnforcementExecutor<TAction>;
|
|
57
|
+
enforcement: ModerationEnforcementConfig<TAction>;
|
|
58
|
+
logger: ModerationLogger;
|
|
59
|
+
reportDecisionExtraFields?: (decision: Decision) => ReportDecisionExtraFields;
|
|
60
|
+
}): (event: ModerationOutboxEvent) => Promise<void> {
|
|
61
|
+
/**
|
|
62
|
+
* Write the decision onto one report.
|
|
63
|
+
*
|
|
64
|
+
* `decision.revision` is passed as the ceiling the store guards on, so it is
|
|
65
|
+
* the DATABASE that refuses a stale write rather than a read-then-write in
|
|
66
|
+
* this process. Deliveries can overlap — CrowdSource retries for 24 hours, and
|
|
67
|
+
* a correction can arrive while the decision it supersedes is still being
|
|
68
|
+
* applied — and an older revision landing last would otherwise overwrite the
|
|
69
|
+
* current answer with a stale one.
|
|
70
|
+
*/
|
|
71
|
+
const applyToReport = async (
|
|
72
|
+
reportId: string,
|
|
73
|
+
decision: Decision,
|
|
74
|
+
enforced: { action: TAction; at: Date | null } | undefined,
|
|
75
|
+
): Promise<boolean> =>
|
|
76
|
+
await input.reports.applyDecision(
|
|
77
|
+
reportId,
|
|
78
|
+
{
|
|
79
|
+
localStatus: localStatusForDecision(decision.status),
|
|
80
|
+
decisionId: decision.id,
|
|
81
|
+
decisionRevision: decision.revision,
|
|
82
|
+
decisionOutcome: decision.outcome,
|
|
83
|
+
decisionStatus: decision.status,
|
|
84
|
+
decidedAt: new Date(decision.publishedAt),
|
|
85
|
+
/**
|
|
86
|
+
* `enforcedAction` is what the application DECIDED to do; `enforcedAt`
|
|
87
|
+
* is when an effect actually landed. They are written separately because
|
|
88
|
+
* they are different claims, and conflating them puts a timestamp on
|
|
89
|
+
* something that never happened — which is the normal case in `observe`
|
|
90
|
+
* mode, and the permanent case for an application with no sanction
|
|
91
|
+
* primitive. An audit row that says "enforced at 14:02" for an effect
|
|
92
|
+
* nobody carried out is not explainable, and every effect being
|
|
93
|
+
* explainable is the invariant.
|
|
94
|
+
*/
|
|
95
|
+
...(enforced === undefined
|
|
96
|
+
? {}
|
|
97
|
+
: {
|
|
98
|
+
enforcedAction: enforced.action,
|
|
99
|
+
...(enforced.at === null ? {} : { enforcedAt: enforced.at }),
|
|
100
|
+
}),
|
|
101
|
+
...(input.reportDecisionExtraFields === undefined
|
|
102
|
+
? {}
|
|
103
|
+
: { extra: input.reportDecisionExtraFields(decision) }),
|
|
104
|
+
},
|
|
105
|
+
decision.revision,
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
return async (event) => {
|
|
109
|
+
const caseId = event.payload.caseId;
|
|
110
|
+
if (caseId === undefined) {
|
|
111
|
+
throw new ModerationDecisionRejectedError(
|
|
112
|
+
'A decision.apply event carried no caseId.',
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Parsed HERE rather than at the webhook.
|
|
118
|
+
*
|
|
119
|
+
* The receiver's job was to prove the bytes came from CrowdSource and to
|
|
120
|
+
* record them; refusing an unrecognised shape at the door would put a real
|
|
121
|
+
* decision back on a retry schedule until it expired. Parsing at the point
|
|
122
|
+
* of use means an event this deployment cannot yet read waits in the outbox
|
|
123
|
+
* until it can.
|
|
124
|
+
*/
|
|
125
|
+
const parsed = DecisionSchema.safeParse(event.payload.decision);
|
|
126
|
+
if (!parsed.success) {
|
|
127
|
+
throw new ModerationDecisionRejectedError(
|
|
128
|
+
`The decision for case ${caseId} does not match the published contract: ${parsed.error.issues
|
|
129
|
+
.map((issue) => `${issue.path.join('.') || '(root)'}: ${issue.message}`)
|
|
130
|
+
.join('; ')}`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
const decision = parsed.data;
|
|
134
|
+
|
|
135
|
+
const reports = await input.reports.findByCaseId(caseId);
|
|
136
|
+
|
|
137
|
+
if (reports.length === 0) {
|
|
138
|
+
/**
|
|
139
|
+
* Retryable, and the race is real rather than theoretical: CrowdSource can
|
|
140
|
+
* decide a case and deliver the webhook while the response that carried
|
|
141
|
+
* the case id back to this deployment is still being written, or while a
|
|
142
|
+
* delivery is being retried. Backing off is correct; dead-lettering would
|
|
143
|
+
* throw the decision away.
|
|
144
|
+
*/
|
|
145
|
+
throw new ModerationDecisionDeferredError(
|
|
146
|
+
`No local report is linked to case ${caseId} yet.`,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* One subject per case. The dedup key includes the subject's external id, so
|
|
152
|
+
* every report merged into a case is about the same object — the first
|
|
153
|
+
* report names it.
|
|
154
|
+
*/
|
|
155
|
+
const [first] = reports;
|
|
156
|
+
const outcomes = await input.executor.apply({
|
|
157
|
+
decision,
|
|
158
|
+
caseId,
|
|
159
|
+
subject: { type: first.reportedType, id: first.reportedId },
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The EFFECTIVE action, not the planned one. `apply` is the only place that
|
|
164
|
+
* knows a planned action could not apply to this particular object, and
|
|
165
|
+
* `recordedAs` is how it says so — see `EnforcementEffect`.
|
|
166
|
+
*/
|
|
167
|
+
const effectiveAction = (outcome: EnforcementOutcome<TAction>): TAction =>
|
|
168
|
+
outcome.recordedAs ?? outcome.action;
|
|
169
|
+
|
|
170
|
+
const enforcedAction = primaryAction(
|
|
171
|
+
outcomes.map(effectiveAction),
|
|
172
|
+
input.enforcement.precedence ?? input.enforcement.actions,
|
|
173
|
+
);
|
|
174
|
+
/**
|
|
175
|
+
* The timestamp belongs to the outcome of THIS action, not to the plan. An
|
|
176
|
+
* action that was claimed and recorded — observe mode, a mode that does not
|
|
177
|
+
* apply it, or an application with nothing to apply — leaves `enforcedAt`
|
|
178
|
+
* unset while `enforcedAction` still says what was decided.
|
|
179
|
+
*/
|
|
180
|
+
const enforced =
|
|
181
|
+
enforcedAction === undefined
|
|
182
|
+
? undefined
|
|
183
|
+
: {
|
|
184
|
+
action: enforcedAction,
|
|
185
|
+
at: outcomes.some(
|
|
186
|
+
(outcome) =>
|
|
187
|
+
effectiveAction(outcome) === enforcedAction &&
|
|
188
|
+
outcome.result === 'applied',
|
|
189
|
+
)
|
|
190
|
+
? new Date()
|
|
191
|
+
: null,
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
let updated = 0;
|
|
195
|
+
for (const report of reports) {
|
|
196
|
+
if (await applyToReport(report.id, decision, enforced)) updated += 1;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
input.logger.info('[CrowdSource] decision applied', {
|
|
200
|
+
caseId,
|
|
201
|
+
decisionId: decision.id,
|
|
202
|
+
revision: decision.revision,
|
|
203
|
+
outcome: decision.outcome,
|
|
204
|
+
reportsMatched: reports.length,
|
|
205
|
+
reportsUpdated: updated,
|
|
206
|
+
actions: outcomes.map((outcome) => `${outcome.action}:${outcome.result}`).join(','),
|
|
207
|
+
});
|
|
208
|
+
};
|
|
209
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import type { CrowdSourceClientProvider } from './client.js';
|
|
2
|
+
import { buildModerationReportInput, type SubjectRegistry } from './evidence.js';
|
|
3
|
+
import type { ModerationReportStore } from './store/types.js';
|
|
4
|
+
import type {
|
|
5
|
+
ModerationLogger,
|
|
6
|
+
ModerationMetrics,
|
|
7
|
+
ModerationOutboxEvent,
|
|
8
|
+
ModerationReportFields,
|
|
9
|
+
ModerationTaxonomy,
|
|
10
|
+
} from './types.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Delivering a stored report to CrowdSource.
|
|
14
|
+
*
|
|
15
|
+
* Everything hard about this is already handled elsewhere and the shape of this
|
|
16
|
+
* file is what is left over: the SDK owns the envelope, the idempotency key, the
|
|
17
|
+
* timeouts, the per-attempt retries and the classification of failures; the
|
|
18
|
+
* outbox owns durability, backoff and dead-lettering. What remains is to
|
|
19
|
+
* describe the material, hand it over, and write down what came back.
|
|
20
|
+
*
|
|
21
|
+
* The failures are as important as the success:
|
|
22
|
+
*
|
|
23
|
+
* - **Nowhere to send it.** The integration is not configured. The event stays
|
|
24
|
+
* pending, untouched, and delivers when it is — a delay, never a loss.
|
|
25
|
+
* - **The object is gone.** Deleted between the report and its delivery. There
|
|
26
|
+
* is nothing to review, so the report closes locally instead of retrying for
|
|
27
|
+
* days.
|
|
28
|
+
* - **The type has no provider.** Unreachable by design — such a report never
|
|
29
|
+
* gets a delivery event — so an event that reaches it is a defect and is
|
|
30
|
+
* dead-lettered rather than retried or filed as a state.
|
|
31
|
+
* - **Anything else** is the SDK's `retryable` to answer, and the outbox obeys
|
|
32
|
+
* it.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/** Thrown when there is nowhere to deliver to yet. Always retryable. */
|
|
36
|
+
export class CrowdSourceUnavailableError extends Error {
|
|
37
|
+
readonly retryable = true;
|
|
38
|
+
|
|
39
|
+
constructor() {
|
|
40
|
+
super('The CrowdSource integration is not configured in this deployment.');
|
|
41
|
+
this.name = 'CrowdSourceUnavailableError';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* A delivery event that cannot become deliverable.
|
|
47
|
+
*
|
|
48
|
+
* `retryable: false` is the field the outbox reads to dead-letter instead of
|
|
49
|
+
* backing off — the same contract every error from `@crowdsource.you/core` answers.
|
|
50
|
+
*/
|
|
51
|
+
export class ModerationDeliveryRejectedError extends Error {
|
|
52
|
+
readonly retryable = false;
|
|
53
|
+
|
|
54
|
+
constructor(message: string) {
|
|
55
|
+
super(message);
|
|
56
|
+
this.name = 'ModerationDeliveryRejectedError';
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function createDeliveryWorker<TReport extends ModerationReportFields, TTx>(input: {
|
|
61
|
+
reports: ModerationReportStore<TReport, TTx>;
|
|
62
|
+
registry: SubjectRegistry;
|
|
63
|
+
taxonomy: ModerationTaxonomy;
|
|
64
|
+
client: CrowdSourceClientProvider;
|
|
65
|
+
logger: ModerationLogger;
|
|
66
|
+
metrics?: ModerationMetrics;
|
|
67
|
+
}): (event: ModerationOutboxEvent) => Promise<void> {
|
|
68
|
+
const count = (result: string): void => {
|
|
69
|
+
input.metrics?.incrementCounter('crowdsource_report_delivery_total', 1, { result });
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return async (event) => {
|
|
73
|
+
const reportId = event.payload.reportId;
|
|
74
|
+
if (reportId === undefined) {
|
|
75
|
+
throw new ModerationDeliveryRejectedError(
|
|
76
|
+
'A report.submit event carried no reportId.',
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const report = await input.reports.findById(reportId);
|
|
81
|
+
if (!report) {
|
|
82
|
+
/**
|
|
83
|
+
* The report is gone but its delivery event survived. Nothing to deliver
|
|
84
|
+
* and nothing to fix, so the event completes — retrying would keep looking
|
|
85
|
+
* for a row that no longer exists.
|
|
86
|
+
*/
|
|
87
|
+
input.logger.warn('[CrowdSource] delivery event has no report', { reportId });
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const crowdsource = input.client.get();
|
|
92
|
+
if (!crowdsource) throw new CrowdSourceUnavailableError();
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* A `ModerationSubjectUnsupportedError` from here is NOT caught. It carries
|
|
96
|
+
* `retryable: false`, so the outbox dead-letters the event and the
|
|
97
|
+
* reconciliation sweep counts it — which is the right channel for a defect
|
|
98
|
+
* that needs a human. Catching it and writing a local state would put the
|
|
99
|
+
* report somewhere nothing alerts on.
|
|
100
|
+
*/
|
|
101
|
+
const described = await buildModerationReportInput({
|
|
102
|
+
report: {
|
|
103
|
+
id: reportId,
|
|
104
|
+
reportedType: report.reportedType,
|
|
105
|
+
reportedId: report.reportedId,
|
|
106
|
+
reporter: report.reporter,
|
|
107
|
+
categories: report.categories,
|
|
108
|
+
...(report.details === undefined ? {} : { details: report.details }),
|
|
109
|
+
createdAt: report.createdAt,
|
|
110
|
+
},
|
|
111
|
+
registry: input.registry,
|
|
112
|
+
taxonomy: input.taxonomy,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
if (described === null) {
|
|
116
|
+
// Nothing left to review, so the report is closed with the reason rather
|
|
117
|
+
// than retried against material that is gone.
|
|
118
|
+
await input.reports.close(
|
|
119
|
+
reportId,
|
|
120
|
+
'The reported content no longer exists, so there is nothing to review.',
|
|
121
|
+
);
|
|
122
|
+
count('content_unavailable');
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let receipt: Awaited<ReturnType<typeof crowdsource.reports.create>>;
|
|
127
|
+
try {
|
|
128
|
+
receipt = await crowdsource.reports.create(described.reportInput);
|
|
129
|
+
} catch (error: unknown) {
|
|
130
|
+
/**
|
|
131
|
+
* The failure is visible on the report itself, not only in the outbox row.
|
|
132
|
+
* `delivery_failed` is what a reporter's receipt and the reconciliation
|
|
133
|
+
* sweep both read; leaving the report at `queued` while the outbox quietly
|
|
134
|
+
* backed off would hide the problem in a collection nobody looks at.
|
|
135
|
+
* Written before rethrowing so the outbox still applies its own backoff or
|
|
136
|
+
* dead-letters the event.
|
|
137
|
+
*/
|
|
138
|
+
await input.reports.markDeliveryFailed(
|
|
139
|
+
reportId,
|
|
140
|
+
// Bounded here rather than by a column width, so both dialects agree: a
|
|
141
|
+
// Mongoose validator throws on overflow and Postgres errors 22001.
|
|
142
|
+
(error instanceof Error ? error.message : String(error)).slice(0, 2_000),
|
|
143
|
+
);
|
|
144
|
+
count('failed');
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
await input.reports.markSubmitted(reportId, {
|
|
149
|
+
crowdSourceReportId: receipt.reportId,
|
|
150
|
+
crowdSourceCaseId: receipt.caseId,
|
|
151
|
+
crowdSourceMerged: receipt.merged,
|
|
152
|
+
contentSnapshotHash: described.snapshotHash,
|
|
153
|
+
submittedAt: new Date(),
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
count(receipt.merged ? 'merged' : 'delivered');
|
|
157
|
+
input.logger.info('[CrowdSource] report delivered', {
|
|
158
|
+
reportId,
|
|
159
|
+
caseId: receipt.caseId,
|
|
160
|
+
merged: receipt.merged,
|
|
161
|
+
});
|
|
162
|
+
};
|
|
163
|
+
}
|