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