@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,270 @@
|
|
|
1
|
+
import { sql } from 'drizzle-orm';
|
|
2
|
+
import { boolean, check, index, integer, jsonb, pgTable, primaryKey, text, varchar, } from 'drizzle-orm/pg-core';
|
|
3
|
+
import { createdAt, inList, timestamptz, updatedAt } from '@oxy.so/db';
|
|
4
|
+
/**
|
|
5
|
+
* The three tables this package owns, in Postgres.
|
|
6
|
+
*
|
|
7
|
+
* These names and constraints are stable application data contracts. The
|
|
8
|
+
* reasoning for each field is repeated on the column because it is not carried
|
|
9
|
+
* by the DDL.
|
|
10
|
+
*
|
|
11
|
+
* Three things differ, and each is a property of Postgres rather than a choice:
|
|
12
|
+
*
|
|
13
|
+
* 1. **A closed value set is `text` plus a CHECK built from the same tuple that
|
|
14
|
+
* types it**, never a Postgres `enum` type. Adding an enforcement action
|
|
15
|
+
* becomes a migration where Mongo only needed a restart, which is the correct
|
|
16
|
+
* trade: a stored action outside the declared set is exactly what the
|
|
17
|
+
* constraint exists to refuse.
|
|
18
|
+
* 2. **The TTL indexes have no counterpart.** Postgres does not reap. Each
|
|
19
|
+
* `expires_at` keeps its index — the sweep's predicate needs it — and the
|
|
20
|
+
* reaping itself becomes an entry in the adopter's expiry registry; see
|
|
21
|
+
* `moderationExpirySweepTargets` in `registries.ts`. A table ported without
|
|
22
|
+
* one grows forever, with no error and no failing test.
|
|
23
|
+
* 3. **The enforcement unique index IS the primary key.** Mongo needed a
|
|
24
|
+
* surrogate `_id` plus a unique index on the idempotency triple; here the
|
|
25
|
+
* triple is the primary key, so there is no second object to keep in step.
|
|
26
|
+
*
|
|
27
|
+
* Every column is named EXPLICITLY. Drizzle can derive a snake_case name from
|
|
28
|
+
* the property, and its derivation mangles digit- and capital-adjacent names
|
|
29
|
+
* (`cacheS3Key` becomes `cache_s_3_key` — a working column with a name nobody
|
|
30
|
+
* chose). The SQL name has to match what the adopter's migration created, so it
|
|
31
|
+
* is written down here rather than computed.
|
|
32
|
+
*/
|
|
33
|
+
/**
|
|
34
|
+
* `timestamptz()` from `@oxy.so/db` takes NO name argument, so these column names
|
|
35
|
+
* are DERIVED from the property by `DATABASE_CASING` rather than written out.
|
|
36
|
+
*
|
|
37
|
+
* That is the one exception to naming every column explicitly, and it is the
|
|
38
|
+
* package's decision rather than this one's: the builder's whole point is that
|
|
39
|
+
* "timestamps are `timestamptz`" is decided once, and re-implementing it here
|
|
40
|
+
* with `timestamp(name, { withTimezone: true })` would be a second copy of the
|
|
41
|
+
* rule that could drift. Every derived name in this file is a plain two-word
|
|
42
|
+
* property (`availableAt`, `expiresAt`, …) with no digit and no adjacent capital,
|
|
43
|
+
* so the derivation has nothing to mangle — and `postgresSchema.test.ts` asserts
|
|
44
|
+
* the EXACT column-name set of every table against the real catalogue, which is
|
|
45
|
+
* what would catch a derivation surprise rather than this comment.
|
|
46
|
+
*/
|
|
47
|
+
/**
|
|
48
|
+
* The three table names are kept beside their definitions. The schema tests
|
|
49
|
+
* assert their exact values so a rename cannot strand deployed data.
|
|
50
|
+
*/
|
|
51
|
+
const OUTBOX_TABLE = 'moderation_outbox';
|
|
52
|
+
const EVENT_TABLE = 'moderation_events';
|
|
53
|
+
const ENFORCEMENT_TABLE = 'moderation_enforcements';
|
|
54
|
+
/** The two kinds of work this package enqueues. Types the column and its CHECK. */
|
|
55
|
+
const OUTBOX_KINDS = ['report.submit', 'decision.apply'];
|
|
56
|
+
const OUTBOX_STATUSES = ['pending', 'processing', 'processed', 'dead_letter'];
|
|
57
|
+
const EVENT_STATES = ['claimed', 'queued', 'ignored'];
|
|
58
|
+
const ENFORCEMENT_MODES = [
|
|
59
|
+
'observe',
|
|
60
|
+
'manual',
|
|
61
|
+
'automatic',
|
|
62
|
+
];
|
|
63
|
+
/**
|
|
64
|
+
* Build the three tables.
|
|
65
|
+
*
|
|
66
|
+
* Call this ONCE and export the result. Two calls produce two distinct sets of
|
|
67
|
+
* drizzle table objects for the same SQL names, and drizzle-kit would then see
|
|
68
|
+
* the schema twice.
|
|
69
|
+
*
|
|
70
|
+
* `enforcementActions` is the adopter's own action union. It types the CHECK on
|
|
71
|
+
* `action` and `recorded_as`, which is why the tables are a function rather than
|
|
72
|
+
* module-level constants.
|
|
73
|
+
*/
|
|
74
|
+
export function moderationTables(options) {
|
|
75
|
+
const actions = inList(options.enforcementActions);
|
|
76
|
+
/**
|
|
77
|
+
* The durable record of moderation work that has to happen but has not
|
|
78
|
+
* happened yet.
|
|
79
|
+
*
|
|
80
|
+
* This table is what makes the intake guarantee true. A 201 from an
|
|
81
|
+
* application's report route means the report and its outbox row committed in
|
|
82
|
+
* ONE transaction — not that a call to CrowdSource succeeded. Delivery is a
|
|
83
|
+
* separate, retried step, and the user is never made to wait for a third party
|
|
84
|
+
* to be reachable.
|
|
85
|
+
*
|
|
86
|
+
* The same shape carries work in the other direction. A decision arriving over
|
|
87
|
+
* a webhook is answered 2xx as soon as it is recorded here, and applied
|
|
88
|
+
* afterwards. Nothing is enqueued that is not already written down: if the
|
|
89
|
+
* dispatcher, the process or the whole task disappears, every pending piece of
|
|
90
|
+
* moderation work is re-derivable by reading this table.
|
|
91
|
+
*/
|
|
92
|
+
const outbox = pgTable(OUTBOX_TABLE, {
|
|
93
|
+
/**
|
|
94
|
+
* The deterministic event id — `moderation:report.submit:<reportId>` or
|
|
95
|
+
* `moderation:decision.apply:<eventId>`.
|
|
96
|
+
*
|
|
97
|
+
* The primary key, and that is the mechanism rather than a convention: it
|
|
98
|
+
* is what makes a repeated enqueue a no-op instead of a second delivery.
|
|
99
|
+
* `text` rather than a generated id, because the WRITER computes it from
|
|
100
|
+
* the report or the inbound event.
|
|
101
|
+
*/
|
|
102
|
+
id: text('id').primaryKey(),
|
|
103
|
+
/**
|
|
104
|
+
* `$type` on the closed sets, so a row read back is typed as the union the
|
|
105
|
+
* CHECK already enforces rather than as a bare `string` the store would
|
|
106
|
+
* have to assert. TypeScript-only: it changes no DDL.
|
|
107
|
+
*/
|
|
108
|
+
kind: text('kind').$type().notNull(),
|
|
109
|
+
/**
|
|
110
|
+
* The payload, stored whole and opaque.
|
|
111
|
+
*
|
|
112
|
+
* `jsonb` rather than columns: a decision document is deliberately loose,
|
|
113
|
+
* and a projection would silently drop whatever a newer CrowdSource added.
|
|
114
|
+
* It is validated against the published contract when it is READ, so an
|
|
115
|
+
* event is never lost to a schema this deployment has not caught up with.
|
|
116
|
+
*/
|
|
117
|
+
payload: jsonb('payload').$type().notNull(),
|
|
118
|
+
status: text('status').$type().notNull().default('pending'),
|
|
119
|
+
/** Counted by the claim; the retry ceiling reads it. */
|
|
120
|
+
attempts: integer('attempts').notNull().default(0),
|
|
121
|
+
availableAt: timestamptz().notNull(),
|
|
122
|
+
leaseOwner: text('lease_owner'),
|
|
123
|
+
leaseUntil: timestamptz(),
|
|
124
|
+
/**
|
|
125
|
+
* The last delivery error, bounded.
|
|
126
|
+
*
|
|
127
|
+
* `varchar(2000)` matches the Mongoose `maxlength`, and the application
|
|
128
|
+
* slices to the same 2000 before writing. Both halves are needed: a
|
|
129
|
+
* Mongoose validator THROWS on overflow and Postgres errors `22001`, so
|
|
130
|
+
* the slice is what makes the two dialects agree rather than one of them
|
|
131
|
+
* failing a delivery over an error message.
|
|
132
|
+
*/
|
|
133
|
+
lastError: varchar('last_error', { length: 2000 }),
|
|
134
|
+
processedAt: timestamptz(),
|
|
135
|
+
/** The retention deadline. Swept, not reaped — see the registry. */
|
|
136
|
+
expiresAt: timestamptz().notNull(),
|
|
137
|
+
createdAt: createdAt(),
|
|
138
|
+
updatedAt: updatedAt(),
|
|
139
|
+
}, (t) => [
|
|
140
|
+
check('moderation_outbox_kind_check', sql `${t.kind} in (${sql.raw(inList(OUTBOX_KINDS))})`),
|
|
141
|
+
check('moderation_outbox_status_check', sql `${t.status} in (${sql.raw(inList(OUTBOX_STATUSES))})`),
|
|
142
|
+
// Due work and expired claims are separate bounded scans.
|
|
143
|
+
index('moderation_outbox_due_idx').on(t.status, t.availableAt, t.createdAt),
|
|
144
|
+
index('moderation_outbox_lease_idx').on(t.status, t.leaseUntil, t.createdAt),
|
|
145
|
+
// The sweep's predicate is `expires_at <= now()`; without this it is a
|
|
146
|
+
// full scan on every run — the cost Mongo's TTL index hid.
|
|
147
|
+
index('moderation_outbox_expires_at_idx').on(t.expiresAt),
|
|
148
|
+
]);
|
|
149
|
+
/**
|
|
150
|
+
* Every webhook event CrowdSource has delivered to this deployment.
|
|
151
|
+
*
|
|
152
|
+
* Two jobs, and they are the same row on purpose.
|
|
153
|
+
*
|
|
154
|
+
* **Deduplication.** A receiver must record the processed event id. `id` IS
|
|
155
|
+
* the event id, so the primary key is the dedupe: a redelivery cannot insert a
|
|
156
|
+
* second row, and a claim therefore cannot succeed twice. Doing this in the
|
|
157
|
+
* database rather than in the SDK's default in-process store is not
|
|
158
|
+
* optimisation — an application running several tasks behind one load balancer
|
|
159
|
+
* would otherwise dedupe only whichever task happened to receive both copies.
|
|
160
|
+
*
|
|
161
|
+
* **Audit.** What arrived, when, and whether it was acted on. `payload` is the
|
|
162
|
+
* event's `data` exactly as delivered.
|
|
163
|
+
*
|
|
164
|
+
* The stored payload is a decision — an outcome, findings, policy versions, a
|
|
165
|
+
* jury summary. It is not the reported material and must not become a place
|
|
166
|
+
* where reported material is kept: nothing in this table is read into a log
|
|
167
|
+
* line.
|
|
168
|
+
*/
|
|
169
|
+
const events = pgTable(EVENT_TABLE, {
|
|
170
|
+
/** The webhook event id. The primary key IS the deduplication. */
|
|
171
|
+
id: text('id').primaryKey(),
|
|
172
|
+
type: text('type'),
|
|
173
|
+
caseId: text('case_id'),
|
|
174
|
+
payload: jsonb('payload'),
|
|
175
|
+
state: text('state').$type().notNull().default('claimed'),
|
|
176
|
+
receivedAt: timestamptz().notNull(),
|
|
177
|
+
queuedAt: timestamptz(),
|
|
178
|
+
expiresAt: timestamptz().notNull(),
|
|
179
|
+
createdAt: createdAt(),
|
|
180
|
+
updatedAt: updatedAt(),
|
|
181
|
+
}, (t) => [
|
|
182
|
+
check('moderation_events_state_check', sql `${t.state} in (${sql.raw(inList(EVENT_STATES))})`),
|
|
183
|
+
index('moderation_events_case_id_idx').on(t.caseId),
|
|
184
|
+
// Operational: what arrived recently, and what never got past `claimed`.
|
|
185
|
+
index('moderation_events_state_received_at_idx').on(t.state, t.receivedAt),
|
|
186
|
+
index('moderation_events_expires_at_idx').on(t.expiresAt),
|
|
187
|
+
]);
|
|
188
|
+
/**
|
|
189
|
+
* What the application did about a decision — one row per action, and the
|
|
190
|
+
* reason it is impossible to do twice.
|
|
191
|
+
*
|
|
192
|
+
* The idempotency key is `decision_id + decision_revision + action`, and here
|
|
193
|
+
* it is the PRIMARY KEY. That is the whole mechanism: a redelivered webhook, a
|
|
194
|
+
* reclaimed outbox lease and a manual replay all try to insert the same row,
|
|
195
|
+
* and only the first can. Checking "have I done this?" with a read before a
|
|
196
|
+
* write would leave the window between them, which is precisely the window a
|
|
197
|
+
* redelivery arrives in.
|
|
198
|
+
*
|
|
199
|
+
* `decision_revision` is in the key for a reason of its own. A correction is a
|
|
200
|
+
* NEW revision that supersedes the old one, so the restore it asks for is a
|
|
201
|
+
* different action from the removal that came before and must be allowed to
|
|
202
|
+
* happen — while still being impossible to apply twice itself.
|
|
203
|
+
*
|
|
204
|
+
* `previous_state` is what makes reversibility real rather than aspirational.
|
|
205
|
+
* It is the application's own opaque record of what an effect replaced, so a
|
|
206
|
+
* restore returns an object to what it WAS and a correction does not silently
|
|
207
|
+
* lift a content warning moderation never set.
|
|
208
|
+
*/
|
|
209
|
+
const enforcements = pgTable(ENFORCEMENT_TABLE, {
|
|
210
|
+
decisionId: text('decision_id').notNull(),
|
|
211
|
+
decisionRevision: integer('decision_revision').notNull(),
|
|
212
|
+
action: text('action').notNull(),
|
|
213
|
+
caseId: text('case_id').notNull(),
|
|
214
|
+
/** The application's own noun. Never a CrowdSource resource id. */
|
|
215
|
+
subjectType: text('subject_type').notNull(),
|
|
216
|
+
subjectId: text('subject_id').notNull(),
|
|
217
|
+
outcome: text('outcome').notNull(),
|
|
218
|
+
recommendedAction: text('recommended_action'),
|
|
219
|
+
/**
|
|
220
|
+
* What the action amounted to, when the effect said it was not what was
|
|
221
|
+
* planned. The `action` column stays the PLANNED one — it is part of the
|
|
222
|
+
* primary key and it is what was decided.
|
|
223
|
+
*/
|
|
224
|
+
recordedAs: text('recorded_as'),
|
|
225
|
+
/** Why this action, in words an operator can read. Never reported material. */
|
|
226
|
+
reason: varchar('reason', { length: 500 }).notNull(),
|
|
227
|
+
mode: text('mode').$type().notNull(),
|
|
228
|
+
/**
|
|
229
|
+
* Whether the effect was actually carried out.
|
|
230
|
+
*
|
|
231
|
+
* `false` in `observe` mode for every action, which is the point of the
|
|
232
|
+
* mode: the plan is recorded and auditable, and nothing is removed.
|
|
233
|
+
*/
|
|
234
|
+
applied: boolean('applied').notNull().default(false),
|
|
235
|
+
appliedAt: timestamptz(),
|
|
236
|
+
/** Why an action was recorded but not carried out. */
|
|
237
|
+
skippedReason: varchar('skipped_reason', { length: 300 }),
|
|
238
|
+
/**
|
|
239
|
+
* What to put back on a reversal. Only set for an action that changed
|
|
240
|
+
* state, so NULL here means "this action displaced nothing" — which is a
|
|
241
|
+
* different claim from "we did not look", and is why the reversal lookup
|
|
242
|
+
* filters on `applied` rather than reading whichever row is newest.
|
|
243
|
+
*/
|
|
244
|
+
previousState: jsonb('previous_state').$type(),
|
|
245
|
+
createdAt: createdAt(),
|
|
246
|
+
updatedAt: updatedAt(),
|
|
247
|
+
}, (t) => [
|
|
248
|
+
/**
|
|
249
|
+
* The idempotency key. Unique because it is the PRIMARY KEY, and
|
|
250
|
+
* load-bearing: without it a redelivered decision removes an object twice,
|
|
251
|
+
* and a redelivered correction restores it twice.
|
|
252
|
+
*/
|
|
253
|
+
primaryKey({
|
|
254
|
+
name: 'moderation_enforcements_pkey',
|
|
255
|
+
columns: [t.decisionId, t.decisionRevision, t.action],
|
|
256
|
+
}),
|
|
257
|
+
check('moderation_enforcements_revision_check', sql `${t.decisionRevision} >= 1`),
|
|
258
|
+
check('moderation_enforcements_action_check', sql `${t.action} in (${sql.raw(actions)})`),
|
|
259
|
+
check('moderation_enforcements_recorded_as_check', sql `${t.recordedAs} is null or ${t.recordedAs} in (${sql.raw(actions)})`),
|
|
260
|
+
check('moderation_enforcements_mode_check', sql `${t.mode} in (${sql.raw(inList(ENFORCEMENT_MODES))})`),
|
|
261
|
+
index('moderation_enforcements_case_id_idx').on(t.caseId),
|
|
262
|
+
// Operational: what has been done to this object, newest first.
|
|
263
|
+
index('moderation_enforcements_subject_chrono_idx').on(t.subjectType, t.subjectId, t.createdAt.desc()),
|
|
264
|
+
// The reversal lookup: the most recent APPLIED row for one action set on
|
|
265
|
+
// one object. `applied` is in the index because it is in every such query.
|
|
266
|
+
index('moderation_enforcements_subject_action_applied_idx').on(t.subjectType, t.subjectId, t.action, t.applied, t.createdAt.desc()),
|
|
267
|
+
]);
|
|
268
|
+
return { outbox, events, enforcements };
|
|
269
|
+
}
|
|
270
|
+
//# sourceMappingURL=tables.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tables.js","sourceRoot":"","sources":["../../../../src/outbox/postgres/tables.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { reportSubmitEventId } from './outbox/service.js';
|
|
2
|
+
/**
|
|
3
|
+
* Finding the reports the pipeline lost sight of.
|
|
4
|
+
*
|
|
5
|
+
* The outbox makes delivery durable, not infallible. Four divergences are
|
|
6
|
+
* possible and none of them announces itself:
|
|
7
|
+
*
|
|
8
|
+
* 1. A report that should have a delivery event and does not — one whose event
|
|
9
|
+
* was dropped by a retention TTL while the deployment was down for longer
|
|
10
|
+
* than the retention window, or one whose enqueue was lost to an operator's
|
|
11
|
+
* intervention.
|
|
12
|
+
* 2. A report stuck at `delivery_failed` whose outbox event has been
|
|
13
|
+
* dead-lettered. That one is not re-queued: something about the payload has
|
|
14
|
+
* to change first, and re-queueing it would spin. It is COUNTED, because the
|
|
15
|
+
* count is the alert.
|
|
16
|
+
* 3. A report `submitted` long ago whose case never came back. Nothing to do
|
|
17
|
+
* locally — the decision is CrowdSource's to publish — but a rising count is
|
|
18
|
+
* how a broken webhook endpoint or a rotated secret becomes visible before
|
|
19
|
+
* somebody notices a quiet moderation queue.
|
|
20
|
+
* 4. A report that was never going anywhere: `received`, because its type has no
|
|
21
|
+
* subject provider, or because it predates the integration. This is the one
|
|
22
|
+
* divergence that is not a fault, so it is counted and NEVER re-queued —
|
|
23
|
+
* re-deriving a delivery event for a report nothing can describe would send
|
|
24
|
+
* it straight to the dead-letter queue and turn a deliberate local-only
|
|
25
|
+
* report into a recurring alert. Counting it is still worth doing: it is the
|
|
26
|
+
* only number that makes "reports stored here that no jury will ever see"
|
|
27
|
+
* visible at all, and that is precisely the cost of accepting them.
|
|
28
|
+
*
|
|
29
|
+
* A sweep only ever RE-DERIVES work from the reports; it never invents any.
|
|
30
|
+
* Everything it enqueues uses the same deterministic event id as the original,
|
|
31
|
+
* so a report that did have an event is untouched rather than delivered twice.
|
|
32
|
+
*/
|
|
33
|
+
const DEFAULT_BATCH_SIZE = 200;
|
|
34
|
+
const MAX_BATCH_SIZE = 1000;
|
|
35
|
+
const DEFAULT_STALE_SUBMITTED_HOURS = 72;
|
|
36
|
+
const DEFAULT_INTERVAL_MS = 15 * 60 * 1000;
|
|
37
|
+
/**
|
|
38
|
+
* One sweep. Bounded, idempotent, safe to run on every task.
|
|
39
|
+
*
|
|
40
|
+
* Reads `queued` and `delivery_failed` reports oldest-first — the index on
|
|
41
|
+
* `{ localStatus, createdAt }` exists for this query — and re-enqueues only
|
|
42
|
+
* those with no outbox event at all.
|
|
43
|
+
*/
|
|
44
|
+
export function createReconciliation(input) {
|
|
45
|
+
const staleHours = input.staleSubmittedHours ?? DEFAULT_STALE_SUBMITTED_HOURS;
|
|
46
|
+
return async (options = {}) => {
|
|
47
|
+
const batchSize = Math.min(Math.max(1, options.batchSize ?? DEFAULT_BATCH_SIZE), MAX_BATCH_SIZE);
|
|
48
|
+
const now = options.now ?? new Date();
|
|
49
|
+
const result = {
|
|
50
|
+
requeued: 0,
|
|
51
|
+
deadLettered: 0,
|
|
52
|
+
awaitingDecision: 0,
|
|
53
|
+
localOnly: 0,
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* `queued` and `delivery_failed` only — the store's own query says so, and
|
|
57
|
+
* the omission of `received` is the safety property rather than an
|
|
58
|
+
* oversight: those reports have no subject provider, so an event re-derived
|
|
59
|
+
* for one would fail as `ModerationSubjectUnsupportedError` on its first
|
|
60
|
+
* attempt and dead-letter. They are counted below instead.
|
|
61
|
+
*/
|
|
62
|
+
const pending = await input.reports.findPendingOldestFirst(batchSize);
|
|
63
|
+
for (const reportId of pending) {
|
|
64
|
+
const eventId = reportSubmitEventId(reportId);
|
|
65
|
+
const status = await input.outbox.statusOf(eventId);
|
|
66
|
+
if (status === 'dead_letter') {
|
|
67
|
+
result.deadLettered += 1;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (status !== null)
|
|
71
|
+
continue;
|
|
72
|
+
/**
|
|
73
|
+
* A transaction for a single upsert, for consistency with intake rather
|
|
74
|
+
* than for atomicity: the enqueue requires a transaction precisely so that
|
|
75
|
+
* no path in this package can write an outbox event outside one. A
|
|
76
|
+
* signature that made it optional would be the crack the next caller slips
|
|
77
|
+
* through, and the runner is what makes taking one here cost a line.
|
|
78
|
+
*/
|
|
79
|
+
await input.transaction.run(async (tx) => {
|
|
80
|
+
await input.outbox.enqueue({ eventId, kind: 'report.submit', payload: { reportId } }, tx);
|
|
81
|
+
});
|
|
82
|
+
result.requeued += 1;
|
|
83
|
+
}
|
|
84
|
+
result.awaitingDecision = await input.reports.countAwaitingDecision(new Date(now.getTime() - staleHours * 60 * 60 * 1000));
|
|
85
|
+
result.localOnly = await input.reports.countLocalOnly();
|
|
86
|
+
if (result.requeued > 0 || result.deadLettered > 0) {
|
|
87
|
+
input.logger.warn('[CrowdSource] reconciliation found divergence', { ...result });
|
|
88
|
+
}
|
|
89
|
+
else if (result.awaitingDecision > 0 || result.localOnly > 0) {
|
|
90
|
+
input.logger.info('[CrowdSource] reports with no decision to apply', {
|
|
91
|
+
awaitingDecision: result.awaitingDecision,
|
|
92
|
+
olderThanHours: staleHours,
|
|
93
|
+
localOnly: result.localOnly,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return result;
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* The reconciliation sweep, on a timer.
|
|
101
|
+
*
|
|
102
|
+
* Start it from a LEADER-elected scheduler only. Unlike the outbox dispatcher,
|
|
103
|
+
* whose per-event lease makes it safe on every task, this sweep scans and counts
|
|
104
|
+
* across the whole collection; running it on every task would multiply that work
|
|
105
|
+
* by the task count for no benefit.
|
|
106
|
+
*
|
|
107
|
+
* Long interval on purpose. It is a safety net for divergences the durable path
|
|
108
|
+
* is supposed to prevent, not a delivery mechanism — a report that needs this
|
|
109
|
+
* sweep to be delivered is already a report something went wrong with.
|
|
110
|
+
*/
|
|
111
|
+
export class ModerationReconciliationJob {
|
|
112
|
+
constructor(options) {
|
|
113
|
+
this.options = options;
|
|
114
|
+
this.timer = null;
|
|
115
|
+
this.inFlight = null;
|
|
116
|
+
this.running = false;
|
|
117
|
+
}
|
|
118
|
+
start() {
|
|
119
|
+
if (this.running)
|
|
120
|
+
return;
|
|
121
|
+
if (!this.options.enabled)
|
|
122
|
+
return;
|
|
123
|
+
const intervalMs = this.options.intervalMs ?? DEFAULT_INTERVAL_MS;
|
|
124
|
+
this.running = true;
|
|
125
|
+
this.timer = setInterval(() => {
|
|
126
|
+
void this.tick();
|
|
127
|
+
}, intervalMs);
|
|
128
|
+
this.timer.unref?.();
|
|
129
|
+
this.options.logger.info('[CrowdSource] reconciliation job started', { intervalMs });
|
|
130
|
+
}
|
|
131
|
+
stop() {
|
|
132
|
+
this.running = false;
|
|
133
|
+
if (this.timer) {
|
|
134
|
+
clearInterval(this.timer);
|
|
135
|
+
this.timer = null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
async tick() {
|
|
139
|
+
if (!this.running || this.inFlight)
|
|
140
|
+
return;
|
|
141
|
+
const work = this.options
|
|
142
|
+
.reconcile()
|
|
143
|
+
.then(() => undefined)
|
|
144
|
+
.catch((error) => {
|
|
145
|
+
this.options.logger.error('[CrowdSource] reconciliation sweep failed', {
|
|
146
|
+
error: error instanceof Error ? error.message : String(error),
|
|
147
|
+
});
|
|
148
|
+
})
|
|
149
|
+
.finally(() => {
|
|
150
|
+
if (this.inFlight === work)
|
|
151
|
+
this.inFlight = null;
|
|
152
|
+
});
|
|
153
|
+
this.inFlight = work;
|
|
154
|
+
await work;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
//# sourceMappingURL=reconciliation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reconciliation.js","sourceRoot":"","sources":["../../../src/outbox/reconciliation.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decision statuses that end the application's side of the case.
|
|
3
|
+
*
|
|
4
|
+
* A `provisional` decision leaves the report at `submitted`: a later revision
|
|
5
|
+
* may supersede it, and a report the application had already closed would have
|
|
6
|
+
* to be reopened. `superseded` is not here either — a superseded revision is not
|
|
7
|
+
* the current answer and must never be the one that closes the report.
|
|
8
|
+
*/
|
|
9
|
+
const TERMINAL_DECISION_STATUSES = new Set(['final', 'corrected']);
|
|
10
|
+
/**
|
|
11
|
+
* Where a decision leaves the report.
|
|
12
|
+
*
|
|
13
|
+
* Takes the decision status as a plain STRING on purpose. This is reached with a
|
|
14
|
+
* value that came off the wire, and a newer CrowdSource introducing a status
|
|
15
|
+
* this version has never seen must be handled rather than throw. An unrecognised
|
|
16
|
+
* status leaves the report open, which is the only safe reading: closing a
|
|
17
|
+
* report on a status nobody has defined would retire a case that may still move.
|
|
18
|
+
*/
|
|
19
|
+
export function localStatusForDecision(decisionStatus) {
|
|
20
|
+
return TERMINAL_DECISION_STATUSES.has(decisionStatus) ? 'closed' : 'submitted';
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=reportStatus.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reportStatus.js","sourceRoot":"","sources":["../../../src/outbox/reportStatus.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AACH,MAAM,0BAA0B,GAAwB,IAAI,GAAG,CAAC,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC,CAAC;AAExF;;;;;;;;GAQG;AACH,MAAM,UAAU,sBAAsB,CAAC,cAAsB;IAC3D,OAAO,0BAA0B,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,WAAW,CAAC;AACjF,CAAC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How long this package keeps the two things it writes down for its own sake.
|
|
3
|
+
*
|
|
4
|
+
* Policy, not storage, which is why it lives in the shared half: a deployment
|
|
5
|
+
* whose outbox rows expired after an hour and whose event rows expired after
|
|
6
|
+
* ninety days would have two different answers to "what happened to that
|
|
7
|
+
* report", and neither backend would consider that an error. Both stores are
|
|
8
|
+
* handed the computed `expiresAt` rather than the window, so the window has one
|
|
9
|
+
* definition.
|
|
10
|
+
*
|
|
11
|
+
* How the deadline is ENFORCED is a backend's business — a Mongo TTL index on
|
|
12
|
+
* `expiresAt`, an expiry sweep on Postgres — but a table registered with
|
|
13
|
+
* neither grows forever with no error and no failing test.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* The outbox.
|
|
17
|
+
*
|
|
18
|
+
* A retention ceiling, so a stalled dispatcher cannot turn the outbox into an
|
|
19
|
+
* unbounded table. Long, because a moderation case can legitimately sit open for
|
|
20
|
+
* weeks and a `dead_letter` event is evidence somebody still has to look at.
|
|
21
|
+
* Operational alerts must fire long before this deadline.
|
|
22
|
+
*/
|
|
23
|
+
export const MODERATION_OUTBOX_RETENTION_SECONDS = 90 * 24 * 60 * 60;
|
|
24
|
+
/**
|
|
25
|
+
* Inbound webhook events.
|
|
26
|
+
*
|
|
27
|
+
* CrowdSource's retry schedule ends at 24 hours, so a dedupe row only has to
|
|
28
|
+
* outlive that. It is kept far longer because the row is also the audit trail of
|
|
29
|
+
* what a third party told this deployment to do, and an enforcement question
|
|
30
|
+
* asked weeks later is answered from here.
|
|
31
|
+
*/
|
|
32
|
+
export const MODERATION_EVENT_RETENTION_SECONDS = 90 * 24 * 60 * 60;
|
|
33
|
+
//# sourceMappingURL=retention.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retention.js","sourceRoot":"","sources":["../../../src/outbox/retention.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,mCAAmC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC;AAErE;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,kCAAkC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The storage port: everything this package writes down, as a contract.
|
|
3
|
+
*
|
|
4
|
+
* INTERNAL, and deliberately so. An adopting application never implements any
|
|
5
|
+
* of this — it imports the PostgreSQL subpath and the implementation ships with
|
|
6
|
+
* the package. The port keeps storage mechanics out of policy code; it is not a
|
|
7
|
+
* seam an application is invited to fill:
|
|
8
|
+
* a port with this many members handed to seven applications would be seven
|
|
9
|
+
* chances to get the revision guard, the lease ownership check or the
|
|
10
|
+
* insert-if-absent wrong, each in a way nothing fails on.
|
|
11
|
+
*
|
|
12
|
+
* Which is why every member below carries the correctness property it has to
|
|
13
|
+
* preserve. The comments are not documentation of an obvious signature; they
|
|
14
|
+
* are the reason the implementation can be audited. A method whose
|
|
15
|
+
* comment says `true` means THIS call took the claim cannot be implemented as
|
|
16
|
+
* "row exists" without the difference being visible.
|
|
17
|
+
*
|
|
18
|
+
* Two rules shape the whole shape:
|
|
19
|
+
*
|
|
20
|
+
* 1. **The store applies transitions; it never decides them.** `fail` takes the
|
|
21
|
+
* status and the next attempt time already computed, `claim` takes an
|
|
22
|
+
* already-computed `leaseUntil`. Backoff, the retry ceiling, the lease-length
|
|
23
|
+
* floor and the retryable/permanent classification live in the shared half,
|
|
24
|
+
* outside the PostgreSQL query implementation.
|
|
25
|
+
* 2. **No opaque record id crosses the port.** An enforcement row is addressed
|
|
26
|
+
* by the natural triple it is keyed on, so the same three values reach the
|
|
27
|
+
* same row through its PostgreSQL composite primary key.
|
|
28
|
+
*/
|
|
29
|
+
export {};
|
|
30
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../../src/outbox/store/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The public surface an application implements.
|
|
3
|
+
*
|
|
4
|
+
* Everything in this file is a seam. Nothing in it knows what a post, a listing,
|
|
5
|
+
* a message or a property is — that knowledge belongs to the application and
|
|
6
|
+
* reaches this package through exactly four ports:
|
|
7
|
+
*
|
|
8
|
+
* 1. {@link ModerationSubjectProvider} — "given one of MY nouns and its id,
|
|
9
|
+
* describe the material".
|
|
10
|
+
* 2. {@link ModerationTaxonomy} — "what my reporters can pick, as universal
|
|
11
|
+
* allegation codes".
|
|
12
|
+
* 3. {@link ModerationEnforcementConfig} — "what I can do about a decision, and
|
|
13
|
+
* how to do and undo it".
|
|
14
|
+
* 4. {@link ModerationStore} — where its reports and this package's own rows are
|
|
15
|
+
* kept, built by the backend factory it chose and passed in whole.
|
|
16
|
+
*
|
|
17
|
+
* Everything else — the outbox, the transaction coupling, delivery, the webhook
|
|
18
|
+
* receiver, deduplication, decision application, enforcement idempotency and
|
|
19
|
+
* reversibility, reconciliation — is identical in every application and is
|
|
20
|
+
* imported, not written.
|
|
21
|
+
*/
|
|
22
|
+
export {};
|
|
23
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/outbox/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG"}
|