@crvouga/atlas 0.3.0 → 0.5.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 (429) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +178 -604
  3. package/dist/bun/src/driver.d.ts +41 -7
  4. package/dist/bun/src/driver.js +47 -6
  5. package/dist/bun/src/index.d.ts +8 -0
  6. package/dist/bun/src/index.js +8 -0
  7. package/dist/core/src/cli.d.ts +14 -1
  8. package/dist/core/src/cli.js +30 -1
  9. package/dist/core/src/config.d.ts +70 -4
  10. package/dist/core/src/config.js +62 -3
  11. package/dist/core/src/events/cloudevents.d.ts +54 -9
  12. package/dist/core/src/events/cloudevents.js +60 -9
  13. package/dist/core/src/graph.d.ts +21 -0
  14. package/dist/core/src/graph.js +26 -0
  15. package/dist/core/src/index.d.ts +11 -0
  16. package/dist/core/src/index.js +11 -0
  17. package/dist/core/src/lint.d.ts +30 -0
  18. package/dist/core/src/lint.js +13 -0
  19. package/dist/core/src/plan.d.ts +39 -1
  20. package/dist/core/src/plan.js +28 -1
  21. package/dist/core/src/privacy.d.ts +29 -2
  22. package/dist/core/src/privacy.js +19 -2
  23. package/dist/core/src/report/formats.d.ts +34 -4
  24. package/dist/core/src/report/formats.js +24 -4
  25. package/dist/core/src/report/markdown.d.ts +15 -0
  26. package/dist/core/src/report/markdown.js +8 -0
  27. package/dist/core/src/run/clients.d.ts +44 -5
  28. package/dist/core/src/run/clients.js +46 -2
  29. package/dist/core/src/run/execute.d.ts +69 -16
  30. package/dist/core/src/run/execute.js +67 -16
  31. package/dist/core/src/run/frames.d.ts +13 -4
  32. package/dist/core/src/run/frames.js +19 -4
  33. package/dist/core/src/run/http.d.ts +34 -1
  34. package/dist/core/src/run/http.js +14 -1
  35. package/dist/core/src/run/types.d.ts +76 -3
  36. package/dist/core/src/run/types.js +19 -3
  37. package/dist/core/src/signals.d.ts +49 -5
  38. package/dist/core/src/signals.js +22 -5
  39. package/dist/core/src/spec/compose.d.ts +23 -1
  40. package/dist/core/src/spec/compose.js +10 -1
  41. package/dist/core/src/spec/index.d.ts +9 -1
  42. package/dist/core/src/spec/index.js +9 -1
  43. package/dist/core/src/spec/load.d.ts +22 -1
  44. package/dist/core/src/spec/load.js +31 -1
  45. package/dist/core/src/spec/parse.d.ts +16 -2
  46. package/dist/core/src/spec/parse.js +25 -2
  47. package/dist/core/src/spec/scxml.d.ts +25 -4
  48. package/dist/core/src/spec/scxml.js +36 -4
  49. package/dist/core/src/spec/types.d.ts +52 -1
  50. package/dist/core/src/spec/types.js +24 -0
  51. package/dist/detox/src/index.d.ts +54 -11
  52. package/dist/detox/src/index.js +45 -7
  53. package/dist/playwright/src/driver.d.ts +39 -3
  54. package/dist/playwright/src/driver.js +27 -3
  55. package/dist/playwright/src/index.d.ts +10 -0
  56. package/dist/playwright/src/index.js +10 -0
  57. package/dist/playwright/src/media.d.ts +50 -4
  58. package/dist/playwright/src/media.js +55 -4
  59. package/dist/playwright/src/signals.d.ts +44 -5
  60. package/dist/playwright/src/signals.js +29 -5
  61. package/dist/reports/src/attachment-files.d.ts +44 -0
  62. package/dist/reports/src/attachment-files.js +121 -0
  63. package/dist/reports/src/attachment-response.d.ts +31 -0
  64. package/dist/reports/src/attachment-response.js +55 -0
  65. package/dist/reports/src/aws-credentials.d.ts +48 -0
  66. package/dist/reports/src/aws-credentials.js +221 -0
  67. package/dist/reports/src/browser-navigation.d.ts +14 -0
  68. package/dist/reports/src/browser-navigation.js +7 -0
  69. package/dist/reports/src/bundle.d.ts +34 -2
  70. package/dist/reports/src/bundle.js +38 -2
  71. package/dist/reports/src/catalog.d.ts +34 -3
  72. package/dist/reports/src/catalog.js +17 -3
  73. package/dist/reports/src/cli-stores.d.ts +36 -0
  74. package/dist/reports/src/cli-stores.js +73 -0
  75. package/dist/reports/src/clock.d.ts +7 -2
  76. package/dist/reports/src/clock.js +5 -2
  77. package/dist/reports/src/completion-order.d.ts +18 -5
  78. package/dist/reports/src/completion-order.js +18 -5
  79. package/dist/reports/src/compose.d.ts +80 -0
  80. package/dist/reports/src/compose.js +117 -0
  81. package/dist/reports/src/copy-store.d.ts +29 -0
  82. package/dist/reports/src/copy-store.js +52 -0
  83. package/dist/reports/src/directory-outline.d.ts +38 -0
  84. package/dist/reports/src/directory-outline.js +103 -0
  85. package/dist/reports/src/directory-store.d.ts +20 -0
  86. package/dist/reports/src/directory-store.js +166 -0
  87. package/dist/reports/src/doctor.d.ts +29 -0
  88. package/dist/reports/src/doctor.js +83 -0
  89. package/dist/reports/src/effective-delta.d.ts +34 -0
  90. package/dist/reports/src/effective-delta.js +25 -0
  91. package/dist/reports/src/evidence-detail.d.ts +64 -0
  92. package/dist/reports/src/evidence-detail.js +92 -0
  93. package/dist/reports/src/evidence-live.d.ts +40 -4
  94. package/dist/reports/src/evidence-live.js +52 -4
  95. package/dist/reports/src/evidence-types.d.ts +70 -1
  96. package/dist/reports/src/evidence-types.js +5 -1
  97. package/dist/reports/src/evidence-view.d.ts +58 -6
  98. package/dist/reports/src/evidence-view.js +44 -5
  99. package/dist/reports/src/explorer-types.d.ts +40 -3
  100. package/dist/reports/src/explorer-types.js +8 -0
  101. package/dist/reports/src/explorer-versions.d.ts +9 -1
  102. package/dist/reports/src/explorer-versions.js +18 -1
  103. package/dist/reports/src/export-evidence.d.ts +16 -1
  104. package/dist/reports/src/export-evidence.js +52 -1
  105. package/dist/reports/src/fetcher.d.ts +27 -3
  106. package/dist/reports/src/fetcher.js +11 -3
  107. package/dist/reports/src/format-date.d.ts +6 -0
  108. package/dist/reports/src/format-date.js +6 -0
  109. package/dist/reports/src/format.d.ts +16 -0
  110. package/dist/reports/src/format.js +10 -0
  111. package/dist/reports/src/frames.d.ts +36 -3
  112. package/dist/reports/src/frames.js +22 -3
  113. package/dist/reports/src/hash.d.ts +18 -0
  114. package/dist/reports/src/hash.js +13 -0
  115. package/dist/reports/src/host.d.ts +29 -0
  116. package/dist/reports/src/host.js +64 -0
  117. package/dist/reports/src/html-frame.d.ts +5 -6
  118. package/dist/reports/src/html-frame.js +12 -1
  119. package/dist/reports/src/http-store.d.ts +52 -0
  120. package/dist/reports/src/http-store.js +145 -0
  121. package/dist/reports/src/http.d.ts +7 -0
  122. package/dist/reports/src/http.js +6 -0
  123. package/dist/reports/src/ingest.d.ts +54 -0
  124. package/dist/reports/src/ingest.js +210 -0
  125. package/dist/reports/src/init.d.ts +40 -0
  126. package/dist/reports/src/init.js +364 -0
  127. package/dist/reports/src/junit.d.ts +33 -6
  128. package/dist/reports/src/junit.js +87 -21
  129. package/dist/reports/src/keyboard.d.ts +20 -0
  130. package/dist/reports/src/keyboard.js +11 -0
  131. package/dist/reports/src/keys.d.ts +21 -3
  132. package/dist/reports/src/keys.js +21 -3
  133. package/dist/reports/src/layer-axes.d.ts +48 -0
  134. package/dist/reports/src/layer-axes.js +71 -0
  135. package/dist/reports/src/layer-catalog.d.ts +28 -1
  136. package/dist/reports/src/layer-catalog.js +19 -2
  137. package/dist/reports/src/layer-merge.d.ts +98 -9
  138. package/dist/reports/src/layer-merge.js +154 -33
  139. package/dist/reports/src/layer-source.d.ts +18 -0
  140. package/dist/reports/src/layer-source.js +14 -0
  141. package/dist/reports/src/layer-store.d.ts +42 -0
  142. package/dist/reports/src/layer-store.js +63 -5
  143. package/dist/reports/src/layer-types.d.ts +34 -1
  144. package/dist/reports/src/layers-route.d.ts +47 -4
  145. package/dist/reports/src/layers-route.js +88 -12
  146. package/dist/reports/src/media-types.d.ts +11 -0
  147. package/dist/reports/src/media-types.js +22 -0
  148. package/dist/reports/src/namespace-guard.d.ts +10 -1
  149. package/dist/reports/src/namespace-guard.js +10 -1
  150. package/dist/reports/src/navigation.d.ts +12 -0
  151. package/dist/reports/src/navigation.js +7 -0
  152. package/dist/reports/src/object-io.d.ts +11 -4
  153. package/dist/reports/src/object-io.js +4 -4
  154. package/dist/reports/src/object-store-conformance.d.ts +27 -0
  155. package/dist/reports/src/object-store-conformance.js +87 -0
  156. package/dist/reports/src/object-store.d.ts +33 -24
  157. package/dist/reports/src/object-store.js +17 -24
  158. package/dist/reports/src/parse-with.d.ts +16 -0
  159. package/dist/reports/src/parse-with.js +14 -0
  160. package/dist/reports/src/performance-part.d.ts +25 -1
  161. package/dist/reports/src/performance-part.js +15 -1
  162. package/dist/reports/src/performance.d.ts +22 -2
  163. package/dist/reports/src/performance.js +12 -2
  164. package/dist/reports/src/persist.d.ts +12 -3
  165. package/dist/reports/src/persist.js +12 -3
  166. package/dist/reports/src/phone.d.ts +18 -1
  167. package/dist/reports/src/phone.js +18 -1
  168. package/dist/reports/src/protocol.d.ts +30 -4
  169. package/dist/reports/src/protocol.js +15 -4
  170. package/dist/reports/src/publication.d.ts +21 -1
  171. package/dist/reports/src/publication.js +21 -1
  172. package/dist/reports/src/publish-checks.d.ts +35 -0
  173. package/dist/reports/src/publish-checks.js +122 -0
  174. package/dist/reports/src/publish.d.ts +44 -9
  175. package/dist/reports/src/publish.js +40 -7
  176. package/dist/reports/src/read.d.ts +46 -16
  177. package/dist/reports/src/read.js +38 -14
  178. package/dist/reports/src/recording-bundle.d.ts +13 -2
  179. package/dist/reports/src/recording-bundle.js +4 -2
  180. package/dist/reports/src/recording-keys.d.ts +15 -1
  181. package/dist/reports/src/recording-keys.js +15 -1
  182. package/dist/reports/src/remote-report.d.ts +28 -7
  183. package/dist/reports/src/remote-report.js +15 -7
  184. package/dist/reports/src/report-cli.d.ts +51 -0
  185. package/dist/reports/src/report-cli.js +485 -0
  186. package/dist/reports/src/report-index.d.ts +99 -0
  187. package/dist/reports/src/report-index.js +337 -0
  188. package/dist/reports/src/report-loader.d.ts +10 -0
  189. package/dist/reports/src/report-loader.js +6 -0
  190. package/dist/reports/src/report-parts.d.ts +29 -3
  191. package/dist/reports/src/report-parts.js +40 -3
  192. package/dist/reports/src/report-status.d.ts +48 -0
  193. package/dist/reports/src/report-status.js +156 -0
  194. package/dist/reports/src/report-types.d.ts +73 -0
  195. package/dist/reports/src/report-types.js +3 -0
  196. package/dist/reports/src/reporter.d.ts +67 -0
  197. package/dist/reports/src/reporter.js +128 -0
  198. package/dist/reports/src/reports-explorer-route.d.ts +541 -0
  199. package/dist/reports/src/reports-explorer-route.js +11 -0
  200. package/dist/reports/src/reports-list-route.d.ts +9 -0
  201. package/dist/reports/src/reports-list-route.js +9 -0
  202. package/dist/reports/src/reports-route.d.ts +561 -22
  203. package/dist/reports/src/reports-route.js +11 -3
  204. package/dist/reports/src/reports-store.d.ts +19 -2
  205. package/dist/reports/src/reports-store.js +10 -2
  206. package/dist/reports/src/resilient-cache.d.ts +14 -1
  207. package/dist/reports/src/resilient-cache.js +8 -1
  208. package/dist/reports/src/retention.d.ts +17 -1
  209. package/dist/reports/src/retention.js +5 -1
  210. package/dist/reports/src/run-bundle.d.ts +63 -0
  211. package/dist/reports/src/run-bundle.js +159 -0
  212. package/dist/reports/src/s3-store.d.ts +79 -0
  213. package/dist/reports/src/s3-store.js +295 -0
  214. package/dist/reports/src/scenario.d.ts +14 -4
  215. package/dist/reports/src/scenario.js +5 -4
  216. package/dist/reports/src/sigv4.d.ts +72 -0
  217. package/dist/reports/src/sigv4.js +96 -0
  218. package/dist/reports/src/snapshot-bundle.d.ts +24 -1
  219. package/dist/reports/src/snapshot-bundle.js +27 -2
  220. package/dist/reports/src/snapshot-surface.d.ts +20 -9
  221. package/dist/reports/src/snapshot-surface.js +32 -19
  222. package/dist/reports/src/source-content.d.ts +23 -0
  223. package/dist/reports/src/source-content.js +30 -1
  224. package/dist/reports/src/source-entries.d.ts +13 -0
  225. package/dist/reports/src/source-entries.js +6 -0
  226. package/dist/reports/src/source-part.d.ts +15 -0
  227. package/dist/reports/src/source-part.js +8 -0
  228. package/dist/reports/src/source-reader.d.ts +11 -0
  229. package/dist/reports/src/source-reader.js +5 -0
  230. package/dist/reports/src/spec-evidence.d.ts +12 -2
  231. package/dist/reports/src/spec-evidence.js +6 -2
  232. package/dist/reports/src/spec-inventory.d.ts +15 -1
  233. package/dist/reports/src/spec-inventory.js +22 -1
  234. package/dist/reports/src/spec-link.d.ts +28 -0
  235. package/dist/reports/src/spec-link.js +70 -0
  236. package/dist/reports/src/spec-set.d.ts +63 -0
  237. package/dist/reports/src/spec-set.js +177 -0
  238. package/dist/reports/src/status.d.ts +10 -0
  239. package/dist/reports/src/status.js +5 -0
  240. package/dist/reports/src/store-prefix.d.ts +28 -0
  241. package/dist/reports/src/store-prefix.js +22 -0
  242. package/dist/reports/src/store-url.d.ts +89 -0
  243. package/dist/reports/src/store-url.js +160 -0
  244. package/dist/reports/src/test-catalog-entity.d.ts +11 -0
  245. package/dist/reports/src/test-catalog-entity.js +6 -0
  246. package/dist/reports/src/types.d.ts +6 -1
  247. package/dist/reports/src/use-report-nav.d.ts +35 -0
  248. package/dist/reports/src/use-report-nav.js +12 -0
  249. package/dist/schema/src/api.d.ts +799 -0
  250. package/dist/schema/src/api.js +201 -0
  251. package/dist/schema/src/cli-results.d.ts +210 -0
  252. package/dist/schema/src/cli-results.js +124 -0
  253. package/dist/schema/src/common.d.ts +53 -0
  254. package/dist/schema/src/common.js +42 -0
  255. package/dist/schema/src/index.d.ts +14 -0
  256. package/dist/schema/src/index.js +14 -0
  257. package/dist/schema/src/layer.d.ts +2273 -0
  258. package/dist/schema/src/layer.js +462 -0
  259. package/dist/schema/src/manifest.d.ts +95 -0
  260. package/dist/schema/src/manifest.js +84 -0
  261. package/dist/schema/src/report-sources.d.ts +28 -0
  262. package/dist/schema/src/report-sources.js +25 -0
  263. package/dist/schema/src/runs-index.d.ts +16 -0
  264. package/dist/schema/src/runs-index.js +15 -0
  265. package/dist/schema/src/server-config.d.ts +93 -0
  266. package/dist/schema/src/server-config.js +73 -0
  267. package/dist/schema/src/spec.d.ts +69 -0
  268. package/dist/schema/src/spec.js +58 -0
  269. package/dist/server/src/app.d.ts +62 -0
  270. package/dist/server/src/app.js +419 -0
  271. package/dist/server/src/auth.d.ts +41 -0
  272. package/dist/server/src/auth.js +59 -0
  273. package/dist/server/src/config.d.ts +83 -0
  274. package/dist/server/src/config.js +187 -0
  275. package/dist/server/src/dev.d.ts +12 -0
  276. package/dist/server/src/dev.js +80 -0
  277. package/dist/server/src/events.d.ts +29 -0
  278. package/dist/server/src/events.js +101 -0
  279. package/dist/server/src/host.d.ts +30 -0
  280. package/dist/server/src/host.js +115 -0
  281. package/dist/server/src/index.d.ts +16 -0
  282. package/dist/server/src/index.js +16 -0
  283. package/dist/server/src/llms.d.ts +19 -0
  284. package/dist/server/src/llms.js +63 -0
  285. package/dist/server/src/main.d.ts +30 -0
  286. package/dist/server/src/main.js +112 -0
  287. package/dist/server/src/openapi.d.ts +59 -0
  288. package/dist/server/src/openapi.js +374 -0
  289. package/dist/server/src/problem.d.ts +8 -0
  290. package/dist/server/src/problem.js +39 -0
  291. package/dist/server/src/site.d.ts +13 -0
  292. package/dist/server/src/site.js +43 -0
  293. package/dist/server/src/store-api.d.ts +28 -0
  294. package/dist/server/src/store-api.js +159 -0
  295. package/dist/server/src/validate.d.ts +71 -0
  296. package/dist/server/src/validate.js +197 -0
  297. package/dist/vitest/src/index.d.ts +27 -5
  298. package/dist/vitest/src/index.js +18 -5
  299. package/dist/webdriver/src/driver.d.ts +63 -11
  300. package/dist/webdriver/src/driver.js +57 -6
  301. package/dist/webdriver/src/index.d.ts +8 -0
  302. package/dist/webdriver/src/index.js +1 -0
  303. package/dist/webdriver/src/protocol.d.ts +75 -6
  304. package/dist/webdriver/src/protocol.js +79 -6
  305. package/docs/agents.md +137 -0
  306. package/docs/deploying.md +345 -0
  307. package/docs/dogfooding.md +175 -0
  308. package/docs/extending.md +242 -0
  309. package/docs/integrating.md +485 -0
  310. package/docs/reference/bun.md +141 -0
  311. package/docs/reference/cli.md +146 -0
  312. package/docs/reference/core.md +2426 -0
  313. package/docs/reference/detox.md +150 -0
  314. package/docs/reference/http-api.md +303 -0
  315. package/docs/reference/playwright.md +303 -0
  316. package/docs/reference/reports.md +7098 -0
  317. package/docs/reference/schema.md +1651 -0
  318. package/docs/reference/server.md +685 -0
  319. package/docs/reference/vitest.md +45 -0
  320. package/docs/reference/webdriver.md +330 -0
  321. package/docs/report-backends.md +144 -0
  322. package/docs/report-contract.md +400 -0
  323. package/docs/reporting-issues.md +160 -0
  324. package/docs/server.md +285 -0
  325. package/docs/statecharts.md +641 -0
  326. package/llms-full.txt +3506 -0
  327. package/llms.txt +47 -0
  328. package/package.json +33 -5
  329. package/packages/core/README.md +2 -1
  330. package/packages/reports/README.md +69 -2
  331. package/packages/reports/STORAGE.md +5 -1
  332. package/packages/schema/README.md +14 -0
  333. package/packages/schema/schemas/check-detail.schema.json +1032 -0
  334. package/packages/schema/schemas/checks-result.schema.json +26 -0
  335. package/packages/schema/schemas/command-error.schema.json +35 -0
  336. package/packages/schema/schemas/doctor-result.schema.json +60 -0
  337. package/packages/schema/schemas/effective-delta.schema.json +490 -0
  338. package/packages/schema/schemas/effective-evidence.schema.json +471 -0
  339. package/packages/schema/schemas/evidence-page.schema.json +271 -0
  340. package/packages/schema/schemas/hello-event.schema.json +22 -0
  341. package/packages/schema/schemas/init-plan.schema.json +347 -0
  342. package/packages/schema/schemas/layer-rows.schema.json +199 -0
  343. package/packages/schema/schemas/layer.schema.json +2888 -0
  344. package/packages/schema/schemas/merge-result.schema.json +40 -0
  345. package/packages/schema/schemas/outline.schema.json +62 -0
  346. package/packages/schema/schemas/problem.schema.json +37 -0
  347. package/packages/schema/schemas/publish-checks.schema.json +1413 -0
  348. package/packages/schema/schemas/publish-result.schema.json +168 -0
  349. package/packages/schema/schemas/ready.schema.json +68 -0
  350. package/packages/schema/schemas/report-event.schema.json +100 -0
  351. package/packages/schema/schemas/report-status.schema.json +370 -0
  352. package/packages/schema/schemas/server-config.schema.json +194 -0
  353. package/packages/schema/schemas/server-info.schema.json +114 -0
  354. package/packages/schema/schemas/spec-set.schema.json +103 -0
  355. package/packages/schema/schemas/store-listing.schema.json +29 -0
  356. package/packages/schema/schemas/store-write-result.schema.json +28 -0
  357. package/packages/schema/schemas/stream-result.schema.json +59 -0
  358. package/packages/server/README.md +43 -0
  359. package/packages/server/openapi.json +6699 -0
  360. package/packages/visualizer/README.md +27 -5
  361. package/packages/visualizer/example/specs/coffee/ordering/cart.qnt +7 -0
  362. package/packages/visualizer/example/specs/coffee/ordering/checkout.openslo.yaml +10 -0
  363. package/packages/visualizer/example/specs/coffee/ordering/drink-price.dmn +24 -0
  364. package/packages/visualizer/example/specs/coffee/ordering/loyalty.feature +7 -0
  365. package/packages/visualizer/example/specs/coffee/ordering/ordering.feature +36 -0
  366. package/packages/visualizer/example/specs/coffee/ordering/payment.als +4 -0
  367. package/packages/visualizer/fixtures/failures/specs/coffee/ordering/cart.qnt +7 -0
  368. package/packages/visualizer/fixtures/failures/specs/coffee/ordering/checkout.openslo.yaml +10 -0
  369. package/packages/visualizer/fixtures/failures/specs/coffee/ordering/drink-price.dmn +24 -0
  370. package/packages/visualizer/fixtures/failures/specs/coffee/ordering/loyalty.feature +7 -0
  371. package/packages/visualizer/fixtures/failures/specs/coffee/ordering/ordering.feature +36 -0
  372. package/packages/visualizer/fixtures/failures/specs/coffee/ordering/payment.als +4 -0
  373. package/packages/visualizer/fixtures/full/specs/coffee/ordering/cart.qnt +7 -0
  374. package/packages/visualizer/fixtures/full/specs/coffee/ordering/checkout.openslo.yaml +10 -0
  375. package/packages/visualizer/fixtures/full/specs/coffee/ordering/drink-price.dmn +24 -0
  376. package/packages/visualizer/fixtures/full/specs/coffee/ordering/loyalty.feature +7 -0
  377. package/packages/visualizer/fixtures/full/specs/coffee/ordering/ordering.feature +36 -0
  378. package/packages/visualizer/fixtures/full/specs/coffee/ordering/payment.als +4 -0
  379. package/packages/visualizer/fixtures/malformed/specs/coffee/ordering/cart.qnt +7 -0
  380. package/packages/visualizer/fixtures/malformed/specs/coffee/ordering/checkout.openslo.yaml +10 -0
  381. package/packages/visualizer/fixtures/malformed/specs/coffee/ordering/drink-price.dmn +24 -0
  382. package/packages/visualizer/fixtures/malformed/specs/coffee/ordering/loyalty.feature +7 -0
  383. package/packages/visualizer/fixtures/malformed/specs/coffee/ordering/ordering.feature +36 -0
  384. package/packages/visualizer/fixtures/malformed/specs/coffee/ordering/payment.als +4 -0
  385. package/packages/visualizer/fixtures/partial/specs/coffee/ordering/cart.qnt +7 -0
  386. package/packages/visualizer/fixtures/partial/specs/coffee/ordering/checkout.openslo.yaml +10 -0
  387. package/packages/visualizer/fixtures/partial/specs/coffee/ordering/drink-price.dmn +24 -0
  388. package/packages/visualizer/fixtures/partial/specs/coffee/ordering/loyalty.feature +7 -0
  389. package/packages/visualizer/fixtures/partial/specs/coffee/ordering/ordering.feature +36 -0
  390. package/packages/visualizer/fixtures/partial/specs/coffee/ordering/payment.als +4 -0
  391. package/packages/visualizer/fixtures/removed/specs/coffee/ordering/cart.qnt +7 -0
  392. package/packages/visualizer/fixtures/removed/specs/coffee/ordering/checkout.openslo.yaml +10 -0
  393. package/packages/visualizer/fixtures/removed/specs/coffee/ordering/drink-price.dmn +24 -0
  394. package/packages/visualizer/fixtures/removed/specs/coffee/ordering/loyalty.feature +7 -0
  395. package/packages/visualizer/fixtures/removed/specs/coffee/ordering/ordering.feature +36 -0
  396. package/packages/visualizer/fixtures/removed/specs/coffee/ordering/payment.als +4 -0
  397. package/packages/visualizer/fixtures/spec-only/specs/coffee/ordering/cart.qnt +7 -0
  398. package/packages/visualizer/fixtures/spec-only/specs/coffee/ordering/checkout.openslo.yaml +10 -0
  399. package/packages/visualizer/fixtures/spec-only/specs/coffee/ordering/drink-price.dmn +24 -0
  400. package/packages/visualizer/fixtures/spec-only/specs/coffee/ordering/loyalty.feature +7 -0
  401. package/packages/visualizer/fixtures/spec-only/specs/coffee/ordering/ordering.feature +36 -0
  402. package/packages/visualizer/fixtures/spec-only/specs/coffee/ordering/payment.als +4 -0
  403. package/packages/visualizer/scripts/make-fixtures.ts +2 -0
  404. package/packages/visualizer/scripts/publish-data.ts +1 -2
  405. package/packages/visualizer/src/app/AppShell.tsx +9 -2
  406. package/packages/visualizer/src/app/report-sources.tsx +6 -4
  407. package/packages/visualizer/src/app/router.tsx +3 -3
  408. package/packages/visualizer/src/components/Header.module.css +35 -0
  409. package/packages/visualizer/src/components/Header.tsx +41 -4
  410. package/packages/visualizer/src/data/adapters/adapter.ts +5 -17
  411. package/packages/visualizer/src/data/adapters/index.ts +10 -1
  412. package/packages/visualizer/src/evidence/api.ts +14 -2
  413. package/packages/visualizer/src/evidence/artifacts.module.css +213 -0
  414. package/packages/visualizer/src/evidence/artifacts.tsx +272 -0
  415. package/packages/visualizer/src/evidence/check-detail.tsx +38 -4
  416. package/packages/visualizer/src/evidence/evidence-context.tsx +74 -12
  417. package/packages/visualizer/src/evidence/evidence.module.css +20 -0
  418. package/packages/visualizer/src/evidence/marks.tsx +2 -0
  419. package/packages/visualizer/src/evidence/overview.tsx +3 -3
  420. package/packages/visualizer/src/evidence/sources-panel.tsx +88 -34
  421. package/packages/visualizer/src/evidence/spec-page.tsx +23 -16
  422. package/packages/visualizer/src/find/Finder.tsx +1 -7
  423. package/packages/visualizer/src/find/open.ts +4 -0
  424. package/packages/visualizer/src/live/live-stream.ts +204 -0
  425. package/packages/visualizer/src/runtime.ts +19 -0
  426. package/packages/visualizer/src/state/ui-store.ts +6 -2
  427. package/packages/visualizer/vite/atlas-server-proxy.ts +36 -0
  428. package/packages/visualizer/vite/example-reports.ts +475 -0
  429. package/packages/visualizer/vite.config.ts +18 -2
@@ -0,0 +1,295 @@
1
+ /**
2
+ * @module S3 object store
3
+ *
4
+ * An `ObjectStore` on AWS S3 or anything that speaks its protocol (Cloudflare R2, MinIO, SeaweedFS, GCS interoperability,
5
+ * Backblaze B2, Wasabi), written without an SDK: requests are signed with Signature Version 4 and sent with `fetch`, so
6
+ * it runs on Bun and Node. Built for many writers and a long-lived reader: connections are reused, requests in flight
7
+ * are capped, throttling and server errors are retried with jittered backoff, a bucket in another region is followed,
8
+ * and expired credentials are refreshed once. Credentials never come from a URL; they come from options or the
9
+ * environment the way an AWS SDK finds them. Importing the module does no work. Change it only with the S3 integration
10
+ * tests passing against a real server: a fake `fetch` cannot tell you S3 accepts your signature.
11
+ *
12
+ * @see docs/report-contract.md#backends
13
+ * @see docs/deploying.md
14
+ */
15
+ import { XMLParser } from 'fast-xml-parser';
16
+ import { credentialChain, staticCredentials } from './aws-credentials.js';
17
+ import { amzDate, presignUrl, sha256Hex, signRequest, uriEncode } from './sigv4.js';
18
+ import { ObjectStoreWithPrefix } from './store-prefix.js';
19
+ /**
20
+ * An error from the store. The message names the bucket and what to do about it (for example the missing IAM
21
+ * permission); `status` and `code` are what the service answered, `null` when it was never reached.
22
+ * `retryable` says whether trying again can help; the store already retried before throwing.
23
+ */
24
+ export class S3Error extends Error {
25
+ status;
26
+ code;
27
+ retryable;
28
+ constructor(message, status, code, retryable = false) {
29
+ super(message);
30
+ this.status = status;
31
+ this.code = code;
32
+ this.retryable = retryable;
33
+ this.name = 'S3Error';
34
+ }
35
+ }
36
+ let parser;
37
+ /** Built on first use, so importing this module does no work. */
38
+ const xml = { parse: (text) => (parser ??= new XMLParser({ ignoreAttributes: true, parseTagValue: false, isArray: name => name === 'Contents' || name === 'CommonPrefixes' })).parse(text) };
39
+ const RETRYABLE_CODES = new Set(['SlowDown', 'RequestTimeout', 'InternalError', 'ServiceUnavailable', 'RequestTimeTooSkewedRetry', 'Throttling', 'ThrottlingException', 'RequestLimitExceeded']);
40
+ function hint(code, status, where, method = 'GET') {
41
+ switch (code) {
42
+ case 'AccessDenied':
43
+ case 'Forbidden':
44
+ return `${where}: access denied. The credentials need s3:GetObject, s3:PutObject and s3:ListBucket on this bucket and prefix (\`atlas report policy\` prints a least-privilege policy), and the bucket must not forbid them.${method === 'GET' || method === 'HEAD' ? ' Note that S3 also answers 403 for an object that does not exist when s3:ListBucket is not granted.' : ''}`;
45
+ case 'InvalidAccessKeyId':
46
+ return `${where}: the access key id is not known to this service. Check AWS_ACCESS_KEY_ID, and that the endpoint and region belong to the same account.`;
47
+ case 'SignatureDoesNotMatch':
48
+ return `${where}: the signature was rejected. The secret key is wrong, or the region/endpoint is not the one the keys belong to, or the clock is far off.`;
49
+ case 'RequestTimeTooSkewed':
50
+ return `${where}: this machine's clock differs from the service's by more than 15 minutes. Fix the clock.`;
51
+ case 'NoSuchBucket':
52
+ return `${where}: the bucket does not exist (or is in another account or endpoint).`;
53
+ case 'ExpiredToken':
54
+ case 'InvalidToken':
55
+ return `${where}: the session token expired. Sign in again (\`aws sso login\`) or refresh the role.`;
56
+ default:
57
+ return `${where}: ${code ?? 'HTTP ' + status}`;
58
+ }
59
+ }
60
+ function parseError(text) {
61
+ try {
62
+ const found = xml.parse(text)?.Error;
63
+ return { code: found?.Code ?? null, message: found?.Message ?? null };
64
+ }
65
+ catch {
66
+ return { code: null, message: null };
67
+ }
68
+ }
69
+ class Gate {
70
+ limit;
71
+ #active = 0;
72
+ #waiting = [];
73
+ constructor(limit) {
74
+ this.limit = limit;
75
+ }
76
+ async run(task) {
77
+ // A finished request hands its place straight to the next in line, so no newcomer can slip in between.
78
+ if (this.#active >= this.limit)
79
+ await new Promise(resolve => this.#waiting.push(resolve));
80
+ else
81
+ this.#active += 1;
82
+ try {
83
+ return await task();
84
+ }
85
+ finally {
86
+ const next = this.#waiting.shift();
87
+ if (next)
88
+ next();
89
+ else
90
+ this.#active -= 1;
91
+ }
92
+ }
93
+ }
94
+ /**
95
+ * An {@link ObjectStore} on S3, or anything that speaks its protocol: Cloudflare R2, MinIO, Google
96
+ * Cloud Storage (interoperability), Backblaze B2, Wasabi, SeaweedFS. No SDK: requests are signed with
97
+ * Signature Version 4 and sent with `fetch`, so it runs on Bun and Node and adds nothing
98
+ * to the install.
99
+ *
100
+ * Made for many writers and a long-lived reader: connections are reused, requests in flight are
101
+ * capped, throttling and server errors are retried with jittered backoff, a bucket in another region
102
+ * is followed, and expired credentials are refreshed once.
103
+ *
104
+ * Behaviour to know: a missing key reads as `null`/`false` only when S3 answers 404. For an account without
105
+ * `s3:ListBucket`, S3 answers 403 for a missing key, which throws `S3Error` with a hint: the publishing credentials
106
+ * need `s3:GetObject`, `s3:PutObject` and `s3:ListBucket` (`atlas report policy` prints a least-privilege policy).
107
+ * `getUri` returns a presigned URL valid for one hour. `deleteMany` sends one request per key, 16 at a time.
108
+ * `listKeys` pages through the whole listing and sorts it; use `listPage` to read part of a large catalog. Nothing is
109
+ * checked or created at construction: the first request is the first contact.
110
+ *
111
+ * @example
112
+ * const store = s3Store({ bucket: 'my-evidence', prefix: 'atlas/my-project', region: 'eu-west-1' });
113
+ * await store.put('layers/manifests/x.json', bytes, 'application/json');
114
+ * const { ok, error } = await store.ping();
115
+ */
116
+ export function s3Store(options) {
117
+ const env = options.env ?? process.env;
118
+ const doFetch = options.fetch ?? fetch;
119
+ const now = options.now ?? (() => new Date());
120
+ const sleep = options.sleep ?? ((ms) => new Promise(resolve => setTimeout(resolve, ms)));
121
+ const retries = options.retries ?? 4;
122
+ const timeoutMs = options.timeoutMs ?? 30_000;
123
+ const gate = new Gate(options.concurrency ?? 64);
124
+ const provider = !options.credentials ? credentialChain({ env, fetch: doFetch })
125
+ : typeof options.credentials === 'function' ? options.credentials : staticCredentials(options.credentials);
126
+ const prefix = (options.prefix ?? '').replace(/^\/+/, '');
127
+ const base = prefix && !prefix.endsWith('/') ? `${prefix}/` : prefix;
128
+ let region = options.region || env.AWS_REGION || env.AWS_DEFAULT_REGION || 'us-east-1';
129
+ const customEndpoint = options.endpoint || env.AWS_ENDPOINT_URL_S3 || env.AWS_ENDPOINT_URL;
130
+ const pathStyle = options.forcePathStyle ?? (Boolean(customEndpoint) || options.bucket.includes('.'));
131
+ const where = `s3://${options.bucket}/${base}`;
132
+ function urlFor(key, query = []) {
133
+ const encodedKey = key.split('/').map(segment => uriEncode(segment)).join('/');
134
+ let url;
135
+ if (customEndpoint) {
136
+ url = new URL(customEndpoint);
137
+ url.pathname = `${url.pathname.replace(/\/$/, '')}/${pathStyle ? `${options.bucket}/` : ''}${encodedKey}`;
138
+ if (!pathStyle)
139
+ url.hostname = `${options.bucket}.${url.hostname}`;
140
+ }
141
+ else if (pathStyle) {
142
+ url = new URL(`https://s3.${region}.amazonaws.com/${options.bucket}/${encodedKey}`);
143
+ }
144
+ else {
145
+ url = new URL(`https://${options.bucket}.s3.${region}.amazonaws.com/${encodedKey}`);
146
+ }
147
+ for (const [name, value] of query)
148
+ url.searchParams.append(name, value);
149
+ return url;
150
+ }
151
+ async function request(method, key, init = {}) {
152
+ let refreshed = false;
153
+ let redirected = false;
154
+ for (let attempt = 0;; attempt++) {
155
+ const credentials = await provider();
156
+ const url = urlFor(key, init.query);
157
+ const payloadHash = init.body ? await sha256Hex(init.body) : await sha256Hex('');
158
+ // One instant for the date header and the signature, or a request that straddles a second is rejected.
159
+ const at = now();
160
+ const { datetime } = amzDate(at);
161
+ const headers = await signRequest({
162
+ method, url, payloadHash, credentials, region, service: 's3', at,
163
+ headers: { 'x-amz-content-sha256': payloadHash, 'x-amz-date': datetime, ...(credentials.sessionToken ? { 'x-amz-security-token': credentials.sessionToken } : {}) }
164
+ });
165
+ if (init.contentType)
166
+ headers['content-type'] = init.contentType;
167
+ let response = null;
168
+ let failure = null;
169
+ // The timeout is for the service to answer, not for a large body to arrive: it stops at the headers.
170
+ let timedOut = false;
171
+ try {
172
+ // The clock starts when the request does, not while it waits its turn behind the others.
173
+ response = await gate.run(async () => {
174
+ const controller = new AbortController();
175
+ const timer = setTimeout(() => { timedOut = true; controller.abort(); }, timeoutMs);
176
+ try {
177
+ return await doFetch(url, { method, headers, ...(init.body ? { body: init.body } : {}), signal: controller.signal });
178
+ }
179
+ finally {
180
+ clearTimeout(timer);
181
+ }
182
+ });
183
+ }
184
+ catch (error) {
185
+ const reason = timedOut ? `timed out after ${timeoutMs}ms` : error instanceof Error ? error.message : String(error);
186
+ failure = new S3Error(`${where}: cannot reach ${url.host} (${reason})`, null, null, true);
187
+ }
188
+ if (response) {
189
+ if (response.ok)
190
+ return response;
191
+ const text = method === 'HEAD' ? '' : await response.text().catch(() => '');
192
+ const { code, message } = parseError(text);
193
+ // A missing key is an answer. A missing bucket is a mistake, and says so.
194
+ if (response.status === 404 && init.allow404 && code !== 'NoSuchBucket')
195
+ return null;
196
+ const bucketRegion = response.headers.get('x-amz-bucket-region');
197
+ if (bucketRegion && bucketRegion !== region && !redirected && [301, 307, 400, 403].includes(response.status)) {
198
+ region = bucketRegion;
199
+ redirected = true;
200
+ continue;
201
+ }
202
+ if ((code === 'ExpiredToken' || code === 'InvalidToken' || code === 'ExpiredTokenException') && !refreshed) {
203
+ provider.invalidate();
204
+ refreshed = true;
205
+ continue;
206
+ }
207
+ const retryable = response.status >= 500 || response.status === 429 || response.status === 408 || RETRYABLE_CODES.has(code ?? '');
208
+ failure = new S3Error(`${hint(code, response.status, `${where}${key.slice(base.length)}`, method)}${message && code !== 'AccessDenied' ? ` (${message})` : ''}`, response.status, code, retryable);
209
+ }
210
+ if (!failure || !failure.retryable || attempt >= retries)
211
+ throw failure;
212
+ // Exponential backoff with full jitter, capped.
213
+ await sleep(Math.random() * Math.min(8_000, 100 * 2 ** attempt));
214
+ }
215
+ }
216
+ const store = {
217
+ label: `s3://${options.bucket}/${base}${customEndpoint ? ` (${new URL(customEndpoint).host})` : ` (${region})`}`,
218
+ credentialSource: () => provider.source(),
219
+ async get(key) {
220
+ const response = await request('GET', base + key, { allow404: true });
221
+ if (!response)
222
+ return null;
223
+ return { body: response.body ?? new Blob([]).stream(), contentType: response.headers.get('content-type') ?? 'application/octet-stream', size: Number(response.headers.get('content-length') ?? 0) };
224
+ },
225
+ async put(key, bytes, contentType) {
226
+ const response = await request('PUT', base + key, { body: bytes, contentType });
227
+ await response?.arrayBuffer();
228
+ },
229
+ async head(key) {
230
+ const response = await request('HEAD', base + key, { allow404: true });
231
+ return response !== null;
232
+ },
233
+ async delete(key) {
234
+ const response = await request('DELETE', base + key, { allow404: true });
235
+ await response?.arrayBuffer();
236
+ },
237
+ async deleteMany(keys) {
238
+ // One request per key, a few at a time: DeleteObjects needs a checksum Web Crypto cannot make (MD5).
239
+ const queue = [...keys];
240
+ await Promise.all(Array.from({ length: Math.min(16, queue.length) }, async () => {
241
+ for (let key = queue.shift(); key !== undefined; key = queue.shift())
242
+ await store.delete(key);
243
+ }));
244
+ },
245
+ async getUri(key) {
246
+ if (!(await store.head(key)))
247
+ return null;
248
+ return presignUrl({ url: urlFor(base + key), credentials: await provider(), region, service: 's3', at: now(), expiresSeconds: 3600 });
249
+ },
250
+ async listKeys(listPrefix = '') {
251
+ const keys = [];
252
+ let token;
253
+ do {
254
+ const query = [['list-type', '2'], ['encoding-type', 'url'], ['prefix', base + listPrefix]];
255
+ if (token)
256
+ query.push(['continuation-token', token]);
257
+ const response = (await request('GET', '', { query }));
258
+ const result = xml.parse(await response.text())?.ListBucketResult;
259
+ if (!result)
260
+ throw new S3Error(`${where}: the listing was not a ListBucketResult`, response.status, null);
261
+ for (const entry of result.Contents ?? [])
262
+ keys.push(decodeURIComponent(String(entry.Key).replace(/\+/g, ' ')).slice(base.length));
263
+ token = result.IsTruncated === 'true' ? result.NextContinuationToken : undefined;
264
+ } while (token);
265
+ // The contract is code-unit order.
266
+ return keys.sort();
267
+ },
268
+ async listPage(listPrefix = '', options = {}) {
269
+ const limit = Math.max(1, Math.min(options.limit ?? 1000, 1000));
270
+ const query = [['list-type', '2'], ['encoding-type', 'url'], ['prefix', base + listPrefix], ['max-keys', String(limit)]];
271
+ if (options.after !== undefined)
272
+ query.push(['start-after', base + options.after]);
273
+ const response = (await request('GET', '', { query }));
274
+ const result = xml.parse(await response.text())?.ListBucketResult;
275
+ if (!result)
276
+ throw new S3Error(`${where}: the listing was not a ListBucketResult`, response.status, null);
277
+ const keys = (result.Contents ?? []).map((entry) => decodeURIComponent(String(entry.Key).replace(/\+/g, ' ')).slice(base.length));
278
+ return { keys, next: result.IsTruncated === 'true' && keys.length ? keys.at(-1) : null };
279
+ },
280
+ withPrefix(extra) {
281
+ return new ObjectStoreWithPrefix(store, extra);
282
+ },
283
+ async ping() {
284
+ const started = performance.now();
285
+ try {
286
+ await request('GET', '', { query: [['list-type', '2'], ['max-keys', '1'], ['prefix', base]] });
287
+ return { ok: true, ms: Math.round(performance.now() - started) };
288
+ }
289
+ catch (error) {
290
+ return { ok: false, ms: Math.round(performance.now() - started), error: error instanceof Error ? error.message : String(error) };
291
+ }
292
+ }
293
+ };
294
+ return store;
295
+ }
@@ -1,15 +1,25 @@
1
+ /**
2
+ * @module Gherkin scenario view of a recorded run
3
+ *
4
+ * Recognises the Gherkin-keyword steps (`Given`, `When`, `Then`, `And`, `But`, `*`) at the top of a recorded step tree
5
+ * and groups the harness actions nested under each, so the viewer can show a run as a readable script. Pure functions
6
+ * over `ReportStep`.
7
+ */
1
8
  import type { ReportStep } from './report-types.js';
9
+ /** One Gherkin line of a scenario and what the harness did while it ran. */
2
10
  export type ScenarioLine = {
11
+ /** The Gherkin step itself. */
3
12
  readonly step: ReportStep;
4
13
  /** Nested harness steps: the actions taken while this line ran. */
5
14
  readonly actions: readonly ReportStep[];
6
15
  };
16
+ /** Whether a step's `kind` (trimmed, any case) is a Gherkin keyword: `given`, `when`, `then`, `and`, `but` or `*`. */
7
17
  export declare function isGherkinStep(step: ReportStep): boolean;
8
- /** Title with the keyword removed, so the script can show the keyword once. */
18
+ /** The step's title with a leading copy of its `kind` removed (case-insensitively, not word-aware), so the script can show the keyword once. Returns the trimmed title unchanged when it does not start with the kind. */
9
19
  export declare function gherkinText(step: ReportStep): string;
10
20
  /**
11
- * Top-level Gherkin lines and the actions nested under each. Null when the
12
- * run is not a scenario (a model check, a property), so the step tree is
13
- * the action log instead.
21
+ * Groups a run's top-level Gherkin steps with the actions nested under each, every descendant step flattened in
22
+ * depth-first order. Returns `null` when no top-level step is a Gherkin step (a model check, a property), so the step
23
+ * tree is the action log instead. Gherkin steps nested deeper than the top level are only listed as actions.
14
24
  */
15
25
  export declare function scenarioScript(steps: readonly ReportStep[]): readonly ScenarioLine[] | null;
@@ -1,8 +1,9 @@
1
1
  const GHERKIN = new Set(['given', 'when', 'then', 'and', 'but', '*']);
2
+ /** Whether a step's `kind` (trimmed, any case) is a Gherkin keyword: `given`, `when`, `then`, `and`, `but` or `*`. */
2
3
  export function isGherkinStep(step) {
3
4
  return GHERKIN.has(step.kind.trim().toLowerCase());
4
5
  }
5
- /** Title with the keyword removed, so the script can show the keyword once. */
6
+ /** The step's title with a leading copy of its `kind` removed (case-insensitively, not word-aware), so the script can show the keyword once. Returns the trimmed title unchanged when it does not start with the kind. */
6
7
  export function gherkinText(step) {
7
8
  const kind = step.kind.trim();
8
9
  const title = step.title.trim();
@@ -19,9 +20,9 @@ function walkActions(steps, out) {
19
20
  }
20
21
  }
21
22
  /**
22
- * Top-level Gherkin lines and the actions nested under each. Null when the
23
- * run is not a scenario (a model check, a property), so the step tree is
24
- * the action log instead.
23
+ * Groups a run's top-level Gherkin steps with the actions nested under each, every descendant step flattened in
24
+ * depth-first order. Returns `null` when no top-level step is a Gherkin step (a model check, a property), so the step
25
+ * tree is the action log instead. Gherkin steps nested deeper than the top level are only listed as actions.
25
26
  */
26
27
  export function scenarioScript(steps) {
27
28
  const lines = steps.filter(isGherkinStep);
@@ -0,0 +1,72 @@
1
+ /**
2
+ * @module AWS Signature Version 4
3
+ *
4
+ * AWS Signature Version 4 request signing and URL presigning, with Web Crypto only, so it runs unchanged on Bun, Node
5
+ * and in a Worker. S3 and every store that speaks its protocol (R2, MinIO, GCS interoperability, B2, Wasabi) sign this
6
+ * way. Pure apart from Web Crypto: the time is passed in, nothing is sent. It is tested against AWS's published signing
7
+ * vectors; change it only with the S3 integration tests passing against a real server.
8
+ *
9
+ * @see docs/report-contract.md#backends
10
+ */
11
+ /** An AWS-style credential. */
12
+ export type Credentials = {
13
+ /** The public key id; sent in the `Authorization` header. */
14
+ readonly accessKeyId: string;
15
+ /** The secret used to derive the signing key. Never sent. */
16
+ readonly secretAccessKey: string;
17
+ /** Temporary-credential token, sent as `x-amz-security-token` by the caller; absent for long-lived keys. */
18
+ readonly sessionToken?: string;
19
+ /** Epoch milliseconds after which these stop working. Absent for long-lived keys. */
20
+ readonly expiresAtMs?: number;
21
+ };
22
+ /** The SHA-256 digest of a string (UTF-8) or bytes, as 64 lowercase hex digits. */
23
+ export declare function sha256Hex(data: Uint8Array | string): Promise<string>;
24
+ /**
25
+ * RFC 3986 percent-encoding of the UTF-8 bytes of `value`, which is what SigV4 asks for: letters, digits and `-_.~`
26
+ * stay, everything else (including `/`, unless `keepSlash`) becomes uppercase `%XX`.
27
+ */
28
+ export declare function uriEncode(value: string, keepSlash?: boolean): string;
29
+ /** Formats a time for signing: `date` is `YYYYMMDD` and `datetime` is `YYYYMMDDTHHMMSSZ`, both UTC. */
30
+ export declare function amzDate(at: Date): {
31
+ readonly date: string;
32
+ readonly datetime: string;
33
+ };
34
+ /** What `signRequest` signs. */
35
+ export type SignInput = {
36
+ /** The HTTP method, uppercase (`GET`, `PUT`). */
37
+ readonly method: string;
38
+ /** The full request URL; its path and query are signed. */
39
+ readonly url: URL;
40
+ /** Every header to sign, lowercase names. `host` is added when missing. */
41
+ readonly headers: Readonly<Record<string, string>>;
42
+ /** SHA-256 hex of the body, or `UNSIGNED-PAYLOAD`. */
43
+ readonly payloadHash: string;
44
+ /** The key to sign with. */
45
+ readonly credentials: Credentials;
46
+ /** The region in the credential scope, such as `us-east-1`. */
47
+ readonly region: string;
48
+ /** The service in the credential scope, `s3` for S3. */
49
+ readonly service: string;
50
+ /** The signing instant. Use the same instant for the `x-amz-date` header, or a request that straddles a second is rejected. */
51
+ readonly at: Date;
52
+ /** S3 keeps the path exactly as given; other services encode it twice. */
53
+ readonly doubleEncodePath?: boolean;
54
+ };
55
+ /**
56
+ * Signs a request and returns the headers to send: those given, plus `host` (when missing) and `authorization`. The
57
+ * caller must send exactly these headers (and any it did not sign must not matter to the service). Does not send
58
+ * anything and does not read the body; `payloadHash` is trusted.
59
+ */
60
+ export declare function signRequest(input: SignInput): Promise<Record<string, string>>;
61
+ /**
62
+ * A query-signed URL anyone can GET until it expires, without credentials: for handing a file to a browser. Valid for
63
+ * `expiresSeconds` from `at`; signs only the `host` header and an unsigned payload, so it works for `GET` only.
64
+ */
65
+ export declare function presignUrl(input: {
66
+ readonly url: URL;
67
+ readonly credentials: Credentials;
68
+ readonly region: string;
69
+ readonly service: string;
70
+ readonly at: Date;
71
+ readonly expiresSeconds: number;
72
+ }): Promise<string>;
@@ -0,0 +1,96 @@
1
+ /**
2
+ * @module AWS Signature Version 4
3
+ *
4
+ * AWS Signature Version 4 request signing and URL presigning, with Web Crypto only, so it runs unchanged on Bun, Node
5
+ * and in a Worker. S3 and every store that speaks its protocol (R2, MinIO, GCS interoperability, B2, Wasabi) sign this
6
+ * way. Pure apart from Web Crypto: the time is passed in, nothing is sent. It is tested against AWS's published signing
7
+ * vectors; change it only with the S3 integration tests passing against a real server.
8
+ *
9
+ * @see docs/report-contract.md#backends
10
+ */
11
+ const encoder = new TextEncoder();
12
+ function hex(bytes) {
13
+ return Array.from(bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes), byte => byte.toString(16).padStart(2, '0')).join('');
14
+ }
15
+ /** The SHA-256 digest of a string (UTF-8) or bytes, as 64 lowercase hex digits. */
16
+ export async function sha256Hex(data) {
17
+ const bytes = typeof data === 'string' ? encoder.encode(data) : data;
18
+ return hex(await crypto.subtle.digest('SHA-256', bytes));
19
+ }
20
+ async function hmac(key, data) {
21
+ const raw = typeof key === 'string' ? encoder.encode(key) : key;
22
+ const imported = await crypto.subtle.importKey('raw', raw, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
23
+ return crypto.subtle.sign('HMAC', imported, encoder.encode(data));
24
+ }
25
+ /**
26
+ * RFC 3986 percent-encoding of the UTF-8 bytes of `value`, which is what SigV4 asks for: letters, digits and `-_.~`
27
+ * stay, everything else (including `/`, unless `keepSlash`) becomes uppercase `%XX`.
28
+ */
29
+ export function uriEncode(value, keepSlash = false) {
30
+ let out = '';
31
+ for (const byte of encoder.encode(value)) {
32
+ const char = String.fromCharCode(byte);
33
+ if ((byte >= 0x41 && byte <= 0x5a) || (byte >= 0x61 && byte <= 0x7a) || (byte >= 0x30 && byte <= 0x39) || char === '-' || char === '_' || char === '.' || char === '~' || (keepSlash && char === '/'))
34
+ out += char;
35
+ else
36
+ out += `%${byte.toString(16).toUpperCase().padStart(2, '0')}`;
37
+ }
38
+ return out;
39
+ }
40
+ /** Formats a time for signing: `date` is `YYYYMMDD` and `datetime` is `YYYYMMDDTHHMMSSZ`, both UTC. */
41
+ export function amzDate(at) {
42
+ const iso = at.toISOString().replace(/[:-]|\.\d{3}/g, '');
43
+ return { date: iso.slice(0, 8), datetime: iso };
44
+ }
45
+ function canonicalQuery(params) {
46
+ return [...params]
47
+ .map(([key, value]) => [uriEncode(key), uriEncode(value)])
48
+ .sort(([a, av], [b, bv]) => (a < b ? -1 : a > b ? 1 : av < bv ? -1 : av > bv ? 1 : 0))
49
+ .map(([key, value]) => `${key}=${value}`)
50
+ .join('&');
51
+ }
52
+ async function signingKey(secret, date, region, service) {
53
+ return hmac(await hmac(await hmac(await hmac(`AWS4${secret}`, date), region), service), 'aws4_request');
54
+ }
55
+ /**
56
+ * Signs a request and returns the headers to send: those given, plus `host` (when missing) and `authorization`. The
57
+ * caller must send exactly these headers (and any it did not sign must not matter to the service). Does not send
58
+ * anything and does not read the body; `payloadHash` is trusted.
59
+ */
60
+ export async function signRequest(input) {
61
+ const { date, datetime } = amzDate(input.at);
62
+ const headers = { ...input.headers };
63
+ headers.host ??= input.url.host;
64
+ const names = Object.keys(headers).map(name => name.toLowerCase()).sort();
65
+ const canonicalHeaders = names.map(name => `${name}:${(headers[name] ?? '').trim().replace(/\s+/g, ' ')}\n`).join('');
66
+ const path = input.url.pathname.split('/').map(segment => uriEncode(decodeURIComponent(segment))).join('/') || '/';
67
+ const canonicalPath = input.doubleEncodePath ? path.split('/').map(segment => uriEncode(segment)).join('/') : path;
68
+ const signedHeaders = names.join(';');
69
+ const canonical = [input.method, canonicalPath, canonicalQuery(input.url.searchParams), canonicalHeaders, signedHeaders, input.payloadHash].join('\n');
70
+ const scope = `${date}/${input.region}/${input.service}/aws4_request`;
71
+ const toSign = ['AWS4-HMAC-SHA256', datetime, scope, await sha256Hex(canonical)].join('\n');
72
+ const signature = hex(await hmac(await signingKey(input.credentials.secretAccessKey, date, input.region, input.service), toSign));
73
+ headers.authorization = `AWS4-HMAC-SHA256 Credential=${input.credentials.accessKeyId}/${scope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
74
+ return headers;
75
+ }
76
+ /**
77
+ * A query-signed URL anyone can GET until it expires, without credentials: for handing a file to a browser. Valid for
78
+ * `expiresSeconds` from `at`; signs only the `host` header and an unsigned payload, so it works for `GET` only.
79
+ */
80
+ export async function presignUrl(input) {
81
+ const { date, datetime } = amzDate(input.at);
82
+ const scope = `${date}/${input.region}/${input.service}/aws4_request`;
83
+ const url = new URL(input.url);
84
+ url.searchParams.set('X-Amz-Algorithm', 'AWS4-HMAC-SHA256');
85
+ url.searchParams.set('X-Amz-Credential', `${input.credentials.accessKeyId}/${scope}`);
86
+ url.searchParams.set('X-Amz-Date', datetime);
87
+ url.searchParams.set('X-Amz-Expires', String(input.expiresSeconds));
88
+ url.searchParams.set('X-Amz-SignedHeaders', 'host');
89
+ if (input.credentials.sessionToken)
90
+ url.searchParams.set('X-Amz-Security-Token', input.credentials.sessionToken);
91
+ const path = url.pathname.split('/').map(segment => uriEncode(decodeURIComponent(segment))).join('/') || '/';
92
+ const canonical = ['GET', path, canonicalQuery(url.searchParams), `host:${url.host}\n`, 'host', 'UNSIGNED-PAYLOAD'].join('\n');
93
+ const toSign = ['AWS4-HMAC-SHA256', datetime, scope, await sha256Hex(canonical)].join('\n');
94
+ url.searchParams.set('X-Amz-Signature', hex(await hmac(await signingKey(input.credentials.secretAccessKey, date, input.region, input.service), toSign)));
95
+ return url.toString();
96
+ }
@@ -1,16 +1,39 @@
1
1
  import type { ObjectStore } from './object-store.js';
2
+ /**
3
+ * Copies `files` from `local` into gzip chunks on `remote`, then writes the index last, so a reader that finds the
4
+ * index can read every chunk it names.
5
+ *
6
+ * Each file is read from `local` at `localPrefix + file` and stored under the name `file` in the index. Chunks are
7
+ * packed up to about 1 MiB of encoded text (a larger single file gets a chunk of its own) and uploaded four at a time
8
+ * to `remotePrefix`. Throws `Missing report snapshot: <key>` when a listed file is absent in `local`, before anything
9
+ * is written to `remote`.
10
+ */
2
11
  export declare function publishSnapshotBundle(args: {
12
+ /** Where the recorded files are now. */
3
13
  readonly local: ObjectStore;
14
+ /** Where the bundle is written. */
4
15
  readonly remote: ObjectStore;
16
+ /** The key prefix of the files in `local`. */
5
17
  readonly localPrefix: string;
18
+ /** The key prefix to write the bundle under; end it with `/`. */
6
19
  readonly remotePrefix: string;
20
+ /** The file names, relative to `localPrefix`. */
7
21
  readonly files: readonly string[];
8
22
  }): Promise<void>;
23
+ /**
24
+ * Reads one file from a bundle written by `publishSnapshotBundle`, decompressing the chunk that holds it. Returns `null`
25
+ * when the bundle has no index, the file is not in the index, or its chunk is missing. Throws `Invalid snapshot chunk`
26
+ * for a chunk name that is not `snapshots-<n>.json.gz`, and a `SyntaxError` for a corrupt index or chunk.
27
+ */
9
28
  export declare function readBundledSnapshot(store: ObjectStore, prefix: string, file: string): Promise<{
10
29
  readonly bytes: Uint8Array;
11
30
  readonly contentType: string;
12
31
  } | null>;
13
- /** A streamed test reads each compressed chunk once even when it contains many frames. */
32
+ /**
33
+ * Streams the requested `files` of a bundle as `{ file, bytes, contentType }`, in the order chunks finish reading (not
34
+ * the order of `files`). A streamed test reads each compressed chunk once even when it contains many frames. Files not
35
+ * in the index and chunks that are missing are skipped silently; a bundle with no index yields nothing.
36
+ */
14
37
  export declare function readBundledSnapshots(store: ObjectStore, prefix: string, files: ReadonlySet<string>): AsyncGenerator<{
15
38
  bytes: Uint8Array;
16
39
  contentType: string;
@@ -1,4 +1,11 @@
1
- /** Indexed gzip chunks keep snapshot uploads small in count and reads bounded. */
1
+ /**
2
+ * @module Snapshot bundles
3
+ *
4
+ * Packs the frame files of one recorded test into a few gzip chunks of roughly 1 MiB (JSON of base64 snapshots), with a
5
+ * small index from file to chunk, so a test with hundreds of frames uploads as a handful of objects and a reader
6
+ * fetches only the chunks it needs. Keys are `<prefix>snapshots.json` (the index, written last) and
7
+ * `<prefix>snapshots-<n>.json.gz`. Reads reject a chunk name that does not look like `snapshots-<n>.json.gz`.
8
+ */
2
9
  import { readBytes, readObjectBytes, readUtf8, writeUtf8, } from './object-io.js';
3
10
  import { eachLimited, inCompletionOrder } from './completion-order.js';
4
11
  const CHUNK_BYTES = 1024 * 1024;
@@ -12,6 +19,15 @@ async function gzip(text) {
12
19
  const input = new Blob([text]).stream();
13
20
  return readBytes(input.pipeThrough(new CompressionStream('gzip')));
14
21
  }
22
+ /**
23
+ * Copies `files` from `local` into gzip chunks on `remote`, then writes the index last, so a reader that finds the
24
+ * index can read every chunk it names.
25
+ *
26
+ * Each file is read from `local` at `localPrefix + file` and stored under the name `file` in the index. Chunks are
27
+ * packed up to about 1 MiB of encoded text (a larger single file gets a chunk of its own) and uploaded four at a time
28
+ * to `remotePrefix`. Throws `Missing report snapshot: <key>` when a listed file is absent in `local`, before anything
29
+ * is written to `remote`.
30
+ */
15
31
  export async function publishSnapshotBundle(args) {
16
32
  const index = {};
17
33
  const chunks = [];
@@ -45,6 +61,11 @@ export async function publishSnapshotBundle(args) {
45
61
  });
46
62
  await writeUtf8(args.remote, `${args.remotePrefix}snapshots.json`, JSON.stringify(index), 'application/json');
47
63
  }
64
+ /**
65
+ * Reads one file from a bundle written by `publishSnapshotBundle`, decompressing the chunk that holds it. Returns `null`
66
+ * when the bundle has no index, the file is not in the index, or its chunk is missing. Throws `Invalid snapshot chunk`
67
+ * for a chunk name that is not `snapshots-<n>.json.gz`, and a `SyntaxError` for a corrupt index or chunk.
68
+ */
48
69
  export async function readBundledSnapshot(store, prefix, file) {
49
70
  const text = await readUtf8(store, `${prefix}snapshots.json`);
50
71
  if (text === null)
@@ -74,7 +95,11 @@ function snapshotBody(snapshot) {
74
95
  const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
75
96
  return { bytes, contentType: snapshot.contentType };
76
97
  }
77
- /** A streamed test reads each compressed chunk once even when it contains many frames. */
98
+ /**
99
+ * Streams the requested `files` of a bundle as `{ file, bytes, contentType }`, in the order chunks finish reading (not
100
+ * the order of `files`). A streamed test reads each compressed chunk once even when it contains many frames. Files not
101
+ * in the index and chunks that are missing are skipped silently; a bundle with no index yields nothing.
102
+ */
78
103
  export async function* readBundledSnapshots(store, prefix, files) {
79
104
  const text = await readUtf8(store, `${prefix}snapshots.json`);
80
105
  if (text === null)