@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,71 @@
1
+ /**
2
+ * `@crowdsource.you/core/outbox` — the application half of a CrowdSource integration.
3
+ *
4
+ * An application that adopts CrowdSource has to solve the same six problems
5
+ * every other application does: store a report and its promise of delivery
6
+ * atomically, deliver it with retries and a dead-letter path, receive signed
7
+ * decisions without a body parser destroying the signature, deduplicate
8
+ * redeliveries across several tasks, apply a decision without a stale revision
9
+ * overwriting a fresh one, and carry out consequences exactly once and
10
+ * reversibly. None of that has anything to do with what the application's
11
+ * objects are.
12
+ *
13
+ * So all of it is here, and an application supplies four things: its subjects,
14
+ * its category mapping, its enforcement tables, and a STORE built by the
15
+ * PostgreSQL store factory.
16
+ *
17
+ * ```ts
18
+ * import { createModerationIntegration } from '@crowdsource.you/core/outbox';
19
+ * import { postgresModerationStore } from '@crowdsource.you/core/outbox/postgres';
20
+ *
21
+ * const moderation = createModerationIntegration({
22
+ * store,
23
+ * crowdSource: { enabled: true, serviceKey, webhookSecret, enforcementMode: 'observe' },
24
+ * subjects: [listingSubjectProvider(), reviewSubjectProvider()],
25
+ * taxonomy: { version: '2026.07', allegationsFor },
26
+ * enforcement: commerceEnforcement,
27
+ * logger,
28
+ * });
29
+ *
30
+ * // Indexes before the first write: the unique ones ARE the exactly-once
31
+ * // mechanism, and an index that does not exist yet refuses nothing.
32
+ * await store.ensureSchema();
33
+ *
34
+ * // BEFORE express.json() — the signature covers the bytes that arrived.
35
+ * app.use('/webhooks', moderation.webhookRouter());
36
+ * app.use(express.json());
37
+ *
38
+ * moderation.dispatcher.start();
39
+ * ```
40
+ *
41
+ * Two invariants are ENFORCED here rather than documented, because both fail
42
+ * silently and neither shows up in a test that only asserts the happy path:
43
+ *
44
+ * 1. Nothing can be enqueued that is not already recorded in the outbox, in the
45
+ * same transaction. {@link ModerationOutboxTransactionError} is thrown by the
46
+ * only writer of that collection when the transaction it was handed is not
47
+ * open.
48
+ * 2. The webhook receiver reads raw bytes. Mounted after a JSON parser it
49
+ * refuses rather than verifying a signature over a re-serialisation.
50
+ *
51
+ * Types come from `@crowdsource.you/contracts` and `@crowdsource.you/core`. This
52
+ * package re-exports none of them: a `Decision` or a `TaxonomyCode` has exactly
53
+ * one definition.
54
+ */
55
+ export { createModerationIntegration } from './integration.js';
56
+ /**
57
+ * The retention windows are storage-independent policy, so they stay here while
58
+ * PostgreSQL-specific tables and stores live behind the `/postgres` subpath.
59
+ */
60
+ export { MODERATION_EVENT_RETENTION_SECONDS, MODERATION_OUTBOX_RETENTION_SECONDS, } from './retention.js';
61
+ export { ModerationOutboxTransactionError, decisionApplyEventId, isRetryableDeliveryError, reportSubmitEventId, } from './outbox/service.js';
62
+ export { ModerationOutboxDispatcher } from './outbox/dispatcher.js';
63
+ export { ModerationReconciliationJob } from './reconciliation.js';
64
+ export { DuplicateReportError } from './intake.js';
65
+ export { CrowdSourceUnavailableError, ModerationDeliveryRejectedError, } from './delivery.js';
66
+ export { ModerationDecisionDeferredError, ModerationDecisionRejectedError, } from './decision.js';
67
+ export { ModerationSubjectUnsupportedError, ModerationTaxonomyError, createSubjectRegistry, snapshotHash, } from './evidence.js';
68
+ export { ModerationRestoreDirectionError, assertRestoreDirection, planEnforcement, primaryAction, } from './enforcement/planner.js';
69
+ export { localStatusForDecision } from './reportStatus.js';
70
+ export { createProcessedEventStore } from './inbound.js';
71
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/outbox/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AAEH,OAAO,EAAE,2BAA2B,EAAE,MAAM,kBAAkB,CAAC;AAG/D;;;GAGG;AACH,OAAO,EACL,kCAAkC,EAClC,mCAAmC,GACpC,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,gCAAgC,EAChC,oBAAoB,EACpB,wBAAwB,EACxB,mBAAmB,GACpB,MAAM,qBAAqB,CAAC;AAsB7B,OAAO,EAAE,0BAA0B,EAAE,MAAM,wBAAwB,CAAC;AACpE,OAAO,EAAE,2BAA2B,EAAE,MAAM,qBAAqB,CAAC;AAGlE,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAEnD,OAAO,EACL,2BAA2B,EAC3B,+BAA+B,GAChC,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,+BAA+B,EAC/B,+BAA+B,GAChC,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,iCAAiC,EACjC,uBAAuB,EACvB,qBAAqB,EACrB,YAAY,GACb,MAAM,eAAe,CAAC;AAGvB,OAAO,EACL,+BAA+B,EAC/B,sBAAsB,EACtB,eAAe,EACf,aAAa,GACd,MAAM,0BAA0B,CAAC;AAGlC,OAAO,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAE3D,OAAO,EAAE,yBAAyB,EAAE,MAAM,cAAc,CAAC"}
@@ -0,0 +1,140 @@
1
+ import { reportSubmitEventId } from './outbox/service.js';
2
+ /**
3
+ * Storing a report and, when there is somewhere to send it, the promise to
4
+ * deliver it — in one operation.
5
+ *
6
+ * This is the only thing in the integration that a user waits for. A 201 from an
7
+ * application's report route means the report row and its outbox event committed
8
+ * together. It does NOT mean CrowdSource accepted anything — CrowdSource may be
9
+ * unreachable, mid-deploy or not yet configured, and the reporter is told their
10
+ * report was received either way, because it was.
11
+ *
12
+ * The transaction is the whole mechanism. Two writes outside one would give two
13
+ * failure modes that are both silent: a report with no delivery event (the
14
+ * report exists, nothing will ever send it, and nobody finds out until somebody
15
+ * asks why a case never opened) or a delivery event with no report (a delivery
16
+ * worker looking up an id that was rolled back). Neither surfaces as an error at
17
+ * the moment it happens, which is exactly why this has to be atomic rather than
18
+ * carefully ordered.
19
+ *
20
+ * The one report with NO delivery event is the one whose type has no subject
21
+ * provider, and that is a different claim entirely: not "delivery failed" but
22
+ * "there was never a route out of this application for this kind of object".
23
+ * Those two must not be conflated, which is why they are different `localStatus`
24
+ * values and why the absent route is written down as a reason rather than
25
+ * inferred from a missing row.
26
+ */
27
+ export class DuplicateReportError extends Error {
28
+ constructor(existing) {
29
+ super('This item has already been reported by this reporter.');
30
+ this.name = 'DuplicateReportError';
31
+ this.existing = existing;
32
+ }
33
+ }
34
+ /**
35
+ * Refuses an identifier that is not a string, before it reaches the store.
36
+ *
37
+ * The input type says these are strings, but a type is erased at runtime and a
38
+ * truthiness check passes anything non-empty — including an object. What that
39
+ * costs depends on the backend, and the difference is worth stating rather than
40
+ * flattening: on Mongo a `{ $ne: null }` becomes a query OPERATOR, so the
41
+ * duplicate lookup matches an UNRELATED report and answers "you already reported
42
+ * this" about somebody else's row; on Postgres a bound parameter cannot become
43
+ * an operator, so that particular failure class does not exist there.
44
+ *
45
+ * The guard is not Mongo's, though. A non-string still reaches the insert and
46
+ * stores something that is not an id where an id belongs, on any backend — and
47
+ * the check lives here rather than at an application's route because this
48
+ * function is exported: a queue worker, a reconciliation script or a future
49
+ * admin path is under no obligation to have passed a route's validation, and a
50
+ * guard that only exists at one caller is a guard that holds until the second
51
+ * one arrives.
52
+ */
53
+ function requireIdentifier(value, field) {
54
+ /**
55
+ * `trim()` and not `length === 0`, because `' '` is not an identifier.
56
+ *
57
+ * A whitespace-only reporter or reported id is not a string somebody meant:
58
+ * it comes from a form field, a trimmed-elsewhere value, or a client that
59
+ * sends `' '` for absent. Stored, it is an id that matches nothing and reads
60
+ * as present — the duplicate check finds no earlier report, the delivery
61
+ * carries it as `externalReportId`, and nothing fails until a human asks why
62
+ * a case names nobody.
63
+ */
64
+ if (typeof value !== 'string' || value.trim() === '') {
65
+ throw new TypeError(`createReport: ${field} must be a non-empty string.`);
66
+ }
67
+ return value;
68
+ }
69
+ /**
70
+ * Why a report is not going anywhere, in words an operator can read.
71
+ *
72
+ * Stored on the row rather than left to be inferred from a missing outbox event.
73
+ * A missing row is also what a lost write looks like, and the two need to be
74
+ * distinguishable months later without re-deriving which types had providers at
75
+ * the time. Bounded by the schema's 300-character limit.
76
+ */
77
+ function localOnlyReason(reportedType) {
78
+ return (`There is no moderation subject provider for '${reportedType}', so this report is ` +
79
+ 'recorded locally and is not sent for community review.');
80
+ }
81
+ /**
82
+ * Store the report, and queue its delivery in the same transaction.
83
+ *
84
+ * Delivery is queued when — and only when — the reported type has a subject
85
+ * provider. A type without one is stored at `received` with the reason recorded,
86
+ * which is the behaviour the application had before CrowdSource existed: the
87
+ * report is a receipt and a local record, and nothing else ever happens to it.
88
+ *
89
+ * That branch is the reason the two writes stay in one transaction rather than
90
+ * being ordered carefully. The condition is read BEFORE the transaction body
91
+ * decides anything, so `localStatus` and the presence of an outbox row are
92
+ * decided together from one fact — a report can never commit as `queued` with
93
+ * nothing to deliver it, nor as `received` with a delivery event that will try
94
+ * anyway.
95
+ *
96
+ * Intake deliberately does not read whether the integration is enabled. A report
97
+ * taken while it is off still gets its delivery event, so turning the flag on
98
+ * delivers the backlog instead of stranding it — the dispatcher is what is
99
+ * gated, not the durable record. Nothing here is conditional on a third party's
100
+ * state; only on whether this application knows how to describe the object at
101
+ * all.
102
+ */
103
+ export function createIntake(input) {
104
+ return async (report) => {
105
+ const reporter = requireIdentifier(report.reporter, 'reporter');
106
+ const reportedId = requireIdentifier(report.reportedId, 'reportedId');
107
+ const reportedType = requireIdentifier(report.reportedType, 'reportedType');
108
+ if (!Array.isArray(report.categories) || report.categories.length === 0) {
109
+ throw new TypeError('createReport: categories must be a non-empty array.');
110
+ }
111
+ for (const category of report.categories) {
112
+ requireIdentifier(category, 'categories[]');
113
+ }
114
+ const deliverable = input.registry.providerFor(reportedType) !== undefined;
115
+ return await input.transaction.run(async (tx) => {
116
+ const existing = await input.reports.findDuplicate({ reporter, reportedId, reportedType }, tx);
117
+ if (existing)
118
+ throw new DuplicateReportError(existing);
119
+ const created = await input.reports.insert({
120
+ reportedType,
121
+ reportedId,
122
+ reporter,
123
+ categories: [...report.categories],
124
+ ...(report.details === undefined ? {} : { details: report.details }),
125
+ localStatus: deliverable ? 'queued' : 'received',
126
+ ...(deliverable ? {} : { localStatusReason: localOnlyReason(reportedType) }),
127
+ ...(report.extra === undefined ? {} : { extra: report.extra }),
128
+ }, tx);
129
+ if (!deliverable)
130
+ return { report: created };
131
+ const outboxEventId = await input.outbox.enqueue({
132
+ eventId: reportSubmitEventId(created.id),
133
+ kind: 'report.submit',
134
+ payload: { reportId: created.id },
135
+ }, tx);
136
+ return { report: created, outboxEventId };
137
+ });
138
+ };
139
+ }
140
+ //# sourceMappingURL=intake.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"intake.js","sourceRoot":"","sources":["../../../src/outbox/intake.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,116 @@
1
+ import { createClientProvider } from './client.js';
2
+ import { createDecisionWorker } from './decision.js';
3
+ import { createDeliveryWorker } from './delivery.js';
4
+ import { createEnforcementExecutor } from './enforcement/executor.js';
5
+ import { assertRestoreDirection } from './enforcement/planner.js';
6
+ import { createSubjectRegistry } from './evidence.js';
7
+ import { createInboundService, createProcessedEventStore } from './inbound.js';
8
+ import { ModerationOutboxDispatcher, createOutboxRouter } from './outbox/dispatcher.js';
9
+ import { createOutboxService } from './outbox/service.js';
10
+ import { createIntake } from './intake.js';
11
+ import { ModerationReconciliationJob, createReconciliation, } from './reconciliation.js';
12
+ import { createWebhookRouter } from './webhook.js';
13
+ export function createModerationIntegration(config) {
14
+ /**
15
+ * Refuse an inverted `restoreAction` before anything is wired. It cannot be
16
+ * caught by the type — both directions are `TAction[]` — and it does not fail
17
+ * at runtime; it applies a punishment on an accepted appeal.
18
+ */
19
+ assertRestoreDirection(config.enforcement);
20
+ const store = config.store;
21
+ const outbox = createOutboxService({ store: store.outbox, logger: config.logger });
22
+ const registry = createSubjectRegistry(config.subjects);
23
+ const client = createClientProvider({
24
+ config: config.crowdSource,
25
+ logger: config.logger,
26
+ });
27
+ const enforcement = createEnforcementExecutor({
28
+ enforcement: store.enforcement,
29
+ config: config.enforcement,
30
+ defaultMode: config.crowdSource.enforcementMode,
31
+ logger: config.logger,
32
+ ...(config.metrics === undefined ? {} : { metrics: config.metrics }),
33
+ });
34
+ const deliverReport = createDeliveryWorker({
35
+ reports: store.reports,
36
+ registry,
37
+ taxonomy: config.taxonomy,
38
+ client,
39
+ logger: config.logger,
40
+ ...(config.metrics === undefined ? {} : { metrics: config.metrics }),
41
+ });
42
+ const applyDecision = createDecisionWorker({
43
+ reports: store.reports,
44
+ executor: enforcement,
45
+ enforcement: config.enforcement,
46
+ logger: config.logger,
47
+ ...(config.reportDecisionExtraFields === undefined
48
+ ? {}
49
+ : { reportDecisionExtraFields: config.reportDecisionExtraFields }),
50
+ });
51
+ const inbound = createInboundService({
52
+ transaction: store.transaction,
53
+ events: store.events,
54
+ outbox,
55
+ });
56
+ const reconcile = createReconciliation({
57
+ transaction: store.transaction,
58
+ reports: store.reports,
59
+ outbox,
60
+ logger: config.logger,
61
+ ...(config.crowdSource.staleSubmittedHours === undefined
62
+ ? {}
63
+ : { staleSubmittedHours: config.crowdSource.staleSubmittedHours }),
64
+ });
65
+ const dispatcher = new ModerationOutboxDispatcher({
66
+ outbox,
67
+ handler: createOutboxRouter({ deliverReport, applyDecision }),
68
+ logger: config.logger,
69
+ enabled: config.crowdSource.enabled,
70
+ ...(config.crowdSource.outboxPollIntervalMs === undefined
71
+ ? {}
72
+ : { pollIntervalMs: config.crowdSource.outboxPollIntervalMs }),
73
+ ...(config.crowdSource.outboxBatchSize === undefined
74
+ ? {}
75
+ : { batchSize: config.crowdSource.outboxBatchSize }),
76
+ });
77
+ const reconciliationJob = new ModerationReconciliationJob({
78
+ reconcile,
79
+ logger: config.logger,
80
+ enabled: config.crowdSource.enabled,
81
+ ...(config.crowdSource.reconciliationIntervalMs === undefined
82
+ ? {}
83
+ : { intervalMs: config.crowdSource.reconciliationIntervalMs }),
84
+ });
85
+ return {
86
+ createReport: createIntake({
87
+ transaction: store.transaction,
88
+ reports: store.reports,
89
+ registry,
90
+ outbox,
91
+ }),
92
+ webhookRouter(options = {}) {
93
+ return createWebhookRouter({
94
+ inbound,
95
+ store: createProcessedEventStore(store.events),
96
+ ...(config.crowdSource.webhookSecret === undefined
97
+ ? {}
98
+ : { secret: config.crowdSource.webhookSecret }),
99
+ ...(config.crowdSource.webhookPreviousSecret === undefined
100
+ ? {}
101
+ : { previousSecret: config.crowdSource.webhookPreviousSecret }),
102
+ logger: config.logger,
103
+ ...(config.metrics === undefined ? {} : { metrics: config.metrics }),
104
+ ...(options.path === undefined ? {} : { path: options.path }),
105
+ });
106
+ },
107
+ dispatcher,
108
+ reconciliationJob,
109
+ reconcile,
110
+ deliverableTypes: () => registry.deliverableTypes(),
111
+ registry,
112
+ enforcement,
113
+ client,
114
+ };
115
+ }
116
+ //# sourceMappingURL=integration.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"integration.js","sourceRoot":"","sources":["../../../src/outbox/integration.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,114 @@
1
+ /**
2
+ * The loop that drains the moderation outbox.
3
+ *
4
+ * A bounded interval, one batch in flight at a time, an abort signal that stops
5
+ * claiming new work but lets the event already being handled reach a durable
6
+ * state.
7
+ *
8
+ * It is NOT leader-gated, and that is a property of the claim rather than an
9
+ * oversight. Every event is taken under a lease with an owner check, so N tasks
10
+ * draining the same collection simply share the work — and a task dying
11
+ * mid-delivery has its lease expire and its event reclaimed, which a single
12
+ * leader would not give us.
13
+ *
14
+ * `enabled` gates the LOOP, never the durable record. Reports taken while the
15
+ * integration is off keep their outbox rows and deliver when it is switched on;
16
+ * running the loop instead would count attempts against a deployment that has
17
+ * nowhere to send anything and dead-letter the backlog it was supposed to
18
+ * preserve.
19
+ */
20
+ const DEFAULT_POLL_INTERVAL_MS = 5000;
21
+ export class ModerationOutboxDispatcher {
22
+ constructor(options) {
23
+ this.options = options;
24
+ this.timer = null;
25
+ this.inFlight = null;
26
+ this.abortController = null;
27
+ this.running = false;
28
+ }
29
+ start() {
30
+ if (this.running)
31
+ return;
32
+ if (!this.options.enabled) {
33
+ this.options.logger.info('[CrowdSource] outbox dispatcher not started: the integration is disabled');
34
+ return;
35
+ }
36
+ const intervalMs = this.options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
37
+ this.running = true;
38
+ this.abortController = new AbortController();
39
+ void this.tick();
40
+ this.timer = setInterval(() => {
41
+ void this.tick();
42
+ }, intervalMs);
43
+ this.timer.unref?.();
44
+ this.options.logger.info('[CrowdSource] outbox dispatcher started', {
45
+ intervalMs,
46
+ batchSize: this.options.batchSize,
47
+ });
48
+ }
49
+ async stop() {
50
+ this.running = false;
51
+ const controller = this.abortController;
52
+ controller?.abort();
53
+ if (this.timer) {
54
+ clearInterval(this.timer);
55
+ this.timer = null;
56
+ }
57
+ await this.inFlight;
58
+ if (this.abortController === controller) {
59
+ this.abortController = null;
60
+ }
61
+ }
62
+ async tick() {
63
+ if (!this.running)
64
+ return;
65
+ if (this.inFlight)
66
+ return this.inFlight;
67
+ const work = this.options.outbox
68
+ .dispatch({
69
+ handler: this.options.handler,
70
+ ...(this.options.batchSize === undefined
71
+ ? {}
72
+ : { batchSize: this.options.batchSize }),
73
+ ...(this.abortController === null
74
+ ? {}
75
+ : { signal: this.abortController.signal }),
76
+ })
77
+ .then(({ processed, failed, deadLettered }) => {
78
+ if (processed > 0 || failed > 0) {
79
+ this.options.logger.info('[CrowdSource] outbox batch complete', {
80
+ processed,
81
+ failed,
82
+ deadLettered,
83
+ });
84
+ }
85
+ })
86
+ .catch((error) => {
87
+ // Claim/database failures happen outside the per-event retry block. Keep
88
+ // the interval alive and avoid an unhandled rejection.
89
+ this.options.logger.error('[CrowdSource] outbox tick failed', {
90
+ error: error instanceof Error ? error.message : String(error),
91
+ });
92
+ })
93
+ .finally(() => {
94
+ if (this.inFlight === work)
95
+ this.inFlight = null;
96
+ });
97
+ this.inFlight = work;
98
+ return work;
99
+ }
100
+ }
101
+ /** Route an event to the worker that owns its kind. */
102
+ export function createOutboxRouter(handlers) {
103
+ return async (event) => {
104
+ switch (event.kind) {
105
+ case 'report.submit':
106
+ await handlers.deliverReport(event);
107
+ return;
108
+ case 'decision.apply':
109
+ await handlers.applyDecision(event);
110
+ return;
111
+ }
112
+ };
113
+ }
114
+ //# sourceMappingURL=dispatcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dispatcher.js","sourceRoot":"","sources":["../../../../src/outbox/outbox/dispatcher.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,MAAM,wBAAwB,GAAG,IAAK,CAAC;AAEvC,MAAM,OAAO,0BAA0B;IAMrC,YACmB,OAOhB;QAPgB,YAAO,GAAP,OAAO,CAOvB;QAbK,UAAK,GAA0C,IAAI,CAAC;QACpD,aAAQ,GAAyB,IAAI,CAAC;QACtC,oBAAe,GAA2B,IAAI,CAAC;QAC/C,YAAO,GAAG,KAAK,CAAC;IAWrB,CAAC;IAEJ,KAAK;QACH,IAAI,IAAI,CAAC,OAAO;YAAE,OAAO;QACzB,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;YAC1B,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CACtB,0EAA0E,CAC3E,CAAC;YACF,OAAO;QACT,CAAC;QACD,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,cAAc,IAAI,wBAAwB,CAAC;QAC3E,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;QACpB,IAAI,CAAC,eAAe,GAAG,IAAI,eAAe,EAAE,CAAC;QAC7C,KAAK,IAAI,CAAC,IAAI,EAAE,CAAC;QACjB,IAAI,CAAC,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE;YAC5B,KAAK,IAAI,CAAC,IAAI,EAAE,CAAC;QACnB,CAAC,EAAE,UAAU,CAAC,CAAC;QACf,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,EAAE,CAAC;QACrB,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,yCAAyC,EAAE;YAClE,UAAU;YACV,SAAS,EAAE,IAAI,CAAC,OAAO,CAAC,SAAS;SAClC,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,IAAI;QACR,IAAI,CAAC,OAAO,GAAG,KAAK,CAAC;QACrB,MAAM,UAAU,GAAG,IAAI,CAAC,eAAe,CAAC;QACxC,UAAU,EAAE,KAAK,EAAE,CAAC;QACpB,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACf,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YAC1B,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;QACpB,CAAC;QACD,MAAM,IAAI,CAAC,QAAQ,CAAC;QACpB,IAAI,IAAI,CAAC,eAAe,KAAK,UAAU,EAAE,CAAC;YACxC,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC;QAC9B,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,IAAI;QAChB,IAAI,CAAC,IAAI,CAAC,OAAO;YAAE,OAAO;QAC1B,IAAI,IAAI,CAAC,QAAQ;YAAE,OAAO,IAAI,CAAC,QAAQ,CAAC;QACxC,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM;aAC7B,QAAQ,CAAC;YACR,OAAO,EAAE,IAAI,CAAC,OAAO,CAAC,OAAO;YAC7B,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,KAAK,SAAS;gBACtC,CAAC,CAAC,EAAE;gBACJ,CAAC,CAAC,EAAE,SAAS,EAAE,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,CAAC;YAC1C,GAAG,CAAC,IAAI,CAAC,eAAe,KAAK,IAAI;gBAC/B,CAAC,CAAC,EAAE;gBACJ,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,eAAe,CAAC,MAAM,EAAE,CAAC;SAC7C,CAAC;aACD,IAAI,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,EAAE,EAAE;YAC5C,IAAI,SAAS,GAAG,CAAC,IAAI,MAAM,GAAG,CAAC,EAAE,CAAC;gBAChC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,qCAAqC,EAAE;oBAC9D,SAAS;oBACT,MAAM;oBACN,YAAY;iBACb,CAAC,CAAC;YACL,CAAC;QACH,CAAC,CAAC;aACD,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;YACxB,yEAAyE;YACzE,uDAAuD;YACvD,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,kCAAkC,EAAE;gBAC5D,KAAK,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;aAC9D,CAAC,CAAC;QACL,CAAC,CAAC;aACD,OAAO,CAAC,GAAG,EAAE;YACZ,IAAI,IAAI,CAAC,QAAQ,KAAK,IAAI;gBAAE,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;QACnD,CAAC,CAAC,CAAC;QACL,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;QACrB,OAAO,IAAI,CAAC;IACd,CAAC;CACF;AAED,uDAAuD;AACvD,MAAM,UAAU,kBAAkB,CAAC,QAGlC;IACC,OAAO,KAAK,EAAE,KAA4B,EAAiB,EAAE;QAC3D,QAAQ,KAAK,CAAC,IAAI,EAAE,CAAC;YACnB,KAAK,eAAe;gBAClB,MAAM,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;gBACpC,OAAO;YACT,KAAK,gBAAgB;gBACnB,MAAM,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;gBACpC,OAAO;QACX,CAAC;IACH,CAAC,CAAC;AACJ,CAAC"}