@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
package/src/index.ts ADDED
@@ -0,0 +1,107 @@
1
+ /**
2
+ * `@crowdsource.you/core` — the TypeScript client for the CrowdSource moderation
3
+ * API.
4
+ *
5
+ * The smallest integration this package supports, in full:
6
+ *
7
+ * ```ts
8
+ * import { CrowdSource } from '@crowdsource.you/core';
9
+ *
10
+ * const crowdsource = new CrowdSource();
11
+ *
12
+ * await crowdsource.reports.create({
13
+ * externalReportId: report.id,
14
+ * reportedBy: { oxyUserId: session.sub },
15
+ * subject: {
16
+ * externalId: post.id,
17
+ * type: 'social.post',
18
+ * author: { oxyUserId: post.authorId },
19
+ * },
20
+ * content: post.text,
21
+ * allegations: ['harassment.targeted_abuse'],
22
+ * });
23
+ * ```
24
+ *
25
+ * One environment variable (`CROWDSOURCE_SERVICE_KEY`) and the object being
26
+ * reported. The Case Envelope, its resource ids, its digests, its relations, its
27
+ * principal bindings, the identity binding proof, the policy version, the
28
+ * retention terms and the idempotency key are all composed from that — see
29
+ * `envelope.ts` for what each one is derived from and why it cannot be left to
30
+ * the caller.
31
+ *
32
+ * Types come from `@crowdsource.you/contracts`. This package re-exports none
33
+ * of them: an integrator imports the contract from the contract package, so a
34
+ * `Decision` or a `TaxonomyCode` has exactly one definition.
35
+ */
36
+
37
+ export { CrowdSource, SERVICE_KEY_ENV_VAR, BASE_URL_ENV_VAR } from './client.js';
38
+ export type { CrowdSourceOptions } from './client.js';
39
+
40
+ export { formatServiceKey, parseServiceKey } from './credential.js';
41
+ export type { ServiceCredential } from './credential.js';
42
+
43
+ export {
44
+ COMMUNITY_REVIEW_FORBIDDEN_ALLEGATIONS,
45
+ DEFAULT_BASE_URL,
46
+ DEFAULT_POLICY,
47
+ DEFAULT_RETENTION_DAYS,
48
+ } from './defaults.js';
49
+
50
+ export { canonicalJson, resourceDigest, sha256Digest } from './digest.js';
51
+ export type { CanonicalValue } from './digest.js';
52
+
53
+ /**
54
+ * `composeCaseEnvelope` is deliberately NOT exported.
55
+ *
56
+ * It is the one function in this package that takes an `applicationId`, and it
57
+ * takes it from `Reports`, which got it from the credential. Exporting it would
58
+ * put an `applicationId` parameter on the public surface — which is the thing
59
+ * Appendix F says must not exist, however carefully the doc comment above it
60
+ * were worded. The envelope a report produces is reachable by reading what
61
+ * `reports.create` sends, not by building one yourself.
62
+ */
63
+ export { CrowdSourceReportInputError, defaultIdempotencyKey } from './envelope.js';
64
+ export type {
65
+ AllegationInput,
66
+ ContextInput,
67
+ ContextRole,
68
+ PrincipalInput,
69
+ ReportInput,
70
+ ReportSubjectInput,
71
+ ResourceInput,
72
+ } from './envelope.js';
73
+
74
+ export {
75
+ CROWDSOURCE_API_ERROR_CODES,
76
+ CrowdSourceApiError,
77
+ CrowdSourceConfigurationError,
78
+ CrowdSourceError,
79
+ CrowdSourceTransportError,
80
+ isCrowdSourceApiError,
81
+ isCrowdSourceApiErrorCode,
82
+ isCrowdSourceError,
83
+ } from './errors.js';
84
+ export type { CrowdSourceApiErrorCode } from './errors.js';
85
+
86
+ export { Reports } from './reports.js';
87
+ export type { ReportReceipt, ReportRequestOptions } from './reports.js';
88
+
89
+ export { Cases, Decisions } from './cases.js';
90
+ export type { CaseView, ReadOptions } from './cases.js';
91
+
92
+
93
+ export { CommunityNotes } from './communityNotes.js';
94
+ export type { CommunityNoteReadOptions, CommunityNoteRequestOptions } from './communityNotes.js';
95
+
96
+ export { WebhookEndpoints } from './webhookEndpoints.js';
97
+ export type {
98
+ RegisterWebhookEndpointInput,
99
+ RotateSecretOptions,
100
+ RotatedWebhookSecret,
101
+ WebhookEndpoint,
102
+ WebhookEndpointRequestOptions,
103
+ WebhookSecret,
104
+ } from './webhookEndpoints.js';
105
+
106
+ export { DEFAULT_MAX_ATTEMPTS, DEFAULT_TIMEOUT_MS } from './transport.js';
107
+ export type { FetchLike } from './transport.js';
@@ -0,0 +1,75 @@
1
+ import { CrowdSource } from '../index.js';
2
+ import type { CrowdSourceConnectionConfig, ModerationLogger } from './types.js';
3
+
4
+ /**
5
+ * The CrowdSource client, built once and only when configured.
6
+ *
7
+ * There is deliberately almost nothing here. The SDK already owns the base URL,
8
+ * the timeouts, the bounded per-attempt retries, the idempotency key and the
9
+ * error classification, and a wrapper that re-implemented any of them would be a
10
+ * second answer to a question that has one. What this adds is exactly two
11
+ * things: the client is absent until the integration is switched on, and it is
12
+ * built once rather than per delivery.
13
+ *
14
+ * `applicationId` appears nowhere — the client reads it off the service key, and
15
+ * there is no option, field or parameter through which one could be passed.
16
+ */
17
+ export interface CrowdSourceClientProvider {
18
+ /**
19
+ * The client, or `undefined` when the integration is not configured.
20
+ *
21
+ * `undefined` rather than a throw: a disabled integration is the normal state
22
+ * of a local checkout and of every deployment before rollout, and a report
23
+ * filed there must still be stored. The delivery worker is what notices there
24
+ * is nowhere to send it.
25
+ *
26
+ * A MISCONFIGURED client — a malformed service key — is a different thing and
27
+ * is logged once at error level. Once, because the alternative is one line per
28
+ * delivery attempt per report, which buries the cause it is meant to reveal.
29
+ */
30
+ get(): CrowdSource | undefined;
31
+ }
32
+
33
+ export function createClientProvider(input: {
34
+ config: CrowdSourceConnectionConfig;
35
+ logger: ModerationLogger;
36
+ }): CrowdSourceClientProvider {
37
+ let client: CrowdSource | null = null;
38
+ let configurationError: string | null = null;
39
+
40
+ return {
41
+ get(): CrowdSource | undefined {
42
+ if (!input.config.enabled) return undefined;
43
+ if (client) return client;
44
+ if (configurationError !== null) return undefined;
45
+
46
+ const serviceKey = input.config.serviceKey;
47
+ if (!serviceKey) {
48
+ configurationError = 'no CrowdSource service key is configured';
49
+ input.logger.error('[CrowdSource] enabled but not configured', {
50
+ reason: configurationError,
51
+ });
52
+ return undefined;
53
+ }
54
+
55
+ try {
56
+ client = new CrowdSource({
57
+ serviceKey,
58
+ ...(input.config.baseUrl === undefined ? {} : { baseUrl: input.config.baseUrl }),
59
+ });
60
+ input.logger.info('[CrowdSource] client ready', {
61
+ applicationId: client.applicationId,
62
+ });
63
+ return client;
64
+ } catch (error: unknown) {
65
+ // The SDK's configuration errors name which part of the key is wrong and
66
+ // never echo the secret, so the message is safe to log.
67
+ configurationError = error instanceof Error ? error.message : String(error);
68
+ input.logger.error('[CrowdSource] service key rejected', {
69
+ reason: configurationError,
70
+ });
71
+ return undefined;
72
+ }
73
+ },
74
+ };
75
+ }
@@ -0,0 +1,209 @@
1
+ import { DecisionSchema, type Decision } from '@crowdsource.you/contracts';
2
+ import type { EnforcementExecutor } from './enforcement/executor.js';
3
+ import { primaryAction } from './enforcement/planner.js';
4
+ import { localStatusForDecision } from './reportStatus.js';
5
+ import type { ModerationReportStore } from './store/types.js';
6
+ import type {
7
+ EnforcementOutcome,
8
+ ModerationEnforcementConfig,
9
+ ModerationLogger,
10
+ ModerationOutboxEvent,
11
+ ModerationReportFields,
12
+ ReportDecisionExtraFields,
13
+ } from './types.js';
14
+
15
+ /**
16
+ * Applying a decision that has already been received and recorded.
17
+ *
18
+ * The webhook answered 2xx long before this runs. What is left is the part that
19
+ * touches several collections: write the decision onto every report that opened
20
+ * or joined the case, and enforce it once.
21
+ *
22
+ * "Once" is the invariant that shapes this file. A hundred reports about the
23
+ * same material produce ONE case and ONE consequence, so enforcement is keyed on
24
+ * the decision and not on the reports — the executor is called a single time no
25
+ * matter how many reports are updated, and its own unique index makes even that
26
+ * call safe to repeat.
27
+ */
28
+
29
+ /** A failure that will not be fixed by trying again. */
30
+ export class ModerationDecisionRejectedError extends Error {
31
+ /** Read by the outbox: `false` dead-letters instead of retrying. */
32
+ readonly retryable = false;
33
+
34
+ constructor(message: string) {
35
+ super(message);
36
+ this.name = 'ModerationDecisionRejectedError';
37
+ }
38
+ }
39
+
40
+ /** A failure that a later attempt can still resolve. */
41
+ export class ModerationDecisionDeferredError extends Error {
42
+ readonly retryable = true;
43
+
44
+ constructor(message: string) {
45
+ super(message);
46
+ this.name = 'ModerationDecisionDeferredError';
47
+ }
48
+ }
49
+
50
+ export function createDecisionWorker<
51
+ TReport extends ModerationReportFields,
52
+ TAction extends string,
53
+ TTx,
54
+ >(input: {
55
+ reports: ModerationReportStore<TReport, TTx>;
56
+ executor: EnforcementExecutor<TAction>;
57
+ enforcement: ModerationEnforcementConfig<TAction>;
58
+ logger: ModerationLogger;
59
+ reportDecisionExtraFields?: (decision: Decision) => ReportDecisionExtraFields;
60
+ }): (event: ModerationOutboxEvent) => Promise<void> {
61
+ /**
62
+ * Write the decision onto one report.
63
+ *
64
+ * `decision.revision` is passed as the ceiling the store guards on, so it is
65
+ * the DATABASE that refuses a stale write rather than a read-then-write in
66
+ * this process. Deliveries can overlap — CrowdSource retries for 24 hours, and
67
+ * a correction can arrive while the decision it supersedes is still being
68
+ * applied — and an older revision landing last would otherwise overwrite the
69
+ * current answer with a stale one.
70
+ */
71
+ const applyToReport = async (
72
+ reportId: string,
73
+ decision: Decision,
74
+ enforced: { action: TAction; at: Date | null } | undefined,
75
+ ): Promise<boolean> =>
76
+ await input.reports.applyDecision(
77
+ reportId,
78
+ {
79
+ localStatus: localStatusForDecision(decision.status),
80
+ decisionId: decision.id,
81
+ decisionRevision: decision.revision,
82
+ decisionOutcome: decision.outcome,
83
+ decisionStatus: decision.status,
84
+ decidedAt: new Date(decision.publishedAt),
85
+ /**
86
+ * `enforcedAction` is what the application DECIDED to do; `enforcedAt`
87
+ * is when an effect actually landed. They are written separately because
88
+ * they are different claims, and conflating them puts a timestamp on
89
+ * something that never happened — which is the normal case in `observe`
90
+ * mode, and the permanent case for an application with no sanction
91
+ * primitive. An audit row that says "enforced at 14:02" for an effect
92
+ * nobody carried out is not explainable, and every effect being
93
+ * explainable is the invariant.
94
+ */
95
+ ...(enforced === undefined
96
+ ? {}
97
+ : {
98
+ enforcedAction: enforced.action,
99
+ ...(enforced.at === null ? {} : { enforcedAt: enforced.at }),
100
+ }),
101
+ ...(input.reportDecisionExtraFields === undefined
102
+ ? {}
103
+ : { extra: input.reportDecisionExtraFields(decision) }),
104
+ },
105
+ decision.revision,
106
+ );
107
+
108
+ return async (event) => {
109
+ const caseId = event.payload.caseId;
110
+ if (caseId === undefined) {
111
+ throw new ModerationDecisionRejectedError(
112
+ 'A decision.apply event carried no caseId.',
113
+ );
114
+ }
115
+
116
+ /**
117
+ * Parsed HERE rather than at the webhook.
118
+ *
119
+ * The receiver's job was to prove the bytes came from CrowdSource and to
120
+ * record them; refusing an unrecognised shape at the door would put a real
121
+ * decision back on a retry schedule until it expired. Parsing at the point
122
+ * of use means an event this deployment cannot yet read waits in the outbox
123
+ * until it can.
124
+ */
125
+ const parsed = DecisionSchema.safeParse(event.payload.decision);
126
+ if (!parsed.success) {
127
+ throw new ModerationDecisionRejectedError(
128
+ `The decision for case ${caseId} does not match the published contract: ${parsed.error.issues
129
+ .map((issue) => `${issue.path.join('.') || '(root)'}: ${issue.message}`)
130
+ .join('; ')}`,
131
+ );
132
+ }
133
+ const decision = parsed.data;
134
+
135
+ const reports = await input.reports.findByCaseId(caseId);
136
+
137
+ if (reports.length === 0) {
138
+ /**
139
+ * Retryable, and the race is real rather than theoretical: CrowdSource can
140
+ * decide a case and deliver the webhook while the response that carried
141
+ * the case id back to this deployment is still being written, or while a
142
+ * delivery is being retried. Backing off is correct; dead-lettering would
143
+ * throw the decision away.
144
+ */
145
+ throw new ModerationDecisionDeferredError(
146
+ `No local report is linked to case ${caseId} yet.`,
147
+ );
148
+ }
149
+
150
+ /**
151
+ * One subject per case. The dedup key includes the subject's external id, so
152
+ * every report merged into a case is about the same object — the first
153
+ * report names it.
154
+ */
155
+ const [first] = reports;
156
+ const outcomes = await input.executor.apply({
157
+ decision,
158
+ caseId,
159
+ subject: { type: first.reportedType, id: first.reportedId },
160
+ });
161
+
162
+ /**
163
+ * The EFFECTIVE action, not the planned one. `apply` is the only place that
164
+ * knows a planned action could not apply to this particular object, and
165
+ * `recordedAs` is how it says so — see `EnforcementEffect`.
166
+ */
167
+ const effectiveAction = (outcome: EnforcementOutcome<TAction>): TAction =>
168
+ outcome.recordedAs ?? outcome.action;
169
+
170
+ const enforcedAction = primaryAction(
171
+ outcomes.map(effectiveAction),
172
+ input.enforcement.precedence ?? input.enforcement.actions,
173
+ );
174
+ /**
175
+ * The timestamp belongs to the outcome of THIS action, not to the plan. An
176
+ * action that was claimed and recorded — observe mode, a mode that does not
177
+ * apply it, or an application with nothing to apply — leaves `enforcedAt`
178
+ * unset while `enforcedAction` still says what was decided.
179
+ */
180
+ const enforced =
181
+ enforcedAction === undefined
182
+ ? undefined
183
+ : {
184
+ action: enforcedAction,
185
+ at: outcomes.some(
186
+ (outcome) =>
187
+ effectiveAction(outcome) === enforcedAction &&
188
+ outcome.result === 'applied',
189
+ )
190
+ ? new Date()
191
+ : null,
192
+ };
193
+
194
+ let updated = 0;
195
+ for (const report of reports) {
196
+ if (await applyToReport(report.id, decision, enforced)) updated += 1;
197
+ }
198
+
199
+ input.logger.info('[CrowdSource] decision applied', {
200
+ caseId,
201
+ decisionId: decision.id,
202
+ revision: decision.revision,
203
+ outcome: decision.outcome,
204
+ reportsMatched: reports.length,
205
+ reportsUpdated: updated,
206
+ actions: outcomes.map((outcome) => `${outcome.action}:${outcome.result}`).join(','),
207
+ });
208
+ };
209
+ }
@@ -0,0 +1,163 @@
1
+ import type { CrowdSourceClientProvider } from './client.js';
2
+ import { buildModerationReportInput, type SubjectRegistry } from './evidence.js';
3
+ import type { ModerationReportStore } from './store/types.js';
4
+ import type {
5
+ ModerationLogger,
6
+ ModerationMetrics,
7
+ ModerationOutboxEvent,
8
+ ModerationReportFields,
9
+ ModerationTaxonomy,
10
+ } from './types.js';
11
+
12
+ /**
13
+ * Delivering a stored report to CrowdSource.
14
+ *
15
+ * Everything hard about this is already handled elsewhere and the shape of this
16
+ * file is what is left over: the SDK owns the envelope, the idempotency key, the
17
+ * timeouts, the per-attempt retries and the classification of failures; the
18
+ * outbox owns durability, backoff and dead-lettering. What remains is to
19
+ * describe the material, hand it over, and write down what came back.
20
+ *
21
+ * The failures are as important as the success:
22
+ *
23
+ * - **Nowhere to send it.** The integration is not configured. The event stays
24
+ * pending, untouched, and delivers when it is — a delay, never a loss.
25
+ * - **The object is gone.** Deleted between the report and its delivery. There
26
+ * is nothing to review, so the report closes locally instead of retrying for
27
+ * days.
28
+ * - **The type has no provider.** Unreachable by design — such a report never
29
+ * gets a delivery event — so an event that reaches it is a defect and is
30
+ * dead-lettered rather than retried or filed as a state.
31
+ * - **Anything else** is the SDK's `retryable` to answer, and the outbox obeys
32
+ * it.
33
+ */
34
+
35
+ /** Thrown when there is nowhere to deliver to yet. Always retryable. */
36
+ export class CrowdSourceUnavailableError extends Error {
37
+ readonly retryable = true;
38
+
39
+ constructor() {
40
+ super('The CrowdSource integration is not configured in this deployment.');
41
+ this.name = 'CrowdSourceUnavailableError';
42
+ }
43
+ }
44
+
45
+ /**
46
+ * A delivery event that cannot become deliverable.
47
+ *
48
+ * `retryable: false` is the field the outbox reads to dead-letter instead of
49
+ * backing off — the same contract every error from `@crowdsource.you/core` answers.
50
+ */
51
+ export class ModerationDeliveryRejectedError extends Error {
52
+ readonly retryable = false;
53
+
54
+ constructor(message: string) {
55
+ super(message);
56
+ this.name = 'ModerationDeliveryRejectedError';
57
+ }
58
+ }
59
+
60
+ export function createDeliveryWorker<TReport extends ModerationReportFields, TTx>(input: {
61
+ reports: ModerationReportStore<TReport, TTx>;
62
+ registry: SubjectRegistry;
63
+ taxonomy: ModerationTaxonomy;
64
+ client: CrowdSourceClientProvider;
65
+ logger: ModerationLogger;
66
+ metrics?: ModerationMetrics;
67
+ }): (event: ModerationOutboxEvent) => Promise<void> {
68
+ const count = (result: string): void => {
69
+ input.metrics?.incrementCounter('crowdsource_report_delivery_total', 1, { result });
70
+ };
71
+
72
+ return async (event) => {
73
+ const reportId = event.payload.reportId;
74
+ if (reportId === undefined) {
75
+ throw new ModerationDeliveryRejectedError(
76
+ 'A report.submit event carried no reportId.',
77
+ );
78
+ }
79
+
80
+ const report = await input.reports.findById(reportId);
81
+ if (!report) {
82
+ /**
83
+ * The report is gone but its delivery event survived. Nothing to deliver
84
+ * and nothing to fix, so the event completes — retrying would keep looking
85
+ * for a row that no longer exists.
86
+ */
87
+ input.logger.warn('[CrowdSource] delivery event has no report', { reportId });
88
+ return;
89
+ }
90
+
91
+ const crowdsource = input.client.get();
92
+ if (!crowdsource) throw new CrowdSourceUnavailableError();
93
+
94
+ /**
95
+ * A `ModerationSubjectUnsupportedError` from here is NOT caught. It carries
96
+ * `retryable: false`, so the outbox dead-letters the event and the
97
+ * reconciliation sweep counts it — which is the right channel for a defect
98
+ * that needs a human. Catching it and writing a local state would put the
99
+ * report somewhere nothing alerts on.
100
+ */
101
+ const described = await buildModerationReportInput({
102
+ report: {
103
+ id: reportId,
104
+ reportedType: report.reportedType,
105
+ reportedId: report.reportedId,
106
+ reporter: report.reporter,
107
+ categories: report.categories,
108
+ ...(report.details === undefined ? {} : { details: report.details }),
109
+ createdAt: report.createdAt,
110
+ },
111
+ registry: input.registry,
112
+ taxonomy: input.taxonomy,
113
+ });
114
+
115
+ if (described === null) {
116
+ // Nothing left to review, so the report is closed with the reason rather
117
+ // than retried against material that is gone.
118
+ await input.reports.close(
119
+ reportId,
120
+ 'The reported content no longer exists, so there is nothing to review.',
121
+ );
122
+ count('content_unavailable');
123
+ return;
124
+ }
125
+
126
+ let receipt: Awaited<ReturnType<typeof crowdsource.reports.create>>;
127
+ try {
128
+ receipt = await crowdsource.reports.create(described.reportInput);
129
+ } catch (error: unknown) {
130
+ /**
131
+ * The failure is visible on the report itself, not only in the outbox row.
132
+ * `delivery_failed` is what a reporter's receipt and the reconciliation
133
+ * sweep both read; leaving the report at `queued` while the outbox quietly
134
+ * backed off would hide the problem in a collection nobody looks at.
135
+ * Written before rethrowing so the outbox still applies its own backoff or
136
+ * dead-letters the event.
137
+ */
138
+ await input.reports.markDeliveryFailed(
139
+ reportId,
140
+ // Bounded here rather than by a column width, so both dialects agree: a
141
+ // Mongoose validator throws on overflow and Postgres errors 22001.
142
+ (error instanceof Error ? error.message : String(error)).slice(0, 2_000),
143
+ );
144
+ count('failed');
145
+ throw error;
146
+ }
147
+
148
+ await input.reports.markSubmitted(reportId, {
149
+ crowdSourceReportId: receipt.reportId,
150
+ crowdSourceCaseId: receipt.caseId,
151
+ crowdSourceMerged: receipt.merged,
152
+ contentSnapshotHash: described.snapshotHash,
153
+ submittedAt: new Date(),
154
+ });
155
+
156
+ count(receipt.merged ? 'merged' : 'delivered');
157
+ input.logger.info('[CrowdSource] report delivered', {
158
+ reportId,
159
+ caseId: receipt.caseId,
160
+ merged: receipt.merged,
161
+ });
162
+ };
163
+ }