@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,753 @@
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
+
23
+ import type { ContextInput, ReportSubjectInput, ResourceInput } from '../index.js';
24
+ import type {
25
+ Decision,
26
+ RecommendedAction,
27
+ Severity,
28
+ TaxonomyCode,
29
+ } from '@crowdsource.you/contracts';
30
+ import type { ModerationStore } from './store/types.js';
31
+
32
+ /* ------------------------------------------------------------------------- */
33
+ /* Subjects — the application's nouns, as universal material */
34
+ /* ------------------------------------------------------------------------- */
35
+
36
+ /**
37
+ * The SDK's resource descriptions, unchanged.
38
+ *
39
+ * Aliased so a provider imports its vocabulary from one place, but these ARE the
40
+ * SDK's types rather than a local restatement: a resource type added to the
41
+ * contract becomes available to every provider the moment the dependency is
42
+ * bumped.
43
+ */
44
+ export type ModerationResource = ResourceInput;
45
+ export type ModerationContextResource = ContextInput;
46
+
47
+ /**
48
+ * One reported object, described.
49
+ *
50
+ * `content` is required because a report with no material is a question a jury
51
+ * cannot answer. An application that cannot produce the material for one of its
52
+ * nouns should not register a provider for it — a reported type with no provider
53
+ * is still accepted and stored, it simply never leaves.
54
+ */
55
+ export interface ModerationSubjectSnapshot {
56
+ /** Identity, type and author of the reported object. */
57
+ readonly subject: ReportSubjectInput;
58
+ /** The reported material itself. A string is shorthand for plain text. */
59
+ readonly content: string | ModerationResource;
60
+ /** Media carried BY the subject. */
61
+ readonly attachments?: readonly ModerationResource[];
62
+ /**
63
+ * Surrounding material a jury needs to judge fairly — the parent of a reply,
64
+ * the listing a review is about. Context, not extra exposure.
65
+ */
66
+ readonly context?: readonly ModerationContextResource[];
67
+ }
68
+
69
+ /**
70
+ * Translates one of the application's nouns into universal material.
71
+ *
72
+ * `subjectType` is declared on the provider rather than returned per snapshot
73
+ * because it is a property of the noun: every Mention post is a `social.post`,
74
+ * every Mercaria product a `commerce.listing`. Keeping it here means the registry
75
+ * can answer "what does this application report?" without loading an object.
76
+ *
77
+ * Two rules keep the seam working, and both are load-bearing rather than
78
+ * stylistic:
79
+ *
80
+ * 1. **A provider returns a DESCRIPTION, never an envelope.** The SDK composes
81
+ * the Case Envelope — resource ids, digests, relations, principal refs, the
82
+ * binding proof, the policy version, the idempotency key — and the case dedup
83
+ * key is computed over exactly those. An application that composed its own
84
+ * envelope would be the reason two reporters about one object opened two
85
+ * cases, and "one penalty per incident" would fail in production with nothing
86
+ * failing in a test.
87
+ * 2. **A provider is pure translation with reads.** It fetches its own object and
88
+ * returns. It does not decide whether to deliver, what the allegation is, or
89
+ * what happens to the report.
90
+ */
91
+ export interface ModerationSubjectProvider {
92
+ /** The application's own name for the noun, as it arrives on a report. */
93
+ readonly reportedType: string;
94
+ /**
95
+ * The namespaced universal subject type, or `custom.<org>.<object_type>`.
96
+ *
97
+ * ## Reporting an ACCOUNT has a tenancy consequence worth knowing first
98
+ *
99
+ * `applicationId` is read off the service credential, so a report this
100
+ * application submits opens a case in ITS tenant. For an object the
101
+ * application owns that is exactly right. For an Oxy IDENTITY it is not: the
102
+ * case names a principal only Oxy can act on, and when a second Oxy
103
+ * application reports the same person under its own credential the dedup key
104
+ * (`applicationId + subject external id + content hash + policy version`)
105
+ * differs by tenant — so one person yields two cases, two juries and two
106
+ * consequences, breaking "one penalty per incident" at a layer nothing inside
107
+ * either application can repair.
108
+ *
109
+ * That is an argument for care, not a prohibition: `identity.profile` is a
110
+ * legitimate subject type, and an application whose own surfaces are what a
111
+ * jury would judge may well register one. What it is not is a way to have
112
+ * somebody's Oxy account sanctioned — an application can never move a
113
+ * reputation figure directly, and cross-application hand-off is a design
114
+ * question the contract does not answer yet. Registering no provider for a
115
+ * reported account is a supported answer: the report is still stored, and
116
+ * still counted, it simply never leaves.
117
+ *
118
+ * Credit: `mercaria` surfaced this while deciding not to give `seller` a
119
+ * provider, and it is app-independent enough to belong here.
120
+ */
121
+ readonly subjectType: string;
122
+ /**
123
+ * Describes the object, or returns `null` when it no longer exists.
124
+ *
125
+ * `null` is not a failure. Content deleted between the report and its delivery
126
+ * is ordinary, and the caller decides what that means — a provider that threw
127
+ * would make deletion look like an outage and be retried for days.
128
+ */
129
+ snapshot(reportedId: string): Promise<ModerationSubjectSnapshot | null>;
130
+ }
131
+
132
+ /* ------------------------------------------------------------------------- */
133
+ /* Taxonomy — the application's categories, as allegations */
134
+ /* ------------------------------------------------------------------------- */
135
+
136
+ /**
137
+ * What a reporter picked, translated into what is being ALLEGED.
138
+ *
139
+ * Versioned because a decision records the policy version it was decided under
140
+ * and this mapping is upstream of that: change what `spam` means and two reports
141
+ * filed a month apart are no longer the same allegation. The version is stamped
142
+ * into the report metadata so a case can be read back against the mapping that
143
+ * produced it. Bump it in the same change that alters a row.
144
+ */
145
+ export interface ModerationTaxonomy {
146
+ readonly version: string;
147
+ /**
148
+ * Extra metadata to stamp on every report this application sends.
149
+ *
150
+ * **Opt-in, and merged UNDER this package's own keys.** An application that
151
+ * sets nothing emits a byte-identical envelope to one that could not set it,
152
+ * which is the property that makes adding this safe: the SDK derives the case
153
+ * envelope from the `ReportInput`, ingress fingerprints that envelope to
154
+ * detect "same external id, different body", and a metadata change is
155
+ * therefore an envelope change. A default that altered the envelope for every
156
+ * adopter at once would turn each of their in-flight retries into a permanent
157
+ * 409, days later, as reports silently stuck in a queue.
158
+ *
159
+ * `taxonomyVersion` and `categories` are written AFTER this, so an entry using
160
+ * either name is ignored rather than shadowing the two keys a case is read
161
+ * back against.
162
+ *
163
+ * The motivating case, because it is the shape worth recognising rather than
164
+ * the specific field: an application that KNOWS a jury is not being given
165
+ * material it can see exists — Syra's agent avatars are bare strings with no
166
+ * digest recorded anywhere, so they cannot be attached — can say so
167
+ * (`evidenceAttachmentsSupported: false`), and a jury then answers
168
+ * `insufficient_context` for the right reason instead of guessing. Losing that
169
+ * degrades decision quality with nothing failing anywhere.
170
+ *
171
+ * Static, not per-report: it is stamped identically on every report, so it
172
+ * cannot vary between two deliveries of the same one.
173
+ *
174
+ * Credit: `syra`, whose adoption found it missing.
175
+ */
176
+ readonly metadata?: Readonly<Record<string, string | number | boolean>>;
177
+ /**
178
+ * The allegation codes for a report's categories.
179
+ *
180
+ * MUST be deterministic and stably ordered for a given input set. Ingress
181
+ * fingerprints the whole envelope to detect "same external id, different
182
+ * body", so a list whose order depended on how a client happened to send its
183
+ * categories turns a legitimate outbox retry into a permanent 409 — days
184
+ * later, as a report silently stuck in a queue. Sort the result.
185
+ *
186
+ * MUST NOT return an empty array: a report with no allegation is not a report.
187
+ * Map anything unrecognised to `other.unclassifiable`.
188
+ */
189
+ allegationsFor(categories: readonly string[]): readonly TaxonomyCode[];
190
+ }
191
+
192
+ /* ------------------------------------------------------------------------- */
193
+ /* Enforcement — what the application does about a decision */
194
+ /* ------------------------------------------------------------------------- */
195
+
196
+ /**
197
+ * How much of a plan a deployment is allowed to carry out.
198
+ *
199
+ * `observe` runs everything except the effect — the plan, the idempotency claim
200
+ * and the audit row are identical to production, so what the mode proves is
201
+ * exactly what will happen when it is switched off.
202
+ */
203
+ export type ModerationEnforcementMode = 'observe' | 'manual' | 'automatic';
204
+
205
+ /** One thing the application decided to do, and why. */
206
+ export interface PlannedEnforcementAction<TAction extends string> {
207
+ readonly action: TAction;
208
+ /** Why, in words an operator reads. Never reported material. */
209
+ readonly reason: string;
210
+ /** The recommendation this came from, when it came from one. */
211
+ readonly recommendedAction?: RecommendedAction;
212
+ }
213
+
214
+ /**
215
+ * What an effect changed, so a later revision can put it back.
216
+ *
217
+ * Opaque to this package and owned by the application: it is written on the
218
+ * enforcement row when an action is applied and handed back to
219
+ * {@link ModerationEnforcementConfig.apply} when a superseding revision reverses
220
+ * it. Keep it small, flat and JSON-serialisable — it is stored in Mongo, and it
221
+ * must never contain reported material.
222
+ */
223
+ export type EnforcementPreviousState = Readonly<
224
+ Record<string, string | number | boolean | null | undefined>
225
+ >;
226
+
227
+ /** The object an enforcement action is about, in the application's own terms. */
228
+ export interface EnforcementSubject {
229
+ /** The application's own noun (`post`, `listing`, …). Never a resource id. */
230
+ readonly type: string;
231
+ readonly id: string;
232
+ }
233
+
234
+ /**
235
+ * The result of carrying out one action.
236
+ *
237
+ * `changed: false` is not a failure — the object is already gone, or there was
238
+ * no restriction to undo. It is recorded with its reason, which is how "we
239
+ * checked and there was nothing to do" stays distinguishable from "we never
240
+ * looked". Throw only for a real failure; the claim is then released so a retry
241
+ * can try again.
242
+ */
243
+ export type EnforcementEffect<TAction extends string = string> =
244
+ | { readonly changed: true; readonly previousState?: EnforcementPreviousState }
245
+ | {
246
+ readonly changed: false;
247
+ readonly reason: string;
248
+ /**
249
+ * What this action actually amounted to, when that is not the action that
250
+ * was planned.
251
+ *
252
+ * The plan is computed before `apply` runs and is deliberately
253
+ * subject-blind, so it can name an action that cannot apply to THIS
254
+ * object. An application whose restore only exists for some of its subject
255
+ * types is the clearest case: a decision clearing a customer plans
256
+ * `reinstate_courier`, because the tables cannot know the subject — and a
257
+ * report then reads "decided: reinstate_courier" about somebody who was
258
+ * never suspended.
259
+ *
260
+ * Setting `recordedAs` corrects the label at the only point that knows:
261
+ * `apply` is the sole place aware that this object has no such lever. The
262
+ * enforcement row keeps the PLANNED action, because that is the
263
+ * idempotency key and what was actually decided, and additionally records
264
+ * this; the report's `enforcedAction` uses this.
265
+ *
266
+ * Only meaningful when nothing changed — an effect that happened is the
267
+ * action that was planned. Must be one of `actions`.
268
+ *
269
+ * Credit: `noted-moovo`, from Moovo's courier/customer asymmetry, where
270
+ * two of three subject types have no suspendable state.
271
+ */
272
+ readonly recordedAs?: TAction;
273
+ };
274
+
275
+ /**
276
+ * The application's half of enforcement: its actions, its mapping, its effects.
277
+ *
278
+ * The idempotency claim, the mode gate, the audit row, the reversal lookup and
279
+ * the release-on-failure are this package's and are not configurable — they are
280
+ * the invariants, not the policy.
281
+ */
282
+ export interface ModerationEnforcementConfig<TAction extends string> {
283
+ /** Every action this application can plan. Used for the stored enum. */
284
+ readonly actions: readonly TAction[];
285
+
286
+ /**
287
+ * The action meaning "deliberately nothing".
288
+ *
289
+ * Never survives beside another action, and is what an otherwise-empty plan
290
+ * becomes: a row saying "we decided to do nothing, and why" is evidence, and
291
+ * an absent row is a question.
292
+ */
293
+ readonly noneAction: TAction;
294
+
295
+ /**
296
+ * The action meaning "a human looks at this".
297
+ *
298
+ * Always survives a collapse, and is where an unmapped recommendation, an
299
+ * unrecognised outcome and an unrecognised severity all go. Dropping it
300
+ * because something else was also done is how a `suspend_user` recommendation
301
+ * gets lost.
302
+ */
303
+ readonly reviewAction: TAction;
304
+
305
+ /**
306
+ * The actions that DO the undoing — `['restore', 'unlabel_sensitive']`, NEVER
307
+ * `['restrict', 'label_sensitive']` — or `null` when there is nothing to undo.
308
+ *
309
+ * **Direction first, because everything below describes the opposite one.**
310
+ * This field holds what the planner EMITS on `no_violation`; `reverses` is the
311
+ * separate map saying what each of those undoes. The rest of this comment
312
+ * necessarily talks about the levers a correction must REVERSE, and a reader
313
+ * arriving from that prose fills in the targets instead of the actors — a
314
+ * mistake two people made within an hour, including the author of the field.
315
+ *
316
+ * It matters more than a naming slip because an inverted value **does not
317
+ * fail**. It type-checks, it plans, and it applies a restriction and a label
318
+ * on an accepted appeal: the correction carrying out the punishment it was
319
+ * correcting, on the one path in the system whose whole purpose is to give
320
+ * something back.
321
+ *
322
+ * **Required, and `null` is a real answer.** Naming an action makes
323
+ * `no_violation` ALWAYS plan it, and that is load-bearing: a correction is a
324
+ * new revision whose outcome is `no_violation` and whose recommendation is
325
+ * frequently `no_action` — which means "take no NEW action", not "leave what
326
+ * you already did in place". Mapping that straight through plans nothing, and
327
+ * the object an earlier revision removed stays removed forever: the appeal
328
+ * succeeded, the case says the content was fine, and nothing ever puts it
329
+ * back. No error, no log line, no failing test.
330
+ *
331
+ * It is required rather than optional because an ABSENT key cannot be told
332
+ * apart from a forgotten one, and forgetting is exactly the silent bug above.
333
+ * `null` says an application considered it and has no restriction to lift —
334
+ * true of an application with no sanction primitive at all, and the compiler
335
+ * makes saying so a deliberate act.
336
+ *
337
+ * **A LIST when more than one action is reversible, and this is the shape most
338
+ * applications actually need.** An application whose levers are "hide it" and
339
+ * "label it" has TWO things a correction must undo, and naming only one leaves
340
+ * the other permanently stuck: the object is un-hidden and stays labelled
341
+ * forever. Every planned restore that finds nothing to undo records
342
+ * `changed: false` with its reason, so listing an action that did not apply
343
+ * costs an audit row rather than a wrong effect — which is the cheap side of
344
+ * the trade.
345
+ *
346
+ * Credit: `mention-finish`, who found `unlabel_sensitive` fully implemented,
347
+ * mode-gated, and reachable from nothing in Mention — the correction fix
348
+ * applied to one of two reversible actions. This package had the same gap.
349
+ */
350
+ readonly restoreAction: TAction | readonly TAction[] | null;
351
+
352
+ /**
353
+ * What each recommendation becomes. Anything unmapped becomes
354
+ * {@link reviewAction}.
355
+ *
356
+ * Optional, because an empty table and an absent one mean the same thing: a
357
+ * recommendation this application has no action for goes to a human, recorded
358
+ * with the recommendation that produced it. An application with no sanction
359
+ * primitive omits it entirely.
360
+ */
361
+ readonly recommendationToAction?: Partial<Record<RecommendedAction, TAction>>;
362
+
363
+ /**
364
+ * What a `violation` with NO recommendation becomes, by highest severity.
365
+ *
366
+ * Be cautious at both ends: a `low`-severity violation nobody recommended
367
+ * anything for is not something to remove an object over, and `critical`
368
+ * material is routed to a specialist team under legal protocol — neither is an
369
+ * automatic effect a mapping table should decide.
370
+ *
371
+ * Optional, and any severity left unmapped falls to {@link reviewAction}. An
372
+ * application with nothing to enforce can omit it entirely; the cautious
373
+ * default is the only honest one when a table says nothing.
374
+ */
375
+ readonly severityFallback?: Partial<Readonly<Record<Severity, TAction>>>;
376
+
377
+ /**
378
+ * Actions that absorb weaker ones when both are planned.
379
+ *
380
+ * `{ restrict: ['label_sensitive', 'none', 'restore'] }` — a removed object
381
+ * does not also need a warning, and recording both would claim two effects
382
+ * where one happened.
383
+ */
384
+ readonly absorb?: Partial<Record<TAction, readonly TAction[]>>;
385
+
386
+ /**
387
+ * Strongest first. Decides the ONE action written onto the report when a plan
388
+ * produced several.
389
+ *
390
+ * Optional; defaults to {@link actions}, so listing `actions` strongest-first
391
+ * is enough for most applications.
392
+ */
393
+ readonly precedence?: readonly TAction[];
394
+
395
+ /**
396
+ * Actions `manual` mode still applies automatically.
397
+ *
398
+ * The reversible, low-consequence half: giving something BACK. Holding those
399
+ * behind a human means a wrongly-removed object stays removed while somebody
400
+ * reads a queue. Taking content down still waits for a person.
401
+ */
402
+ readonly reversibleActions?: readonly TAction[];
403
+
404
+ /**
405
+ * Carry out one action, or say why there was nothing to carry out.
406
+ *
407
+ * Called at most once per `decisionId + revision + action`, and only when the
408
+ * mode allows it. `previousState` is handed back to a later reversal.
409
+ *
410
+ * **Optional.** An application with no sanction primitive at all — nothing to
411
+ * remove, restrict, label or suspend — omits it, and every planned action is
412
+ * recorded as `recorded` with a reason. That is a supported shape rather than
413
+ * an unfinished one: the plan, the idempotency claim and the audit row are
414
+ * still real, so "CrowdSource decided this and we have no way to carry it
415
+ * out" is written down instead of being lost. Omitting `apply` is NOT the same
416
+ * as `observe` mode — the mode is a deployment choice that can be switched
417
+ * off, and this is a property of the application.
418
+ */
419
+ apply?(input: {
420
+ readonly action: TAction;
421
+ readonly subject: EnforcementSubject;
422
+ /**
423
+ * What the most recent APPLIED row for this subject recorded, when there is
424
+ * one. This is how a restore returns an object to what it WAS rather than to
425
+ * a guess — and how a correction knows not to lift a content warning that
426
+ * moderation never set.
427
+ */
428
+ readonly previousState?: EnforcementPreviousState;
429
+ /** The action whose `previousState` was found, when one was. */
430
+ readonly previousAction?: TAction;
431
+ readonly decision: Decision;
432
+ }): Promise<EnforcementEffect<TAction>>;
433
+
434
+ /**
435
+ * The action — or actions — whose earlier `previousState` `apply` should be
436
+ * given, per action.
437
+ *
438
+ * `{ restore: 'restrict', unlabel_sensitive: 'label_sensitive' }` — a restore
439
+ * needs to know what the restriction replaced. Omit for an action that undoes
440
+ * nothing.
441
+ *
442
+ * A LIST when one action reverses several, which is the ordinary shape once an
443
+ * application has more than one lever:
444
+ * `{ restore: ['restrict', 'request_changes', 'freeze_transaction'] }`. The
445
+ * lookup takes the most recent APPLIED row across the whole set, so `apply` is
446
+ * handed whatever actually happened last rather than whatever single action
447
+ * was declared — and `previousAction` names the row that was found, not the
448
+ * list it came from.
449
+ *
450
+ * A single value is not merely awkward for such an application: it pushes it
451
+ * into re-querying the ledger itself, which is exactly where the
452
+ * `applied: true` filter lives. Re-implementing that per application is
453
+ * per-application chances to omit it, and omitting it means a reversal reads a
454
+ * row whose effect never happened.
455
+ *
456
+ * Credit: `mercaria`, whose `restore` reverses any of three depending on which
457
+ * one the decision actually applied.
458
+ */
459
+ readonly reverses?: Partial<Record<TAction, TAction | readonly TAction[]>>;
460
+ }
461
+
462
+ /* ------------------------------------------------------------------------- */
463
+ /* Reports — the application's own model, with a shared moderation half */
464
+ /* ------------------------------------------------------------------------- */
465
+
466
+ /**
467
+ * Where a report stands in THIS application, independent of any verdict.
468
+ *
469
+ * Separate from whatever verdict field the application already had, because the
470
+ * two answer different questions: this one is "did it get out of here and come
471
+ * back", and it is the axis every query in this package uses.
472
+ *
473
+ * - `received` — stored, and never going anywhere: the reported type has no
474
+ * subject provider. Not a failure; a deliberate local-only report.
475
+ * - `queued` — stored with a durable delivery event, in one transaction.
476
+ * - `submitted` — CrowdSource has it; a case exists.
477
+ * - `delivery_failed` — the last delivery attempt failed. The outbox is still
478
+ * retrying or has dead-lettered it.
479
+ * - `closed` — a final or corrected decision was applied, or the material is
480
+ * gone and there is nothing to review.
481
+ */
482
+ export type ModerationLocalStatus =
483
+ | 'received'
484
+ | 'queued'
485
+ | 'submitted'
486
+ | 'delivery_failed'
487
+ | 'closed';
488
+
489
+ /**
490
+ * The fields this package reads and writes on an application's report.
491
+ *
492
+ * The application owns the model, the collection, its own enums and any extra
493
+ * fields; it composes the schema from `moderationReportSchemaFields` and passes
494
+ * the resulting model in. Every query that carries a correctness property — the
495
+ * decision-revision guard above all — lives in this package rather than being
496
+ * re-derived seven times.
497
+ */
498
+ export interface ModerationReportFields {
499
+ /**
500
+ * The report's own id, as a string.
501
+ *
502
+ * Declared here rather than left to whatever the backend calls its primary
503
+ * key, because the core needs it in exactly one shape and in several places:
504
+ * the delivery event's payload carries it, the deterministic event id is
505
+ * derived from it, and reconciliation re-derives that id from the report. A
506
+ * Re-deriving it from an ORM-specific record at each call site is how an
507
+ * application ends up disagreeing with itself about the report id.
508
+ */
509
+ id: string;
510
+
511
+ reportedType: string;
512
+ reportedId: string;
513
+ /** The reporting Oxy user id. The Oxy subject IS the binding proof. */
514
+ reporter: string;
515
+ categories: string[];
516
+ details?: string;
517
+
518
+ localStatus: ModerationLocalStatus;
519
+ /** Why a report is not going anywhere, in words an operator can read. */
520
+ localStatusReason?: string;
521
+
522
+ crowdSourceReportId?: string;
523
+ crowdSourceCaseId?: string;
524
+ crowdSourceMerged?: boolean;
525
+ contentSnapshotHash?: string;
526
+ submittedAt?: Date;
527
+ lastDeliveryError?: string;
528
+
529
+ decisionId?: string;
530
+ decisionRevision?: number;
531
+ decisionOutcome?: string;
532
+ decisionStatus?: string;
533
+ decidedAt?: Date;
534
+ enforcedAction?: string;
535
+ enforcedAt?: Date;
536
+
537
+ createdAt: Date;
538
+ updatedAt: Date;
539
+ }
540
+
541
+ /**
542
+ * Extra `$set` fields to write when a decision lands on a report.
543
+ *
544
+ * The escape hatch for an application that already had a verdict field before it
545
+ * adopted CrowdSource and must keep it working. A new application needs none of
546
+ * this: `localStatus` and the `decision*` fields are the whole answer.
547
+ *
548
+ * Two status fields maintained by two call sites is how they drift, so an
549
+ * application that has one derives it HERE, from the decision, and nowhere else.
550
+ */
551
+ export type ReportDecisionExtraFields = Readonly<
552
+ Record<string, string | number | boolean | Date>
553
+ >;
554
+
555
+ /* ------------------------------------------------------------------------- */
556
+ /* Host services */
557
+ /* ------------------------------------------------------------------------- */
558
+
559
+ /**
560
+ * The application's logger.
561
+ *
562
+ * Structurally compatible with pino, winston and Mention's own. Nothing in this
563
+ * package logs reported material — the contexts are ids, counts, states and
564
+ * bounded error messages.
565
+ */
566
+ export interface ModerationLogger {
567
+ info(message: string, context?: Record<string, unknown>): void;
568
+ warn(message: string, context?: Record<string, unknown>): void;
569
+ error(message: string, context?: Record<string, unknown>): void;
570
+ }
571
+
572
+ /** Optional counters. Labels are bounded; no value is ever user-supplied text. */
573
+ export interface ModerationMetrics {
574
+ incrementCounter(name: string, value: number, labels: Record<string, string>): void;
575
+ }
576
+
577
+ /** Everything about talking to CrowdSource, and whether to at all. */
578
+ export interface CrowdSourceConnectionConfig {
579
+ /**
580
+ * Gates the DISPATCHER, never the durable record.
581
+ *
582
+ * Reports taken while the integration is off keep their outbox rows and
583
+ * deliver when it is switched on. Running the loop instead would count
584
+ * attempts against a deployment that has nowhere to send anything and
585
+ * dead-letter the backlog it was meant to preserve.
586
+ */
587
+ readonly enabled: boolean;
588
+ /** `applicationId:credentialId:secret`. The only source of `applicationId`. */
589
+ readonly serviceKey?: string;
590
+ readonly baseUrl?: string;
591
+ readonly webhookSecret?: string;
592
+ /** Accepted during a secret rotation. */
593
+ readonly webhookPreviousSecret?: string;
594
+ readonly enforcementMode: ModerationEnforcementMode;
595
+ readonly outboxPollIntervalMs?: number;
596
+ readonly outboxBatchSize?: number;
597
+ /** How long a `submitted` report may wait before it is worth counting. */
598
+ readonly staleSubmittedHours?: number;
599
+ readonly reconciliationIntervalMs?: number;
600
+ }
601
+
602
+ /**
603
+ * Everything the integration needs, in one object.
604
+ *
605
+ * `TReport` is the application's own report document type and must structurally
606
+ * satisfy {@link ModerationReportFields}; `TAction` is the union of its
607
+ * enforcement actions; `TTx` is whatever its backend calls a transaction, and is
608
+ * inferred from `store` rather than written by anyone.
609
+ */
610
+ export interface ModerationIntegrationConfig<
611
+ TReport extends ModerationReportFields,
612
+ TAction extends string,
613
+ TTx,
614
+ > {
615
+ /**
616
+ * Where everything this package writes goes.
617
+ *
618
+ * Built by `postgresModerationStore` from
619
+ * `@crowdsource.you/core/outbox/postgres` and passed in whole rather than assembled
620
+ * here. A store built over two connections would type-check and quietly put a
621
+ * report and its outbox row in different transactions.
622
+ */
623
+ readonly store: ModerationStore<TReport, TTx>;
624
+ readonly crowdSource: CrowdSourceConnectionConfig;
625
+
626
+ /**
627
+ * Every noun this application can send for review.
628
+ *
629
+ * This list decides DELIVERY and nothing else. A reported type WITHOUT a
630
+ * provider is still accepted and still stored — it simply never leaves. Making
631
+ * it an admission gate breaks an application's existing report surfaces on the
632
+ * day it adopts CrowdSource, and incremental adoption one subject type at a
633
+ * time is the property that makes this package adoptable at all.
634
+ */
635
+ readonly subjects: readonly ModerationSubjectProvider[];
636
+ readonly taxonomy: ModerationTaxonomy;
637
+ readonly enforcement: ModerationEnforcementConfig<TAction>;
638
+
639
+ readonly logger: ModerationLogger;
640
+ readonly metrics?: ModerationMetrics;
641
+
642
+ /** See {@link ReportDecisionExtraFields}. Omit unless you have a legacy field. */
643
+ readonly reportDecisionExtraFields?: (decision: Decision) => ReportDecisionExtraFields;
644
+ }
645
+
646
+ /* ------------------------------------------------------------------------- */
647
+ /* Outbox */
648
+ /* ------------------------------------------------------------------------- */
649
+
650
+ export type ModerationOutboxKind = 'report.submit' | 'decision.apply';
651
+
652
+ export type ModerationOutboxStatus =
653
+ | 'pending'
654
+ | 'processing'
655
+ | 'processed'
656
+ | 'dead_letter';
657
+
658
+ export interface ModerationOutboxPayload {
659
+ /** The local report id, for `report.submit`. */
660
+ reportId?: string;
661
+ /** The inbound webhook event id, for `decision.apply`. */
662
+ eventId?: string;
663
+ caseId?: string;
664
+ /**
665
+ * The decision exactly as CrowdSource published it.
666
+ *
667
+ * Stored whole and opaque rather than projected into columns: the decision
668
+ * document is deliberately loose, and a projection would silently drop
669
+ * whatever a newer CrowdSource added. Validated against the published contract
670
+ * when it is READ, so an event is never lost to a schema this deployment has
671
+ * not caught up with.
672
+ */
673
+ decision?: unknown;
674
+ }
675
+
676
+ export interface ModerationOutboxEvent {
677
+ /**
678
+ * The deterministic event id — `moderation:report.submit:<reportId>` or
679
+ * `moderation:decision.apply:<eventId>`.
680
+ *
681
+ * It IS the primary key on both backends, which is what makes a repeated
682
+ * enqueue a no-op rather than a second delivery. Handlers key their downstream
683
+ * effects on it, because the contract is at-least-once.
684
+ */
685
+ id: string;
686
+ kind: ModerationOutboxKind;
687
+ payload: ModerationOutboxPayload;
688
+ attempts: number;
689
+ availableAt: Date;
690
+ leaseOwner?: string;
691
+ leaseUntil?: Date;
692
+ expiresAt: Date;
693
+ createdAt: Date;
694
+ }
695
+
696
+ /* ------------------------------------------------------------------------- */
697
+ /* Results */
698
+ /* ------------------------------------------------------------------------- */
699
+
700
+ export interface CreateReportInput {
701
+ reporter: string;
702
+ reportedType: string;
703
+ reportedId: string;
704
+ categories: readonly string[];
705
+ details?: string;
706
+ /**
707
+ * Extra fields to store on the report document, for application columns this
708
+ * package knows nothing about. Never used in a query filter here.
709
+ */
710
+ extra?: Readonly<Record<string, unknown>>;
711
+ }
712
+
713
+ export interface CreateReportResult<TReport> {
714
+ report: TReport;
715
+ /**
716
+ * The durable delivery event.
717
+ *
718
+ * Absent exactly when the reported type has no subject provider — the report
719
+ * was stored and there is nothing to deliver it, by design rather than by
720
+ * failure.
721
+ */
722
+ outboxEventId?: string;
723
+ }
724
+
725
+ export interface ModerationDispatchResult {
726
+ processed: number;
727
+ failed: number;
728
+ deadLettered: number;
729
+ }
730
+
731
+ export interface ModerationReconciliationResult {
732
+ /** Reports that had no delivery event and now have one. */
733
+ requeued: number;
734
+ /** Reports whose delivery event is dead-lettered. Needs a human. */
735
+ deadLettered: number;
736
+ /** Reports submitted long ago with no decision yet. */
737
+ awaitingDecision: number;
738
+ /** Reports stored with no route to review at all. Never re-queued. */
739
+ localOnly: number;
740
+ }
741
+
742
+ export interface EnforcementOutcome<TAction extends string> {
743
+ /** The action that was PLANNED and claimed. Never rewritten. */
744
+ action: TAction;
745
+ /** What it amounted to, when `apply` said so. See {@link EnforcementEffect}. */
746
+ recordedAs?: TAction;
747
+ /**
748
+ * `applied` — the effect happened. `recorded` — claimed and deliberately not
749
+ * carried out (observe/manual mode, or nothing to do). `duplicate` — another
750
+ * delivery of this same decision revision already handled it.
751
+ */
752
+ result: 'applied' | 'recorded' | 'duplicate';
753
+ }