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