@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,153 @@
|
|
|
1
|
+
import { DecisionSchema } from '@crowdsource.you/contracts';
|
|
2
|
+
import { primaryAction } from './enforcement/planner.js';
|
|
3
|
+
import { localStatusForDecision } from './reportStatus.js';
|
|
4
|
+
/**
|
|
5
|
+
* Applying a decision that has already been received and recorded.
|
|
6
|
+
*
|
|
7
|
+
* The webhook answered 2xx long before this runs. What is left is the part that
|
|
8
|
+
* touches several collections: write the decision onto every report that opened
|
|
9
|
+
* or joined the case, and enforce it once.
|
|
10
|
+
*
|
|
11
|
+
* "Once" is the invariant that shapes this file. A hundred reports about the
|
|
12
|
+
* same material produce ONE case and ONE consequence, so enforcement is keyed on
|
|
13
|
+
* the decision and not on the reports — the executor is called a single time no
|
|
14
|
+
* matter how many reports are updated, and its own unique index makes even that
|
|
15
|
+
* call safe to repeat.
|
|
16
|
+
*/
|
|
17
|
+
/** A failure that will not be fixed by trying again. */
|
|
18
|
+
export class ModerationDecisionRejectedError extends Error {
|
|
19
|
+
constructor(message) {
|
|
20
|
+
super(message);
|
|
21
|
+
/** Read by the outbox: `false` dead-letters instead of retrying. */
|
|
22
|
+
this.retryable = false;
|
|
23
|
+
this.name = 'ModerationDecisionRejectedError';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** A failure that a later attempt can still resolve. */
|
|
27
|
+
export class ModerationDecisionDeferredError extends Error {
|
|
28
|
+
constructor(message) {
|
|
29
|
+
super(message);
|
|
30
|
+
this.retryable = true;
|
|
31
|
+
this.name = 'ModerationDecisionDeferredError';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export function createDecisionWorker(input) {
|
|
35
|
+
/**
|
|
36
|
+
* Write the decision onto one report.
|
|
37
|
+
*
|
|
38
|
+
* `decision.revision` is passed as the ceiling the store guards on, so it is
|
|
39
|
+
* the DATABASE that refuses a stale write rather than a read-then-write in
|
|
40
|
+
* this process. Deliveries can overlap — CrowdSource retries for 24 hours, and
|
|
41
|
+
* a correction can arrive while the decision it supersedes is still being
|
|
42
|
+
* applied — and an older revision landing last would otherwise overwrite the
|
|
43
|
+
* current answer with a stale one.
|
|
44
|
+
*/
|
|
45
|
+
const applyToReport = async (reportId, decision, enforced) => await input.reports.applyDecision(reportId, {
|
|
46
|
+
localStatus: localStatusForDecision(decision.status),
|
|
47
|
+
decisionId: decision.id,
|
|
48
|
+
decisionRevision: decision.revision,
|
|
49
|
+
decisionOutcome: decision.outcome,
|
|
50
|
+
decisionStatus: decision.status,
|
|
51
|
+
decidedAt: new Date(decision.publishedAt),
|
|
52
|
+
/**
|
|
53
|
+
* `enforcedAction` is what the application DECIDED to do; `enforcedAt`
|
|
54
|
+
* is when an effect actually landed. They are written separately because
|
|
55
|
+
* they are different claims, and conflating them puts a timestamp on
|
|
56
|
+
* something that never happened — which is the normal case in `observe`
|
|
57
|
+
* mode, and the permanent case for an application with no sanction
|
|
58
|
+
* primitive. An audit row that says "enforced at 14:02" for an effect
|
|
59
|
+
* nobody carried out is not explainable, and every effect being
|
|
60
|
+
* explainable is the invariant.
|
|
61
|
+
*/
|
|
62
|
+
...(enforced === undefined
|
|
63
|
+
? {}
|
|
64
|
+
: {
|
|
65
|
+
enforcedAction: enforced.action,
|
|
66
|
+
...(enforced.at === null ? {} : { enforcedAt: enforced.at }),
|
|
67
|
+
}),
|
|
68
|
+
...(input.reportDecisionExtraFields === undefined
|
|
69
|
+
? {}
|
|
70
|
+
: { extra: input.reportDecisionExtraFields(decision) }),
|
|
71
|
+
}, decision.revision);
|
|
72
|
+
return async (event) => {
|
|
73
|
+
const caseId = event.payload.caseId;
|
|
74
|
+
if (caseId === undefined) {
|
|
75
|
+
throw new ModerationDecisionRejectedError('A decision.apply event carried no caseId.');
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Parsed HERE rather than at the webhook.
|
|
79
|
+
*
|
|
80
|
+
* The receiver's job was to prove the bytes came from CrowdSource and to
|
|
81
|
+
* record them; refusing an unrecognised shape at the door would put a real
|
|
82
|
+
* decision back on a retry schedule until it expired. Parsing at the point
|
|
83
|
+
* of use means an event this deployment cannot yet read waits in the outbox
|
|
84
|
+
* until it can.
|
|
85
|
+
*/
|
|
86
|
+
const parsed = DecisionSchema.safeParse(event.payload.decision);
|
|
87
|
+
if (!parsed.success) {
|
|
88
|
+
throw new ModerationDecisionRejectedError(`The decision for case ${caseId} does not match the published contract: ${parsed.error.issues
|
|
89
|
+
.map((issue) => `${issue.path.join('.') || '(root)'}: ${issue.message}`)
|
|
90
|
+
.join('; ')}`);
|
|
91
|
+
}
|
|
92
|
+
const decision = parsed.data;
|
|
93
|
+
const reports = await input.reports.findByCaseId(caseId);
|
|
94
|
+
if (reports.length === 0) {
|
|
95
|
+
/**
|
|
96
|
+
* Retryable, and the race is real rather than theoretical: CrowdSource can
|
|
97
|
+
* decide a case and deliver the webhook while the response that carried
|
|
98
|
+
* the case id back to this deployment is still being written, or while a
|
|
99
|
+
* delivery is being retried. Backing off is correct; dead-lettering would
|
|
100
|
+
* throw the decision away.
|
|
101
|
+
*/
|
|
102
|
+
throw new ModerationDecisionDeferredError(`No local report is linked to case ${caseId} yet.`);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* One subject per case. The dedup key includes the subject's external id, so
|
|
106
|
+
* every report merged into a case is about the same object — the first
|
|
107
|
+
* report names it.
|
|
108
|
+
*/
|
|
109
|
+
const [first] = reports;
|
|
110
|
+
const outcomes = await input.executor.apply({
|
|
111
|
+
decision,
|
|
112
|
+
caseId,
|
|
113
|
+
subject: { type: first.reportedType, id: first.reportedId },
|
|
114
|
+
});
|
|
115
|
+
/**
|
|
116
|
+
* The EFFECTIVE action, not the planned one. `apply` is the only place that
|
|
117
|
+
* knows a planned action could not apply to this particular object, and
|
|
118
|
+
* `recordedAs` is how it says so — see `EnforcementEffect`.
|
|
119
|
+
*/
|
|
120
|
+
const effectiveAction = (outcome) => outcome.recordedAs ?? outcome.action;
|
|
121
|
+
const enforcedAction = primaryAction(outcomes.map(effectiveAction), input.enforcement.precedence ?? input.enforcement.actions);
|
|
122
|
+
/**
|
|
123
|
+
* The timestamp belongs to the outcome of THIS action, not to the plan. An
|
|
124
|
+
* action that was claimed and recorded — observe mode, a mode that does not
|
|
125
|
+
* apply it, or an application with nothing to apply — leaves `enforcedAt`
|
|
126
|
+
* unset while `enforcedAction` still says what was decided.
|
|
127
|
+
*/
|
|
128
|
+
const enforced = enforcedAction === undefined
|
|
129
|
+
? undefined
|
|
130
|
+
: {
|
|
131
|
+
action: enforcedAction,
|
|
132
|
+
at: outcomes.some((outcome) => effectiveAction(outcome) === enforcedAction &&
|
|
133
|
+
outcome.result === 'applied')
|
|
134
|
+
? new Date()
|
|
135
|
+
: null,
|
|
136
|
+
};
|
|
137
|
+
let updated = 0;
|
|
138
|
+
for (const report of reports) {
|
|
139
|
+
if (await applyToReport(report.id, decision, enforced))
|
|
140
|
+
updated += 1;
|
|
141
|
+
}
|
|
142
|
+
input.logger.info('[CrowdSource] decision applied', {
|
|
143
|
+
caseId,
|
|
144
|
+
decisionId: decision.id,
|
|
145
|
+
revision: decision.revision,
|
|
146
|
+
outcome: decision.outcome,
|
|
147
|
+
reportsMatched: reports.length,
|
|
148
|
+
reportsUpdated: updated,
|
|
149
|
+
actions: outcomes.map((outcome) => `${outcome.action}:${outcome.result}`).join(','),
|
|
150
|
+
});
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
//# sourceMappingURL=decision.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decision.js","sourceRoot":"","sources":["../../../src/outbox/decision.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { buildModerationReportInput } from './evidence.js';
|
|
2
|
+
/**
|
|
3
|
+
* Delivering a stored report to CrowdSource.
|
|
4
|
+
*
|
|
5
|
+
* Everything hard about this is already handled elsewhere and the shape of this
|
|
6
|
+
* file is what is left over: the SDK owns the envelope, the idempotency key, the
|
|
7
|
+
* timeouts, the per-attempt retries and the classification of failures; the
|
|
8
|
+
* outbox owns durability, backoff and dead-lettering. What remains is to
|
|
9
|
+
* describe the material, hand it over, and write down what came back.
|
|
10
|
+
*
|
|
11
|
+
* The failures are as important as the success:
|
|
12
|
+
*
|
|
13
|
+
* - **Nowhere to send it.** The integration is not configured. The event stays
|
|
14
|
+
* pending, untouched, and delivers when it is — a delay, never a loss.
|
|
15
|
+
* - **The object is gone.** Deleted between the report and its delivery. There
|
|
16
|
+
* is nothing to review, so the report closes locally instead of retrying for
|
|
17
|
+
* days.
|
|
18
|
+
* - **The type has no provider.** Unreachable by design — such a report never
|
|
19
|
+
* gets a delivery event — so an event that reaches it is a defect and is
|
|
20
|
+
* dead-lettered rather than retried or filed as a state.
|
|
21
|
+
* - **Anything else** is the SDK's `retryable` to answer, and the outbox obeys
|
|
22
|
+
* it.
|
|
23
|
+
*/
|
|
24
|
+
/** Thrown when there is nowhere to deliver to yet. Always retryable. */
|
|
25
|
+
export class CrowdSourceUnavailableError extends Error {
|
|
26
|
+
constructor() {
|
|
27
|
+
super('The CrowdSource integration is not configured in this deployment.');
|
|
28
|
+
this.retryable = true;
|
|
29
|
+
this.name = 'CrowdSourceUnavailableError';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* A delivery event that cannot become deliverable.
|
|
34
|
+
*
|
|
35
|
+
* `retryable: false` is the field the outbox reads to dead-letter instead of
|
|
36
|
+
* backing off — the same contract every error from `@crowdsource.you/core` answers.
|
|
37
|
+
*/
|
|
38
|
+
export class ModerationDeliveryRejectedError extends Error {
|
|
39
|
+
constructor(message) {
|
|
40
|
+
super(message);
|
|
41
|
+
this.retryable = false;
|
|
42
|
+
this.name = 'ModerationDeliveryRejectedError';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export function createDeliveryWorker(input) {
|
|
46
|
+
const count = (result) => {
|
|
47
|
+
input.metrics?.incrementCounter('crowdsource_report_delivery_total', 1, { result });
|
|
48
|
+
};
|
|
49
|
+
return async (event) => {
|
|
50
|
+
const reportId = event.payload.reportId;
|
|
51
|
+
if (reportId === undefined) {
|
|
52
|
+
throw new ModerationDeliveryRejectedError('A report.submit event carried no reportId.');
|
|
53
|
+
}
|
|
54
|
+
const report = await input.reports.findById(reportId);
|
|
55
|
+
if (!report) {
|
|
56
|
+
/**
|
|
57
|
+
* The report is gone but its delivery event survived. Nothing to deliver
|
|
58
|
+
* and nothing to fix, so the event completes — retrying would keep looking
|
|
59
|
+
* for a row that no longer exists.
|
|
60
|
+
*/
|
|
61
|
+
input.logger.warn('[CrowdSource] delivery event has no report', { reportId });
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const crowdsource = input.client.get();
|
|
65
|
+
if (!crowdsource)
|
|
66
|
+
throw new CrowdSourceUnavailableError();
|
|
67
|
+
/**
|
|
68
|
+
* A `ModerationSubjectUnsupportedError` from here is NOT caught. It carries
|
|
69
|
+
* `retryable: false`, so the outbox dead-letters the event and the
|
|
70
|
+
* reconciliation sweep counts it — which is the right channel for a defect
|
|
71
|
+
* that needs a human. Catching it and writing a local state would put the
|
|
72
|
+
* report somewhere nothing alerts on.
|
|
73
|
+
*/
|
|
74
|
+
const described = await buildModerationReportInput({
|
|
75
|
+
report: {
|
|
76
|
+
id: reportId,
|
|
77
|
+
reportedType: report.reportedType,
|
|
78
|
+
reportedId: report.reportedId,
|
|
79
|
+
reporter: report.reporter,
|
|
80
|
+
categories: report.categories,
|
|
81
|
+
...(report.details === undefined ? {} : { details: report.details }),
|
|
82
|
+
createdAt: report.createdAt,
|
|
83
|
+
},
|
|
84
|
+
registry: input.registry,
|
|
85
|
+
taxonomy: input.taxonomy,
|
|
86
|
+
});
|
|
87
|
+
if (described === null) {
|
|
88
|
+
// Nothing left to review, so the report is closed with the reason rather
|
|
89
|
+
// than retried against material that is gone.
|
|
90
|
+
await input.reports.close(reportId, 'The reported content no longer exists, so there is nothing to review.');
|
|
91
|
+
count('content_unavailable');
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
let receipt;
|
|
95
|
+
try {
|
|
96
|
+
receipt = await crowdsource.reports.create(described.reportInput);
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
/**
|
|
100
|
+
* The failure is visible on the report itself, not only in the outbox row.
|
|
101
|
+
* `delivery_failed` is what a reporter's receipt and the reconciliation
|
|
102
|
+
* sweep both read; leaving the report at `queued` while the outbox quietly
|
|
103
|
+
* backed off would hide the problem in a collection nobody looks at.
|
|
104
|
+
* Written before rethrowing so the outbox still applies its own backoff or
|
|
105
|
+
* dead-letters the event.
|
|
106
|
+
*/
|
|
107
|
+
await input.reports.markDeliveryFailed(reportId,
|
|
108
|
+
// Bounded here rather than by a column width, so both dialects agree: a
|
|
109
|
+
// Mongoose validator throws on overflow and Postgres errors 22001.
|
|
110
|
+
(error instanceof Error ? error.message : String(error)).slice(0, 2000));
|
|
111
|
+
count('failed');
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
await input.reports.markSubmitted(reportId, {
|
|
115
|
+
crowdSourceReportId: receipt.reportId,
|
|
116
|
+
crowdSourceCaseId: receipt.caseId,
|
|
117
|
+
crowdSourceMerged: receipt.merged,
|
|
118
|
+
contentSnapshotHash: described.snapshotHash,
|
|
119
|
+
submittedAt: new Date(),
|
|
120
|
+
});
|
|
121
|
+
count(receipt.merged ? 'merged' : 'delivered');
|
|
122
|
+
input.logger.info('[CrowdSource] report delivered', {
|
|
123
|
+
reportId,
|
|
124
|
+
caseId: receipt.caseId,
|
|
125
|
+
merged: receipt.merged,
|
|
126
|
+
});
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=delivery.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"delivery.js","sourceRoot":"","sources":["../../../src/outbox/delivery.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { planEnforcement } from './planner.js';
|
|
2
|
+
/**
|
|
3
|
+
* Carrying out a decision, exactly once.
|
|
4
|
+
*
|
|
5
|
+
* Two guarantees, and everything here exists for one of them.
|
|
6
|
+
*
|
|
7
|
+
* **Once.** The idempotency key is `decisionId + revision + action`, and the
|
|
8
|
+
* unique index on the enforcement collection is that key. Each action CLAIMS its
|
|
9
|
+
* row before doing anything; a second attempt — a redelivered webhook, a
|
|
10
|
+
* reclaimed outbox lease, a manual replay — loses the insert and does nothing.
|
|
11
|
+
* Reading "have I done this?" and then acting would leave the gap between the
|
|
12
|
+
* two, which is exactly when a redelivery arrives.
|
|
13
|
+
*
|
|
14
|
+
* **Reversibly.** Every action that changes state records what the state WAS,
|
|
15
|
+
* and a reversal puts that back. So a correction does not lift a content warning
|
|
16
|
+
* the author set themselves, and a restore returns an object to the state it
|
|
17
|
+
* actually had rather than to a guess at one.
|
|
18
|
+
*
|
|
19
|
+
* `observe` mode runs all of this except the effect. That is deliberate: the
|
|
20
|
+
* plan, the claim and the record are identical to production, so what the mode
|
|
21
|
+
* proves is exactly what will happen when it is switched off — and the audit
|
|
22
|
+
* trail is real rather than a log line saying a decision was seen.
|
|
23
|
+
*
|
|
24
|
+
* The application supplies the tables and one `apply`. Everything above is this
|
|
25
|
+
* package's and is not configurable, because it is the invariant rather than the
|
|
26
|
+
* policy.
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* Whether the current mode allows this action to actually happen.
|
|
30
|
+
*
|
|
31
|
+
* `observe` allows nothing — that is the mode. `manual` allows what the
|
|
32
|
+
* application declared reversible: those actions give something BACK, and
|
|
33
|
+
* holding them behind a human review means a wrongly-removed object stays
|
|
34
|
+
* removed while somebody reads a queue. Taking content down still waits for a
|
|
35
|
+
* person. `automatic` allows the mapped set.
|
|
36
|
+
*/
|
|
37
|
+
function modeAllows(mode, action, config) {
|
|
38
|
+
switch (mode) {
|
|
39
|
+
case 'observe':
|
|
40
|
+
return false;
|
|
41
|
+
case 'manual':
|
|
42
|
+
return (config.reversibleActions ?? []).includes(action);
|
|
43
|
+
case 'automatic':
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export function createEnforcementExecutor(input) {
|
|
48
|
+
const { enforcement, config, logger } = input;
|
|
49
|
+
const count = (action, mode, result) => {
|
|
50
|
+
input.metrics?.incrementCounter('crowdsource_enforcement_total', 1, {
|
|
51
|
+
action,
|
|
52
|
+
mode,
|
|
53
|
+
result,
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* What the earlier action this one reverses left behind.
|
|
58
|
+
*
|
|
59
|
+
* Read from the most recent APPLIED row, so an action that was recorded but
|
|
60
|
+
* never carried out cannot be mistaken for one that changed something. This is
|
|
61
|
+
* the mechanism behind "only lift what moderation set": an absent row means
|
|
62
|
+
* the state was not moderation's doing, and the application's `apply` decides
|
|
63
|
+
* what to do with that.
|
|
64
|
+
*/
|
|
65
|
+
const previousStateFor = async (action, subject) => {
|
|
66
|
+
const reversed = config.reverses?.[action];
|
|
67
|
+
if (reversed === undefined)
|
|
68
|
+
return undefined;
|
|
69
|
+
/**
|
|
70
|
+
* One action may reverse several. The most recent APPLIED row across the
|
|
71
|
+
* whole set wins, so `apply` receives what actually happened last rather
|
|
72
|
+
* than what a single declared action happened to be.
|
|
73
|
+
*/
|
|
74
|
+
const candidates = typeof reversed === 'string' ? [reversed] : reversed;
|
|
75
|
+
if (candidates.length === 0)
|
|
76
|
+
return undefined;
|
|
77
|
+
const row = await enforcement.latestApplied({
|
|
78
|
+
subjectType: subject.type,
|
|
79
|
+
subjectId: subject.id,
|
|
80
|
+
actions: candidates,
|
|
81
|
+
});
|
|
82
|
+
if (row === null)
|
|
83
|
+
return undefined;
|
|
84
|
+
/**
|
|
85
|
+
* The row's own action, narrowed THROUGH the declared set rather than cast.
|
|
86
|
+
* The query guarantees membership; finding it here is what lets
|
|
87
|
+
* `previousAction` be a `TAction` without asserting that it is one.
|
|
88
|
+
*/
|
|
89
|
+
const previousAction = candidates.find((candidate) => candidate === row.action);
|
|
90
|
+
if (previousAction === undefined)
|
|
91
|
+
return undefined;
|
|
92
|
+
return {
|
|
93
|
+
...(row.previousState === undefined ? {} : { previousState: row.previousState }),
|
|
94
|
+
previousAction,
|
|
95
|
+
};
|
|
96
|
+
};
|
|
97
|
+
const applyOne = async (planned, context, mode) => {
|
|
98
|
+
const { decision, caseId, subject } = context;
|
|
99
|
+
/**
|
|
100
|
+
* The claim, and the ONLY way this row is ever addressed afterwards: the
|
|
101
|
+
* idempotency key itself, never a record id the insert happened to return.
|
|
102
|
+
* `false` means another delivery of this same decision revision already
|
|
103
|
+
* holds it, which is an answer rather than an error — a read of "have I done
|
|
104
|
+
* this?" before writing would leave the gap a redelivery arrives in.
|
|
105
|
+
*/
|
|
106
|
+
const key = {
|
|
107
|
+
decisionId: decision.id,
|
|
108
|
+
decisionRevision: decision.revision,
|
|
109
|
+
action: planned.action,
|
|
110
|
+
};
|
|
111
|
+
const claimed = await enforcement.claim({
|
|
112
|
+
...key,
|
|
113
|
+
caseId,
|
|
114
|
+
subjectType: subject.type,
|
|
115
|
+
subjectId: subject.id,
|
|
116
|
+
outcome: decision.outcome,
|
|
117
|
+
...(planned.recommendedAction === undefined
|
|
118
|
+
? {}
|
|
119
|
+
: { recommendedAction: planned.recommendedAction }),
|
|
120
|
+
reason: planned.reason.slice(0, 500),
|
|
121
|
+
mode,
|
|
122
|
+
now: new Date(),
|
|
123
|
+
});
|
|
124
|
+
if (!claimed) {
|
|
125
|
+
count(planned.action, mode, 'duplicate');
|
|
126
|
+
return { action: planned.action, result: 'duplicate' };
|
|
127
|
+
}
|
|
128
|
+
if (!modeAllows(mode, planned.action, config)) {
|
|
129
|
+
await enforcement.markSkipped(key, {
|
|
130
|
+
skippedReason: mode === 'observe'
|
|
131
|
+
? 'observe mode: recorded, not applied'
|
|
132
|
+
: `${mode} mode does not apply '${planned.action}' automatically`,
|
|
133
|
+
now: new Date(),
|
|
134
|
+
});
|
|
135
|
+
count(planned.action, mode, 'recorded');
|
|
136
|
+
return { action: planned.action, result: 'recorded' };
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
const reversal = await previousStateFor(planned.action, subject);
|
|
140
|
+
/**
|
|
141
|
+
* An application with no sanction primitive supplies no `apply`, and every
|
|
142
|
+
* planned action is recorded with the reason rather than silently
|
|
143
|
+
* dropped. The plan, the claim and the audit row stay real, so "CrowdSource
|
|
144
|
+
* decided this and this application has no way to carry it out" is written
|
|
145
|
+
* down — which is the record that would justify building the primitive.
|
|
146
|
+
*/
|
|
147
|
+
const noPrimitive = {
|
|
148
|
+
changed: false,
|
|
149
|
+
reason: 'This application has no enforcement primitive for any action',
|
|
150
|
+
};
|
|
151
|
+
const effect = await (config.apply?.({
|
|
152
|
+
action: planned.action,
|
|
153
|
+
subject,
|
|
154
|
+
...(reversal ?? {}),
|
|
155
|
+
decision,
|
|
156
|
+
}) ?? Promise.resolve(noPrimitive));
|
|
157
|
+
if (!effect.changed) {
|
|
158
|
+
/**
|
|
159
|
+
* `recordedAs` corrects the LABEL, never the claim. The row keeps the
|
|
160
|
+
* planned action because that is half the idempotency key and is what
|
|
161
|
+
* was decided; the effective label rides alongside it and is what
|
|
162
|
+
* reaches the report.
|
|
163
|
+
*/
|
|
164
|
+
await enforcement.markSkipped(key, {
|
|
165
|
+
skippedReason: effect.reason.slice(0, 300),
|
|
166
|
+
...(effect.recordedAs === undefined ? {} : { recordedAs: effect.recordedAs }),
|
|
167
|
+
now: new Date(),
|
|
168
|
+
});
|
|
169
|
+
count(planned.action, mode, 'recorded');
|
|
170
|
+
return {
|
|
171
|
+
action: planned.action,
|
|
172
|
+
result: 'recorded',
|
|
173
|
+
...(effect.recordedAs === undefined ? {} : { recordedAs: effect.recordedAs }),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
const appliedAt = new Date();
|
|
177
|
+
await enforcement.markApplied(key, {
|
|
178
|
+
appliedAt,
|
|
179
|
+
...(effect.previousState === undefined
|
|
180
|
+
? {}
|
|
181
|
+
: { previousState: effect.previousState }),
|
|
182
|
+
now: appliedAt,
|
|
183
|
+
});
|
|
184
|
+
count(planned.action, mode, 'applied');
|
|
185
|
+
return { action: planned.action, result: 'applied' };
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
/**
|
|
189
|
+
* The claim goes back so a retry can try again. Keeping it would make a
|
|
190
|
+
* transient failure permanent: the action would be deduplicated away
|
|
191
|
+
* forever and the decision would silently never be carried out.
|
|
192
|
+
*/
|
|
193
|
+
await enforcement.releaseClaim(key);
|
|
194
|
+
logger.error('[CrowdSource] enforcement effect failed, claim released', {
|
|
195
|
+
decisionId: decision.id,
|
|
196
|
+
revision: decision.revision,
|
|
197
|
+
action: planned.action,
|
|
198
|
+
error: error instanceof Error ? error.message : String(error),
|
|
199
|
+
});
|
|
200
|
+
throw error;
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
return {
|
|
204
|
+
async apply(applyInput) {
|
|
205
|
+
const mode = applyInput.mode ?? input.defaultMode;
|
|
206
|
+
const plan = planEnforcement(applyInput.decision, config);
|
|
207
|
+
const outcomes = [];
|
|
208
|
+
for (const planned of plan) {
|
|
209
|
+
outcomes.push(await applyOne(planned, {
|
|
210
|
+
decision: applyInput.decision,
|
|
211
|
+
caseId: applyInput.caseId,
|
|
212
|
+
subject: applyInput.subject,
|
|
213
|
+
}, mode));
|
|
214
|
+
}
|
|
215
|
+
return outcomes;
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
//# sourceMappingURL=executor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"executor.js","sourceRoot":"","sources":["../../../../src/outbox/enforcement/executor.ts"],"names":[],"mappings":"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"}
|