@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,432 @@
1
+ /**
2
+ * Composing a Case Envelope out of what an application already has.
3
+ *
4
+ * §5 is a rich document contract — resources with stable ids, relations between
5
+ * them, pseudonymous principal bindings, digests, a policy reference, privacy
6
+ * terms. Handing that to an integrator and asking them to fill it in is how a
7
+ * moderation integration becomes a two-week project, and it is how the parts
8
+ * that MATTER get filled in wrong: a random resource id or a per-report binding
9
+ * proof inside the hashed projection means two people reporting the same post
10
+ * open two cases, and "one penalty per incident" (Appendix F) fails in
11
+ * production with nothing failing in a test.
12
+ *
13
+ * So the integrator describes the object — its id, its author, its text, its
14
+ * attachments, what is alleged about it — and this module derives the rest:
15
+ *
16
+ * * **Resource ids are positional and stable** (`res_subject`,
17
+ * `res_attachment_1`, `res_context_1`). Two reporters describing the same
18
+ * post through this SDK produce byte-identical resource lists, which is what
19
+ * makes §7.3's dedup key match.
20
+ * * **Principal refs are derived from the identity**, never generated. Same
21
+ * reason: the content snapshot the server hashes includes every principal
22
+ * the material points at, so a random ref would fragment the case.
23
+ * * **Digests are computed** (see `digest.ts`).
24
+ * * **Relations are derived** from the role each resource was given.
25
+ * * **The binding proof is the Oxy `sub`.** §11.14 requires proof that a
26
+ * pseudonymous ref corresponds to a real identity before anything can touch
27
+ * Oxy Trust. Sign in with Oxy already produced exactly that: a subject
28
+ * issued for this `applicationId`. There is no separate binding step to
29
+ * implement, and this module does not invent one.
30
+ * * **`applicationId` comes from the credential** (see `credential.ts`). No
31
+ * input on this page can carry one.
32
+ */
33
+
34
+ import {
35
+ CaseEnvelopeSchema,
36
+ CASE_ENVELOPE_SCHEMA_VERSION,
37
+ type Allegation,
38
+ type CaseEnvelope,
39
+ type CasePolicyRef,
40
+ type CasePrivacy,
41
+ type CaseUrgency,
42
+ type MetadataBag,
43
+ type PrincipalBinding,
44
+ type PrincipalType,
45
+ type Relation,
46
+ type Resource,
47
+ type SubjectType,
48
+ type TaxonomyCode,
49
+ } from '@crowdsource.you/contracts';
50
+
51
+ import { DEFAULT_POLICY, allegationsForbiddingCommunityReview, defaultPrivacy } from './defaults.js';
52
+ import { canonicalJson, resourceDigest, sha256Digest, type CanonicalValue } from './digest.js';
53
+ import { CrowdSourceError } from './errors.js';
54
+
55
+ /** `Omit` that survives a discriminated union instead of collapsing it. */
56
+ type DistributiveOmit<T, K extends keyof never> = T extends unknown ? Omit<T, K> : never;
57
+
58
+ /**
59
+ * A resource as an integrator describes it: the material, without the envelope
60
+ * plumbing.
61
+ *
62
+ * Derived from the published `Resource` union rather than restated, so a
63
+ * resource type added to the contract is available here the moment the
64
+ * contracts dependency is bumped, and a field removed from it stops compiling
65
+ * on the next build instead of being silently ignored at ingress.
66
+ */
67
+ export type ResourceInput = DistributiveOmit<
68
+ Resource,
69
+ 'id' | 'role' | 'sha256' | 'authorPrincipalRef' | 'createdAt'
70
+ > & {
71
+ /** The moment of the ORIGINAL object, not of the report (§5.2). */
72
+ readonly createdAt?: Date | string;
73
+ };
74
+
75
+ /** Why a context resource is attached to the subject (§5.5). */
76
+ export type ContextRole = 'parent' | 'quoted' | 'context' | 'evidence';
77
+
78
+ export type ContextInput = ResourceInput & { readonly role: ContextRole };
79
+
80
+ /**
81
+ * A person or account, as the application knows them.
82
+ *
83
+ * `oxyUserId` is the OAuth `sub` from Sign in with Oxy. Supplying it is what
84
+ * makes an actor addressable by Oxy Trust at all — §11.14 admits no effect
85
+ * without a binding proof, and the `sub` issued for this application IS that
86
+ * proof. `id` is the application's own identifier for the same actor, for
87
+ * applications that keep their own user table; it is what a reviewer's
88
+ * pseudonymous ref is derived from and never shown to a jury.
89
+ */
90
+ export interface PrincipalInput {
91
+ readonly oxyUserId?: string;
92
+ readonly id?: string;
93
+ /** Defaults to `oxy_user` with an `oxyUserId`, `local_user` without one. */
94
+ readonly type?: PrincipalType;
95
+ }
96
+
97
+ /** What the reporter claims, not what is true (§6.2). */
98
+ export interface AllegationInput {
99
+ readonly code: TaxonomyCode;
100
+ /** The reporter's own words. Never shown as evidence of the claim. */
101
+ readonly details?: string;
102
+ }
103
+
104
+ export interface ReportSubjectInput {
105
+ /** The application's own id for the object being reported. */
106
+ readonly externalId: string;
107
+ /** §5.4, e.g. `social.post` or `custom.<organization>.<object_type>`. */
108
+ readonly type: SubjectType;
109
+ /** Where the application's own users see it. Never fetched by a jury. */
110
+ readonly permalink?: string;
111
+ readonly author?: PrincipalInput;
112
+ }
113
+
114
+ export interface ReportInput {
115
+ /** The application's own report id. Also the default idempotency key. */
116
+ readonly externalReportId: string;
117
+ readonly subject: ReportSubjectInput;
118
+ /** The reported material. A string is shorthand for a plain-text resource. */
119
+ readonly content: string | ResourceInput;
120
+ readonly attachments?: readonly ResourceInput[];
121
+ /** Replies, quotes, surrounding messages — context without extra exposure. */
122
+ readonly context?: readonly ContextInput[];
123
+ readonly allegations: readonly (TaxonomyCode | AllegationInput)[];
124
+ /** Who reported it. Omit for a report the application raised itself. */
125
+ readonly reportedBy?: PrincipalInput;
126
+ /** Defaults to the pinned baseline policy version — see `defaults.ts`. */
127
+ readonly policy?: CasePolicyRef;
128
+ readonly privacy?: Partial<CasePrivacy>;
129
+ readonly urgency?: CaseUrgency;
130
+ readonly metadata?: MetadataBag;
131
+ /**
132
+ * When the USER reported it (§5.1 `source.submittedAt`) — the timestamp on the
133
+ * application's own outbox row, not the moment of delivery.
134
+ *
135
+ * There is no default, and that is load-bearing rather than an omission. The
136
+ * ingress fingerprints the whole `{ externalReportId, envelope }` to detect
137
+ * §10.5's "external id reused with different content", so ANY value this
138
+ * client invented per attempt would make a legitimate retry from an outbox a
139
+ * permanent 409 — the failure would appear as moderation work silently
140
+ * stuck in a delivery queue, days after the integration was written. Omitted
141
+ * when absent: §5.1 makes `source` optional and §5.8 leaves it out.
142
+ */
143
+ readonly submittedAt?: Date | string;
144
+ /** Appendix D. Defaults to `report.<externalReportId>`. */
145
+ readonly idempotencyKey?: string;
146
+ }
147
+
148
+ /** Raised when an input cannot become a valid envelope. Never retryable. */
149
+ export class CrowdSourceReportInputError extends CrowdSourceError {
150
+ constructor(message: string) {
151
+ super(message, false);
152
+ }
153
+ }
154
+
155
+ /** How the application declares its own environment (§5.1 `source`). */
156
+ export type SourceEnvironment = 'production' | 'sandbox';
157
+
158
+ export interface EnvelopeComposition {
159
+ readonly applicationId: string;
160
+ readonly environment: SourceEnvironment;
161
+ }
162
+
163
+ const SUBJECT_RESOURCE_ID = 'res_subject';
164
+
165
+ function isoTimestamp(value: Date | string): string {
166
+ return value instanceof Date ? value.toISOString() : value;
167
+ }
168
+
169
+ /**
170
+ * A principal ref derived from the identity, so it is the same value for every
171
+ * reporter.
172
+ *
173
+ * Opaque on purpose: §9.1 keeps identities away from the jury, and a ref that
174
+ * embedded a username would put one on the reviewer's screen. Truncated to 128
175
+ * bits, which is far past any collision risk within one envelope's 50-binding
176
+ * ceiling and keeps the ref short enough to read in a case document.
177
+ */
178
+ function principalRef(type: PrincipalType, identity: string): string {
179
+ return `p_${sha256Digest(canonicalJson([type, identity])).slice('sha256:'.length, 'sha256:'.length + 32)}`;
180
+ }
181
+
182
+ interface ResolvedPrincipal {
183
+ readonly ref: string;
184
+ readonly binding: PrincipalBinding;
185
+ }
186
+
187
+ function resolvePrincipal(input: PrincipalInput, role: string): ResolvedPrincipal {
188
+ const type: PrincipalType =
189
+ input.type ?? (input.oxyUserId === undefined ? 'local_user' : 'oxy_user');
190
+
191
+ if (type === 'oxy_user' && input.oxyUserId === undefined) {
192
+ throw new CrowdSourceReportInputError(
193
+ `The ${role} is declared as an oxy_user but carries no oxyUserId. The Oxy subject IS the binding proof (§11.14); without it there is nothing to bind.`,
194
+ );
195
+ }
196
+
197
+ const externalPrincipalId = input.id ?? input.oxyUserId;
198
+ if (externalPrincipalId === undefined) {
199
+ throw new CrowdSourceReportInputError(
200
+ `The ${role} carries neither an oxyUserId nor an id, so there is no identity to reference.`,
201
+ );
202
+ }
203
+
204
+ const ref = principalRef(type, externalPrincipalId);
205
+ return {
206
+ ref,
207
+ binding: {
208
+ principalRef: ref,
209
+ type,
210
+ externalPrincipalId,
211
+ /**
212
+ * The `sub` itself. §11.14 asks for proof that this ref is that identity,
213
+ * and an OAuth subject issued to this application for this user is that
214
+ * proof — it is not a receipt POINTING at one. Deliberately absent for
215
+ * every other principal type: they have no Oxy identity to move, and
216
+ * requiring a proof from them would lock out every tenant whose users are
217
+ * not Oxy users.
218
+ */
219
+ ...(type === 'oxy_user' && input.oxyUserId !== undefined
220
+ ? { bindingProofId: input.oxyUserId }
221
+ : {}),
222
+ },
223
+ };
224
+ }
225
+
226
+ /** The object a resource's digest is taken over — see `digest.ts`. */
227
+ function reviewableRepresentation(resource: ResourceInput): CanonicalValue {
228
+ if (resource.type === 'custom') {
229
+ return { type: resource.type, schemaId: resource.schemaId, data: resource.payload };
230
+ }
231
+ return { type: resource.type, data: 'data' in resource ? resource.data : null };
232
+ }
233
+
234
+ /**
235
+ * Turns one described resource into a contract resource.
236
+ *
237
+ * The digest is the only field invented here. An asset-backed resource does not
238
+ * get one: its bytes are outside the envelope and `asset.sha256` already
239
+ * identifies them, which is the split the contract makes.
240
+ *
241
+ * Returns `unknown` rather than `Resource`, deliberately. The value is assembled
242
+ * from caller data and only becomes a `Resource` when `CaseEnvelopeSchema`
243
+ * parses it at the end of `composeCaseEnvelope`. Declaring it a `Resource` here
244
+ * would need a cast, and a cast is a claim the type system cannot check — the
245
+ * parse is what actually establishes it.
246
+ */
247
+ function composeResource(
248
+ input: ResourceInput,
249
+ id: string,
250
+ role: Resource['role'],
251
+ authorPrincipalRef: string | undefined,
252
+ ): unknown {
253
+ const { createdAt, ...rest } = input;
254
+
255
+ return {
256
+ ...rest,
257
+ id,
258
+ role,
259
+ ...(createdAt === undefined ? {} : { createdAt: isoTimestamp(createdAt) }),
260
+ ...(authorPrincipalRef === undefined ? {} : { authorPrincipalRef }),
261
+ ...('asset' in input ? {} : { sha256: resourceDigest(reviewableRepresentation(input)) }),
262
+ };
263
+ }
264
+
265
+ const CONTEXT_RELATIONS: Readonly<Record<ContextRole, Relation['type']>> = Object.freeze({
266
+ parent: 'replies_to',
267
+ quoted: 'quotes',
268
+ context: 'contextualizes',
269
+ evidence: 'refers_to',
270
+ });
271
+
272
+ function normalisedAllegations(
273
+ input: readonly (TaxonomyCode | AllegationInput)[],
274
+ ): readonly AllegationInput[] {
275
+ return input.map((entry) => (typeof entry === 'string' ? { code: entry } : entry));
276
+ }
277
+
278
+ /**
279
+ * Composes and VALIDATES a Case Envelope.
280
+ *
281
+ * The parse at the end is not belt-and-braces. Everything above assembles a
282
+ * document from caller data — a subject type, a text body, an asset digest —
283
+ * and the contract is where a bad one is caught. Catching it here means the
284
+ * integrator sees the field and the reason at the call site, in their own
285
+ * process, instead of reading a 422 out of a delivery worker's log an hour
286
+ * later.
287
+ */
288
+ export function composeCaseEnvelope(
289
+ input: ReportInput,
290
+ composition: EnvelopeComposition,
291
+ ): CaseEnvelope {
292
+ if (input.submittedAt === undefined && composition.environment === 'sandbox') {
293
+ throw new CrowdSourceReportInputError(
294
+ 'A sandbox report must declare submittedAt: the environment travels in `source`, and `source` cannot be composed without a submission time this client would otherwise have to invent — which would make every retry of this report a 409.',
295
+ );
296
+ }
297
+
298
+ const allegations = normalisedAllegations(input.allegations);
299
+ const allegationCodes = allegations.map((allegation) => allegation.code);
300
+
301
+ const bindings: PrincipalBinding[] = [];
302
+ const rememberPrincipal = (principal: ResolvedPrincipal): string => {
303
+ if (!bindings.some((binding) => binding.principalRef === principal.ref)) {
304
+ bindings.push(principal.binding);
305
+ }
306
+ return principal.ref;
307
+ };
308
+
309
+ const authorRef =
310
+ input.subject.author === undefined
311
+ ? undefined
312
+ : rememberPrincipal(resolvePrincipal(input.subject.author, 'subject author'));
313
+ const reporterRef =
314
+ input.reportedBy === undefined
315
+ ? undefined
316
+ : rememberPrincipal(resolvePrincipal(input.reportedBy, 'reporter'));
317
+
318
+ const subjectContent: ResourceInput =
319
+ typeof input.content === 'string'
320
+ ? { type: 'text', data: { text: input.content } }
321
+ : input.content;
322
+
323
+ const resources: unknown[] = [
324
+ composeResource(subjectContent, SUBJECT_RESOURCE_ID, 'subject', authorRef),
325
+ ];
326
+ const relations: Relation[] = [];
327
+
328
+ (input.attachments ?? []).forEach((attachment, index) => {
329
+ const id = `res_attachment_${index + 1}`;
330
+ resources.push(composeResource(attachment, id, 'attachment', authorRef));
331
+ relations.push({ from: SUBJECT_RESOURCE_ID, type: 'has_attachment', to: id });
332
+ });
333
+
334
+ (input.context ?? []).forEach((entry, index) => {
335
+ const id = `res_context_${index + 1}`;
336
+ const { role, ...resource } = entry;
337
+ resources.push(composeResource(resource, id, role, undefined));
338
+ relations.push(
339
+ role === 'context'
340
+ ? { from: id, type: CONTEXT_RELATIONS[role], to: SUBJECT_RESOURCE_ID }
341
+ : { from: SUBJECT_RESOURCE_ID, type: CONTEXT_RELATIONS[role], to: id },
342
+ );
343
+ });
344
+
345
+ const privacy = resolvePrivacy(input.privacy, allegationCodes);
346
+
347
+ const envelope: unknown = {
348
+ schemaVersion: CASE_ENVELOPE_SCHEMA_VERSION,
349
+ applicationId: composition.applicationId,
350
+ externalReportId: input.externalReportId,
351
+ ...(input.submittedAt === undefined
352
+ ? {}
353
+ : {
354
+ source: {
355
+ environment: composition.environment,
356
+ submittedAt: isoTimestamp(input.submittedAt),
357
+ },
358
+ }),
359
+ subject: {
360
+ externalId: input.subject.externalId,
361
+ type: input.subject.type,
362
+ primaryResourceId: SUBJECT_RESOURCE_ID,
363
+ ...(input.subject.permalink === undefined ? {} : { permalink: input.subject.permalink }),
364
+ },
365
+ principalBindings: bindings,
366
+ resources,
367
+ relations,
368
+ allegations: allegations.map(
369
+ (allegation): Allegation => ({
370
+ code: allegation.code,
371
+ ...(allegation.details === undefined ? {} : { details: allegation.details }),
372
+ ...(reporterRef === undefined ? {} : { reporterPrincipalRef: reporterRef }),
373
+ }),
374
+ ),
375
+ policy: input.policy ?? DEFAULT_POLICY,
376
+ privacy,
377
+ ...(input.urgency === undefined ? {} : { urgency: input.urgency }),
378
+ ...(input.metadata === undefined ? {} : { metadata: input.metadata }),
379
+ };
380
+
381
+ const parsed = CaseEnvelopeSchema.safeParse(envelope);
382
+ if (!parsed.success) {
383
+ throw new CrowdSourceReportInputError(
384
+ `This report cannot be composed into a valid Case Envelope — ${parsed.error.issues
385
+ .map((issue) => `${issue.path.join('.') || '(root)'}: ${issue.message}`)
386
+ .join('; ')}`,
387
+ );
388
+ }
389
+ return parsed.data;
390
+ }
391
+
392
+ /**
393
+ * Privacy terms, with §7.5's routing rule as a floor the caller cannot lift.
394
+ *
395
+ * An explicit `allowCommunityReview: true` alongside an allegation §7.5 keeps
396
+ * away from a community jury is REFUSED, not corrected. Correcting it would
397
+ * leave the application believing it had asked for something it did not get,
398
+ * and the next thing built on that belief would be wrong too.
399
+ */
400
+ function resolvePrivacy(
401
+ requested: Partial<CasePrivacy> | undefined,
402
+ allegationCodes: readonly TaxonomyCode[],
403
+ ): CasePrivacy {
404
+ const base = defaultPrivacy(allegationCodes);
405
+ if (requested === undefined) return base;
406
+
407
+ const forbidden = allegationsForbiddingCommunityReview(allegationCodes);
408
+ if (requested.allowCommunityReview === true && forbidden.length > 0) {
409
+ throw new CrowdSourceReportInputError(
410
+ `A report alleging ${forbidden.join(', ')} cannot set allowCommunityReview: true — §7.5 routes this material to a specialist team, never to a community jury.`,
411
+ );
412
+ }
413
+
414
+ return {
415
+ ...base,
416
+ ...(requested.retentionDays === undefined ? {} : { retentionDays: requested.retentionDays }),
417
+ ...(requested.allowCommunityReview === undefined
418
+ ? {}
419
+ : { allowCommunityReview: requested.allowCommunityReview }),
420
+ ...(requested.containsPersonalData === undefined
421
+ ? {}
422
+ : { containsPersonalData: requested.containsPersonalData }),
423
+ ...(requested.sensitivityHint === undefined
424
+ ? {}
425
+ : { sensitivityHint: requested.sensitivityHint }),
426
+ };
427
+ }
428
+
429
+ /** Appendix D: the same report re-delivered must return the same `reportId`. */
430
+ export function defaultIdempotencyKey(externalReportId: string): string {
431
+ return `report.${externalReportId}`;
432
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,165 @@
1
+ /**
2
+ * The failures an integrator can observe, and the one question they all answer.
3
+ *
4
+ * §7.1 puts the durable retry path in the INTEGRATOR's outbox: a 2xx from their
5
+ * own application means the report is stored locally and will be delivered
6
+ * eventually, never that a synchronous call to CrowdSource succeeded. So the
7
+ * only thing an outbox worker needs from a failure is whether re-delivering the
8
+ * same payload can still succeed. That is `retryable`, and every error below
9
+ * answers it — an integrator branching on `instanceof` or on a status code is
10
+ * re-deriving something this class already knows.
11
+ *
12
+ * The distinction that matters most is 409. §10.5 gives it one meaning: the same
13
+ * `externalReportId` arrived with a different body. Retrying that forever never
14
+ * succeeds and the payload is the thing that has to change, so it is the one
15
+ * 4xx an outbox must stop on rather than back off from.
16
+ */
17
+
18
+ /**
19
+ * The machine-readable codes §10.5 assigns a meaning to.
20
+ *
21
+ * Declared here rather than imported because `@crowdsource.you/contracts`
22
+ * does not publish the HTTP error vocabulary — it publishes the documents that
23
+ * travel over HTTP. That is a gap worth closing in contracts rather than a
24
+ * decision to state the list twice: the backend has the same union in
25
+ * `http/apiError.ts`, and the two must not drift. Until it moves, this is the
26
+ * copy integrators compile against.
27
+ */
28
+ export const CROWDSOURCE_API_ERROR_CODES = [
29
+ 'invalid_request',
30
+ 'unauthorized',
31
+ 'forbidden',
32
+ 'not_found',
33
+ 'conflict',
34
+ 'payload_too_large',
35
+ 'unprocessable_envelope',
36
+ 'rate_limited',
37
+ 'internal_error',
38
+ 'service_unavailable',
39
+ ] as const;
40
+
41
+ export type CrowdSourceApiErrorCode = (typeof CROWDSOURCE_API_ERROR_CODES)[number];
42
+
43
+ const API_ERROR_CODE_SET: ReadonlySet<string> = new Set(CROWDSOURCE_API_ERROR_CODES);
44
+
45
+ export function isCrowdSourceApiErrorCode(value: unknown): value is CrowdSourceApiErrorCode {
46
+ return typeof value === 'string' && API_ERROR_CODE_SET.has(value);
47
+ }
48
+
49
+ /**
50
+ * The code to report when the response carried no usable one — a proxy, a load
51
+ * balancer or a gateway answering instead of the service.
52
+ */
53
+ export function apiErrorCodeForStatus(status: number): CrowdSourceApiErrorCode {
54
+ switch (status) {
55
+ case 400:
56
+ return 'invalid_request';
57
+ case 401:
58
+ return 'unauthorized';
59
+ case 403:
60
+ return 'forbidden';
61
+ case 404:
62
+ return 'not_found';
63
+ case 409:
64
+ return 'conflict';
65
+ case 413:
66
+ return 'payload_too_large';
67
+ case 422:
68
+ return 'unprocessable_envelope';
69
+ case 429:
70
+ return 'rate_limited';
71
+ case 503:
72
+ return 'service_unavailable';
73
+ default:
74
+ return 'internal_error';
75
+ }
76
+ }
77
+
78
+ /** The base of every error this package throws. */
79
+ export class CrowdSourceError extends Error {
80
+ /** Whether re-delivering the same payload can still succeed. */
81
+ readonly retryable: boolean;
82
+
83
+ constructor(message: string, retryable: boolean, options?: { cause?: unknown }) {
84
+ super(message, options);
85
+ this.name = new.target.name;
86
+ this.retryable = retryable;
87
+ }
88
+ }
89
+
90
+ /**
91
+ * The client is not configured to be able to make the call.
92
+ *
93
+ * Never retryable: a missing service key, a malformed one or a base URL that is
94
+ * not a URL do not become correct by waiting.
95
+ */
96
+ export class CrowdSourceConfigurationError extends CrowdSourceError {
97
+ constructor(message: string) {
98
+ super(message, false);
99
+ }
100
+ }
101
+
102
+ /**
103
+ * The request never produced an HTTP response — DNS, connection, TLS, timeout,
104
+ * or an abort.
105
+ *
106
+ * Retryable, with one exception: an abort the CALLER asked for. Retrying that
107
+ * would ignore the instruction, so it is surfaced as not retryable and the
108
+ * caller decides what happens next.
109
+ */
110
+ export class CrowdSourceTransportError extends CrowdSourceError {
111
+ constructor(message: string, options: { cause?: unknown; retryable?: boolean } = {}) {
112
+ super(message, options.retryable ?? true, { cause: options.cause });
113
+ }
114
+ }
115
+
116
+ /**
117
+ * CrowdSource answered, and the answer was a refusal (§10.5).
118
+ *
119
+ * `code` is the machine-readable code from the response body when the service
120
+ * sent one. A proxy or load balancer that answers instead of the service does
121
+ * not, so it is derived from the status in that case rather than left undefined
122
+ * — an integrator switching on `code` should not have to handle "the 502 came
123
+ * from the ALB" as a separate shape.
124
+ */
125
+ export class CrowdSourceApiError extends CrowdSourceError {
126
+ readonly status: number;
127
+ readonly code: CrowdSourceApiErrorCode;
128
+ readonly details?: Readonly<Record<string, string | number | boolean>>;
129
+
130
+ constructor(input: {
131
+ status: number;
132
+ code: CrowdSourceApiErrorCode;
133
+ message: string;
134
+ details?: Readonly<Record<string, string | number | boolean>>;
135
+ }) {
136
+ super(input.message, RETRYABLE_STATUSES.has(input.status));
137
+ this.status = input.status;
138
+ this.code = input.code;
139
+ this.details = input.details;
140
+ }
141
+
142
+ /** True when this is §10.5's "external id reused with a different body". */
143
+ get isPayloadConflict(): boolean {
144
+ return this.code === 'conflict';
145
+ }
146
+ }
147
+
148
+ /**
149
+ * The statuses a retry of the SAME payload can still resolve.
150
+ *
151
+ * 429 and 503 are the two §10.5 names for "come back later". 5xx is included
152
+ * because a defect on the service side is not a defect in the payload, and the
153
+ * idempotency key makes the retry free of duplicates either way. Every 4xx below
154
+ * 429 is excluded deliberately: a rejected envelope, a missing scope, a revoked
155
+ * credential and a payload conflict all stay rejected.
156
+ */
157
+ const RETRYABLE_STATUSES: ReadonlySet<number> = new Set([408, 425, 429, 500, 502, 503, 504]);
158
+
159
+ export function isCrowdSourceError(error: unknown): error is CrowdSourceError {
160
+ return error instanceof CrowdSourceError;
161
+ }
162
+
163
+ export function isCrowdSourceApiError(error: unknown): error is CrowdSourceApiError {
164
+ return error instanceof CrowdSourceApiError;
165
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * `@crowdsource.you/core/express` — receiving CrowdSource webhooks safely.
3
+ *
4
+ * The whole integration:
5
+ *
6
+ * ```ts
7
+ * import { crowdsourceWebhooks } from '@crowdsource.you/core/express';
8
+ *
9
+ * app.post('/webhooks/crowdsource', crowdsourceWebhooks({
10
+ * on: {
11
+ * 'case.decided': async (event) => {
12
+ * await moderationQueue.add(event.id, event.data);
13
+ * },
14
+ * },
15
+ * }));
16
+ * ```
17
+ *
18
+ * The secret comes from `CROWDSOURCE_WEBHOOK_SECRET`, the raw body is read by
19
+ * the handler itself, the signature is verified in constant time over the bytes
20
+ * that arrived, a stale or replayed delivery is refused, and an event type this
21
+ * integration does not handle is acknowledged and ignored. None of that is
22
+ * optional and none of it can be got wrong by mounting things in the wrong
23
+ * order — see `middleware.ts` for what happens when a body parser gets there
24
+ * first.
25
+ */
26
+
27
+ export {
28
+ crowdsourceWebhooks,
29
+ CrowdSourceWebhookConfigurationError,
30
+ WEBHOOK_PREVIOUS_SECRET_ENV_VAR,
31
+ WEBHOOK_SECRET_ENV_VAR,
32
+ } from './middleware.js';
33
+ export type { CrowdSourceWebhooksOptions, WebhookEventHandlers } from './middleware.js';
34
+
35
+ export { memoryProcessedEventStore } from './store.js';
36
+ export type { MemoryProcessedEventStoreOptions, ProcessedEventStore } from './store.js';
37
+
38
+ export {
39
+ signedPayloadBytes,
40
+ verifyWebhookDelivery,
41
+ WEBHOOK_REJECTIONS,
42
+ } from './verify.js';
43
+ export type { WebhookRejection, WebhookVerification, WebhookVerificationInput } from './verify.js';