@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,231 @@
1
+ import { createHash } from 'crypto';
2
+ import type { ReportInput } from '../index.js';
3
+ import type {
4
+ ModerationReportFields,
5
+ ModerationSubjectProvider,
6
+ ModerationSubjectSnapshot,
7
+ ModerationTaxonomy,
8
+ } from './types.js';
9
+
10
+ /**
11
+ * Turning a stored report into the thing the SDK delivers.
12
+ *
13
+ * This is NOT a case-envelope builder, and the difference matters enough to
14
+ * name: `@crowdsource.you/core` builds the Case Envelope, and it deliberately does
15
+ * not export the function that does it. What this module produces is the SDK's
16
+ * `ReportInput` — a description of the material — and the SDK derives the
17
+ * envelope from it: resource ids, relations, digests, pseudonymous principal
18
+ * refs, the identity binding proof, the pinned policy version, the privacy terms
19
+ * and the idempotency key.
20
+ *
21
+ * That is not a technicality. Those derived values are exactly what the case
22
+ * dedup key is computed over, so an application that composed its own envelope
23
+ * would be the reason two reporters about one object opened two cases — and "one
24
+ * penalty per incident" would fail in production with nothing failing in a test.
25
+ * Building the description and letting the SDK build the document is what keeps
26
+ * that property true for every application at once.
27
+ *
28
+ * So this module does three things and no more: ask the registry for a snapshot
29
+ * of the reported object, translate the reporter's categories into allegations,
30
+ * and assemble both with the report's own identity.
31
+ */
32
+
33
+ /**
34
+ * The material could not be described, because nothing can describe it.
35
+ *
36
+ * This is a DEFECT, not a state, and it should be unreachable. A report whose
37
+ * type has no subject provider never gets a delivery event in the first place —
38
+ * intake decides that from the same registry this module reads — so an event
39
+ * that arrives here has been created by something that bypassed intake, or by a
40
+ * deployment where a provider was removed while its reports were still in
41
+ * flight.
42
+ *
43
+ * `retryable: false` therefore dead-letters the outbox event, so the
44
+ * reconciliation sweep counts it and a human looks. The alternative — writing a
45
+ * local state — would file a genuine defect in the one place that looks
46
+ * identical to the deliberate local-only reports, and nothing would ever alert
47
+ * on it.
48
+ *
49
+ * Separate from "the object is gone": a deleted object is a fact about the world
50
+ * and closes the report normally.
51
+ */
52
+ export class ModerationSubjectUnsupportedError extends Error {
53
+ readonly retryable = false;
54
+
55
+ constructor(reportedType: string) {
56
+ super(`No moderation subject provider is registered for '${reportedType}'.`);
57
+ this.name = 'ModerationSubjectUnsupportedError';
58
+ }
59
+ }
60
+
61
+ /**
62
+ * The application's taxonomy produced no allegation for a report.
63
+ *
64
+ * Not retryable and never silently defaulted. A report with no allegation is not
65
+ * a report, and substituting `other.unclassifiable` here would put a claim in
66
+ * front of a jury that no reporter made and that no version of the mapping
67
+ * records — the taxonomy is the application's to fix, and dead-lettering is what
68
+ * makes somebody fix it.
69
+ */
70
+ export class ModerationTaxonomyError extends Error {
71
+ readonly retryable = false;
72
+
73
+ constructor(categories: readonly string[]) {
74
+ super(
75
+ `The report taxonomy produced no allegation for categories [${categories.join(', ')}]. ` +
76
+ 'Map every category to a code; use `other.unclassifiable` for one the universal taxonomy cannot name.',
77
+ );
78
+ this.name = 'ModerationTaxonomyError';
79
+ }
80
+ }
81
+
82
+ /**
83
+ * SHA-256 of the snapshot, stored on the report.
84
+ *
85
+ * Taken over the described MATERIAL, not over the whole `ReportInput`: the
86
+ * report id, the reporter and the allegations are properties of the report, and
87
+ * including them would mean two people reporting identical content produced
88
+ * different hashes — which is the opposite of what this hash is for. Key order
89
+ * is fixed by the literal below rather than by `Object.keys`, so the digest is
90
+ * stable across Node versions and across a refactor that reorders a field.
91
+ */
92
+ export function snapshotHash(snapshot: ModerationSubjectSnapshot): string {
93
+ const canonical = JSON.stringify({
94
+ subject: {
95
+ externalId: snapshot.subject.externalId,
96
+ type: snapshot.subject.type,
97
+ author: snapshot.subject.author?.oxyUserId ?? null,
98
+ },
99
+ content: snapshot.content,
100
+ attachments: snapshot.attachments ?? [],
101
+ context: snapshot.context ?? [],
102
+ });
103
+ return `sha256:${createHash('sha256').update(canonical, 'utf8').digest('hex')}`;
104
+ }
105
+
106
+ /** The subject providers, indexed by the application's own reported type. */
107
+ export interface SubjectRegistry {
108
+ /**
109
+ * The provider for a reported type, or `undefined` when it is not deliverable.
110
+ *
111
+ * The single authority on whether a report leaves this deployment. Intake asks
112
+ * before queueing a delivery, and this module asks again when it builds one; a
113
+ * type this returns `undefined` for is stored and never enqueued.
114
+ */
115
+ providerFor(reportedType: string): ModerationSubjectProvider | undefined;
116
+ /**
117
+ * The reported types wired to CrowdSource, as the registry itself sees them.
118
+ *
119
+ * Exists so an application can pin the set in a test. That is not ceremony:
120
+ * the difference between a delivered type and a local-only one is invisible in
121
+ * a 201, so registering a provider — or forgetting to — is a change no
122
+ * response body would reveal.
123
+ */
124
+ deliverableTypes(): string[];
125
+ }
126
+
127
+ export function createSubjectRegistry(
128
+ providers: readonly ModerationSubjectProvider[],
129
+ ): SubjectRegistry {
130
+ const byType = new Map<string, ModerationSubjectProvider>();
131
+ for (const provider of providers) {
132
+ const existing = byType.get(provider.reportedType);
133
+ if (existing) {
134
+ /**
135
+ * Two providers for one noun is not a merge to resolve quietly: whichever
136
+ * one lost would describe nothing, and the subject type a jury reasons
137
+ * about would depend on array order.
138
+ */
139
+ throw new Error(
140
+ `Two moderation subject providers are registered for '${provider.reportedType}' ` +
141
+ `('${existing.subjectType}' and '${provider.subjectType}'). A reported type has one provider.`,
142
+ );
143
+ }
144
+ byType.set(provider.reportedType, provider);
145
+ }
146
+
147
+ return {
148
+ providerFor: (reportedType) => byType.get(reportedType),
149
+ deliverableTypes: () => Array.from(byType.keys()),
150
+ };
151
+ }
152
+
153
+ export interface ModerationReportInput {
154
+ /** What the SDK delivers. */
155
+ readonly reportInput: ReportInput;
156
+ /** The digest to store on the local report. */
157
+ readonly snapshotHash: string;
158
+ }
159
+
160
+ /**
161
+ * Describe a stored report for delivery, or report why it cannot be described.
162
+ *
163
+ * Returns `null` when the reported object no longer exists. That is not an error
164
+ * either: content deleted between the report and its delivery is ordinary, and
165
+ * evidence stays available through retention on CrowdSource's side — but an
166
+ * object the application never got to snapshot has no evidence to keep.
167
+ */
168
+ export async function buildModerationReportInput(input: {
169
+ report: Pick<
170
+ ModerationReportFields,
171
+ 'reportedType' | 'reportedId' | 'reporter' | 'categories' | 'details' | 'createdAt'
172
+ > & { id: string };
173
+ registry: SubjectRegistry;
174
+ taxonomy: ModerationTaxonomy;
175
+ }): Promise<ModerationReportInput | null> {
176
+ const { report, registry, taxonomy } = input;
177
+ const provider = registry.providerFor(report.reportedType);
178
+ if (!provider) throw new ModerationSubjectUnsupportedError(report.reportedType);
179
+
180
+ const snapshot = await provider.snapshot(report.reportedId);
181
+ if (!snapshot) return null;
182
+
183
+ const allegationCodes = taxonomy.allegationsFor(report.categories);
184
+ if (allegationCodes.length === 0) throw new ModerationTaxonomyError(report.categories);
185
+ const details = report.details?.trim();
186
+
187
+ return {
188
+ reportInput: {
189
+ externalReportId: report.id,
190
+ subject: snapshot.subject,
191
+ content: snapshot.content,
192
+ ...(snapshot.attachments === undefined ? {} : { attachments: snapshot.attachments }),
193
+ ...(snapshot.context === undefined ? {} : { context: snapshot.context }),
194
+ /**
195
+ * The reporter's own words ride on the FIRST allegation only.
196
+ *
197
+ * Repeating one free-text field across every code would say the reporter
198
+ * wrote it about each of them separately, and details are the reporter's
199
+ * claim and never evidence for it.
200
+ */
201
+ allegations: allegationCodes.map((code, index) =>
202
+ index === 0 && details ? { code, details } : { code },
203
+ ),
204
+ /**
205
+ * The Oxy subject IS the binding proof. Reporters are stored as Oxy user
206
+ * ids, so there is no separate binding step to implement here.
207
+ */
208
+ reportedBy: { oxyUserId: report.reporter },
209
+ /**
210
+ * The moment the USER reported it — the local report's own timestamp, not
211
+ * the moment of delivery. Any value invented per attempt would make every
212
+ * retry from the outbox a permanent 409.
213
+ */
214
+ submittedAt: report.createdAt,
215
+ metadata: {
216
+ /**
217
+ * The application's own entries FIRST, so the two below cannot be
218
+ * shadowed. An adopter that names `taxonomyVersion` or `categories` gets
219
+ * them overwritten rather than winning — a case has to be readable back
220
+ * against the mapping that produced it, and that is not negotiable per
221
+ * application.
222
+ */
223
+ ...taxonomy.metadata,
224
+ /** So a case can be read back against the mapping that produced it. */
225
+ taxonomyVersion: taxonomy.version,
226
+ categories: [...report.categories].sort().join(','),
227
+ },
228
+ },
229
+ snapshotHash: snapshotHash(snapshot),
230
+ };
231
+ }
@@ -0,0 +1,140 @@
1
+ import type { ProcessedEventStore } from '../express/index.js';
2
+ import { decisionApplyEventId, type OutboxService } from './outbox/service.js';
3
+ import { MODERATION_EVENT_RETENTION_SECONDS } from './retention.js';
4
+ import type { ModerationEventStore, ModerationTransactionRunner } from './store/types.js';
5
+
6
+ /**
7
+ * What happens between "a signed decision arrived" and "2xx".
8
+ *
9
+ * The receiver's contract is to answer quickly and queue the processing. So
10
+ * exactly two writes happen here, in ONE transaction — the event's audit row is
11
+ * completed and a durable `decision.apply` event is created — and the dispatcher
12
+ * does the rest.
13
+ *
14
+ * The transaction is what makes the dedupe safe. The middleware has already
15
+ * claimed the event id by inserting the row (see {@link createProcessedEventStore});
16
+ * if completing that row and queueing the work were two operations, a crash
17
+ * between them would leave an event that is permanently deduplicated with no
18
+ * work queued — a decision silently lost, with a row that says it arrived.
19
+ * Committing both together means the only two possible outcomes are "recorded
20
+ * and queued" or "neither", and "neither" releases the claim and gets
21
+ * redelivered.
22
+ */
23
+
24
+ /**
25
+ * The webhook dedupe store, backed by the moderation event log.
26
+ *
27
+ * `@crowdsource.you/core/express` defaults to an in-process store and says exactly
28
+ * when that is not enough: two instances behind a load balancer each keep their
29
+ * own, so a redelivery landing on the other instance is not deduplicated. Every
30
+ * Oxy backend runs several tasks behind one load balancer, so this is that case.
31
+ *
32
+ * The claim/release contract is the SDK's, and it is the right one. A row
33
+ * inserted BEFORE the handler runs means a concurrent redelivery cannot also run
34
+ * it; deleting that row when the handler THROWS means the sender's retry
35
+ * schedule can still deliver the event later. Recording the id only after
36
+ * success would let two copies run at once; recording it before and never
37
+ * releasing would make a transient failure permanent and lose a decision
38
+ * silently.
39
+ *
40
+ * The retention window is computed HERE rather than in a store, so both backends
41
+ * keep an audit row for the same length of time.
42
+ */
43
+ export function createProcessedEventStore<TTx>(
44
+ events: ModerationEventStore<TTx>,
45
+ ): ProcessedEventStore {
46
+ return {
47
+ async claim(eventId: string): Promise<boolean> {
48
+ const now = new Date();
49
+ return await events.claim({
50
+ eventId,
51
+ receivedAt: now,
52
+ expiresAt: new Date(now.getTime() + MODERATION_EVENT_RETENTION_SECONDS * 1_000),
53
+ });
54
+ },
55
+
56
+ async release(eventId: string): Promise<void> {
57
+ await events.release(eventId);
58
+ },
59
+ };
60
+ }
61
+
62
+ export interface RecordDecisionEventInput {
63
+ eventId: string;
64
+ type: string;
65
+ caseId: string;
66
+ /**
67
+ * The decision as delivered.
68
+ *
69
+ * `unknown`, deliberately. It is stored whole and parsed against the published
70
+ * contract by the worker that acts on it — these payloads are deliberately
71
+ * loose, and validating here would mean an event whose shape this deployment
72
+ * does not recognise yet is refused at the door and retried until it
73
+ * dead-letters, instead of being kept until the code catches up.
74
+ */
75
+ decision: unknown;
76
+ }
77
+
78
+ export interface InboundService {
79
+ /** Record a decision-bearing event and queue its application. */
80
+ recordDecisionEvent(input: RecordDecisionEventInput): Promise<void>;
81
+ /**
82
+ * Record an event there is nothing to do about.
83
+ *
84
+ * `case.created`, `case.escalated`, `case.closed` and any type a newer
85
+ * CrowdSource introduces. No outbox row, because no work — but the row is
86
+ * kept, because "did CrowdSource tell us about this case, and when" is the
87
+ * first question asked when a report looks stuck, and it has to be answerable.
88
+ */
89
+ recordIgnoredEvent(input: {
90
+ eventId: string;
91
+ type: string;
92
+ caseId?: string;
93
+ }): Promise<void>;
94
+ }
95
+
96
+ export function createInboundService<TTx>(input: {
97
+ transaction: ModerationTransactionRunner<TTx>;
98
+ events: ModerationEventStore<TTx>;
99
+ outbox: OutboxService<TTx>;
100
+ }): InboundService {
101
+ return {
102
+ async recordDecisionEvent(event) {
103
+ await input.transaction.run(async (tx) => {
104
+ const now = new Date();
105
+ await input.events.markQueued(
106
+ {
107
+ eventId: event.eventId,
108
+ type: event.type,
109
+ caseId: event.caseId,
110
+ payload: { caseId: event.caseId, decision: event.decision },
111
+ now,
112
+ },
113
+ tx,
114
+ );
115
+
116
+ await input.outbox.enqueue(
117
+ {
118
+ eventId: decisionApplyEventId(event.eventId),
119
+ kind: 'decision.apply',
120
+ payload: {
121
+ eventId: event.eventId,
122
+ caseId: event.caseId,
123
+ decision: event.decision,
124
+ },
125
+ },
126
+ tx,
127
+ );
128
+ });
129
+ },
130
+
131
+ async recordIgnoredEvent(event) {
132
+ await input.events.markIgnored({
133
+ eventId: event.eventId,
134
+ type: event.type,
135
+ ...(event.caseId === undefined ? {} : { caseId: event.caseId }),
136
+ now: new Date(),
137
+ });
138
+ },
139
+ };
140
+ }
@@ -0,0 +1,160 @@
1
+ /**
2
+ * `@crowdsource.you/core/outbox` — the application half of a CrowdSource integration.
3
+ *
4
+ * An application that adopts CrowdSource has to solve the same six problems
5
+ * every other application does: store a report and its promise of delivery
6
+ * atomically, deliver it with retries and a dead-letter path, receive signed
7
+ * decisions without a body parser destroying the signature, deduplicate
8
+ * redeliveries across several tasks, apply a decision without a stale revision
9
+ * overwriting a fresh one, and carry out consequences exactly once and
10
+ * reversibly. None of that has anything to do with what the application's
11
+ * objects are.
12
+ *
13
+ * So all of it is here, and an application supplies four things: its subjects,
14
+ * its category mapping, its enforcement tables, and a STORE built by the
15
+ * PostgreSQL store factory.
16
+ *
17
+ * ```ts
18
+ * import { createModerationIntegration } from '@crowdsource.you/core/outbox';
19
+ * import { postgresModerationStore } from '@crowdsource.you/core/outbox/postgres';
20
+ *
21
+ * const moderation = createModerationIntegration({
22
+ * store,
23
+ * crowdSource: { enabled: true, serviceKey, webhookSecret, enforcementMode: 'observe' },
24
+ * subjects: [listingSubjectProvider(), reviewSubjectProvider()],
25
+ * taxonomy: { version: '2026.07', allegationsFor },
26
+ * enforcement: commerceEnforcement,
27
+ * logger,
28
+ * });
29
+ *
30
+ * // Indexes before the first write: the unique ones ARE the exactly-once
31
+ * // mechanism, and an index that does not exist yet refuses nothing.
32
+ * await store.ensureSchema();
33
+ *
34
+ * // BEFORE express.json() — the signature covers the bytes that arrived.
35
+ * app.use('/webhooks', moderation.webhookRouter());
36
+ * app.use(express.json());
37
+ *
38
+ * moderation.dispatcher.start();
39
+ * ```
40
+ *
41
+ * Two invariants are ENFORCED here rather than documented, because both fail
42
+ * silently and neither shows up in a test that only asserts the happy path:
43
+ *
44
+ * 1. Nothing can be enqueued that is not already recorded in the outbox, in the
45
+ * same transaction. {@link ModerationOutboxTransactionError} is thrown by the
46
+ * only writer of that collection when the transaction it was handed is not
47
+ * open.
48
+ * 2. The webhook receiver reads raw bytes. Mounted after a JSON parser it
49
+ * refuses rather than verifying a signature over a re-serialisation.
50
+ *
51
+ * Types come from `@crowdsource.you/contracts` and `@crowdsource.you/core`. This
52
+ * package re-exports none of them: a `Decision` or a `TaxonomyCode` has exactly
53
+ * one definition.
54
+ */
55
+
56
+ export { createModerationIntegration } from './integration.js';
57
+ export type { ModerationIntegration } from './integration.js';
58
+
59
+ /**
60
+ * The retention windows are storage-independent policy, so they stay here while
61
+ * PostgreSQL-specific tables and stores live behind the `/postgres` subpath.
62
+ */
63
+ export {
64
+ MODERATION_EVENT_RETENTION_SECONDS,
65
+ MODERATION_OUTBOX_RETENTION_SECONDS,
66
+ } from './retention.js';
67
+
68
+ export {
69
+ ModerationOutboxTransactionError,
70
+ decisionApplyEventId,
71
+ isRetryableDeliveryError,
72
+ reportSubmitEventId,
73
+ } from './outbox/service.js';
74
+ export type {
75
+ ModerationOutboxFailure,
76
+ ModerationOutboxHandler,
77
+ OutboxDrain,
78
+ OutboxService,
79
+ } from './outbox/service.js';
80
+
81
+ export type {
82
+ ModerationEnforcementInsert,
83
+ ModerationEnforcementKey,
84
+ ModerationEnforcementStore,
85
+ ModerationEventStore,
86
+ ModerationOutboxStore,
87
+ ModerationReportDecisionUpdate,
88
+ ModerationReportInsert,
89
+ ModerationReportRef,
90
+ ModerationReportStore,
91
+ ModerationStore,
92
+ ModerationTransactionRunner,
93
+ } from './store/types.js';
94
+
95
+ export { ModerationOutboxDispatcher } from './outbox/dispatcher.js';
96
+ export { ModerationReconciliationJob } from './reconciliation.js';
97
+ export type { ReconcileModerationReports } from './reconciliation.js';
98
+
99
+ export { DuplicateReportError } from './intake.js';
100
+
101
+ export {
102
+ CrowdSourceUnavailableError,
103
+ ModerationDeliveryRejectedError,
104
+ } from './delivery.js';
105
+
106
+ export {
107
+ ModerationDecisionDeferredError,
108
+ ModerationDecisionRejectedError,
109
+ } from './decision.js';
110
+
111
+ export {
112
+ ModerationSubjectUnsupportedError,
113
+ ModerationTaxonomyError,
114
+ createSubjectRegistry,
115
+ snapshotHash,
116
+ } from './evidence.js';
117
+ export type { ModerationReportInput, SubjectRegistry } from './evidence.js';
118
+
119
+ export {
120
+ ModerationRestoreDirectionError,
121
+ assertRestoreDirection,
122
+ planEnforcement,
123
+ primaryAction,
124
+ } from './enforcement/planner.js';
125
+ export type { EnforcementExecutor } from './enforcement/executor.js';
126
+
127
+ export { localStatusForDecision } from './reportStatus.js';
128
+
129
+ export { createProcessedEventStore } from './inbound.js';
130
+ export type { InboundService, RecordDecisionEventInput } from './inbound.js';
131
+
132
+ export type {
133
+ CreateReportInput,
134
+ CreateReportResult,
135
+ CrowdSourceConnectionConfig,
136
+ EnforcementEffect,
137
+ EnforcementOutcome,
138
+ EnforcementPreviousState,
139
+ EnforcementSubject,
140
+ ModerationContextResource,
141
+ ModerationDispatchResult,
142
+ ModerationEnforcementConfig,
143
+ ModerationEnforcementMode,
144
+ ModerationIntegrationConfig,
145
+ ModerationLocalStatus,
146
+ ModerationLogger,
147
+ ModerationMetrics,
148
+ ModerationOutboxEvent,
149
+ ModerationOutboxKind,
150
+ ModerationOutboxPayload,
151
+ ModerationOutboxStatus,
152
+ ModerationReconciliationResult,
153
+ ModerationReportFields,
154
+ ModerationResource,
155
+ ModerationSubjectProvider,
156
+ ModerationSubjectSnapshot,
157
+ ModerationTaxonomy,
158
+ PlannedEnforcementAction,
159
+ ReportDecisionExtraFields,
160
+ } from './types.js';