@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.
Files changed (339) hide show
  1. package/README.md +571 -0
  2. package/dist/cases.d.ts +66 -0
  3. package/dist/cases.d.ts.map +1 -0
  4. package/dist/cases.js +79 -0
  5. package/dist/cases.js.map +1 -0
  6. package/dist/client.d.ts +90 -0
  7. package/dist/client.d.ts.map +1 -0
  8. package/dist/client.js +125 -0
  9. package/dist/client.js.map +1 -0
  10. package/dist/communityNotes.d.ts +49 -0
  11. package/dist/communityNotes.d.ts.map +1 -0
  12. package/dist/communityNotes.js +104 -0
  13. package/dist/communityNotes.js.map +1 -0
  14. package/dist/credential.d.ts +58 -0
  15. package/dist/credential.d.ts.map +1 -0
  16. package/dist/credential.js +71 -0
  17. package/dist/credential.js.map +1 -0
  18. package/dist/defaults.d.ts +61 -0
  19. package/dist/defaults.d.ts.map +1 -0
  20. package/dist/defaults.js +82 -0
  21. package/dist/defaults.js.map +1 -0
  22. package/dist/digest.d.ts +44 -0
  23. package/dist/digest.d.ts.map +1 -0
  24. package/dist/digest.js +79 -0
  25. package/dist/digest.js.map +1 -0
  26. package/dist/envelope.d.ts +143 -0
  27. package/dist/envelope.d.ts.map +1 -0
  28. package/dist/envelope.js +258 -0
  29. package/dist/envelope.js.map +1 -0
  30. package/dist/errors.d.ts +90 -0
  31. package/dist/errors.d.ts.map +1 -0
  32. package/dist/errors.js +151 -0
  33. package/dist/errors.js.map +1 -0
  34. package/dist/esm/cases.js +74 -0
  35. package/dist/esm/cases.js.map +1 -0
  36. package/dist/esm/client.js +121 -0
  37. package/dist/esm/client.js.map +1 -0
  38. package/dist/esm/communityNotes.js +100 -0
  39. package/dist/esm/communityNotes.js.map +1 -0
  40. package/dist/esm/credential.js +67 -0
  41. package/dist/esm/credential.js.map +1 -0
  42. package/dist/esm/defaults.js +77 -0
  43. package/dist/esm/defaults.js.map +1 -0
  44. package/dist/esm/digest.js +74 -0
  45. package/dist/esm/digest.js.map +1 -0
  46. package/dist/esm/envelope.js +252 -0
  47. package/dist/esm/envelope.js.map +1 -0
  48. package/dist/esm/errors.js +140 -0
  49. package/dist/esm/errors.js.map +1 -0
  50. package/dist/esm/express/index.js +29 -0
  51. package/dist/esm/express/index.js.map +1 -0
  52. package/dist/esm/express/middleware.js +293 -0
  53. package/dist/esm/express/middleware.js.map +1 -0
  54. package/dist/esm/express/store.js +69 -0
  55. package/dist/esm/express/store.js.map +1 -0
  56. package/dist/esm/express/verify.js +133 -0
  57. package/dist/esm/express/verify.js.map +1 -0
  58. package/dist/esm/index.js +57 -0
  59. package/dist/esm/index.js.map +1 -0
  60. package/dist/esm/outbox/client.js +43 -0
  61. package/dist/esm/outbox/client.js.map +1 -0
  62. package/dist/esm/outbox/decision.js +153 -0
  63. package/dist/esm/outbox/decision.js.map +1 -0
  64. package/dist/esm/outbox/delivery.js +129 -0
  65. package/dist/esm/outbox/delivery.js.map +1 -0
  66. package/dist/esm/outbox/enforcement/executor.js +219 -0
  67. package/dist/esm/outbox/enforcement/executor.js.map +1 -0
  68. package/dist/esm/outbox/enforcement/planner.js +326 -0
  69. package/dist/esm/outbox/enforcement/planner.js.map +1 -0
  70. package/dist/esm/outbox/evidence.js +173 -0
  71. package/dist/esm/outbox/evidence.js.map +1 -0
  72. package/dist/esm/outbox/inbound.js +87 -0
  73. package/dist/esm/outbox/inbound.js.map +1 -0
  74. package/dist/esm/outbox/index.js +71 -0
  75. package/dist/esm/outbox/index.js.map +1 -0
  76. package/dist/esm/outbox/intake.js +140 -0
  77. package/dist/esm/outbox/intake.js.map +1 -0
  78. package/dist/esm/outbox/integration.js +116 -0
  79. package/dist/esm/outbox/integration.js.map +1 -0
  80. package/dist/esm/outbox/outbox/dispatcher.js +114 -0
  81. package/dist/esm/outbox/outbox/dispatcher.js.map +1 -0
  82. package/dist/esm/outbox/outbox/service.js +271 -0
  83. package/dist/esm/outbox/outbox/service.js.map +1 -0
  84. package/dist/esm/outbox/postgres/index.js +41 -0
  85. package/dist/esm/outbox/postgres/index.js.map +1 -0
  86. package/dist/esm/outbox/postgres/registries.js +155 -0
  87. package/dist/esm/outbox/postgres/registries.js.map +1 -0
  88. package/dist/esm/outbox/postgres/reportColumns.js +191 -0
  89. package/dist/esm/outbox/postgres/reportColumns.js.map +1 -0
  90. package/dist/esm/outbox/postgres/store/enforcement.js +158 -0
  91. package/dist/esm/outbox/postgres/store/enforcement.js.map +1 -0
  92. package/dist/esm/outbox/postgres/store/events.js +101 -0
  93. package/dist/esm/outbox/postgres/store/events.js.map +1 -0
  94. package/dist/esm/outbox/postgres/store/index.js +59 -0
  95. package/dist/esm/outbox/postgres/store/index.js.map +1 -0
  96. package/dist/esm/outbox/postgres/store/outbox.js +207 -0
  97. package/dist/esm/outbox/postgres/store/outbox.js.map +1 -0
  98. package/dist/esm/outbox/postgres/store/reports.js +231 -0
  99. package/dist/esm/outbox/postgres/store/reports.js.map +1 -0
  100. package/dist/esm/outbox/postgres/store/transaction.js +29 -0
  101. package/dist/esm/outbox/postgres/store/transaction.js.map +1 -0
  102. package/dist/esm/outbox/postgres/tables.js +270 -0
  103. package/dist/esm/outbox/postgres/tables.js.map +1 -0
  104. package/dist/esm/outbox/reconciliation.js +157 -0
  105. package/dist/esm/outbox/reconciliation.js.map +1 -0
  106. package/dist/esm/outbox/reportStatus.js +22 -0
  107. package/dist/esm/outbox/reportStatus.js.map +1 -0
  108. package/dist/esm/outbox/retention.js +33 -0
  109. package/dist/esm/outbox/retention.js.map +1 -0
  110. package/dist/esm/outbox/store/types.js +30 -0
  111. package/dist/esm/outbox/store/types.js.map +1 -0
  112. package/dist/esm/outbox/types.js +23 -0
  113. package/dist/esm/outbox/types.js.map +1 -0
  114. package/dist/esm/outbox/webhook.js +145 -0
  115. package/dist/esm/outbox/webhook.js.map +1 -0
  116. package/dist/esm/package.json +1 -0
  117. package/dist/esm/reports.js +74 -0
  118. package/dist/esm/reports.js.map +1 -0
  119. package/dist/esm/testing/fixtures.js +132 -0
  120. package/dist/esm/testing/fixtures.js.map +1 -0
  121. package/dist/esm/testing/index.js +33 -0
  122. package/dist/esm/testing/index.js.map +1 -0
  123. package/dist/esm/testing/sandbox.js +337 -0
  124. package/dist/esm/testing/sandbox.js.map +1 -0
  125. package/dist/esm/testing/webhook-simulator.js +93 -0
  126. package/dist/esm/testing/webhook-simulator.js.map +1 -0
  127. package/dist/esm/transport.js +177 -0
  128. package/dist/esm/transport.js.map +1 -0
  129. package/dist/esm/webhookEndpoints.js +139 -0
  130. package/dist/esm/webhookEndpoints.js.map +1 -0
  131. package/dist/express/index.d.ts +32 -0
  132. package/dist/express/index.d.ts.map +1 -0
  133. package/dist/express/index.js +40 -0
  134. package/dist/express/index.js.map +1 -0
  135. package/dist/express/middleware.d.ts +97 -0
  136. package/dist/express/middleware.d.ts.map +1 -0
  137. package/dist/express/middleware.js +298 -0
  138. package/dist/express/middleware.js.map +1 -0
  139. package/dist/express/store.d.ts +45 -0
  140. package/dist/express/store.d.ts.map +1 -0
  141. package/dist/express/store.js +72 -0
  142. package/dist/express/store.js.map +1 -0
  143. package/dist/express/verify.d.ts +86 -0
  144. package/dist/express/verify.d.ts.map +1 -0
  145. package/dist/express/verify.js +138 -0
  146. package/dist/express/verify.js.map +1 -0
  147. package/dist/index.d.ts +67 -0
  148. package/dist/index.d.ts.map +1 -0
  149. package/dist/index.js +89 -0
  150. package/dist/index.js.map +1 -0
  151. package/dist/outbox/client.d.ts +35 -0
  152. package/dist/outbox/client.d.ts.map +1 -0
  153. package/dist/outbox/client.js +46 -0
  154. package/dist/outbox/client.js.map +1 -0
  155. package/dist/outbox/decision.d.ts +36 -0
  156. package/dist/outbox/decision.d.ts.map +1 -0
  157. package/dist/outbox/decision.js +159 -0
  158. package/dist/outbox/decision.js.map +1 -0
  159. package/dist/outbox/delivery.d.ts +50 -0
  160. package/dist/outbox/delivery.d.ts.map +1 -0
  161. package/dist/outbox/delivery.js +135 -0
  162. package/dist/outbox/delivery.js.map +1 -0
  163. package/dist/outbox/enforcement/executor.d.ts +26 -0
  164. package/dist/outbox/enforcement/executor.d.ts.map +1 -0
  165. package/dist/outbox/enforcement/executor.js +222 -0
  166. package/dist/outbox/enforcement/executor.js.map +1 -0
  167. package/dist/outbox/enforcement/planner.d.ts +50 -0
  168. package/dist/outbox/enforcement/planner.d.ts.map +1 -0
  169. package/dist/outbox/enforcement/planner.js +333 -0
  170. package/dist/outbox/enforcement/planner.js.map +1 -0
  171. package/dist/outbox/evidence.d.ts +114 -0
  172. package/dist/outbox/evidence.d.ts.map +1 -0
  173. package/dist/outbox/evidence.js +181 -0
  174. package/dist/outbox/evidence.js.map +1 -0
  175. package/dist/outbox/inbound.d.ts +78 -0
  176. package/dist/outbox/inbound.d.ts.map +1 -0
  177. package/dist/outbox/inbound.js +91 -0
  178. package/dist/outbox/inbound.js.map +1 -0
  179. package/dist/outbox/index.d.ts +79 -0
  180. package/dist/outbox/index.d.ts.map +1 -0
  181. package/dist/outbox/index.js +98 -0
  182. package/dist/outbox/index.js.map +1 -0
  183. package/dist/outbox/intake.d.ts +62 -0
  184. package/dist/outbox/intake.d.ts.map +1 -0
  185. package/dist/outbox/intake.js +145 -0
  186. package/dist/outbox/intake.js.map +1 -0
  187. package/dist/outbox/integration.d.ts +62 -0
  188. package/dist/outbox/integration.d.ts.map +1 -0
  189. package/dist/outbox/integration.js +119 -0
  190. package/dist/outbox/integration.js.map +1 -0
  191. package/dist/outbox/outbox/dispatcher.d.ts +26 -0
  192. package/dist/outbox/outbox/dispatcher.d.ts.map +1 -0
  193. package/dist/outbox/outbox/dispatcher.js +119 -0
  194. package/dist/outbox/outbox/dispatcher.js.map +1 -0
  195. package/dist/outbox/outbox/service.d.ts +124 -0
  196. package/dist/outbox/outbox/service.d.ts.map +1 -0
  197. package/dist/outbox/outbox/service.js +279 -0
  198. package/dist/outbox/outbox/service.js.map +1 -0
  199. package/dist/outbox/postgres/index.d.ts +44 -0
  200. package/dist/outbox/postgres/index.d.ts.map +1 -0
  201. package/dist/outbox/postgres/index.js +51 -0
  202. package/dist/outbox/postgres/index.js.map +1 -0
  203. package/dist/outbox/postgres/registries.d.ts +69 -0
  204. package/dist/outbox/postgres/registries.d.ts.map +1 -0
  205. package/dist/outbox/postgres/registries.js +159 -0
  206. package/dist/outbox/postgres/registries.js.map +1 -0
  207. package/dist/outbox/postgres/reportColumns.d.ts +154 -0
  208. package/dist/outbox/postgres/reportColumns.d.ts.map +1 -0
  209. package/dist/outbox/postgres/reportColumns.js +195 -0
  210. package/dist/outbox/postgres/reportColumns.js.map +1 -0
  211. package/dist/outbox/postgres/store/enforcement.d.ts +33 -0
  212. package/dist/outbox/postgres/store/enforcement.d.ts.map +1 -0
  213. package/dist/outbox/postgres/store/enforcement.js +161 -0
  214. package/dist/outbox/postgres/store/enforcement.js.map +1 -0
  215. package/dist/outbox/postgres/store/events.d.ts +46 -0
  216. package/dist/outbox/postgres/store/events.d.ts.map +1 -0
  217. package/dist/outbox/postgres/store/events.js +104 -0
  218. package/dist/outbox/postgres/store/events.js.map +1 -0
  219. package/dist/outbox/postgres/store/index.d.ts +27 -0
  220. package/dist/outbox/postgres/store/index.d.ts.map +1 -0
  221. package/dist/outbox/postgres/store/index.js +62 -0
  222. package/dist/outbox/postgres/store/index.js.map +1 -0
  223. package/dist/outbox/postgres/store/outbox.d.ts +8 -0
  224. package/dist/outbox/postgres/store/outbox.d.ts.map +1 -0
  225. package/dist/outbox/postgres/store/outbox.js +210 -0
  226. package/dist/outbox/postgres/store/outbox.js.map +1 -0
  227. package/dist/outbox/postgres/store/reports.d.ts +69 -0
  228. package/dist/outbox/postgres/store/reports.d.ts.map +1 -0
  229. package/dist/outbox/postgres/store/reports.js +234 -0
  230. package/dist/outbox/postgres/store/reports.js.map +1 -0
  231. package/dist/outbox/postgres/store/transaction.d.ts +49 -0
  232. package/dist/outbox/postgres/store/transaction.d.ts.map +1 -0
  233. package/dist/outbox/postgres/store/transaction.js +32 -0
  234. package/dist/outbox/postgres/store/transaction.js.map +1 -0
  235. package/dist/outbox/postgres/tables.d.ts +737 -0
  236. package/dist/outbox/postgres/tables.d.ts.map +1 -0
  237. package/dist/outbox/postgres/tables.js +273 -0
  238. package/dist/outbox/postgres/tables.js.map +1 -0
  239. package/dist/outbox/reconciliation.d.ts +49 -0
  240. package/dist/outbox/reconciliation.d.ts.map +1 -0
  241. package/dist/outbox/reconciliation.js +162 -0
  242. package/dist/outbox/reconciliation.js.map +1 -0
  243. package/dist/outbox/reportStatus.d.ts +12 -0
  244. package/dist/outbox/reportStatus.d.ts.map +1 -0
  245. package/dist/outbox/reportStatus.js +25 -0
  246. package/dist/outbox/reportStatus.js.map +1 -0
  247. package/dist/outbox/retention.d.ts +33 -0
  248. package/dist/outbox/retention.d.ts.map +1 -0
  249. package/dist/outbox/retention.js +36 -0
  250. package/dist/outbox/retention.js.map +1 -0
  251. package/dist/outbox/store/types.d.ts +466 -0
  252. package/dist/outbox/store/types.d.ts.map +1 -0
  253. package/dist/outbox/store/types.js +31 -0
  254. package/dist/outbox/store/types.js.map +1 -0
  255. package/dist/outbox/types.d.ts +660 -0
  256. package/dist/outbox/types.d.ts.map +1 -0
  257. package/dist/outbox/types.js +24 -0
  258. package/dist/outbox/types.js.map +1 -0
  259. package/dist/outbox/webhook.d.ts +15 -0
  260. package/dist/outbox/webhook.d.ts.map +1 -0
  261. package/dist/outbox/webhook.js +148 -0
  262. package/dist/outbox/webhook.js.map +1 -0
  263. package/dist/reports.d.ts +73 -0
  264. package/dist/reports.d.ts.map +1 -0
  265. package/dist/reports.js +78 -0
  266. package/dist/reports.js.map +1 -0
  267. package/dist/testing/fixtures.d.ts +62 -0
  268. package/dist/testing/fixtures.d.ts.map +1 -0
  269. package/dist/testing/fixtures.js +137 -0
  270. package/dist/testing/fixtures.js.map +1 -0
  271. package/dist/testing/index.d.ts +36 -0
  272. package/dist/testing/index.d.ts.map +1 -0
  273. package/dist/testing/index.js +43 -0
  274. package/dist/testing/index.js.map +1 -0
  275. package/dist/testing/sandbox.d.ts +109 -0
  276. package/dist/testing/sandbox.d.ts.map +1 -0
  277. package/dist/testing/sandbox.js +342 -0
  278. package/dist/testing/sandbox.js.map +1 -0
  279. package/dist/testing/webhook-simulator.d.ts +78 -0
  280. package/dist/testing/webhook-simulator.d.ts.map +1 -0
  281. package/dist/testing/webhook-simulator.js +98 -0
  282. package/dist/testing/webhook-simulator.js.map +1 -0
  283. package/dist/transport.d.ts +59 -0
  284. package/dist/transport.d.ts.map +1 -0
  285. package/dist/transport.js +181 -0
  286. package/dist/transport.js.map +1 -0
  287. package/dist/webhookEndpoints.d.ts +147 -0
  288. package/dist/webhookEndpoints.d.ts.map +1 -0
  289. package/dist/webhookEndpoints.js +143 -0
  290. package/dist/webhookEndpoints.js.map +1 -0
  291. package/package.json +127 -0
  292. package/src/cases.ts +119 -0
  293. package/src/client.ts +202 -0
  294. package/src/communityNotes.ts +194 -0
  295. package/src/credential.ts +100 -0
  296. package/src/defaults.ts +87 -0
  297. package/src/digest.ts +91 -0
  298. package/src/envelope.ts +432 -0
  299. package/src/errors.ts +165 -0
  300. package/src/express/index.ts +43 -0
  301. package/src/express/middleware.ts +387 -0
  302. package/src/express/store.ts +85 -0
  303. package/src/express/verify.ts +166 -0
  304. package/src/index.ts +107 -0
  305. package/src/outbox/client.ts +75 -0
  306. package/src/outbox/decision.ts +209 -0
  307. package/src/outbox/delivery.ts +163 -0
  308. package/src/outbox/enforcement/executor.ts +288 -0
  309. package/src/outbox/enforcement/planner.ts +377 -0
  310. package/src/outbox/evidence.ts +231 -0
  311. package/src/outbox/inbound.ts +140 -0
  312. package/src/outbox/index.ts +160 -0
  313. package/src/outbox/intake.ts +175 -0
  314. package/src/outbox/integration.ts +205 -0
  315. package/src/outbox/outbox/dispatcher.ts +131 -0
  316. package/src/outbox/outbox/service.ts +444 -0
  317. package/src/outbox/postgres/index.ts +59 -0
  318. package/src/outbox/postgres/registries.ts +177 -0
  319. package/src/outbox/postgres/reportColumns.ts +281 -0
  320. package/src/outbox/postgres/store/enforcement.ts +186 -0
  321. package/src/outbox/postgres/store/events.ts +112 -0
  322. package/src/outbox/postgres/store/index.ts +72 -0
  323. package/src/outbox/postgres/store/outbox.ts +273 -0
  324. package/src/outbox/postgres/store/reports.ts +349 -0
  325. package/src/outbox/postgres/store/transaction.ts +64 -0
  326. package/src/outbox/postgres/tables.ts +339 -0
  327. package/src/outbox/reconciliation.ts +199 -0
  328. package/src/outbox/reportStatus.ts +24 -0
  329. package/src/outbox/retention.ts +34 -0
  330. package/src/outbox/store/types.ts +528 -0
  331. package/src/outbox/types.ts +753 -0
  332. package/src/outbox/webhook.ts +165 -0
  333. package/src/reports.ts +138 -0
  334. package/src/testing/fixtures.ts +183 -0
  335. package/src/testing/index.ts +57 -0
  336. package/src/testing/sandbox.ts +469 -0
  337. package/src/testing/webhook-simulator.ts +166 -0
  338. package/src/transport.ts +250 -0
  339. 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"}