@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,326 @@
1
+ /**
2
+ * Deciding what an application will do about a decision — and nothing else.
3
+ *
4
+ * Pure: no database, no clock, no configuration. A decision in, a plan out. That
5
+ * is what makes the mapping testable as a table rather than as an integration
6
+ * scenario, and it is why `observe` mode is a real audit rather than a comment —
7
+ * the plan is computed identically in every mode and only its EXECUTION is
8
+ * gated.
9
+ *
10
+ * ## The application maps recommendations, not findings
11
+ *
12
+ * `decision.recommendedActions` is what this reads, falling back to severity
13
+ * only when a violation arrives with no recommendation at all.
14
+ *
15
+ * The reason is a division of labour: the jury classified the material and the
16
+ * consensus engine turned that into a recommendation under a versioned policy.
17
+ * An application that re-derived its action from raw severity would be quietly
18
+ * re-deciding the case with a second, unversioned policy of its own — and the
19
+ * two would diverge the first time CrowdSource's policy was updated. The
20
+ * fallback exists because a `violation` the application did nothing about would
21
+ * be worse than a mapped one.
22
+ *
23
+ * ## Why this ALGORITHM is shared and only the TABLES are per-application
24
+ *
25
+ * Every application has different actions, so the tables have to be its own. The
26
+ * algorithm must not be, because it carries a correctness property that is
27
+ * invisible by construction — see {@link withRestoreForNoViolation}. An
28
+ * application writing its own planner is a chance to ship that bug silently, and
29
+ * no test written against the application's own mapping would catch it.
30
+ */
31
+ const SEVERITY_ORDER = ['low', 'medium', 'high', 'critical'];
32
+ /**
33
+ * Recommendations that ask for NO effect.
34
+ *
35
+ * An unmapped recommendation goes to `reviewAction`, because a recommendation
36
+ * this application has no action for is a decision a human should see. These
37
+ * three are the exception, and the distinction is not cosmetic: they are
38
+ * CrowdSource saying "take no action", so routing them to review would put a
39
+ * human in front of every cleared case and bury the ones that need looking at.
40
+ *
41
+ * An application's own table still wins — it may genuinely want `allow` to mean
42
+ * something — but the default for an application that maps nothing has to be
43
+ * "do nothing" rather than "wake somebody".
44
+ */
45
+ const NO_EFFECT_RECOMMENDATIONS = new Set([
46
+ 'allow',
47
+ 'no_action',
48
+ 'no_global_effect',
49
+ ]);
50
+ function actionForRecommendation(recommended, config) {
51
+ const mapped = config.recommendationToAction?.[recommended];
52
+ if (mapped !== undefined)
53
+ return mapped;
54
+ return NO_EFFECT_RECOMMENDATIONS.has(recommended)
55
+ ? config.noneAction
56
+ : config.reviewAction;
57
+ }
58
+ function highestSeverity(decision) {
59
+ let highest;
60
+ for (const finding of decision.findings) {
61
+ if (highest === undefined ||
62
+ SEVERITY_ORDER.indexOf(finding.severity) > SEVERITY_ORDER.indexOf(highest)) {
63
+ highest = finding.severity;
64
+ }
65
+ }
66
+ return highest;
67
+ }
68
+ /**
69
+ * `no_violation` always carries a restore, whatever it recommended.
70
+ *
71
+ * This exists because of a failure that is very easy to ship and very hard to
72
+ * see. A correction is a new revision whose outcome is `no_violation`, and its
73
+ * recommendation is frequently `no_action` — which is CrowdSource saying "take
74
+ * no NEW action", not "leave what you already did in place". Mapping that
75
+ * straight through plans nothing, and the object an earlier revision removed
76
+ * stays removed forever: the appeal succeeded, the case says the content was
77
+ * fine, and nothing in the application ever puts it back. No error, no log line,
78
+ * no failing test anywhere else.
79
+ *
80
+ * `allow`, `restore` and `no_action` are listed together as an application's
81
+ * options for `no_violation` precisely because choosing between them needs
82
+ * knowledge only the application has — whether it did something earlier. So the
83
+ * plan always includes the restore, and the executor records "there was nothing
84
+ * restricted" when that is the case, which is evidence rather than a silent
85
+ * no-op.
86
+ */
87
+ function restoreActions(declared) {
88
+ if (declared === null)
89
+ return [];
90
+ return typeof declared === 'string' ? [declared] : declared;
91
+ }
92
+ function withRestoreForNoViolation(decision, planned, declared) {
93
+ /**
94
+ * `null` is a decision the application made, not an omission — the type
95
+ * requires it to be written down. An application with no sanction primitive
96
+ * has nothing an earlier revision could have removed, so there is nothing for
97
+ * a correction to put back.
98
+ */
99
+ if (decision.outcome !== 'no_violation')
100
+ return planned;
101
+ /**
102
+ * EVERY declared restore, not just the first. An application whose levers are
103
+ * "hide it" and "label it" has two things a correction must undo, and planning
104
+ * one leaves the other stuck forever — the object is un-hidden and stays
105
+ * labelled, with no error and nothing failing. That is the same failure this
106
+ * function exists to prevent, applied to the second reversible action instead
107
+ * of the first.
108
+ *
109
+ * A restore with nothing to undo is not waste: the executor records
110
+ * `changed: false` with its reason, which is evidence that it was checked.
111
+ */
112
+ const missing = restoreActions(declared).filter((action) => !planned.some((entry) => entry.action === action));
113
+ if (missing.length === 0)
114
+ return planned;
115
+ return [
116
+ ...planned,
117
+ ...missing.map((action) => ({
118
+ action,
119
+ reason: 'No violation: undo any earlier restriction',
120
+ })),
121
+ ];
122
+ }
123
+ /**
124
+ * Collapse a plan to the actions that can coexist.
125
+ *
126
+ * A decision may recommend both removal and a label; a removed object does not
127
+ * need one, and recording both would claim two effects where one happened. So an
128
+ * action absorbs whatever the application declared it absorbs, and the explicit
129
+ * "nothing" never survives alongside anything else.
130
+ *
131
+ * `reviewAction` always survives — it is a note for a human, and dropping it
132
+ * because something else was also done is how a `suspend_user` recommendation
133
+ * gets lost.
134
+ */
135
+ function collapse(actions, config) {
136
+ const byAction = new Map();
137
+ for (const planned of actions) {
138
+ if (!byAction.has(planned.action))
139
+ byAction.set(planned.action, planned);
140
+ }
141
+ for (const [action, absorbed] of Object.entries(config.absorb ?? {})) {
142
+ if (!byAction.has(action))
143
+ continue;
144
+ for (const loser of absorbed ?? []) {
145
+ if (loser === config.reviewAction)
146
+ continue;
147
+ byAction.delete(loser);
148
+ }
149
+ }
150
+ if (byAction.size > 1)
151
+ byAction.delete(config.noneAction);
152
+ return Array.from(byAction.values());
153
+ }
154
+ /**
155
+ * What the application will do about this decision.
156
+ *
157
+ * Never empty: a decision that produces no action produces an explicit
158
+ * `noneAction`, because a row saying "we decided to do nothing, and why" is
159
+ * evidence and an absent row is a question.
160
+ */
161
+ /**
162
+ * Raised when `restoreAction` names actions that are being UNDONE rather than
163
+ * actions that DO the undoing.
164
+ *
165
+ * A doc line is not enough for this one. An inverted `restoreAction` type-checks
166
+ * — both directions are `TAction[]` — and it does not fail at runtime either: it
167
+ * plans, it claims, and it applies a restriction and a label on an accepted
168
+ * appeal. It succeeds at the opposite thing, on the one path whose whole purpose
169
+ * is to give something back.
170
+ *
171
+ * The signal is unambiguous when it exists. `reverses` maps each undoing action
172
+ * to what it undoes, so its VALUES are targets and its KEYS are actors. An
173
+ * action that appears only as a target has been declared as a restore by
174
+ * someone reading the targets, which is exactly the mistake this catches.
175
+ *
176
+ * Deliberately narrow: an action absent from `reverses` entirely is fine (it may
177
+ * undo something needing no previous state), and one that is both a key and a
178
+ * value is fine (it undoes and is undone). Only "a target, and never an actor"
179
+ * is reported, so a correct configuration cannot trip it.
180
+ */
181
+ export class ModerationRestoreDirectionError extends Error {
182
+ constructor(inverted) {
183
+ super(`restoreAction names [${inverted.join(', ')}], which 'reverses' lists as actions ` +
184
+ 'being UNDONE rather than actions that do the undoing. restoreAction holds what ' +
185
+ "the planner emits on 'no_violation' — the restoring actions, e.g. " +
186
+ "['restore', 'unlabel_sensitive'] — not the restrictions being lifted. As written, " +
187
+ 'an accepted appeal would apply the punishment it was correcting.');
188
+ this.name = 'ModerationRestoreDirectionError';
189
+ }
190
+ }
191
+ /**
192
+ * Refuse an inverted `restoreAction` at construction rather than at the first
193
+ * correction, which may be weeks later and reads as a moderation decision
194
+ * rather than a configuration error.
195
+ */
196
+ export function assertRestoreDirection(config) {
197
+ const reverses = config.reverses ?? {};
198
+ const entries = Object.entries(reverses);
199
+ const actors = new Set(entries.map(([actor]) => actor));
200
+ const targets = new Set();
201
+ for (const [, undone] of entries) {
202
+ if (undone === undefined)
203
+ continue;
204
+ for (const action of typeof undone === 'string' ? [undone] : undone) {
205
+ targets.add(action);
206
+ }
207
+ }
208
+ const inverted = restoreActions(config.restoreAction).filter((action) => targets.has(action) && !actors.has(action));
209
+ if (inverted.length > 0)
210
+ throw new ModerationRestoreDirectionError(inverted);
211
+ }
212
+ export function planEnforcement(decision, config) {
213
+ const fromRecommendations = decision.recommendedActions.map((recommended) => ({
214
+ action: actionForRecommendation(recommended.action, config),
215
+ reason: `CrowdSource recommended ${recommended.action}`,
216
+ recommendedAction: recommended.action,
217
+ }));
218
+ if (fromRecommendations.length > 0) {
219
+ const collapsed = collapse(withRestoreForNoViolation(decision, fromRecommendations, config.restoreAction), config);
220
+ return collapsed.length > 0
221
+ ? collapsed
222
+ : [
223
+ {
224
+ action: config.noneAction,
225
+ reason: 'No recommended action maps to an effect this application has',
226
+ },
227
+ ];
228
+ }
229
+ switch (decision.outcome) {
230
+ case 'violation': {
231
+ const severity = highestSeverity(decision);
232
+ /**
233
+ * A `violation` with no findings cannot happen — the contract refuses it —
234
+ * so an absent severity here means a newer CrowdSource sent something this
235
+ * code has not seen. A human looks at it rather than a default removing an
236
+ * object.
237
+ */
238
+ if (severity === undefined) {
239
+ return [
240
+ {
241
+ action: config.reviewAction,
242
+ reason: 'Violation carried no finding severity this version understands',
243
+ },
244
+ ];
245
+ }
246
+ return [
247
+ {
248
+ action: config.severityFallback?.[severity] ?? config.reviewAction,
249
+ reason: `Violation with no recommended action, highest severity ${severity}`,
250
+ },
251
+ ];
252
+ }
253
+ case 'no_violation':
254
+ /**
255
+ * A restore, always planned — even when nothing was restricted. The
256
+ * executor records it as not applied with the reason, which is how "we
257
+ * checked and there was nothing to undo" is distinguishable from "we never
258
+ * looked".
259
+ */
260
+ {
261
+ const declared = restoreActions(config.restoreAction);
262
+ return declared.length === 0
263
+ ? [
264
+ {
265
+ action: config.noneAction,
266
+ reason: 'No violation, and this application has nothing to restore',
267
+ },
268
+ ]
269
+ : declared.map((action) => ({
270
+ action,
271
+ reason: 'No violation: undo any earlier restriction',
272
+ }));
273
+ }
274
+ case 'insufficient_context':
275
+ case 'inconclusive':
276
+ case 'escalated':
277
+ /**
278
+ * None of these is "remove", and none is "it was fine": absence of
279
+ * consensus is neither guilt nor innocence, so the application changes
280
+ * nothing on its own and asks a human.
281
+ */
282
+ return [
283
+ {
284
+ action: config.reviewAction,
285
+ reason: `Outcome ${decision.outcome}: no automatic action, internal review`,
286
+ },
287
+ ];
288
+ case 'content_unavailable':
289
+ case 'duplicate':
290
+ return [
291
+ {
292
+ action: config.noneAction,
293
+ reason: `Outcome ${decision.outcome}: nothing to enforce`,
294
+ },
295
+ ];
296
+ default:
297
+ /**
298
+ * An outcome the contract does not currently define. A newer server must
299
+ * not break an older client, and the safe reading of an unknown outcome is
300
+ * a human, never a default effect.
301
+ */
302
+ return [
303
+ {
304
+ action: config.reviewAction,
305
+ reason: 'Decision outcome not recognised by this version of the application',
306
+ },
307
+ ];
308
+ }
309
+ }
310
+ /**
311
+ * The one action worth recording on the report.
312
+ *
313
+ * One field, several planned actions, so it has to be the one that answers "what
314
+ * happened to this object". The application's `precedence` decides, strongest
315
+ * first — and `reviewAction` should sit near the end but ABOVE `noneAction`,
316
+ * because a reporter told "nothing happened" when a human is about to look is
317
+ * being told something untrue.
318
+ */
319
+ export function primaryAction(actions, precedence) {
320
+ for (const candidate of precedence) {
321
+ if (actions.includes(candidate))
322
+ return candidate;
323
+ }
324
+ return actions[0];
325
+ }
326
+ //# sourceMappingURL=planner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"planner.js","sourceRoot":"","sources":["../../../../src/outbox/enforcement/planner.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,173 @@
1
+ import { createHash } from 'crypto';
2
+ /**
3
+ * Turning a stored report into the thing the SDK delivers.
4
+ *
5
+ * This is NOT a case-envelope builder, and the difference matters enough to
6
+ * name: `@crowdsource.you/core` builds the Case Envelope, and it deliberately does
7
+ * not export the function that does it. What this module produces is the SDK's
8
+ * `ReportInput` — a description of the material — and the SDK derives the
9
+ * envelope from it: resource ids, relations, digests, pseudonymous principal
10
+ * refs, the identity binding proof, the pinned policy version, the privacy terms
11
+ * and the idempotency key.
12
+ *
13
+ * That is not a technicality. Those derived values are exactly what the case
14
+ * dedup key is computed over, so an application that composed its own envelope
15
+ * would be the reason two reporters about one object opened two cases — and "one
16
+ * penalty per incident" would fail in production with nothing failing in a test.
17
+ * Building the description and letting the SDK build the document is what keeps
18
+ * that property true for every application at once.
19
+ *
20
+ * So this module does three things and no more: ask the registry for a snapshot
21
+ * of the reported object, translate the reporter's categories into allegations,
22
+ * and assemble both with the report's own identity.
23
+ */
24
+ /**
25
+ * The material could not be described, because nothing can describe it.
26
+ *
27
+ * This is a DEFECT, not a state, and it should be unreachable. A report whose
28
+ * type has no subject provider never gets a delivery event in the first place —
29
+ * intake decides that from the same registry this module reads — so an event
30
+ * that arrives here has been created by something that bypassed intake, or by a
31
+ * deployment where a provider was removed while its reports were still in
32
+ * flight.
33
+ *
34
+ * `retryable: false` therefore dead-letters the outbox event, so the
35
+ * reconciliation sweep counts it and a human looks. The alternative — writing a
36
+ * local state — would file a genuine defect in the one place that looks
37
+ * identical to the deliberate local-only reports, and nothing would ever alert
38
+ * on it.
39
+ *
40
+ * Separate from "the object is gone": a deleted object is a fact about the world
41
+ * and closes the report normally.
42
+ */
43
+ export class ModerationSubjectUnsupportedError extends Error {
44
+ constructor(reportedType) {
45
+ super(`No moderation subject provider is registered for '${reportedType}'.`);
46
+ this.retryable = false;
47
+ this.name = 'ModerationSubjectUnsupportedError';
48
+ }
49
+ }
50
+ /**
51
+ * The application's taxonomy produced no allegation for a report.
52
+ *
53
+ * Not retryable and never silently defaulted. A report with no allegation is not
54
+ * a report, and substituting `other.unclassifiable` here would put a claim in
55
+ * front of a jury that no reporter made and that no version of the mapping
56
+ * records — the taxonomy is the application's to fix, and dead-lettering is what
57
+ * makes somebody fix it.
58
+ */
59
+ export class ModerationTaxonomyError extends Error {
60
+ constructor(categories) {
61
+ super(`The report taxonomy produced no allegation for categories [${categories.join(', ')}]. ` +
62
+ 'Map every category to a code; use `other.unclassifiable` for one the universal taxonomy cannot name.');
63
+ this.retryable = false;
64
+ this.name = 'ModerationTaxonomyError';
65
+ }
66
+ }
67
+ /**
68
+ * SHA-256 of the snapshot, stored on the report.
69
+ *
70
+ * Taken over the described MATERIAL, not over the whole `ReportInput`: the
71
+ * report id, the reporter and the allegations are properties of the report, and
72
+ * including them would mean two people reporting identical content produced
73
+ * different hashes — which is the opposite of what this hash is for. Key order
74
+ * is fixed by the literal below rather than by `Object.keys`, so the digest is
75
+ * stable across Node versions and across a refactor that reorders a field.
76
+ */
77
+ export function snapshotHash(snapshot) {
78
+ const canonical = JSON.stringify({
79
+ subject: {
80
+ externalId: snapshot.subject.externalId,
81
+ type: snapshot.subject.type,
82
+ author: snapshot.subject.author?.oxyUserId ?? null,
83
+ },
84
+ content: snapshot.content,
85
+ attachments: snapshot.attachments ?? [],
86
+ context: snapshot.context ?? [],
87
+ });
88
+ return `sha256:${createHash('sha256').update(canonical, 'utf8').digest('hex')}`;
89
+ }
90
+ export function createSubjectRegistry(providers) {
91
+ const byType = new Map();
92
+ for (const provider of providers) {
93
+ const existing = byType.get(provider.reportedType);
94
+ if (existing) {
95
+ /**
96
+ * Two providers for one noun is not a merge to resolve quietly: whichever
97
+ * one lost would describe nothing, and the subject type a jury reasons
98
+ * about would depend on array order.
99
+ */
100
+ throw new Error(`Two moderation subject providers are registered for '${provider.reportedType}' ` +
101
+ `('${existing.subjectType}' and '${provider.subjectType}'). A reported type has one provider.`);
102
+ }
103
+ byType.set(provider.reportedType, provider);
104
+ }
105
+ return {
106
+ providerFor: (reportedType) => byType.get(reportedType),
107
+ deliverableTypes: () => Array.from(byType.keys()),
108
+ };
109
+ }
110
+ /**
111
+ * Describe a stored report for delivery, or report why it cannot be described.
112
+ *
113
+ * Returns `null` when the reported object no longer exists. That is not an error
114
+ * either: content deleted between the report and its delivery is ordinary, and
115
+ * evidence stays available through retention on CrowdSource's side — but an
116
+ * object the application never got to snapshot has no evidence to keep.
117
+ */
118
+ export async function buildModerationReportInput(input) {
119
+ const { report, registry, taxonomy } = input;
120
+ const provider = registry.providerFor(report.reportedType);
121
+ if (!provider)
122
+ throw new ModerationSubjectUnsupportedError(report.reportedType);
123
+ const snapshot = await provider.snapshot(report.reportedId);
124
+ if (!snapshot)
125
+ return null;
126
+ const allegationCodes = taxonomy.allegationsFor(report.categories);
127
+ if (allegationCodes.length === 0)
128
+ throw new ModerationTaxonomyError(report.categories);
129
+ const details = report.details?.trim();
130
+ return {
131
+ reportInput: {
132
+ externalReportId: report.id,
133
+ subject: snapshot.subject,
134
+ content: snapshot.content,
135
+ ...(snapshot.attachments === undefined ? {} : { attachments: snapshot.attachments }),
136
+ ...(snapshot.context === undefined ? {} : { context: snapshot.context }),
137
+ /**
138
+ * The reporter's own words ride on the FIRST allegation only.
139
+ *
140
+ * Repeating one free-text field across every code would say the reporter
141
+ * wrote it about each of them separately, and details are the reporter's
142
+ * claim and never evidence for it.
143
+ */
144
+ allegations: allegationCodes.map((code, index) => index === 0 && details ? { code, details } : { code }),
145
+ /**
146
+ * The Oxy subject IS the binding proof. Reporters are stored as Oxy user
147
+ * ids, so there is no separate binding step to implement here.
148
+ */
149
+ reportedBy: { oxyUserId: report.reporter },
150
+ /**
151
+ * The moment the USER reported it — the local report's own timestamp, not
152
+ * the moment of delivery. Any value invented per attempt would make every
153
+ * retry from the outbox a permanent 409.
154
+ */
155
+ submittedAt: report.createdAt,
156
+ metadata: {
157
+ /**
158
+ * The application's own entries FIRST, so the two below cannot be
159
+ * shadowed. An adopter that names `taxonomyVersion` or `categories` gets
160
+ * them overwritten rather than winning — a case has to be readable back
161
+ * against the mapping that produced it, and that is not negotiable per
162
+ * application.
163
+ */
164
+ ...taxonomy.metadata,
165
+ /** So a case can be read back against the mapping that produced it. */
166
+ taxonomyVersion: taxonomy.version,
167
+ categories: [...report.categories].sort().join(','),
168
+ },
169
+ },
170
+ snapshotHash: snapshotHash(snapshot),
171
+ };
172
+ }
173
+ //# sourceMappingURL=evidence.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"evidence.js","sourceRoot":"","sources":["../../../src/outbox/evidence.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,87 @@
1
+ import { decisionApplyEventId } from './outbox/service.js';
2
+ import { MODERATION_EVENT_RETENTION_SECONDS } from './retention.js';
3
+ /**
4
+ * What happens between "a signed decision arrived" and "2xx".
5
+ *
6
+ * The receiver's contract is to answer quickly and queue the processing. So
7
+ * exactly two writes happen here, in ONE transaction — the event's audit row is
8
+ * completed and a durable `decision.apply` event is created — and the dispatcher
9
+ * does the rest.
10
+ *
11
+ * The transaction is what makes the dedupe safe. The middleware has already
12
+ * claimed the event id by inserting the row (see {@link createProcessedEventStore});
13
+ * if completing that row and queueing the work were two operations, a crash
14
+ * between them would leave an event that is permanently deduplicated with no
15
+ * work queued — a decision silently lost, with a row that says it arrived.
16
+ * Committing both together means the only two possible outcomes are "recorded
17
+ * and queued" or "neither", and "neither" releases the claim and gets
18
+ * redelivered.
19
+ */
20
+ /**
21
+ * The webhook dedupe store, backed by the moderation event log.
22
+ *
23
+ * `@crowdsource.you/core/express` defaults to an in-process store and says exactly
24
+ * when that is not enough: two instances behind a load balancer each keep their
25
+ * own, so a redelivery landing on the other instance is not deduplicated. Every
26
+ * Oxy backend runs several tasks behind one load balancer, so this is that case.
27
+ *
28
+ * The claim/release contract is the SDK's, and it is the right one. A row
29
+ * inserted BEFORE the handler runs means a concurrent redelivery cannot also run
30
+ * it; deleting that row when the handler THROWS means the sender's retry
31
+ * schedule can still deliver the event later. Recording the id only after
32
+ * success would let two copies run at once; recording it before and never
33
+ * releasing would make a transient failure permanent and lose a decision
34
+ * silently.
35
+ *
36
+ * The retention window is computed HERE rather than in a store, so both backends
37
+ * keep an audit row for the same length of time.
38
+ */
39
+ export function createProcessedEventStore(events) {
40
+ return {
41
+ async claim(eventId) {
42
+ const now = new Date();
43
+ return await events.claim({
44
+ eventId,
45
+ receivedAt: now,
46
+ expiresAt: new Date(now.getTime() + MODERATION_EVENT_RETENTION_SECONDS * 1000),
47
+ });
48
+ },
49
+ async release(eventId) {
50
+ await events.release(eventId);
51
+ },
52
+ };
53
+ }
54
+ export function createInboundService(input) {
55
+ return {
56
+ async recordDecisionEvent(event) {
57
+ await input.transaction.run(async (tx) => {
58
+ const now = new Date();
59
+ await input.events.markQueued({
60
+ eventId: event.eventId,
61
+ type: event.type,
62
+ caseId: event.caseId,
63
+ payload: { caseId: event.caseId, decision: event.decision },
64
+ now,
65
+ }, tx);
66
+ await input.outbox.enqueue({
67
+ eventId: decisionApplyEventId(event.eventId),
68
+ kind: 'decision.apply',
69
+ payload: {
70
+ eventId: event.eventId,
71
+ caseId: event.caseId,
72
+ decision: event.decision,
73
+ },
74
+ }, tx);
75
+ });
76
+ },
77
+ async recordIgnoredEvent(event) {
78
+ await input.events.markIgnored({
79
+ eventId: event.eventId,
80
+ type: event.type,
81
+ ...(event.caseId === undefined ? {} : { caseId: event.caseId }),
82
+ now: new Date(),
83
+ });
84
+ },
85
+ };
86
+ }
87
+ //# sourceMappingURL=inbound.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inbound.js","sourceRoot":"","sources":["../../../src/outbox/inbound.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,oBAAoB,EAAsB,MAAM,qBAAqB,CAAC;AAC/E,OAAO,EAAE,kCAAkC,EAAE,MAAM,gBAAgB,CAAC;AAGpE;;;;;;;;;;;;;;;;GAgBG;AAEH;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,yBAAyB,CACvC,MAAiC;IAEjC,OAAO;QACL,KAAK,CAAC,KAAK,CAAC,OAAe;YACzB,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC;YACvB,OAAO,MAAM,MAAM,CAAC,KAAK,CAAC;gBACxB,OAAO;gBACP,UAAU,EAAE,GAAG;gBACf,SAAS,EAAE,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,kCAAkC,GAAG,IAAK,CAAC;aAChF,CAAC,CAAC;QACL,CAAC;QAED,KAAK,CAAC,OAAO,CAAC,OAAe;YAC3B,MAAM,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QAChC,CAAC;KACF,CAAC;AACJ,CAAC;AAoCD,MAAM,UAAU,oBAAoB,CAAM,KAIzC;IACC,OAAO;QACL,KAAK,CAAC,mBAAmB,CAAC,KAAK;YAC7B,MAAM,KAAK,CAAC,WAAW,CAAC,GAAG,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE;gBACvC,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC;gBACvB,MAAM,KAAK,CAAC,MAAM,CAAC,UAAU,CAC3B;oBACE,OAAO,EAAE,KAAK,CAAC,OAAO;oBACtB,IAAI,EAAE,KAAK,CAAC,IAAI;oBAChB,MAAM,EAAE,KAAK,CAAC,MAAM;oBACpB,OAAO,EAAE,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE;oBAC3D,GAAG;iBACJ,EACD,EAAE,CACH,CAAC;gBAEF,MAAM,KAAK,CAAC,MAAM,CAAC,OAAO,CACxB;oBACE,OAAO,EAAE,oBAAoB,CAAC,KAAK,CAAC,OAAO,CAAC;oBAC5C,IAAI,EAAE,gBAAgB;oBACtB,OAAO,EAAE;wBACP,OAAO,EAAE,KAAK,CAAC,OAAO;wBACtB,MAAM,EAAE,KAAK,CAAC,MAAM;wBACpB,QAAQ,EAAE,KAAK,CAAC,QAAQ;qBACzB;iBACF,EACD,EAAE,CACH,CAAC;YACJ,CAAC,CAAC,CAAC;QACL,CAAC;QAED,KAAK,CAAC,kBAAkB,CAAC,KAAK;YAC5B,MAAM,KAAK,CAAC,MAAM,CAAC,WAAW,CAAC;gBAC7B,OAAO,EAAE,KAAK,CAAC,OAAO;gBACtB,IAAI,EAAE,KAAK,CAAC,IAAI;gBAChB,GAAG,CAAC,KAAK,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC;gBAC/D,GAAG,EAAE,IAAI,IAAI,EAAE;aAChB,CAAC,CAAC;QACL,CAAC;KACF,CAAC;AACJ,CAAC"}