@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,337 @@
1
+ /**
2
+ * An in-process CrowdSource, so an integration can be exercised end to end
3
+ * before a jury exists.
4
+ *
5
+ * The path an application actually cares about is: deliver a report → get a
6
+ * case → a decision is published → a signed webhook arrives → enforcement runs.
7
+ * Today the middle of that does not exist anywhere — sortition, review and
8
+ * consensus are not built, so nothing publishes a decision. Waiting for them
9
+ * means an integrator cannot write, or test, the half of their code that
10
+ * matters most: what they DO when a decision says `violation`.
11
+ *
12
+ * So this sandbox implements the two ends and lets the test drive the middle.
13
+ * It accepts reports over a `fetch` the real `@crowdsource.you/core` client can be
14
+ * pointed at — same transport, same idempotency, same 409, same tenant check —
15
+ * and `decide()` publishes a decision the way consensus eventually will, which
16
+ * `deliver()` then sends as a genuinely signed `case.decided`.
17
+ *
18
+ * **What it is and is not.** It is a faithful implementation of the RULES an
19
+ * integrator's code depends on: `applicationId` from the credential, an
20
+ * idempotency key that returns the same `reportId`, a 409 for a reused
21
+ * `externalReportId` with a changed body, and §7.3's "two reports about the same
22
+ * version of the same content are one case". It is NOT the service, it holds no
23
+ * state between processes, and where it and the backend ever disagree the
24
+ * backend is right. The deduplication projection below mirrors the backend's
25
+ * `modules/evidence/contentSnapshot.ts`; that logic belongs in the contracts
26
+ * package so both sides share one implementation, and until it moves this file
27
+ * is the second copy — which is a thing to know rather than a thing to rely on.
28
+ */
29
+ import { createHash, randomUUID } from 'node:crypto';
30
+ import { CreateReportRequestSchema, DecisionSchema, KnownWebhookEventSchema, PRINCIPAL_TARGETED_RELATION_TYPES, UNIVERSAL_TAXONOMY_VERSION, } from '@crowdsource.you/contracts';
31
+ import { WebhookSimulator } from './webhook-simulator.js';
32
+ const DEFAULT_BASE_URL = 'https://sandbox.crowdsource.test';
33
+ function publicId(prefix) {
34
+ return `${prefix}_${randomUUID().replace(/-/g, '')}`;
35
+ }
36
+ function sha256Hex(value) {
37
+ return createHash('sha256').update(value, 'utf8').digest('hex');
38
+ }
39
+ /**
40
+ * §7.3's `contentEnvelopeHash`, over the same projection the backend hashes.
41
+ *
42
+ * The three normalisations that make two reporters agree, restated so a reader
43
+ * does not have to open the backend to know what is excluded: order is removed
44
+ * where it is not meaning, proof-of-delivery fields are dropped from principal
45
+ * bindings, and nothing per-reporter is included at all — no
46
+ * `externalReportId`, no `source`, no `allegations`, no reporter binding, no
47
+ * `urgency`, no `metadata`.
48
+ */
49
+ function contentHashOf(envelope) {
50
+ const referencedPrincipals = new Set();
51
+ for (const resource of envelope.resources) {
52
+ if (resource.authorPrincipalRef !== undefined) {
53
+ referencedPrincipals.add(resource.authorPrincipalRef);
54
+ }
55
+ if (resource.type === 'listing' && resource.data.sellerRef !== undefined) {
56
+ referencedPrincipals.add(resource.data.sellerRef);
57
+ }
58
+ }
59
+ for (const relation of envelope.relations) {
60
+ if (PRINCIPAL_TARGETED_RELATION_TYPES.some((type) => type === relation.type)) {
61
+ referencedPrincipals.add(relation.to);
62
+ }
63
+ }
64
+ const snapshot = {
65
+ schemaVersion: envelope.schemaVersion,
66
+ subject: envelope.subject,
67
+ resources: [...envelope.resources].sort((left, right) => (left.id < right.id ? -1 : 1)),
68
+ relations: [...envelope.relations].sort((left, right) => `${left.from} ${left.type} ${left.to}` < `${right.from} ${right.type} ${right.to}` ? -1 : 1),
69
+ principals: envelope.principalBindings
70
+ .filter((binding) => referencedPrincipals.has(binding.principalRef))
71
+ .sort((left, right) => (left.principalRef < right.principalRef ? -1 : 1))
72
+ .map((binding) => ({
73
+ principalRef: binding.principalRef,
74
+ type: binding.type,
75
+ externalPrincipalId: binding.externalPrincipalId,
76
+ })),
77
+ };
78
+ return sha256Hex(JSON.stringify(snapshot));
79
+ }
80
+ function jsonResponse(status, body) {
81
+ return new Response(JSON.stringify(body), {
82
+ status,
83
+ headers: { 'content-type': 'application/json' },
84
+ });
85
+ }
86
+ function apiError(status, code, message) {
87
+ return jsonResponse(status, { error: { code, message } });
88
+ }
89
+ export class CrowdSourceSandbox {
90
+ constructor(options = {}) {
91
+ this.reportsById = new Map();
92
+ this.reportsByIdempotencyKey = new Map();
93
+ this.reportsByExternalId = new Map();
94
+ this.casesById = new Map();
95
+ this.caseIdByDedupKey = new Map();
96
+ this.decisionsById = new Map();
97
+ /** The `fetch` to hand `new CrowdSource({ fetch, serviceKey, baseUrl })`. */
98
+ this.fetch = async (input, init) => {
99
+ const url = new URL(typeof input === 'string' ? input : input instanceof URL ? input.href : input.url);
100
+ const method = (init?.method ?? 'GET').toUpperCase();
101
+ const headers = new Headers(init?.headers);
102
+ if (headers.get('authorization') !== `Bearer ${this.bearerToken}`) {
103
+ return apiError(401, 'unauthorized', 'The service credential is missing or invalid.');
104
+ }
105
+ if (method === 'POST' && url.pathname === '/v1/reports') {
106
+ return this.createReport(headers, init?.body);
107
+ }
108
+ if (method === 'GET' && url.pathname.startsWith('/v1/reports/')) {
109
+ return this.readReport(decodeURIComponent(url.pathname.slice('/v1/reports/'.length)));
110
+ }
111
+ if (method === 'GET' && url.pathname.startsWith('/v1/cases/')) {
112
+ return this.readCase(decodeURIComponent(url.pathname.slice('/v1/cases/'.length)));
113
+ }
114
+ if (method === 'GET' && url.pathname.startsWith('/v1/decisions/')) {
115
+ return this.readDecision(decodeURIComponent(url.pathname.slice('/v1/decisions/'.length)));
116
+ }
117
+ /**
118
+ * Everything else, including the upload routes, answers 404 — the same thing
119
+ * the deployed backend does today. A sandbox that faked an endpoint the
120
+ * service does not serve would let an integration pass its tests and fail on
121
+ * its first real call.
122
+ */
123
+ return apiError(404, 'not_found', `The sandbox does not serve ${method} ${url.pathname}.`);
124
+ };
125
+ this.applicationId = options.applicationId ?? publicId('app');
126
+ this.organizationId = options.organizationId ?? publicId('org');
127
+ this.baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
128
+ this.webhookSecret = options.webhookSecret ?? randomUUID();
129
+ const credentialId = publicId('csk');
130
+ const secret = randomUUID().replace(/-/g, '');
131
+ this.bearerToken = `${credentialId}.${secret}`;
132
+ this.serviceKey = [this.applicationId, credentialId, secret].join(':');
133
+ }
134
+ get reports() {
135
+ return [...this.reportsById.values()];
136
+ }
137
+ get cases() {
138
+ return [...this.casesById.values()];
139
+ }
140
+ /**
141
+ * Publishes a decision for a case, the way consensus eventually will.
142
+ *
143
+ * A published revision is immutable (Appendix F), so calling this twice
144
+ * produces revision 2 superseding revision 1 rather than editing anything.
145
+ */
146
+ decide(caseId, input = {}) {
147
+ const stored = this.casesById.get(caseId);
148
+ if (!stored)
149
+ throw new Error(`The sandbox holds no case '${caseId}'.`);
150
+ const previous = stored.decisions.at(-1);
151
+ const revision = (previous?.revision ?? 0) + 1;
152
+ const outcome = input.outcome ?? 'violation';
153
+ const decision = DecisionSchema.parse({
154
+ id: publicId('dec'),
155
+ caseId,
156
+ revision,
157
+ status: input.status ?? 'final',
158
+ outcome,
159
+ contextSufficiency: outcome === 'insufficient_context' ? 'insufficient' : 'sufficient',
160
+ confidence: input.confidence ?? 1,
161
+ findings: input.findings ??
162
+ (outcome === 'violation'
163
+ ? [
164
+ {
165
+ code: stored.allegationCodes[0] ?? 'other.unclassifiable',
166
+ resourceIds: ['res_subject'],
167
+ severity: 'medium',
168
+ scope: 'application_local',
169
+ attribution: 'author',
170
+ },
171
+ ]
172
+ : []),
173
+ recommendedActions: input.recommendedActions ??
174
+ (outcome === 'violation' ? [{ action: 'remove_or_restrict' }] : [{ action: 'no_action' }]),
175
+ jury: { size: 3, decisiveVotes: 3, winningVotes: 3, agreement: 1, specialistPresent: false },
176
+ policyVersions: {
177
+ taxonomy: UNIVERSAL_TAXONOMY_VERSION,
178
+ application: stored.policy.version,
179
+ oxyConduct: stored.policy.version,
180
+ },
181
+ ...(previous === undefined ? {} : { supersedesDecisionId: previous.id }),
182
+ publishedAt: new Date().toISOString(),
183
+ });
184
+ this.decisionsById.set(decision.id, decision);
185
+ this.casesById.set(caseId, { ...stored, decisions: [...stored.decisions, decision] });
186
+ return decision;
187
+ }
188
+ /** The `case.decided` (or `decision.corrected`) event for a decision. */
189
+ eventFor(decision) {
190
+ return KnownWebhookEventSchema.parse({
191
+ id: publicId('evt'),
192
+ type: decision.revision === 1 ? 'case.decided' : 'decision.corrected',
193
+ createdAt: new Date().toISOString(),
194
+ organizationId: this.organizationId,
195
+ applicationId: this.applicationId,
196
+ data: { caseId: decision.caseId, decision },
197
+ });
198
+ }
199
+ /** Signs and POSTs an event to the integration's webhook endpoint. */
200
+ async deliver(url, event) {
201
+ return await new WebhookSimulator({ secret: this.webhookSecret, url }).deliver(event);
202
+ }
203
+ createReport(headers, body) {
204
+ const idempotencyKey = headers.get('idempotency-key');
205
+ if (idempotencyKey === null || idempotencyKey.length === 0) {
206
+ return apiError(400, 'invalid_request', 'The Idempotency-Key header is required.');
207
+ }
208
+ if (typeof body !== 'string') {
209
+ return apiError(400, 'invalid_request', 'The request body must be JSON.');
210
+ }
211
+ let payload;
212
+ try {
213
+ payload = JSON.parse(body);
214
+ }
215
+ catch {
216
+ return apiError(400, 'invalid_request', 'The request body must be JSON.');
217
+ }
218
+ const parsed = CreateReportRequestSchema.safeParse(payload);
219
+ if (!parsed.success) {
220
+ return apiError(422, 'unprocessable_envelope', `The envelope cannot be processed — ${parsed.error.issues
221
+ .map((issue) => `${issue.path.join('.') || '(root)'}: ${issue.message}`)
222
+ .join('; ')}`);
223
+ }
224
+ const { externalReportId, envelope } = parsed.data;
225
+ if (envelope.applicationId !== this.applicationId) {
226
+ return apiError(403, 'forbidden', 'The envelope names a different applicationId than the credential it was delivered with.');
227
+ }
228
+ const payloadHash = sha256Hex(JSON.stringify(parsed.data));
229
+ /**
230
+ * Appendix D, both halves. The same key with the same payload is the SAME
231
+ * report and returns the same `reportId`; the same `externalReportId` with a
232
+ * DIFFERENT payload is §10.5's 409 — the two cannot both be that report, and
233
+ * no retry resolves it.
234
+ */
235
+ const byKey = this.reportsByIdempotencyKey.get(idempotencyKey);
236
+ if (byKey !== undefined) {
237
+ if (byKey.payloadHash !== payloadHash) {
238
+ return apiError(409, 'conflict', 'This Idempotency-Key was already used with a different payload.');
239
+ }
240
+ return this.receiptResponse(byKey);
241
+ }
242
+ const byExternalId = this.reportsByExternalId.get(externalReportId);
243
+ if (byExternalId !== undefined) {
244
+ if (byExternalId.payloadHash !== payloadHash) {
245
+ return apiError(409, 'conflict', `externalReportId '${externalReportId}' was already delivered with different content.`);
246
+ }
247
+ return this.receiptResponse(byExternalId);
248
+ }
249
+ const dedupKey = sha256Hex([
250
+ this.applicationId,
251
+ envelope.subject.externalId,
252
+ contentHashOf(envelope),
253
+ `${envelope.policy.policySetId}@${envelope.policy.version}`,
254
+ ].join(':'));
255
+ const existingCaseId = this.caseIdByDedupKey.get(dedupKey);
256
+ const merged = existingCaseId !== undefined;
257
+ const caseId = existingCaseId ?? publicId('case');
258
+ const report = {
259
+ reportId: publicId('rpt'),
260
+ externalReportId,
261
+ caseId,
262
+ idempotencyKey,
263
+ envelope,
264
+ status: merged ? 'merged' : 'received',
265
+ receivedAt: new Date().toISOString(),
266
+ payloadHash,
267
+ };
268
+ this.reportsById.set(report.reportId, report);
269
+ this.reportsByIdempotencyKey.set(idempotencyKey, report);
270
+ this.reportsByExternalId.set(externalReportId, report);
271
+ const existingCase = this.casesById.get(caseId);
272
+ this.casesById.set(caseId, {
273
+ caseId,
274
+ externalSubjectId: envelope.subject.externalId,
275
+ dedupKey,
276
+ policy: envelope.policy,
277
+ allegationCodes: [
278
+ ...new Set([
279
+ ...(existingCase?.allegationCodes ?? []),
280
+ ...envelope.allegations.map((allegation) => allegation.code),
281
+ ]),
282
+ ],
283
+ reportIds: [...(existingCase?.reportIds ?? []), report.reportId],
284
+ decisions: existingCase?.decisions ?? [],
285
+ });
286
+ this.caseIdByDedupKey.set(dedupKey, caseId);
287
+ return this.receiptResponse(report);
288
+ }
289
+ receiptResponse(report) {
290
+ return jsonResponse(202, {
291
+ reportId: report.reportId,
292
+ caseId: report.caseId,
293
+ status: report.status,
294
+ merged: report.status === 'merged',
295
+ });
296
+ }
297
+ readReport(reportId) {
298
+ const report = this.reportsById.get(reportId);
299
+ if (!report)
300
+ return apiError(404, 'not_found', 'No such report.');
301
+ return jsonResponse(200, {
302
+ reportId: report.reportId,
303
+ externalReportId: report.externalReportId,
304
+ caseId: report.caseId,
305
+ status: report.status,
306
+ receivedAt: report.receivedAt,
307
+ });
308
+ }
309
+ readCase(caseId) {
310
+ const stored = this.casesById.get(caseId);
311
+ if (!stored)
312
+ return apiError(404, 'not_found', 'No such case.');
313
+ return jsonResponse(200, {
314
+ caseId: stored.caseId,
315
+ status: stored.decisions.length > 0 ? 'decided' : 'awaiting_review',
316
+ subject: { externalId: stored.externalSubjectId, type: 'social.post' },
317
+ policy: stored.policy,
318
+ taxonomyVersion: UNIVERSAL_TAXONOMY_VERSION,
319
+ allegationCodes: stored.allegationCodes,
320
+ reportCount: stored.reportIds.length,
321
+ sensitivityClass: 'standard',
322
+ currentRevision: stored.decisions.length,
323
+ createdAt: new Date(0).toISOString(),
324
+ updatedAt: new Date().toISOString(),
325
+ });
326
+ }
327
+ readDecision(decisionId) {
328
+ const decision = this.decisionsById.get(decisionId);
329
+ if (!decision)
330
+ return apiError(404, 'not_found', 'No such decision.');
331
+ return jsonResponse(200, decision);
332
+ }
333
+ }
334
+ export function createCrowdSourceSandbox(options = {}) {
335
+ return new CrowdSourceSandbox(options);
336
+ }
337
+ //# sourceMappingURL=sandbox.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sandbox.js","sourceRoot":"","sources":["../../../src/testing/sandbox.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,93 @@
1
+ /**
2
+ * The webhook simulator — the producer side of §10.8.
3
+ *
4
+ * An integrator can point this at their own receiver and watch a real delivery
5
+ * arrive, signed exactly as CrowdSource signs one, without a jury having sat and
6
+ * without anything being enforced against a real user.
7
+ *
8
+ * It deliberately also makes it easy to send a delivery that is WRONG:
9
+ * `expired`, `wrongSecret`, `tampered` and a raw `signature` override are here
10
+ * so an integration test can assert that the receiver REFUSES those. A test
11
+ * suite that only ever sends valid deliveries proves the receiver can say yes.
12
+ * Proving it can say no is the part that matters, and it is the part nobody
13
+ * writes unless the tooling makes it a one-liner.
14
+ *
15
+ * Signing lives here rather than in `@crowdsource.you/core/express` because the two
16
+ * are opposite sides of the contract: the verifier must never import a signer
17
+ * that could be "corrected" until they agree with each other and both disagree
18
+ * with the service. What they share instead is
19
+ * `buildWebhookSignedPayload` from the contracts package, which is the one
20
+ * definition of what gets signed.
21
+ */
22
+ import { createHmac } from 'node:crypto';
23
+ import { WEBHOOK_EVENT_ID_HEADER, WEBHOOK_SIGNATURE_HEADER, WEBHOOK_SIGNATURE_VERSION, WEBHOOK_TIMESTAMP_HEADER, buildWebhookSignedPayload, } from '@crowdsource.you/contracts';
24
+ function eventIdOf(event) {
25
+ if (typeof event === 'object' && event !== null && 'id' in event) {
26
+ const id = event.id;
27
+ if (typeof id === 'string')
28
+ return id;
29
+ }
30
+ throw new TypeError('A webhook event must carry a string `id` (§10.7).');
31
+ }
32
+ /** Signs one delivery. */
33
+ export function signWebhookDelivery(input) {
34
+ const signedBody = JSON.stringify(input.event);
35
+ const timestamp = String(input.timestampSeconds ?? Math.floor(Date.now() / 1000));
36
+ const signature = input.signature ??
37
+ `${WEBHOOK_SIGNATURE_VERSION}=${createHmac('sha256', input.secret)
38
+ .update(buildWebhookSignedPayload(timestamp, signedBody), 'utf8')
39
+ .digest('hex')}`;
40
+ return {
41
+ headers: {
42
+ 'content-type': 'application/json',
43
+ [WEBHOOK_EVENT_ID_HEADER]: input.eventId ?? eventIdOf(input.event),
44
+ [WEBHOOK_TIMESTAMP_HEADER]: timestamp,
45
+ [WEBHOOK_SIGNATURE_HEADER]: signature,
46
+ },
47
+ // The tampered body is sent WITHOUT re-signing, which is the whole point:
48
+ // the signature above covers `signedBody` and the receiver is handed
49
+ // something else.
50
+ body: input.tamperedBody ?? signedBody,
51
+ };
52
+ }
53
+ /** Comfortably outside the five-minute window, in either direction. */
54
+ const EXPIRED_SKEW_SECONDS = 3600;
55
+ export class WebhookSimulator {
56
+ constructor(options) {
57
+ this.options = options;
58
+ }
59
+ /** Signs a delivery without sending it. */
60
+ sign(event, overrides = {}) {
61
+ return signWebhookDelivery({
62
+ secret: overrides.wrongSecret ?? this.options.secret,
63
+ event,
64
+ timestampSeconds: overrides.timestampSeconds ??
65
+ Math.floor(Date.now() / 1000) - (overrides.expired === true ? EXPIRED_SKEW_SECONDS : 0),
66
+ ...(overrides.tamperedBody === undefined ? {} : { tamperedBody: overrides.tamperedBody }),
67
+ ...(overrides.signature === undefined ? {} : { signature: overrides.signature }),
68
+ ...(overrides.eventId === undefined ? {} : { eventId: overrides.eventId }),
69
+ });
70
+ }
71
+ /** Signs and POSTs a delivery to the receiver. */
72
+ async deliver(event, overrides = {}) {
73
+ const delivery = this.sign(event, overrides);
74
+ const fetchImpl = this.options.fetch ?? globalThis.fetch;
75
+ const response = await fetchImpl(this.options.url, {
76
+ method: 'POST',
77
+ headers: delivery.headers,
78
+ body: delivery.body,
79
+ });
80
+ const text = await response.text();
81
+ let body = null;
82
+ if (text.length > 0) {
83
+ try {
84
+ body = JSON.parse(text);
85
+ }
86
+ catch {
87
+ body = text;
88
+ }
89
+ }
90
+ return { status: response.status, body };
91
+ }
92
+ }
93
+ //# sourceMappingURL=webhook-simulator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook-simulator.js","sourceRoot":"","sources":["../../../src/testing/webhook-simulator.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,177 @@
1
+ /**
2
+ * The HTTP transport: one authenticated call, and what happens when it fails.
3
+ *
4
+ * Two things here are worth reading before changing anything.
5
+ *
6
+ * **Retries do not replace the integrator's outbox.** §7.1 is explicit that the
7
+ * durable retry path belongs to the application: it stores the report and an
8
+ * outbox row in one operation, and a worker re-delivers until CrowdSource
9
+ * accepts. The retries below only smooth over a blip inside a single delivery
10
+ * attempt — they are bounded, they only fire for the statuses §10.5 marks as
11
+ * "come back later", and when they give up the error they throw still says
12
+ * `retryable: true` so the outbox keeps its job.
13
+ *
14
+ * **A retry is only safe because the write is idempotent.** Every write this
15
+ * client makes carries an `Idempotency-Key` (Appendix D), so a request that was
16
+ * received but whose response was lost returns the same `reportId` on the next
17
+ * attempt rather than creating a second report. A write without one must not be
18
+ * retried, and `request` refuses to retry a mutation that has no key rather than
19
+ * trusting the caller to have thought about it.
20
+ */
21
+ import { CrowdSourceApiError, CrowdSourceTransportError, apiErrorCodeForStatus, isCrowdSourceApiErrorCode, } from './errors.js';
22
+ /** How long a single attempt may take before it is abandoned. */
23
+ export const DEFAULT_TIMEOUT_MS = 10000;
24
+ /** Attempts per call, including the first. */
25
+ export const DEFAULT_MAX_ATTEMPTS = 3;
26
+ /** The base of the exponential backoff between attempts. */
27
+ const RETRY_BASE_DELAY_MS = 250;
28
+ /** A cap so a `Retry-After` of an hour cannot hang a delivery worker. */
29
+ const RETRY_DELAY_CEILING_MS = 5000;
30
+ function parsedErrorBody(payload) {
31
+ if (typeof payload !== 'object' || payload === null || !('error' in payload))
32
+ return null;
33
+ const error = payload.error;
34
+ if (typeof error !== 'object' || error === null)
35
+ return null;
36
+ const record = { ...error };
37
+ if (typeof record.code !== 'string' || typeof record.message !== 'string')
38
+ return null;
39
+ const details = typeof record.details === 'object' && record.details !== null
40
+ ? record.details
41
+ : undefined;
42
+ return { code: record.code, message: record.message, details };
43
+ }
44
+ /** `Retry-After` in either of its two legal forms, in milliseconds. */
45
+ function retryAfterMs(header) {
46
+ if (header === null)
47
+ return null;
48
+ const seconds = Number(header);
49
+ if (Number.isFinite(seconds) && seconds >= 0)
50
+ return seconds * 1000;
51
+ const date = Date.parse(header);
52
+ if (Number.isNaN(date))
53
+ return null;
54
+ return Math.max(0, date - Date.now());
55
+ }
56
+ /**
57
+ * Full jitter over an exponential base. Without jitter, every worker that hit
58
+ * the same 503 comes back at the same millisecond and reproduces it.
59
+ */
60
+ function backoffMs(attempt) {
61
+ const ceiling = Math.min(RETRY_BASE_DELAY_MS * 2 ** (attempt - 1), RETRY_DELAY_CEILING_MS);
62
+ return Math.random() * ceiling;
63
+ }
64
+ function delay(ms, signal) {
65
+ return new Promise((resolve, reject) => {
66
+ const timer = setTimeout(() => {
67
+ signal?.removeEventListener('abort', onAbort);
68
+ resolve();
69
+ }, ms);
70
+ const onAbort = () => {
71
+ clearTimeout(timer);
72
+ reject(new CrowdSourceTransportError('The request was aborted.', { retryable: false }));
73
+ };
74
+ if (signal?.aborted === true) {
75
+ onAbort();
76
+ return;
77
+ }
78
+ signal?.addEventListener('abort', onAbort, { once: true });
79
+ });
80
+ }
81
+ export class Transport {
82
+ constructor(config) {
83
+ this.config = config;
84
+ }
85
+ async request(request) {
86
+ if (request.method !== 'GET' && request.idempotencyKey === undefined) {
87
+ throw new Error(`A ${request.method} to ${request.path} was made without an idempotency key. Appendix D requires one on every write.`);
88
+ }
89
+ let lastError = null;
90
+ for (let attempt = 1; attempt <= this.config.maxAttempts; attempt += 1) {
91
+ const outcome = await this.attempt(request);
92
+ if (outcome.ok)
93
+ return outcome.value;
94
+ lastError = outcome.error;
95
+ if (!outcome.error.retryable || attempt === this.config.maxAttempts)
96
+ break;
97
+ await delay(outcome.retryAfterMs ?? backoffMs(attempt), request.signal);
98
+ }
99
+ // Unreachable with `maxAttempts >= 1`; the loop either returns or assigns.
100
+ if (lastError === null) {
101
+ throw new CrowdSourceTransportError('The request was never attempted.', {
102
+ retryable: false,
103
+ });
104
+ }
105
+ throw lastError;
106
+ }
107
+ async attempt(request) {
108
+ const headers = {
109
+ accept: 'application/json',
110
+ // Resolved per ATTEMPT, not per call: a retry that outlives a short-lived
111
+ // token must carry the new one, and this is the only place that knows an
112
+ // attempt is starting.
113
+ authorization: `Bearer ${await this.config.bearerToken()}`,
114
+ };
115
+ if (request.body !== undefined)
116
+ headers['content-type'] = 'application/json';
117
+ if (request.idempotencyKey !== undefined) {
118
+ headers['idempotency-key'] = request.idempotencyKey;
119
+ }
120
+ let response;
121
+ try {
122
+ response = await this.config.fetch(`${this.config.baseUrl}${request.path}`, {
123
+ method: request.method,
124
+ headers,
125
+ body: request.body === undefined ? undefined : JSON.stringify(request.body),
126
+ signal: this.attemptSignal(request.signal),
127
+ });
128
+ }
129
+ catch (cause) {
130
+ const callerAborted = request.signal?.aborted === true;
131
+ return {
132
+ ok: false,
133
+ error: new CrowdSourceTransportError(callerAborted
134
+ ? 'The request was aborted by the caller.'
135
+ : `The request to ${request.path} did not complete.`, { cause, retryable: !callerAborted }),
136
+ };
137
+ }
138
+ if (response.ok) {
139
+ return { ok: true, value: (await this.readJson(response)) };
140
+ }
141
+ const payload = await this.readJson(response).catch(() => null);
142
+ const body = parsedErrorBody(payload);
143
+ const code = body !== null && isCrowdSourceApiErrorCode(body.code)
144
+ ? body.code
145
+ : apiErrorCodeForStatus(response.status);
146
+ return {
147
+ ok: false,
148
+ error: new CrowdSourceApiError({
149
+ status: response.status,
150
+ code,
151
+ message: body?.message ?? `CrowdSource answered ${response.status} for ${request.path}.`,
152
+ details: body?.details,
153
+ }),
154
+ retryAfterMs: retryAfterMs(response.headers.get('retry-after')) ?? undefined,
155
+ };
156
+ }
157
+ /** The caller's cancellation and this attempt's timeout, as one signal. */
158
+ attemptSignal(callerSignal) {
159
+ const timeout = AbortSignal.timeout(this.config.timeoutMs);
160
+ return callerSignal === undefined ? timeout : AbortSignal.any([callerSignal, timeout]);
161
+ }
162
+ async readJson(response) {
163
+ const text = await response.text();
164
+ if (text.length === 0)
165
+ return null;
166
+ try {
167
+ return JSON.parse(text);
168
+ }
169
+ catch (cause) {
170
+ throw new CrowdSourceTransportError('CrowdSource answered with a body that is not JSON.', {
171
+ cause,
172
+ retryable: false,
173
+ });
174
+ }
175
+ }
176
+ }
177
+ //# sourceMappingURL=transport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport.js","sourceRoot":"","sources":["../../src/transport.ts"],"names":[],"mappings":"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"}