@storyshelf/core 0.6.2 → 0.7.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 (628) hide show
  1. package/README.md +49 -32
  2. package/dist/adapter/capture-queue.d.mts +44 -2
  3. package/dist/adapter/capture-runner.d.mts +127 -10
  4. package/dist/adapter/capture-runner.mjs +5 -12
  5. package/dist/adapter/database.d.mts +27 -8
  6. package/dist/adapter/database.mjs.map +1 -1
  7. package/dist/adapter/design.d.mts +85 -0
  8. package/dist/adapter/design.mjs +28 -0
  9. package/dist/adapter/design.mjs.map +1 -0
  10. package/dist/adapter/metadata.d.mts +3 -10
  11. package/dist/adapter/notifier.d.mts +6 -3
  12. package/dist/adapter/notifier.mjs +5 -1
  13. package/dist/adapter/oauth.d.mts +52 -0
  14. package/dist/adapter/setup.d.mts +4 -5
  15. package/dist/adapter/setup.mjs +2 -1
  16. package/dist/adapter/setup.mjs.map +1 -1
  17. package/dist/adapter/webhook-events.mjs +1 -1
  18. package/dist/adapters/capture-target-offers.d.mts +28 -0
  19. package/dist/adapters/capture-target-offers.mjs +58 -0
  20. package/dist/adapters/capture-target-offers.mjs.map +1 -0
  21. package/dist/adapters/capture-target.d.mts +71 -0
  22. package/dist/adapters/capture-target.mjs +76 -0
  23. package/dist/adapters/capture-target.mjs.map +1 -0
  24. package/dist/adapters/notifier/build-comment.d.mts +24 -0
  25. package/dist/adapters/notifier/build-comment.mjs +24 -0
  26. package/dist/adapters/notifier/build-comment.mjs.map +1 -0
  27. package/dist/adapters/notifier/build-state.d.mts +99 -0
  28. package/dist/adapters/notifier/build-state.mjs +218 -0
  29. package/dist/adapters/notifier/build-state.mjs.map +1 -0
  30. package/dist/adapters/notifier/events.d.mts +4 -2
  31. package/dist/adapters/notifier/events.mjs +31 -18
  32. package/dist/adapters/notifier/events.mjs.map +1 -1
  33. package/dist/adapters/notifier/provider.d.mts +53 -7
  34. package/dist/adapters/notifier/provider.mjs +12 -0
  35. package/dist/adapters/notifier/provider.mjs.map +1 -0
  36. package/dist/adapters/notifier/state.d.mts +24 -0
  37. package/dist/adapters/notifier/state.mjs +66 -0
  38. package/dist/adapters/notifier/state.mjs.map +1 -0
  39. package/dist/adapters/notifier/types.d.mts +51 -2
  40. package/dist/adapters/remote-browser.d.mts +87 -0
  41. package/dist/adapters/remote-browser.mjs +103 -0
  42. package/dist/adapters/remote-browser.mjs.map +1 -0
  43. package/dist/adapters/render-runs.d.mts +25 -0
  44. package/dist/adapters/render-runs.mjs +41 -0
  45. package/dist/adapters/render-runs.mjs.map +1 -0
  46. package/dist/adapters/setup-runner.mjs +2 -4
  47. package/dist/adapters/setup-runner.mjs.map +1 -1
  48. package/dist/adapters/sources.d.mts +12 -0
  49. package/dist/adapters/sources.mjs +20 -0
  50. package/dist/adapters/sources.mjs.map +1 -0
  51. package/dist/adapters/validate.mjs +25 -18
  52. package/dist/adapters/validate.mjs.map +1 -1
  53. package/dist/ai.d.mts +105 -0
  54. package/dist/ai.mjs +19 -0
  55. package/dist/ai.mjs.map +1 -0
  56. package/dist/capture/a11y/axe-source.d.mts +18 -0
  57. package/dist/capture/a11y/axe-source.mjs +37 -0
  58. package/dist/capture/a11y/axe-source.mjs.map +1 -0
  59. package/dist/capture/a11y/in-page.d.mts +27 -0
  60. package/dist/capture/a11y/in-page.mjs +60 -0
  61. package/dist/capture/a11y/in-page.mjs.map +1 -0
  62. package/dist/capture/a11y/map.d.mts +24 -0
  63. package/dist/capture/a11y/map.mjs +48 -0
  64. package/dist/capture/a11y/map.mjs.map +1 -0
  65. package/dist/capture/a11y/options.d.mts +23 -0
  66. package/dist/capture/a11y/options.mjs +75 -0
  67. package/dist/capture/a11y/options.mjs.map +1 -0
  68. package/dist/capture/adapter.d.mts +41 -3
  69. package/dist/capture/adapter.mjs.map +1 -1
  70. package/dist/capture/affected.d.mts +2 -0
  71. package/dist/capture/affected.mjs +13 -4
  72. package/dist/capture/affected.mjs.map +1 -1
  73. package/dist/capture/attempt-logger.mjs +84 -0
  74. package/dist/capture/attempt-logger.mjs.map +1 -0
  75. package/dist/capture/browsers.d.mts +35 -0
  76. package/dist/capture/browsers.mjs +67 -0
  77. package/dist/capture/browsers.mjs.map +1 -0
  78. package/dist/capture/build-state/deliver.d.mts +32 -0
  79. package/dist/capture/build-state/deliver.mjs +118 -0
  80. package/dist/capture/build-state/deliver.mjs.map +1 -0
  81. package/dist/capture/build-state/index.d.mts +8 -0
  82. package/dist/capture/build-state/notify-job.d.mts +56 -0
  83. package/dist/capture/build-state/notify-job.mjs +133 -0
  84. package/dist/capture/build-state/notify-job.mjs.map +1 -0
  85. package/dist/capture/build-state/publisher.d.mts +23 -0
  86. package/dist/capture/build-state/publisher.mjs +107 -0
  87. package/dist/capture/build-state/publisher.mjs.map +1 -0
  88. package/dist/capture/build-state/resume.d.mts +9 -0
  89. package/dist/capture/build-state/resume.mjs +47 -0
  90. package/dist/capture/build-state/resume.mjs.map +1 -0
  91. package/dist/capture/build-state/types.d.mts +49 -0
  92. package/dist/capture/catalog/catalogue.d.mts +22 -0
  93. package/dist/capture/catalog/catalogue.mjs +53 -0
  94. package/dist/capture/catalog/catalogue.mjs.map +1 -0
  95. package/dist/capture/catalog/components.d.mts +23 -0
  96. package/dist/capture/catalog/components.mjs +55 -0
  97. package/dist/capture/catalog/components.mjs.map +1 -0
  98. package/dist/capture/catalog/history.d.mts +30 -0
  99. package/dist/capture/catalog/history.mjs +51 -0
  100. package/dist/capture/catalog/history.mjs.map +1 -0
  101. package/dist/capture/catalog/hook.d.mts +14 -0
  102. package/dist/capture/catalog/hook.mjs +19 -0
  103. package/dist/capture/catalog/hook.mjs.map +1 -0
  104. package/dist/capture/catalog/index.d.mts +11 -0
  105. package/dist/capture/catalog/load.d.mts +26 -0
  106. package/dist/capture/catalog/load.mjs +49 -0
  107. package/dist/capture/catalog/load.mjs.map +1 -0
  108. package/dist/capture/catalog/parse.d.mts +15 -0
  109. package/dist/capture/catalog/parse.mjs +56 -0
  110. package/dist/capture/catalog/parse.mjs.map +1 -0
  111. package/dist/capture/catalog/read.d.mts +12 -0
  112. package/dist/capture/catalog/read.mjs +33 -0
  113. package/dist/capture/catalog/read.mjs.map +1 -0
  114. package/dist/capture/catalog/types.d.mts +33 -0
  115. package/dist/capture/consumer/capture-handler.d.mts +58 -0
  116. package/dist/capture/consumer/capture-handler.mjs +118 -0
  117. package/dist/capture/consumer/capture-handler.mjs.map +1 -0
  118. package/dist/capture/consumer/idle.d.mts +17 -0
  119. package/dist/capture/consumer/idle.mjs +25 -0
  120. package/dist/capture/consumer/idle.mjs.map +1 -0
  121. package/dist/capture/consumer/index.d.mts +13 -0
  122. package/dist/capture/consumer/index.mjs +139 -0
  123. package/dist/capture/consumer/index.mjs.map +1 -0
  124. package/dist/capture/consumer/loop.mjs +110 -0
  125. package/dist/capture/consumer/loop.mjs.map +1 -0
  126. package/dist/capture/consumer/metrics.d.mts +41 -0
  127. package/dist/capture/consumer/metrics.mjs +38 -0
  128. package/dist/capture/consumer/metrics.mjs.map +1 -0
  129. package/dist/capture/consumer/retry.d.mts +33 -0
  130. package/dist/capture/consumer/retry.mjs +34 -0
  131. package/dist/capture/consumer/retry.mjs.map +1 -0
  132. package/dist/capture/consumer/run-job.mjs +195 -0
  133. package/dist/capture/consumer/run-job.mjs.map +1 -0
  134. package/dist/capture/consumer/startup.mjs +38 -0
  135. package/dist/capture/consumer/startup.mjs.map +1 -0
  136. package/dist/capture/consumer/types.d.mts +97 -0
  137. package/dist/capture/crash-failure.d.mts +25 -0
  138. package/dist/capture/crash-failure.mjs +70 -0
  139. package/dist/capture/crash-failure.mjs.map +1 -0
  140. package/dist/capture/db-queue/cadence.d.mts +32 -0
  141. package/dist/capture/db-queue/cadence.mjs +38 -0
  142. package/dist/capture/db-queue/cadence.mjs.map +1 -0
  143. package/dist/capture/db-queue/claim.d.mts +9 -0
  144. package/dist/capture/db-queue/claim.mjs +132 -0
  145. package/dist/capture/db-queue/claim.mjs.map +1 -0
  146. package/dist/capture/db-queue/clock.mjs +12 -0
  147. package/dist/capture/db-queue/clock.mjs.map +1 -0
  148. package/dist/capture/db-queue/codec.mjs +76 -0
  149. package/dist/capture/db-queue/codec.mjs.map +1 -0
  150. package/dist/capture/db-queue/index.d.mts +20 -0
  151. package/dist/capture/db-queue/index.mjs +89 -0
  152. package/dist/capture/db-queue/index.mjs.map +1 -0
  153. package/dist/capture/db-queue/keeper.d.mts +36 -0
  154. package/dist/capture/db-queue/keeper.mjs +92 -0
  155. package/dist/capture/db-queue/keeper.mjs.map +1 -0
  156. package/dist/capture/db-queue/lease.mjs +46 -0
  157. package/dist/capture/db-queue/lease.mjs.map +1 -0
  158. package/dist/capture/db-queue/lifecycle.mjs +36 -0
  159. package/dist/capture/db-queue/lifecycle.mjs.map +1 -0
  160. package/dist/capture/db-queue/operations.mjs +94 -0
  161. package/dist/capture/db-queue/operations.mjs.map +1 -0
  162. package/dist/capture/db-queue/poll-loop.d.mts +30 -0
  163. package/dist/capture/db-queue/poll-loop.mjs +89 -0
  164. package/dist/capture/db-queue/poll-loop.mjs.map +1 -0
  165. package/dist/capture/db-queue/poll.d.mts +10 -0
  166. package/dist/capture/db-queue/poll.mjs +69 -0
  167. package/dist/capture/db-queue/poll.mjs.map +1 -0
  168. package/dist/capture/db-queue/timers.d.mts +12 -0
  169. package/dist/capture/db-queue/timers.mjs +37 -0
  170. package/dist/capture/db-queue/timers.mjs.map +1 -0
  171. package/dist/capture/db-queue/types.d.mts +83 -0
  172. package/dist/capture/db-queue/types.mjs +12 -0
  173. package/dist/capture/db-queue/types.mjs.map +1 -0
  174. package/dist/capture/db-queue/wake.mjs +48 -0
  175. package/dist/capture/db-queue/wake.mjs.map +1 -0
  176. package/dist/capture/design.mjs +39 -0
  177. package/dist/capture/design.mjs.map +1 -0
  178. package/dist/capture/diff-record.mjs +23 -3
  179. package/dist/capture/diff-record.mjs.map +1 -1
  180. package/dist/capture/dispatch.d.mts +12 -7
  181. package/dist/capture/dispatch.mjs +86 -107
  182. package/dist/capture/dispatch.mjs.map +1 -1
  183. package/dist/capture/enrich.mjs +60 -0
  184. package/dist/capture/enrich.mjs.map +1 -0
  185. package/dist/capture/failure-sets.mjs +22 -0
  186. package/dist/capture/failure-sets.mjs.map +1 -0
  187. package/dist/capture/fingerprint.d.mts +42 -0
  188. package/dist/capture/fingerprint.mjs +83 -0
  189. package/dist/capture/fingerprint.mjs.map +1 -0
  190. package/dist/capture/forked/channel.d.mts +25 -0
  191. package/dist/capture/forked/child.d.mts +34 -0
  192. package/dist/capture/forked/child.mjs +202 -0
  193. package/dist/capture/forked/child.mjs.map +1 -0
  194. package/dist/capture/forked/crash-tracker.mjs +23 -0
  195. package/dist/capture/forked/crash-tracker.mjs.map +1 -0
  196. package/dist/capture/forked/decision.d.mts +26 -0
  197. package/dist/capture/forked/decision.mjs +27 -0
  198. package/dist/capture/forked/decision.mjs.map +1 -0
  199. package/dist/capture/forked/exit.mjs +12 -0
  200. package/dist/capture/forked/exit.mjs.map +1 -0
  201. package/dist/capture/forked/fork.d.mts +31 -0
  202. package/dist/capture/forked/fork.mjs +93 -0
  203. package/dist/capture/forked/fork.mjs.map +1 -0
  204. package/dist/capture/forked/forked-capture.d.mts +40 -0
  205. package/dist/capture/forked/forked-capture.mjs +240 -0
  206. package/dist/capture/forked/forked-capture.mjs.map +1 -0
  207. package/dist/capture/forked/lease-book.mjs +46 -0
  208. package/dist/capture/forked/lease-book.mjs.map +1 -0
  209. package/dist/capture/forked/protocol.d.mts +38 -0
  210. package/dist/capture/forked/protocol.mjs +25 -0
  211. package/dist/capture/forked/protocol.mjs.map +1 -0
  212. package/dist/capture/forked/runs.mjs +75 -0
  213. package/dist/capture/forked/runs.mjs.map +1 -0
  214. package/dist/capture/isolate.mjs +141 -0
  215. package/dist/capture/isolate.mjs.map +1 -0
  216. package/dist/capture/job-lease.d.mts +28 -0
  217. package/dist/capture/job-lease.mjs +57 -0
  218. package/dist/capture/job-lease.mjs.map +1 -0
  219. package/dist/capture/matrix.mjs +298 -0
  220. package/dist/capture/matrix.mjs.map +1 -0
  221. package/dist/capture/modes.d.mts +25 -0
  222. package/dist/capture/modes.mjs +107 -0
  223. package/dist/capture/modes.mjs.map +1 -0
  224. package/dist/capture/orchestrator.d.mts +37 -5
  225. package/dist/capture/orchestrator.mjs +118 -107
  226. package/dist/capture/orchestrator.mjs.map +1 -1
  227. package/dist/capture/pipeline.d.mts +9 -2
  228. package/dist/capture/pipeline.mjs +250 -45
  229. package/dist/capture/pipeline.mjs.map +1 -1
  230. package/dist/capture/progress.d.mts +11 -0
  231. package/dist/capture/progress.mjs +28 -0
  232. package/dist/capture/progress.mjs.map +1 -0
  233. package/dist/capture/publish/accept.d.mts +39 -0
  234. package/dist/capture/publish/accept.mjs +28 -0
  235. package/dist/capture/publish/accept.mjs.map +1 -0
  236. package/dist/capture/publish/complete.d.mts +46 -0
  237. package/dist/capture/publish/complete.mjs +64 -0
  238. package/dist/capture/publish/complete.mjs.map +1 -0
  239. package/dist/capture/publish/extract.d.mts +20 -0
  240. package/dist/capture/publish/extract.mjs +119 -0
  241. package/dist/capture/publish/extract.mjs.map +1 -0
  242. package/dist/capture/publish/handler.d.mts +28 -0
  243. package/dist/capture/publish/handler.mjs +62 -0
  244. package/dist/capture/publish/handler.mjs.map +1 -0
  245. package/dist/capture/publish/index.d.mts +10 -0
  246. package/dist/capture/publish/limits.d.mts +37 -0
  247. package/dist/capture/publish/limits.mjs +39 -0
  248. package/dist/capture/publish/limits.mjs.map +1 -0
  249. package/dist/capture/publish/published.d.mts +15 -0
  250. package/dist/capture/publish/published.mjs +18 -0
  251. package/dist/capture/publish/published.mjs.map +1 -0
  252. package/dist/capture/publish/run.d.mts +33 -0
  253. package/dist/capture/publish/run.mjs +30 -0
  254. package/dist/capture/publish/run.mjs.map +1 -0
  255. package/dist/capture/rediff.d.mts +1 -1
  256. package/dist/capture/rediff.mjs +4 -3
  257. package/dist/capture/rediff.mjs.map +1 -1
  258. package/dist/capture/render.mjs +53 -0
  259. package/dist/capture/render.mjs.map +1 -0
  260. package/dist/capture/scratch.d.mts +44 -0
  261. package/dist/capture/scratch.mjs +138 -0
  262. package/dist/capture/scratch.mjs.map +1 -0
  263. package/dist/capture/shard-plan.mjs +128 -0
  264. package/dist/capture/shard-plan.mjs.map +1 -0
  265. package/dist/capture/shards/context.mjs +51 -0
  266. package/dist/capture/shards/context.mjs.map +1 -0
  267. package/dist/capture/shards/execute.mjs +174 -0
  268. package/dist/capture/shards/execute.mjs.map +1 -0
  269. package/dist/capture/shards/fan-out.mjs +126 -0
  270. package/dist/capture/shards/fan-out.mjs.map +1 -0
  271. package/dist/capture/shards/history.mjs +35 -0
  272. package/dist/capture/shards/history.mjs.map +1 -0
  273. package/dist/capture/shards/reduce.mjs +97 -0
  274. package/dist/capture/shards/reduce.mjs.map +1 -0
  275. package/dist/capture/sizing.mjs +1 -11
  276. package/dist/capture/sizing.mjs.map +1 -1
  277. package/dist/capture/skip-checks.mjs +2 -65
  278. package/dist/capture/skip-checks.mjs.map +1 -1
  279. package/dist/capture/statics.d.mts +7 -8
  280. package/dist/capture/statics.mjs +25 -71
  281. package/dist/capture/statics.mjs.map +1 -1
  282. package/dist/capture/storybook-urls.d.mts +40 -0
  283. package/dist/capture/storybook-urls.mjs +41 -0
  284. package/dist/capture/storybook-urls.mjs.map +1 -0
  285. package/dist/capture/storybook.d.mts +12 -4
  286. package/dist/capture/storybook.mjs +56 -16
  287. package/dist/capture/storybook.mjs.map +1 -1
  288. package/dist/capture/target.mjs +21 -0
  289. package/dist/capture/target.mjs.map +1 -0
  290. package/dist/capture/targets.d.mts +25 -0
  291. package/dist/capture/targets.mjs +32 -0
  292. package/dist/capture/targets.mjs.map +1 -0
  293. package/dist/capture/tests.d.mts +37 -0
  294. package/dist/capture/tests.mjs +126 -0
  295. package/dist/capture/tests.mjs.map +1 -0
  296. package/dist/capture/token-chunks.mjs +74 -0
  297. package/dist/capture/token-chunks.mjs.map +1 -0
  298. package/dist/capture.d.mts +60 -7
  299. package/dist/capture.mjs +48 -3
  300. package/dist/config.d.mts +121 -12
  301. package/dist/config.mjs +9 -6
  302. package/dist/config.mjs.map +1 -1
  303. package/dist/diff.d.mts +9 -3
  304. package/dist/diff.mjs +16 -3
  305. package/dist/diff.mjs.map +1 -0
  306. package/dist/index.d.mts +1 -1
  307. package/dist/insights/budget.d.mts +20 -0
  308. package/dist/insights/budget.mjs +33 -0
  309. package/dist/insights/budget.mjs.map +1 -0
  310. package/dist/insights/envelope.d.mts +59 -0
  311. package/dist/insights/envelope.mjs +60 -0
  312. package/dist/insights/envelope.mjs.map +1 -0
  313. package/dist/insights/evidence-health.d.mts +32 -0
  314. package/dist/insights/evidence-health.mjs +28 -0
  315. package/dist/insights/evidence-health.mjs.map +1 -0
  316. package/dist/insights/evidence.d.mts +55 -0
  317. package/dist/insights/evidence.mjs +64 -0
  318. package/dist/insights/evidence.mjs.map +1 -0
  319. package/dist/insights/hash.d.mts +18 -0
  320. package/dist/insights/hash.mjs +21 -0
  321. package/dist/insights/hash.mjs.map +1 -0
  322. package/dist/insights/limits.d.mts +24 -0
  323. package/dist/insights/limits.mjs +24 -0
  324. package/dist/insights/limits.mjs.map +1 -0
  325. package/dist/insights/prompts/v1.d.mts +13 -0
  326. package/dist/insights/prompts/v1.mjs +34 -0
  327. package/dist/insights/prompts/v1.mjs.map +1 -0
  328. package/dist/insights/redact.d.mts +9 -0
  329. package/dist/insights/redact.mjs +24 -0
  330. package/dist/insights/redact.mjs.map +1 -0
  331. package/dist/insights/text.mjs +44 -0
  332. package/dist/insights/text.mjs.map +1 -0
  333. package/dist/insights.d.mts +11 -0
  334. package/dist/insights.mjs +12 -0
  335. package/dist/logger.mjs +17 -1
  336. package/dist/logger.mjs.map +1 -1
  337. package/dist/models/ai-budget-alert.d.mts +23 -0
  338. package/dist/models/ai-budget-alert.mjs +38 -0
  339. package/dist/models/ai-budget-alert.mjs.map +1 -0
  340. package/dist/models/ai-usage.d.mts +40 -0
  341. package/dist/models/ai-usage.mjs +61 -0
  342. package/dist/models/ai-usage.mjs.map +1 -0
  343. package/dist/models/baseline-archive.d.mts +84 -0
  344. package/dist/models/baseline-archive.mjs +106 -0
  345. package/dist/models/baseline-archive.mjs.map +1 -0
  346. package/dist/models/baseline-export.d.mts +19 -0
  347. package/dist/models/baseline-export.mjs +136 -0
  348. package/dist/models/baseline-export.mjs.map +1 -0
  349. package/dist/models/baseline-import.mjs +118 -0
  350. package/dist/models/baseline-import.mjs.map +1 -0
  351. package/dist/models/baseline-transfer.d.mts +49 -0
  352. package/dist/models/baseline-transfer.mjs +121 -0
  353. package/dist/models/baseline-transfer.mjs.map +1 -0
  354. package/dist/models/baseline.d.mts +32 -5
  355. package/dist/models/baseline.mjs +65 -20
  356. package/dist/models/baseline.mjs.map +1 -1
  357. package/dist/models/build-component.d.mts +28 -0
  358. package/dist/models/build-component.mjs +55 -0
  359. package/dist/models/build-component.mjs.map +1 -0
  360. package/dist/models/build-review-status.d.mts +29 -0
  361. package/dist/models/build-review-status.mjs +37 -0
  362. package/dist/models/build-review-status.mjs.map +1 -0
  363. package/dist/models/build-review.d.mts +28 -0
  364. package/dist/models/build-review.mjs +54 -0
  365. package/dist/models/build-review.mjs.map +1 -0
  366. package/dist/models/build.d.mts +25 -3
  367. package/dist/models/build.mjs +50 -11
  368. package/dist/models/build.mjs.map +1 -1
  369. package/dist/models/capture-attempt.d.mts +7 -1
  370. package/dist/models/capture-attempt.mjs +29 -4
  371. package/dist/models/capture-attempt.mjs.map +1 -1
  372. package/dist/models/capture-job.d.mts +106 -0
  373. package/dist/models/capture-job.mjs +288 -0
  374. package/dist/models/capture-job.mjs.map +1 -0
  375. package/dist/models/capture-log.d.mts +1 -1
  376. package/dist/models/capture-log.mjs +4 -3
  377. package/dist/models/capture-log.mjs.map +1 -1
  378. package/dist/models/capture-shard.d.mts +62 -0
  379. package/dist/models/capture-shard.mjs +190 -0
  380. package/dist/models/capture-shard.mjs.map +1 -0
  381. package/dist/models/changeset.d.mts +48 -0
  382. package/dist/models/changeset.mjs +81 -0
  383. package/dist/models/changeset.mjs.map +1 -0
  384. package/dist/models/columns.mjs +19 -0
  385. package/dist/models/columns.mjs.map +1 -0
  386. package/dist/models/comment.d.mts +4 -1
  387. package/dist/models/comment.mjs +7 -4
  388. package/dist/models/comment.mjs.map +1 -1
  389. package/dist/models/component.d.mts +43 -0
  390. package/dist/models/component.mjs +60 -0
  391. package/dist/models/component.mjs.map +1 -0
  392. package/dist/models/connections.d.mts +41 -0
  393. package/dist/models/connections.mjs +112 -0
  394. package/dist/models/connections.mjs.map +1 -0
  395. package/dist/models/content-ref.d.mts +1 -1
  396. package/dist/models/content-ref.mjs +1 -1
  397. package/dist/models/content-ref.mjs.map +1 -1
  398. package/dist/models/credentials.mjs +12 -2
  399. package/dist/models/credentials.mjs.map +1 -1
  400. package/dist/models/design-credentials.d.mts +41 -0
  401. package/dist/models/design-credentials.mjs +86 -0
  402. package/dist/models/design-credentials.mjs.map +1 -0
  403. package/dist/models/design-health.d.mts +16 -0
  404. package/dist/models/design-health.mjs +25 -0
  405. package/dist/models/design-health.mjs.map +1 -0
  406. package/dist/models/design-nodes.d.mts +83 -0
  407. package/dist/models/design-nodes.mjs +222 -0
  408. package/dist/models/design-nodes.mjs.map +1 -0
  409. package/dist/models/design-renders.mjs +74 -0
  410. package/dist/models/design-renders.mjs.map +1 -0
  411. package/dist/models/design-resolve.d.mts +29 -0
  412. package/dist/models/design-resolve.mjs +41 -0
  413. package/dist/models/design-resolve.mjs.map +1 -0
  414. package/dist/models/design.d.mts +39 -0
  415. package/dist/models/design.mjs +90 -0
  416. package/dist/models/design.mjs.map +1 -0
  417. package/dist/models/inherited-ratio.d.mts +21 -0
  418. package/dist/models/inherited-ratio.mjs +22 -0
  419. package/dist/models/inherited-ratio.mjs.map +1 -0
  420. package/dist/models/insight.d.mts +70 -0
  421. package/dist/models/insight.mjs +162 -0
  422. package/dist/models/insight.mjs.map +1 -0
  423. package/dist/models/label.d.mts +4 -2
  424. package/dist/models/label.mjs +25 -10
  425. package/dist/models/label.mjs.map +1 -1
  426. package/dist/models/member.d.mts +1 -1
  427. package/dist/models/member.mjs +5 -4
  428. package/dist/models/member.mjs.map +1 -1
  429. package/dist/models/notification-channel.d.mts +37 -3
  430. package/dist/models/notification-channel.mjs +88 -9
  431. package/dist/models/notification-channel.mjs.map +1 -1
  432. package/dist/models/notification-subscription.d.mts +1 -2
  433. package/dist/models/notification-subscription.mjs +7 -9
  434. package/dist/models/notification-subscription.mjs.map +1 -1
  435. package/dist/models/project-group-mapping.d.mts +6 -1
  436. package/dist/models/project-group-mapping.mjs +14 -4
  437. package/dist/models/project-group-mapping.mjs.map +1 -1
  438. package/dist/models/project.d.mts +9 -1
  439. package/dist/models/project.mjs +9 -4
  440. package/dist/models/project.mjs.map +1 -1
  441. package/dist/models/public-origin.d.mts +63 -0
  442. package/dist/models/public-origin.mjs +114 -0
  443. package/dist/models/public-origin.mjs.map +1 -0
  444. package/dist/models/review-switch-off.d.mts +14 -0
  445. package/dist/models/review-switch-off.mjs +26 -0
  446. package/dist/models/review-switch-off.mjs.map +1 -0
  447. package/dist/models/site-settings.d.mts +23 -0
  448. package/dist/models/site-settings.mjs +48 -0
  449. package/dist/models/site-settings.mjs.map +1 -0
  450. package/dist/models/snapshot.d.mts +36 -1
  451. package/dist/models/snapshot.mjs +50 -3
  452. package/dist/models/snapshot.mjs.map +1 -1
  453. package/dist/models/token.d.mts +13 -1
  454. package/dist/models/token.mjs +23 -5
  455. package/dist/models/token.mjs.map +1 -1
  456. package/dist/models/usage.d.mts +76 -0
  457. package/dist/models/usage.mjs +149 -0
  458. package/dist/models/usage.mjs.map +1 -0
  459. package/dist/models/user.d.mts +1 -1
  460. package/dist/models/user.mjs +4 -3
  461. package/dist/models/user.mjs.map +1 -1
  462. package/dist/models/webhook.d.mts +15 -1
  463. package/dist/models/webhook.mjs +33 -4
  464. package/dist/models/webhook.mjs.map +1 -1
  465. package/dist/models.d.mts +26 -3
  466. package/dist/models.mjs +27 -5
  467. package/dist/options.d.mts +18 -0
  468. package/dist/options.mjs +90 -0
  469. package/dist/options.mjs.map +1 -0
  470. package/dist/paths.d.mts +19 -4
  471. package/dist/paths.mjs +51 -10
  472. package/dist/paths.mjs.map +1 -1
  473. package/dist/retention/ai.d.mts +18 -0
  474. package/dist/retention/ai.mjs +23 -0
  475. package/dist/retention/ai.mjs.map +1 -0
  476. package/dist/retention/pending.mjs +47 -0
  477. package/dist/retention/pending.mjs.map +1 -0
  478. package/dist/retention/screenshot-sweep.d.mts +51 -0
  479. package/dist/retention/screenshot-sweep.mjs +128 -0
  480. package/dist/retention/screenshot-sweep.mjs.map +1 -0
  481. package/dist/retention.d.mts +16 -2
  482. package/dist/retention.mjs +71 -14
  483. package/dist/retention.mjs.map +1 -1
  484. package/dist/schema/baseline.d.mts +6 -0
  485. package/dist/schema/build-component.d.mts +25 -0
  486. package/dist/schema/build-review.d.mts +17 -0
  487. package/dist/schema/build.d.mts +7 -0
  488. package/dist/schema/capture-job.d.mts +60 -0
  489. package/dist/schema/capture-shard.d.mts +35 -0
  490. package/dist/schema/comment.d.mts +2 -0
  491. package/dist/schema/connection.d.mts +24 -0
  492. package/dist/schema/design.d.mts +58 -0
  493. package/dist/schema/insight.d.mts +54 -0
  494. package/dist/schema/notification.d.mts +4 -0
  495. package/dist/schema/project.d.mts +21 -0
  496. package/dist/schema/site-setting.d.mts +21 -0
  497. package/dist/schema/snapshot.d.mts +12 -0
  498. package/dist/schema/usage.d.mts +30 -0
  499. package/dist/schema.d.mts +11 -3
  500. package/dist/serverless.d.mts +21 -0
  501. package/dist/serverless.mjs +37 -0
  502. package/dist/serverless.mjs.map +1 -0
  503. package/dist/test-helpers/create-project.d.mts +1 -1
  504. package/dist/test-helpers/deferred.d.mts +39 -0
  505. package/dist/test-helpers/deferred.mjs +61 -0
  506. package/dist/test-helpers/deferred.mjs.map +1 -0
  507. package/dist/test-helpers/design-contract.d.mts +21 -0
  508. package/dist/test-helpers/design-contract.mjs +40 -0
  509. package/dist/test-helpers/design-contract.mjs.map +1 -0
  510. package/dist/test-helpers/fake-database.mjs +14 -2
  511. package/dist/test-helpers/fake-database.mjs.map +1 -1
  512. package/dist/test-helpers/fake-design.d.mts +43 -0
  513. package/dist/test-helpers/fake-design.mjs +114 -0
  514. package/dist/test-helpers/fake-design.mjs.map +1 -0
  515. package/dist/test-helpers/fake-notifier.d.mts +21 -0
  516. package/dist/test-helpers/fake-notifier.mjs +44 -0
  517. package/dist/test-helpers/fake-notifier.mjs.map +1 -0
  518. package/dist/test-helpers/fake-tables.mjs +255 -67
  519. package/dist/test-helpers/fake-tables.mjs.map +1 -1
  520. package/dist/test-helpers/manual-clock.d.mts +27 -0
  521. package/dist/test-helpers/manual-clock.mjs +56 -0
  522. package/dist/test-helpers/manual-clock.mjs.map +1 -0
  523. package/dist/test-helpers/notifier-contract.d.mts +7 -1
  524. package/dist/test-helpers/notifier-contract.mjs +32 -3
  525. package/dist/test-helpers/notifier-contract.mjs.map +1 -1
  526. package/dist/test-helpers/queue-contract.d.mts +16 -2
  527. package/dist/test-helpers/queue-contract.mjs +195 -1
  528. package/dist/test-helpers/queue-contract.mjs.map +1 -1
  529. package/dist/test-helpers/queue-multi-instance.d.mts +20 -0
  530. package/dist/test-helpers/queue-multi-instance.mjs +206 -0
  531. package/dist/test-helpers/queue-multi-instance.mjs.map +1 -0
  532. package/dist/test-helpers/runner-contract.mjs +11 -0
  533. package/dist/test-helpers/runner-contract.mjs.map +1 -1
  534. package/dist/test-helpers/sql-chunks.mjs +23 -0
  535. package/dist/test-helpers/sql-chunks.mjs.map +1 -1
  536. package/dist/test-helpers/storage-contract.mjs +1 -1
  537. package/dist/test-helpers.d.mts +9 -4
  538. package/dist/test-helpers.mjs +8 -3
  539. package/dist/types.d.mts +14 -3
  540. package/dist/types.mjs +21 -4
  541. package/dist/types.mjs.map +1 -1
  542. package/dist/urls.d.mts +4 -0
  543. package/dist/urls.mjs +11 -2
  544. package/dist/urls.mjs.map +1 -1
  545. package/dist/utils/adaptive-concurrency.d.mts +66 -0
  546. package/dist/utils/adaptive-concurrency.mjs +149 -0
  547. package/dist/utils/adaptive-concurrency.mjs.map +1 -0
  548. package/dist/utils/connect-retry.d.mts +26 -0
  549. package/dist/utils/connect-retry.mjs +43 -0
  550. package/dist/utils/connect-retry.mjs.map +1 -0
  551. package/dist/utils/environment.d.mts +19 -0
  552. package/dist/utils/environment.mjs +59 -0
  553. package/dist/utils/environment.mjs.map +1 -0
  554. package/dist/utils/http.d.mts +17 -1
  555. package/dist/utils/http.mjs +22 -3
  556. package/dist/utils/http.mjs.map +1 -1
  557. package/dist/utils/map-adaptive.d.mts +15 -0
  558. package/dist/utils/map-adaptive.mjs +72 -0
  559. package/dist/utils/map-adaptive.mjs.map +1 -0
  560. package/dist/utils/map-limit.d.mts +10 -0
  561. package/dist/utils/map-limit.mjs +32 -0
  562. package/dist/utils/map-limit.mjs.map +1 -0
  563. package/dist/utils/page-limiter.d.mts +37 -0
  564. package/dist/utils/page-limiter.mjs +56 -0
  565. package/dist/utils/page-limiter.mjs.map +1 -0
  566. package/dist/utils/public-origin.d.mts +20 -0
  567. package/dist/utils/public-origin.mjs +75 -0
  568. package/dist/utils/public-origin.mjs.map +1 -0
  569. package/dist/utils/redact.mjs +3 -0
  570. package/dist/utils/redact.mjs.map +1 -1
  571. package/dist/utils/remote-environment.d.mts +39 -0
  572. package/dist/utils/remote-environment.mjs +114 -0
  573. package/dist/utils/remote-environment.mjs.map +1 -0
  574. package/dist/utils/resources.d.mts +64 -0
  575. package/dist/utils/resources.mjs +122 -0
  576. package/dist/utils/resources.mjs.map +1 -0
  577. package/dist/utils/screenshot-store.d.mts +15 -0
  578. package/dist/utils/screenshot-store.mjs +41 -0
  579. package/dist/utils/screenshot-store.mjs.map +1 -0
  580. package/dist/utils/session-gate.d.mts +22 -0
  581. package/dist/utils/session-gate.mjs +53 -0
  582. package/dist/utils/session-gate.mjs.map +1 -0
  583. package/dist/utils/storybook-token.d.mts +81 -0
  584. package/dist/utils/storybook-token.mjs +134 -0
  585. package/dist/utils/storybook-token.mjs.map +1 -0
  586. package/dist/utils/storybook-url.d.mts +15 -0
  587. package/dist/utils/storybook-url.mjs +21 -0
  588. package/dist/utils/storybook-url.mjs.map +1 -0
  589. package/dist/utils/timing.mjs +1 -1
  590. package/dist/utils/timing.mjs.map +1 -1
  591. package/dist/utils/zip-read.mjs +129 -0
  592. package/dist/utils/zip-read.mjs.map +1 -0
  593. package/dist/utils/zip-write.d.mts +18 -0
  594. package/dist/utils/zip-write.mjs +188 -0
  595. package/dist/utils/zip-write.mjs.map +1 -0
  596. package/dist/utils.d.mts +17 -3
  597. package/dist/utils.mjs +18 -4
  598. package/package.json +32 -23
  599. package/dist/adapter/capture-runner.mjs.map +0 -1
  600. package/dist/adapter/git-host/comments.d.mts +0 -41
  601. package/dist/adapter/git-host/comments.mjs +0 -54
  602. package/dist/adapter/git-host/comments.mjs.map +0 -1
  603. package/dist/adapter/git-host/helpers.d.mts +0 -13
  604. package/dist/adapter/git-host/helpers.mjs +0 -30
  605. package/dist/adapter/git-host/helpers.mjs.map +0 -1
  606. package/dist/adapter/git-host.d.mts +0 -42
  607. package/dist/capture/queue.d.mts +0 -38
  608. package/dist/capture/queue.mjs +0 -98
  609. package/dist/capture/queue.mjs.map +0 -1
  610. package/dist/capture/recovery.d.mts +0 -21
  611. package/dist/capture/recovery.mjs +0 -83
  612. package/dist/capture/recovery.mjs.map +0 -1
  613. package/dist/capture/status-fanout.mjs +0 -72
  614. package/dist/capture/status-fanout.mjs.map +0 -1
  615. package/dist/diff/engine.d.mts +0 -16
  616. package/dist/diff/engine.mjs +0 -87
  617. package/dist/diff/engine.mjs.map +0 -1
  618. package/dist/diff/options.d.mts +0 -42
  619. package/dist/diff/options.mjs +0 -15
  620. package/dist/diff/options.mjs.map +0 -1
  621. package/dist/models/status-config.d.mts +0 -38
  622. package/dist/models/status-config.mjs +0 -63
  623. package/dist/models/status-config.mjs.map +0 -1
  624. package/dist/schema/status-config.d.mts +0 -16
  625. package/dist/test-helpers/git-contract.d.mts +0 -11
  626. package/dist/test-helpers/git-contract.mjs +0 -34
  627. package/dist/test-helpers/git-contract.mjs.map +0 -1
  628. /package/dist/adapter/{git-host.mjs → oauth.mjs} +0 -0
@@ -0,0 +1,122 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { availableParallelism, totalmem } from "node:os";
5
+ import { readFileSync } from "node:fs";
6
+ //#region src/utils/resources.ts
7
+ const MB = 1048576;
8
+ /** Headroom for Node, the server and Chromium's browser process. */
9
+ const BASE_MEMORY_MB = 512;
10
+ /** Typical cost of one rendering page. */
11
+ const PAGE_MEMORY_MB = 300;
12
+ const MAX_PAGES = 4;
13
+ /** Ceiling for the adaptive controller, in Chromium-page slots. */
14
+ const MAX_ADAPTIVE_PAGES = 8;
15
+ /**
16
+ * Detect the CPU and memory budget. Container limits win over the host's: a
17
+ * 0.5 vCPU / 1 GB container on a large machine must be sized as 0.5 / 1 GB.
18
+ */
19
+ function detectResources() {
20
+ const cpus = Math.min(availableParallelism(), cgroupCpus() ?? Infinity);
21
+ const constrained = typeof process.constrainedMemory === "function" ? process.constrainedMemory() : 0;
22
+ return {
23
+ cpus,
24
+ memoryBytes: constrained > 0 && constrained < totalmem() ? constrained : totalmem()
25
+ };
26
+ }
27
+ /**
28
+ * Cost of one page in Chromium-page slots. Firefox and WebKit pages cost about twice a Chromium
29
+ * page in CPU time (ray.run cross-browser benchmarks: Firefox +71%, WebKit +60%; some Playwright
30
+ * reports show WebKit worse), so they get half the page budget. Takes a capture target too.
31
+ */
32
+ function engineWeight(browser) {
33
+ const engine = browser?.split("@")[0];
34
+ return engine === "firefox" || engine === "webkit" ? 2 : 1;
35
+ }
36
+ /**
37
+ * Pages to render at once: no more than the CPUs (extra pages only slow each
38
+ * other down and trip timeouts) and no more than memory allows. A heavier engine
39
+ * (see {@link engineWeight}) spends the same budget on fewer pages.
40
+ */
41
+ function recommendedPageConcurrency(resources = detectResources(), browser) {
42
+ const budget = Math.min(Math.ceil(resources.cpus), memoryPages(resources), MAX_PAGES);
43
+ return Math.max(1, Math.floor(budget / engineWeight(browser)));
44
+ }
45
+ /**
46
+ * Most pages the adaptive controller may reach: memory is a hard limit, but CPU is only a
47
+ * starting point (latency tells us when it is saturated), so allow up to twice the CPUs.
48
+ */
49
+ function maxPageConcurrency(resources = detectResources(), browser) {
50
+ const budget = Math.min(Math.ceil(resources.cpus) * 2, memoryPages(resources), MAX_ADAPTIVE_PAGES);
51
+ return Math.max(recommendedPageConcurrency(resources, browser), Math.floor(budget / engineWeight(browser)));
52
+ }
53
+ function memoryPages(resources) {
54
+ return Math.floor((resources.memoryBytes / MB - BASE_MEMORY_MB) / PAGE_MEMORY_MB);
55
+ }
56
+ /** Builds to capture at once: each launches its own browser, so only on roomy hosts. */
57
+ function recommendedBuildConcurrency(resources = detectResources()) {
58
+ return resources.cpus >= 2 && resources.memoryBytes >= 3072 * MB ? 2 : 1;
59
+ }
60
+ /** Playwright's 30 s default assumes a full CPU; starved containers parse the Storybook bundle slower. */
61
+ function recommendedNavigationTimeoutMs(resources = detectResources()) {
62
+ if (resources.cpus < 1) return 9e4;
63
+ if (resources.cpus < 2) return 6e4;
64
+ return 3e4;
65
+ }
66
+ /** Captures between browser restarts: Chromium grows slowly over a long run, so recycle it. */
67
+ const DEFAULT_RECYCLE_BROWSER_EVERY = 150;
68
+ /** Loggable summary of the host budget and the capture settings derived from it. */
69
+ function describeSizing(settings) {
70
+ const resources = detectResources();
71
+ return {
72
+ cpus: Math.round(resources.cpus * 100) / 100,
73
+ memoryMb: Math.round(resources.memoryBytes / MB),
74
+ pages: settings.concurrency ?? recommendedPageConcurrency(resources, settings.browser),
75
+ maxPages: settings.concurrency ?? maxPageConcurrency(resources, settings.browser),
76
+ navigationTimeoutMs: settings.navigationTimeoutMs ?? recommendedNavigationTimeoutMs(resources),
77
+ recycleBrowserEvery: settings.recycleBrowserEvery ?? 150
78
+ };
79
+ }
80
+ /** Summarise the host budget and effective capture concurrency, for the System page. */
81
+ function describeCaptureHost(configuredBuilds, resources = detectResources()) {
82
+ return {
83
+ cpus: Math.round(resources.cpus * 100) / 100,
84
+ memoryMb: Math.round(resources.memoryBytes / MB),
85
+ builds: {
86
+ value: configuredBuilds ?? recommendedBuildConcurrency(resources),
87
+ auto: configuredBuilds === void 0
88
+ },
89
+ pages: {
90
+ start: recommendedPageConcurrency(resources),
91
+ max: maxPageConcurrency(resources)
92
+ }
93
+ };
94
+ }
95
+ function cgroupCpus() {
96
+ return cgroupV2Cpus() ?? cgroupV1Cpus();
97
+ }
98
+ function cgroupV2Cpus() {
99
+ const [quota, period] = readText("/sys/fs/cgroup/cpu.max")?.trim().split(/\s+/u) ?? [];
100
+ return quotaToCpus(quota, period);
101
+ }
102
+ function cgroupV1Cpus() {
103
+ const quota = readText("/sys/fs/cgroup/cpu/cpu.cfs_quota_us")?.trim();
104
+ const period = readText("/sys/fs/cgroup/cpu/cpu.cfs_period_us")?.trim();
105
+ return quotaToCpus(quota, period);
106
+ }
107
+ function quotaToCpus(quota, period) {
108
+ const q = Number(quota);
109
+ const p = Number(period);
110
+ return Number.isFinite(q) && q > 0 && Number.isFinite(p) && p > 0 ? q / p : null;
111
+ }
112
+ function readText(path) {
113
+ try {
114
+ return readFileSync(path, "utf8");
115
+ } catch {
116
+ return null;
117
+ }
118
+ }
119
+ //#endregion
120
+ export { DEFAULT_RECYCLE_BROWSER_EVERY, describeCaptureHost, describeSizing, detectResources, engineWeight, maxPageConcurrency, recommendedBuildConcurrency, recommendedNavigationTimeoutMs, recommendedPageConcurrency };
121
+
122
+ //# sourceMappingURL=resources.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resources.mjs","names":[],"sources":["../../src/utils/resources.ts"],"sourcesContent":["import { readFileSync } from \"node:fs\";\nimport { availableParallelism, totalmem } from \"node:os\";\n\n/** CPU and memory actually available to this process (container limits included). */\nexport interface HostResources {\n /** Fractional vCPUs: a cgroup quota of 0.5 CPU reports `0.5`. */\n cpus: number;\n memoryBytes: number;\n}\n\nconst MB = 1024 * 1024;\n/** Headroom for Node, the server and Chromium's browser process. */\nconst BASE_MEMORY_MB = 512;\n/** Typical cost of one rendering page. */\nconst PAGE_MEMORY_MB = 300;\nconst MAX_PAGES = 4;\n/** Ceiling for the adaptive controller, in Chromium-page slots. */\nconst MAX_ADAPTIVE_PAGES = 8;\n\n/**\n * Detect the CPU and memory budget. Container limits win over the host's: a\n * 0.5 vCPU / 1 GB container on a large machine must be sized as 0.5 / 1 GB.\n */\nexport function detectResources(): HostResources {\n const cpus = Math.min(availableParallelism(), cgroupCpus() ?? Infinity);\n const constrained =\n typeof process.constrainedMemory === \"function\" ? process.constrainedMemory() : 0;\n const memoryBytes = constrained > 0 && constrained < totalmem() ? constrained : totalmem();\n return { cpus, memoryBytes };\n}\n\n/**\n * Cost of one page in Chromium-page slots. Firefox and WebKit pages cost about twice a Chromium\n * page in CPU time (ray.run cross-browser benchmarks: Firefox +71%, WebKit +60%; some Playwright\n * reports show WebKit worse), so they get half the page budget. Takes a capture target too.\n */\nexport function engineWeight(browser?: string): number {\n // A target is `<engine>[@<name>]`; the engine decides the page cost.\n const engine = browser?.split(\"@\")[0];\n return engine === \"firefox\" || engine === \"webkit\" ? 2 : 1;\n}\n\n/**\n * Pages to render at once: no more than the CPUs (extra pages only slow each\n * other down and trip timeouts) and no more than memory allows. A heavier engine\n * (see {@link engineWeight}) spends the same budget on fewer pages.\n */\nexport function recommendedPageConcurrency(\n resources: HostResources = detectResources(),\n browser?: string,\n): number {\n const budget = Math.min(Math.ceil(resources.cpus), memoryPages(resources), MAX_PAGES);\n return Math.max(1, Math.floor(budget / engineWeight(browser)));\n}\n\n/**\n * Most pages the adaptive controller may reach: memory is a hard limit, but CPU is only a\n * starting point (latency tells us when it is saturated), so allow up to twice the CPUs.\n */\nexport function maxPageConcurrency(\n resources: HostResources = detectResources(),\n browser?: string,\n): number {\n const budget = Math.min(\n Math.ceil(resources.cpus) * 2,\n memoryPages(resources),\n MAX_ADAPTIVE_PAGES,\n );\n return Math.max(\n recommendedPageConcurrency(resources, browser),\n Math.floor(budget / engineWeight(browser)),\n );\n}\n\nfunction memoryPages(resources: HostResources): number {\n return Math.floor((resources.memoryBytes / MB - BASE_MEMORY_MB) / PAGE_MEMORY_MB);\n}\n\n/** Builds to capture at once: each launches its own browser, so only on roomy hosts. */\nexport function recommendedBuildConcurrency(resources: HostResources = detectResources()): number {\n return resources.cpus >= 2 && resources.memoryBytes >= 3 * 1024 * MB ? 2 : 1;\n}\n\n/** Playwright's 30 s default assumes a full CPU; starved containers parse the Storybook bundle slower. */\nexport function recommendedNavigationTimeoutMs(\n resources: HostResources = detectResources(),\n): number {\n if (resources.cpus < 1) return 90_000;\n if (resources.cpus < 2) return 60_000;\n return 30_000;\n}\n\n/** Captures between browser restarts: Chromium grows slowly over a long run, so recycle it. */\nexport const DEFAULT_RECYCLE_BROWSER_EVERY = 150;\n\n/** Loggable summary of the host budget and the capture settings derived from it. */\nexport function describeSizing(settings: {\n browser?: string;\n concurrency?: number;\n navigationTimeoutMs?: number;\n recycleBrowserEvery?: number;\n}): Record<string, number> {\n const resources = detectResources();\n return {\n cpus: Math.round(resources.cpus * 100) / 100,\n memoryMb: Math.round(resources.memoryBytes / MB),\n pages: settings.concurrency ?? recommendedPageConcurrency(resources, settings.browser),\n maxPages: settings.concurrency ?? maxPageConcurrency(resources, settings.browser),\n navigationTimeoutMs: settings.navigationTimeoutMs ?? recommendedNavigationTimeoutMs(resources),\n recycleBrowserEvery: settings.recycleBrowserEvery ?? DEFAULT_RECYCLE_BROWSER_EVERY,\n };\n}\n\n/** What capture will use on this host: detected resources and the effective concurrency. */\nexport interface CaptureHostSummary {\n cpus: number;\n memoryMb: number;\n /** Builds captured at once, and whether that was sized from the hardware or configured. */\n builds: { value: number; auto: boolean };\n /** Pages per build when the runner is not pinned: where it starts and the most it may reach (Chromium). */\n pages: { start: number; max: number };\n}\n\n/** Summarise the host budget and effective capture concurrency, for the System page. */\nexport function describeCaptureHost(\n configuredBuilds?: number,\n resources: HostResources = detectResources(),\n): CaptureHostSummary {\n return {\n cpus: Math.round(resources.cpus * 100) / 100,\n memoryMb: Math.round(resources.memoryBytes / MB),\n builds: {\n value: configuredBuilds ?? recommendedBuildConcurrency(resources),\n auto: configuredBuilds === undefined,\n },\n pages: { start: recommendedPageConcurrency(resources), max: maxPageConcurrency(resources) },\n };\n}\n\nfunction cgroupCpus(): number | null {\n return cgroupV2Cpus() ?? cgroupV1Cpus();\n}\n\nfunction cgroupV2Cpus(): number | null {\n const [quota, period] = readText(\"/sys/fs/cgroup/cpu.max\")?.trim().split(/\\s+/u) ?? [];\n return quotaToCpus(quota, period);\n}\n\nfunction cgroupV1Cpus(): number | null {\n const quota = readText(\"/sys/fs/cgroup/cpu/cpu.cfs_quota_us\")?.trim();\n const period = readText(\"/sys/fs/cgroup/cpu/cpu.cfs_period_us\")?.trim();\n return quotaToCpus(quota, period);\n}\n\nfunction quotaToCpus(quota: string | undefined, period: string | undefined): number | null {\n const q = Number(quota);\n const p = Number(period);\n return Number.isFinite(q) && q > 0 && Number.isFinite(p) && p > 0 ? q / p : null;\n}\n\nfunction readText(path: string): string | null {\n try {\n return readFileSync(path, \"utf8\");\n } catch {\n return null;\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ import __tsdown_shims_path from 'node:path';
2
+ import __tsdown_shims_url from 'node:url';
3
+ import { StorageAdapter } from "../adapter/storage.mjs";
4
+ //#region src/utils/screenshot-store.d.ts
5
+ /**
6
+ * Store a screenshot under the SHA-256 of its bytes and return its path.
7
+ *
8
+ * An unchanged story renders the same bytes build after build, so the object usually exists
9
+ * already and nothing is uploaded. Snapshots and baselines then point at the same object, and
10
+ * identical paths mean identical pixels (the diff can be skipped without reading either image).
11
+ */
12
+ declare function storeScreenshot(storage: StorageAdapter, projectId: string, data: Buffer): Promise<string>;
13
+ //#endregion
14
+ export { storeScreenshot };
15
+ //# sourceMappingURL=screenshot-store.d.mts.map
@@ -0,0 +1,41 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { ulid } from "./ulid.mjs";
5
+ import { contentDir, contentPath } from "../paths.mjs";
6
+ import { createHash } from "node:crypto";
7
+ //#region src/utils/screenshot-store.ts
8
+ /** Content-addressed screenshot storage: identical bytes are stored once per project. */
9
+ /**
10
+ * Store a screenshot under the SHA-256 of its bytes and return its path.
11
+ *
12
+ * An unchanged story renders the same bytes build after build, so the object usually exists
13
+ * already and nothing is uploaded. Snapshots and baselines then point at the same object, and
14
+ * identical paths mean identical pixels (the diff can be skipped without reading either image).
15
+ */
16
+ async function storeScreenshot(storage, projectId, data) {
17
+ const path = contentPath(projectId, createHash("sha256").update(data).digest("hex"));
18
+ if (!await storage.exists(path)) await storage.write(path, data);
19
+ return path;
20
+ }
21
+ /** Folder inside a project's content directory holding in-flight reuse claims. */
22
+ const CLAIMS_DIR = ".claims";
23
+ /**
24
+ * Run `work` under a reuse claim the screenshot sweep honours: while the claim exists (and is
25
+ * younger than the sweep's grace period) nothing in the project's content is deleted. For work
26
+ * outside a capture that reuses objects before writing the rows that reference them (baseline
27
+ * import, approving a legacy snapshot); a capture is covered by its build's status instead.
28
+ */
29
+ async function withContentClaim(storage, projectId, work) {
30
+ const path = `${contentDir(projectId)}/${CLAIMS_DIR}/${ulid()}.json`;
31
+ await storage.write(path, Buffer.from(JSON.stringify({ at: Date.now() })));
32
+ try {
33
+ return await work();
34
+ } finally {
35
+ await storage.delete(path).catch(() => false);
36
+ }
37
+ }
38
+ //#endregion
39
+ export { CLAIMS_DIR, storeScreenshot, withContentClaim };
40
+
41
+ //# sourceMappingURL=screenshot-store.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"screenshot-store.mjs","names":[],"sources":["../../src/utils/screenshot-store.ts"],"sourcesContent":["/** Content-addressed screenshot storage: identical bytes are stored once per project. */\nimport { createHash } from \"node:crypto\";\nimport type { StorageAdapter } from \"../adapters/storage.ts\";\nimport { contentDir, contentPath } from \"./paths.ts\";\nimport { ulid } from \"./ulid.ts\";\n\n/**\n * Store a screenshot under the SHA-256 of its bytes and return its path.\n *\n * An unchanged story renders the same bytes build after build, so the object usually exists\n * already and nothing is uploaded. Snapshots and baselines then point at the same object, and\n * identical paths mean identical pixels (the diff can be skipped without reading either image).\n */\nexport async function storeScreenshot(\n storage: StorageAdapter,\n projectId: string,\n data: Buffer,\n): Promise<string> {\n const path = contentPath(projectId, createHash(\"sha256\").update(data).digest(\"hex\"));\n if (!(await storage.exists(path))) {\n await storage.write(path, data);\n }\n return path;\n}\n\n/** Folder inside a project's content directory holding in-flight reuse claims. */\nexport const CLAIMS_DIR = \".claims\";\n\n/**\n * Run `work` under a reuse claim the screenshot sweep honours: while the claim exists (and is\n * younger than the sweep's grace period) nothing in the project's content is deleted. For work\n * outside a capture that reuses objects before writing the rows that reference them (baseline\n * import, approving a legacy snapshot); a capture is covered by its build's status instead.\n */\nexport async function withContentClaim<T>(\n storage: StorageAdapter,\n projectId: string,\n work: () => Promise<T>,\n): Promise<T> {\n const path = `${contentDir(projectId)}/${CLAIMS_DIR}/${ulid()}.json`;\n await storage.write(path, Buffer.from(JSON.stringify({ at: Date.now() })));\n try {\n return await work();\n } finally {\n await storage.delete(path).catch(() => false);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;AAaA,eAAsB,gBACpB,SACA,WACA,MACiB;CACjB,MAAM,OAAO,YAAY,WAAW,WAAW,QAAQ,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,OAAO,KAAK,CAAC;CACnF,IAAI,CAAE,MAAM,QAAQ,OAAO,IAAI,GAC7B,MAAM,QAAQ,MAAM,MAAM,IAAI;CAEhC,OAAO;AACT;;AAGA,MAAa,aAAa;;;;;;;AAQ1B,eAAsB,iBACpB,SACA,WACA,MACY;CACZ,MAAM,OAAO,GAAG,WAAW,SAAS,EAAE,GAAG,WAAW,GAAG,KAAK,EAAE;CAC9D,MAAM,QAAQ,MAAM,MAAM,OAAO,KAAK,KAAK,UAAU,EAAE,IAAI,KAAK,IAAI,EAAE,CAAC,CAAC,CAAC;CACzE,IAAI;EACF,OAAO,MAAM,KAAK;CACpB,UAAU;EACR,MAAM,QAAQ,OAAO,IAAI,CAAC,CAAC,YAAY,KAAK;CAC9C;AACF"}
@@ -0,0 +1,22 @@
1
+ import __tsdown_shims_path from 'node:path';
2
+ import __tsdown_shims_url from 'node:url';
3
+ //#region src/utils/session-gate.d.ts
4
+ /** Per-target session caps for remote browsers, shared by every render in the process (ADR 0007, decision 4). */
5
+ /** Runs work under a fixed number of sessions. */
6
+ interface SessionGate {
7
+ /** Sessions the plan allows. */
8
+ readonly size: number;
9
+ /** Run `fn` once a session is free. */
10
+ run<T>(fn: () => Promise<T>): Promise<T>;
11
+ }
12
+ /**
13
+ * The gate of `target`, created on first use. One gate per target for the whole process, so
14
+ * concurrent shards share the cap instead of multiplying it. A later call with another `sessions`
15
+ * keeps the first gate: the size is the runner's configuration and does not change under it.
16
+ */
17
+ declare function sessionGateFor(target: string, sessions: number): SessionGate;
18
+ /** A gate of its own (not shared through {@link sessionGateFor}); `size` below 1 counts as 1. */
19
+ declare function createSessionGate(size: number): SessionGate;
20
+ //#endregion
21
+ export { SessionGate, createSessionGate, sessionGateFor };
22
+ //# sourceMappingURL=session-gate.d.mts.map
@@ -0,0 +1,53 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ //#region src/utils/session-gate.ts
5
+ const gates = /* @__PURE__ */ new Map();
6
+ /**
7
+ * The gate of `target`, created on first use. One gate per target for the whole process, so
8
+ * concurrent shards share the cap instead of multiplying it. A later call with another `sessions`
9
+ * keeps the first gate: the size is the runner's configuration and does not change under it.
10
+ */
11
+ function sessionGateFor(target, sessions) {
12
+ let gate = gates.get(target);
13
+ if (gate === void 0) {
14
+ gate = createSessionGate(sessions);
15
+ gates.set(target, gate);
16
+ }
17
+ return gate;
18
+ }
19
+ /** A gate of its own (not shared through {@link sessionGateFor}); `size` below 1 counts as 1. */
20
+ function createSessionGate(size) {
21
+ const limit = Math.max(1, Math.floor(size));
22
+ let active = 0;
23
+ const waiting = [];
24
+ const release = () => {
25
+ const next = waiting.shift();
26
+ if (next === void 0) active -= 1;
27
+ else next();
28
+ };
29
+ const acquire = async () => {
30
+ if (active < limit) {
31
+ active += 1;
32
+ return;
33
+ }
34
+ await new Promise((resolve) => {
35
+ waiting.push(resolve);
36
+ });
37
+ };
38
+ return {
39
+ size: limit,
40
+ run: async (fn) => {
41
+ await acquire();
42
+ try {
43
+ return await fn();
44
+ } finally {
45
+ release();
46
+ }
47
+ }
48
+ };
49
+ }
50
+ //#endregion
51
+ export { createSessionGate, sessionGateFor };
52
+
53
+ //# sourceMappingURL=session-gate.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-gate.mjs","names":[],"sources":["../../src/utils/session-gate.ts"],"sourcesContent":["/** Per-target session caps for remote browsers, shared by every render in the process (ADR 0007, decision 4). */\n\n/** Runs work under a fixed number of sessions. */\nexport interface SessionGate {\n /** Sessions the plan allows. */\n readonly size: number;\n /** Run `fn` once a session is free. */\n run<T>(fn: () => Promise<T>): Promise<T>;\n}\n\nconst gates = new Map<string, SessionGate>();\n\n/**\n * The gate of `target`, created on first use. One gate per target for the whole process, so\n * concurrent shards share the cap instead of multiplying it. A later call with another `sessions`\n * keeps the first gate: the size is the runner's configuration and does not change under it.\n */\nexport function sessionGateFor(target: string, sessions: number): SessionGate {\n let gate = gates.get(target);\n if (gate === undefined) {\n gate = createSessionGate(sessions);\n gates.set(target, gate);\n }\n return gate;\n}\n\n/** A gate of its own (not shared through {@link sessionGateFor}); `size` below 1 counts as 1. */\nexport function createSessionGate(size: number): SessionGate {\n const limit = Math.max(1, Math.floor(size));\n let active = 0;\n const waiting: (() => void)[] = [];\n const release = (): void => {\n const next = waiting.shift();\n if (next === undefined) {\n active -= 1;\n } else {\n next();\n }\n };\n const acquire = async (): Promise<void> => {\n if (active < limit) {\n active += 1;\n return;\n }\n await new Promise<void>((resolve) => {\n waiting.push(resolve);\n });\n };\n return {\n size: limit,\n run: async (fn) => {\n await acquire();\n try {\n return await fn();\n } finally {\n release();\n }\n },\n };\n}\n"],"mappings":";;;;AAUA,MAAM,wBAAQ,IAAI,IAAyB;;;;;;AAO3C,SAAgB,eAAe,QAAgB,UAA+B;CAC5E,IAAI,OAAO,MAAM,IAAI,MAAM;CAC3B,IAAI,SAAS,KAAA,GAAW;EACtB,OAAO,kBAAkB,QAAQ;EACjC,MAAM,IAAI,QAAQ,IAAI;CACxB;CACA,OAAO;AACT;;AAGA,SAAgB,kBAAkB,MAA2B;CAC3D,MAAM,QAAQ,KAAK,IAAI,GAAG,KAAK,MAAM,IAAI,CAAC;CAC1C,IAAI,SAAS;CACb,MAAM,UAA0B,CAAC;CACjC,MAAM,gBAAsB;EAC1B,MAAM,OAAO,QAAQ,MAAM;EAC3B,IAAI,SAAS,KAAA,GACX,UAAU;OAEV,KAAK;CAET;CACA,MAAM,UAAU,YAA2B;EACzC,IAAI,SAAS,OAAO;GAClB,UAAU;GACV;EACF;EACA,MAAM,IAAI,SAAe,YAAY;GACnC,QAAQ,KAAK,OAAO;EACtB,CAAC;CACH;CACA,OAAO;EACL,MAAM;EACN,KAAK,OAAO,OAAO;GACjB,MAAM,QAAQ;GACd,IAAI;IACF,OAAO,MAAM,GAAG;GAClB,UAAU;IACR,QAAQ;GACV;EACF;CACF;AACF"}
@@ -0,0 +1,81 @@
1
+ import __tsdown_shims_path from 'node:path';
2
+ import __tsdown_shims_url from 'node:url';
3
+ import { SecretInput } from "./encrypt.mjs";
4
+ //#region src/utils/storybook-token.d.ts
5
+ /** The path prefix every signed Storybook URL starts with; `/_/` is the unrelated short link. */
6
+ declare const STORYBOOK_TOKEN_PREFIX = "/_s/";
7
+ /** Regex source of a token, shared by the shape check and the log redaction. */
8
+ declare const STORYBOOK_TOKEN_SOURCE = "[A-Za-z0-9_-]{8,200}\\.[A-Za-z0-9_-]{43}";
9
+ /** Floor of a derived lifetime, in seconds (2 hours). */
10
+ declare const DEFAULT_STORYBOOK_URL_TTL = 7200;
11
+ /** Shortest lifetime a token may be minted with (a render needs at least this). */
12
+ declare const MIN_STORYBOOK_URL_TTL = 60;
13
+ /** Ceiling of a derived lifetime and the longest accepted: a token claiming more is refused, whoever signed it. */
14
+ declare const MAX_STORYBOOK_URL_TTL = 86400;
15
+ /** Slack added to a render budget when deriving a lifetime (queue wait, retries, clock skew). */
16
+ declare const STORYBOOK_URL_TTL_GRACE = 300;
17
+ /** Extra time granted per capture of a render pass, in ms; sizes a pass's budget and its slices. */
18
+ declare const PER_CAPTURE_BUDGET_MS = 15000;
19
+ /** Smallest slice (captures), so a very short project override never renders a page per call. */
20
+ declare const MIN_CAPTURES_PER_SLICE = 50;
21
+ /**
22
+ * Shortest lifetime a project may set as its override (seconds): one minimum slice's budget plus
23
+ * the grace, so a slice never outlives its token. Single owner of the rule; every parser of the
24
+ * override (project API, settings form, page hint) reads it through {@link storybookUrlTtlOverrideError}.
25
+ */
26
+ declare const MIN_STORYBOOK_URL_TTL_OVERRIDE: number;
27
+ /** What a valid token grants: read-only access to one build's static files until `expiresAt`. */
28
+ interface StorybookTokenClaims {
29
+ projectId: string;
30
+ buildId: string;
31
+ /** Unix seconds. */
32
+ expiresAt: number;
33
+ }
34
+ /** Inputs for {@link signStorybookToken}. */
35
+ interface SignStorybookTokenInput {
36
+ /** The server secret (a rotation pair signs with its current key); throws when empty. */
37
+ secret: SecretInput;
38
+ projectId: string;
39
+ buildId: string;
40
+ /** Lifetime in seconds; defaults to {@link DEFAULT_STORYBOOK_URL_TTL}. See {@link storybookUrlTtl}. */
41
+ ttlSeconds?: number | undefined;
42
+ /** Clock override for tests (ms). */
43
+ now?: number | undefined;
44
+ }
45
+ /** Why a token was refused (kept apart from the response, which is the same for all). */
46
+ type StorybookTokenFailure = "malformed" | "signature" | "expired" | "scope";
47
+ /** The outcome of {@link verifyStorybookToken}. */
48
+ type StorybookTokenResult = {
49
+ ok: true;
50
+ claims: StorybookTokenClaims;
51
+ } | {
52
+ ok: false;
53
+ reason: StorybookTokenFailure;
54
+ };
55
+ /** Sign a token for one build. Throws on an empty secret, bad ids or a lifetime out of range. */
56
+ declare function signStorybookToken(input: SignStorybookTokenInput): string;
57
+ /**
58
+ * Check a token against the current secret and, during a rotation, the previous one. The signature
59
+ * is checked (in constant time) before the payload is read, and every failure looks the same to
60
+ * the caller except for `reason`, which is for the server's own log.
61
+ */
62
+ declare function verifyStorybookToken(token: unknown, secret: SecretInput, now?: number): StorybookTokenResult;
63
+ /** The path of a token's root: `/_s/<token>/`. */
64
+ declare function storybookTokenPath(token: string): string;
65
+ /**
66
+ * The lifetime of a render's tokens: the project's override when it has one, otherwise the render
67
+ * budget (seconds) plus {@link STORYBOOK_URL_TTL_GRACE}, never below {@link DEFAULT_STORYBOOK_URL_TTL}
68
+ * and never above {@link MAX_STORYBOOK_URL_TTL}. A pass cannot outlive its budget, so a token
69
+ * derived from it lasts as long as the pass that carries it.
70
+ */
71
+ declare function storybookUrlTtl(input: {
72
+ override?: number | null | undefined;
73
+ budgetMs: number;
74
+ }): number;
75
+ /** The reason a project's lifetime override is refused, or `undefined` when it is acceptable. */
76
+ declare function storybookUrlTtlOverrideError(seconds: number): string | undefined;
77
+ /** Throws unless `seconds` is an integer within the allowed lifetime range. */
78
+ declare function assertStorybookUrlTtl(seconds: number): void;
79
+ //#endregion
80
+ export { DEFAULT_STORYBOOK_URL_TTL, MAX_STORYBOOK_URL_TTL, MIN_CAPTURES_PER_SLICE, MIN_STORYBOOK_URL_TTL, MIN_STORYBOOK_URL_TTL_OVERRIDE, PER_CAPTURE_BUDGET_MS, STORYBOOK_TOKEN_PREFIX, STORYBOOK_TOKEN_SOURCE, STORYBOOK_URL_TTL_GRACE, SignStorybookTokenInput, StorybookTokenClaims, StorybookTokenFailure, StorybookTokenResult, assertStorybookUrlTtl, signStorybookToken, storybookTokenPath, storybookUrlTtl, storybookUrlTtlOverrideError, verifyStorybookToken };
81
+ //# sourceMappingURL=storybook-token.d.mts.map
@@ -0,0 +1,134 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { timingSafeEqualString } from "./hash.mjs";
5
+ import { createHmac } from "node:crypto";
6
+ //#region src/utils/storybook-token.ts
7
+ /**
8
+ * Signed Storybook tokens (ADR 0007, decision 5): the single owner of everything about the path
9
+ * prefix of the URL a remote browser opens to read one build's published Storybook, `/_s/<token>/`:
10
+ * the prefix, the token shape, the derived signing key, signing, verification and the lifetime
11
+ * rules. Stateless: `base64url("<projectId>:<buildId>:<exp>")`, a dot, and the base64url
12
+ * HMAC-SHA-256 of that first part. Imported through `@storyshelf/core/utils`.
13
+ */
14
+ /** The path prefix every signed Storybook URL starts with; `/_/` is the unrelated short link. */
15
+ const STORYBOOK_TOKEN_PREFIX = "/_s/";
16
+ /** Regex source of a token, shared by the shape check and the log redaction. */
17
+ const STORYBOOK_TOKEN_SOURCE = "[A-Za-z0-9_-]{8,200}\\.[A-Za-z0-9_-]{43}";
18
+ /** Floor of a derived lifetime, in seconds (2 hours). */
19
+ const DEFAULT_STORYBOOK_URL_TTL = 7200;
20
+ /** Shortest lifetime a token may be minted with (a render needs at least this). */
21
+ const MIN_STORYBOOK_URL_TTL = 60;
22
+ /** Ceiling of a derived lifetime and the longest accepted: a token claiming more is refused, whoever signed it. */
23
+ const MAX_STORYBOOK_URL_TTL = 86400;
24
+ /** Slack added to a render budget when deriving a lifetime (queue wait, retries, clock skew). */
25
+ const STORYBOOK_URL_TTL_GRACE = 300;
26
+ /** Extra time granted per capture of a render pass, in ms; sizes a pass's budget and its slices. */
27
+ const PER_CAPTURE_BUDGET_MS = 15e3;
28
+ /** Smallest slice (captures), so a very short project override never renders a page per call. */
29
+ const MIN_CAPTURES_PER_SLICE = 50;
30
+ /**
31
+ * Shortest lifetime a project may set as its override (seconds): one minimum slice's budget plus
32
+ * the grace, so a slice never outlives its token. Single owner of the rule; every parser of the
33
+ * override (project API, settings form, page hint) reads it through {@link storybookUrlTtlOverrideError}.
34
+ */
35
+ const MIN_STORYBOOK_URL_TTL_OVERRIDE = Math.ceil(50 * PER_CAPTURE_BUDGET_MS / 1e3) + 300;
36
+ /** Domain separation: the signing key is derived from the secret with this context, nothing else uses it. */
37
+ const KEY_CONTEXT = "storyshelf/storybook-url/v1";
38
+ const TOKEN_SHAPE = new RegExp(`^${STORYBOOK_TOKEN_SOURCE}$`, "u");
39
+ const CLAIMS_SHAPE = /^([A-Za-z0-9_-]{1,64}):([A-Za-z0-9_-]{1,64}):(\d{1,12})$/u;
40
+ /** Sign a token for one build. Throws on an empty secret, bad ids or a lifetime out of range. */
41
+ function signStorybookToken(input) {
42
+ const ttl = input.ttlSeconds ?? 7200;
43
+ assertStorybookUrlTtl(ttl);
44
+ const secret = currentSecret(input.secret);
45
+ const expiresAt = Math.floor((input.now ?? Date.now()) / 1e3) + ttl;
46
+ const claims = `${input.projectId}:${input.buildId}:${expiresAt}`;
47
+ if (!CLAIMS_SHAPE.test(claims)) throw new Error("Cannot sign a Storybook URL: unexpected project or build id");
48
+ const payload = Buffer.from(claims, "utf8").toString("base64url");
49
+ return `${payload}.${signature(secret, payload)}`;
50
+ }
51
+ /**
52
+ * Check a token against the current secret and, during a rotation, the previous one. The signature
53
+ * is checked (in constant time) before the payload is read, and every failure looks the same to
54
+ * the caller except for `reason`, which is for the server's own log.
55
+ */
56
+ function verifyStorybookToken(token, secret, now = Date.now()) {
57
+ if (typeof token !== "string" || !TOKEN_SHAPE.test(token)) return {
58
+ ok: false,
59
+ reason: "malformed"
60
+ };
61
+ const [payload = "", given = ""] = token.split(".");
62
+ if (!secretsOf(secret).some((key) => timingSafeEqualString(signature(key, payload), given))) return {
63
+ ok: false,
64
+ reason: "signature"
65
+ };
66
+ return claimsOf(payload, Math.floor(now / 1e3));
67
+ }
68
+ /** The path of a token's root: `/_s/<token>/`. */
69
+ function storybookTokenPath(token) {
70
+ return `${STORYBOOK_TOKEN_PREFIX}${token}/`;
71
+ }
72
+ /**
73
+ * The lifetime of a render's tokens: the project's override when it has one, otherwise the render
74
+ * budget (seconds) plus {@link STORYBOOK_URL_TTL_GRACE}, never below {@link DEFAULT_STORYBOOK_URL_TTL}
75
+ * and never above {@link MAX_STORYBOOK_URL_TTL}. A pass cannot outlive its budget, so a token
76
+ * derived from it lasts as long as the pass that carries it.
77
+ */
78
+ function storybookUrlTtl(input) {
79
+ if (input.override !== void 0 && input.override !== null) return input.override;
80
+ const derived = Math.ceil(input.budgetMs / 1e3) + 300;
81
+ return Math.min(MAX_STORYBOOK_URL_TTL, Math.max(DEFAULT_STORYBOOK_URL_TTL, derived));
82
+ }
83
+ /** The reason a project's lifetime override is refused, or `undefined` when it is acceptable. */
84
+ function storybookUrlTtlOverrideError(seconds) {
85
+ if (Number.isInteger(seconds) && seconds >= MIN_STORYBOOK_URL_TTL_OVERRIDE && seconds <= 86400) return;
86
+ return `The signed URL lifetime is a whole number of seconds from ${MIN_STORYBOOK_URL_TTL_OVERRIDE} (one slice of 50 captures plus the 300 s grace) to ${MAX_STORYBOOK_URL_TTL}`;
87
+ }
88
+ /** Throws unless `seconds` is an integer within the allowed lifetime range. */
89
+ function assertStorybookUrlTtl(seconds) {
90
+ if (!Number.isInteger(seconds) || seconds < 60 || seconds > 86400) throw new Error(`A Storybook URL lifetime must be a whole number of seconds from 60 to ${MAX_STORYBOOK_URL_TTL}, got ${String(seconds)}`);
91
+ }
92
+ function claimsOf(payload, nowSeconds) {
93
+ const match = CLAIMS_SHAPE.exec(Buffer.from(payload, "base64url").toString("utf8"));
94
+ if (!match) return {
95
+ ok: false,
96
+ reason: "malformed"
97
+ };
98
+ const [, projectId = "", buildId = "", exp = ""] = match;
99
+ const expiresAt = Number(exp);
100
+ if (expiresAt <= nowSeconds) return {
101
+ ok: false,
102
+ reason: "expired"
103
+ };
104
+ if (expiresAt > nowSeconds + 86400) return {
105
+ ok: false,
106
+ reason: "scope"
107
+ };
108
+ return {
109
+ ok: true,
110
+ claims: {
111
+ projectId,
112
+ buildId,
113
+ expiresAt
114
+ }
115
+ };
116
+ }
117
+ /** The key tokens are signed with; throws when none is configured. */
118
+ function currentSecret(secret) {
119
+ const current = typeof secret === "object" ? secret.current : secret;
120
+ if (!current) throw new Error("Cannot sign a Storybook URL: ShelfConfig.secret is not configured");
121
+ return current;
122
+ }
123
+ /** Every key a token may verify with: the current secret and, during a rotation, the previous one. */
124
+ function secretsOf(secret) {
125
+ return (typeof secret === "object" ? [secret.current, secret.previous] : [secret]).filter((key) => key !== void 0 && key !== "");
126
+ }
127
+ function signature(secret, payload) {
128
+ const key = createHmac("sha256", secret).update(KEY_CONTEXT).digest();
129
+ return createHmac("sha256", key).update(payload).digest("base64url");
130
+ }
131
+ //#endregion
132
+ export { DEFAULT_STORYBOOK_URL_TTL, MAX_STORYBOOK_URL_TTL, MIN_CAPTURES_PER_SLICE, MIN_STORYBOOK_URL_TTL, MIN_STORYBOOK_URL_TTL_OVERRIDE, PER_CAPTURE_BUDGET_MS, STORYBOOK_TOKEN_PREFIX, STORYBOOK_TOKEN_SOURCE, STORYBOOK_URL_TTL_GRACE, assertStorybookUrlTtl, signStorybookToken, storybookTokenPath, storybookUrlTtl, storybookUrlTtlOverrideError, verifyStorybookToken };
133
+
134
+ //# sourceMappingURL=storybook-token.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storybook-token.mjs","names":[],"sources":["../../src/utils/storybook-token.ts"],"sourcesContent":["/**\n * Signed Storybook tokens (ADR 0007, decision 5): the single owner of everything about the path\n * prefix of the URL a remote browser opens to read one build's published Storybook, `/_s/<token>/`:\n * the prefix, the token shape, the derived signing key, signing, verification and the lifetime\n * rules. Stateless: `base64url(\"<projectId>:<buildId>:<exp>\")`, a dot, and the base64url\n * HMAC-SHA-256 of that first part. Imported through `@storyshelf/core/utils`.\n */\nimport { createHmac } from \"node:crypto\";\nimport type { SecretInput } from \"./encrypt.ts\";\nimport { timingSafeEqualString } from \"./hash.ts\";\n\n/** The path prefix every signed Storybook URL starts with; `/_/` is the unrelated short link. */\nexport const STORYBOOK_TOKEN_PREFIX = \"/_s/\";\n/** Regex source of a token, shared by the shape check and the log redaction. */\nexport const STORYBOOK_TOKEN_SOURCE = \"[A-Za-z0-9_-]{8,200}\\\\.[A-Za-z0-9_-]{43}\";\n\n/** Floor of a derived lifetime, in seconds (2 hours). */\nexport const DEFAULT_STORYBOOK_URL_TTL = 7200;\n/** Shortest lifetime a token may be minted with (a render needs at least this). */\nexport const MIN_STORYBOOK_URL_TTL = 60;\n/** Ceiling of a derived lifetime and the longest accepted: a token claiming more is refused, whoever signed it. */\nexport const MAX_STORYBOOK_URL_TTL = 86_400;\n/** Slack added to a render budget when deriving a lifetime (queue wait, retries, clock skew). */\nexport const STORYBOOK_URL_TTL_GRACE = 300;\n\n/** Extra time granted per capture of a render pass, in ms; sizes a pass's budget and its slices. */\nexport const PER_CAPTURE_BUDGET_MS = 15_000;\n/** Smallest slice (captures), so a very short project override never renders a page per call. */\nexport const MIN_CAPTURES_PER_SLICE = 50;\n/**\n * Shortest lifetime a project may set as its override (seconds): one minimum slice's budget plus\n * the grace, so a slice never outlives its token. Single owner of the rule; every parser of the\n * override (project API, settings form, page hint) reads it through {@link storybookUrlTtlOverrideError}.\n */\nexport const MIN_STORYBOOK_URL_TTL_OVERRIDE =\n Math.ceil((MIN_CAPTURES_PER_SLICE * PER_CAPTURE_BUDGET_MS) / 1000) + STORYBOOK_URL_TTL_GRACE;\n\n/** Domain separation: the signing key is derived from the secret with this context, nothing else uses it. */\nconst KEY_CONTEXT = \"storyshelf/storybook-url/v1\";\nconst TOKEN_SHAPE = new RegExp(`^${STORYBOOK_TOKEN_SOURCE}$`, \"u\");\nconst CLAIMS_SHAPE = /^([A-Za-z0-9_-]{1,64}):([A-Za-z0-9_-]{1,64}):(\\d{1,12})$/u;\n\n/** What a valid token grants: read-only access to one build's static files until `expiresAt`. */\nexport interface StorybookTokenClaims {\n projectId: string;\n buildId: string;\n /** Unix seconds. */\n expiresAt: number;\n}\n\n/** Inputs for {@link signStorybookToken}. */\nexport interface SignStorybookTokenInput {\n /** The server secret (a rotation pair signs with its current key); throws when empty. */\n secret: SecretInput;\n projectId: string;\n buildId: string;\n /** Lifetime in seconds; defaults to {@link DEFAULT_STORYBOOK_URL_TTL}. See {@link storybookUrlTtl}. */\n ttlSeconds?: number | undefined;\n /** Clock override for tests (ms). */\n now?: number | undefined;\n}\n\n/** Why a token was refused (kept apart from the response, which is the same for all). */\nexport type StorybookTokenFailure = \"malformed\" | \"signature\" | \"expired\" | \"scope\";\n\n/** The outcome of {@link verifyStorybookToken}. */\nexport type StorybookTokenResult =\n | { ok: true; claims: StorybookTokenClaims }\n | { ok: false; reason: StorybookTokenFailure };\n\n/** Sign a token for one build. Throws on an empty secret, bad ids or a lifetime out of range. */\nexport function signStorybookToken(input: SignStorybookTokenInput): string {\n const ttl = input.ttlSeconds ?? DEFAULT_STORYBOOK_URL_TTL;\n assertStorybookUrlTtl(ttl);\n const secret = currentSecret(input.secret);\n const expiresAt = Math.floor((input.now ?? Date.now()) / 1000) + ttl;\n const claims = `${input.projectId}:${input.buildId}:${expiresAt}`;\n if (!CLAIMS_SHAPE.test(claims)) {\n throw new Error(\"Cannot sign a Storybook URL: unexpected project or build id\");\n }\n const payload = Buffer.from(claims, \"utf8\").toString(\"base64url\");\n return `${payload}.${signature(secret, payload)}`;\n}\n\n/**\n * Check a token against the current secret and, during a rotation, the previous one. The signature\n * is checked (in constant time) before the payload is read, and every failure looks the same to\n * the caller except for `reason`, which is for the server's own log.\n */\nexport function verifyStorybookToken(\n token: unknown,\n secret: SecretInput,\n now: number = Date.now(),\n): StorybookTokenResult {\n if (typeof token !== \"string\" || !TOKEN_SHAPE.test(token)) {\n return { ok: false, reason: \"malformed\" };\n }\n const [payload = \"\", given = \"\"] = token.split(\".\");\n const signed = secretsOf(secret).some((key) =>\n timingSafeEqualString(signature(key, payload), given),\n );\n if (!signed) {\n return { ok: false, reason: \"signature\" };\n }\n return claimsOf(payload, Math.floor(now / 1000));\n}\n\n/** The path of a token's root: `/_s/<token>/`. */\nexport function storybookTokenPath(token: string): string {\n return `${STORYBOOK_TOKEN_PREFIX}${token}/`;\n}\n\n/**\n * The lifetime of a render's tokens: the project's override when it has one, otherwise the render\n * budget (seconds) plus {@link STORYBOOK_URL_TTL_GRACE}, never below {@link DEFAULT_STORYBOOK_URL_TTL}\n * and never above {@link MAX_STORYBOOK_URL_TTL}. A pass cannot outlive its budget, so a token\n * derived from it lasts as long as the pass that carries it.\n */\nexport function storybookUrlTtl(input: {\n override?: number | null | undefined;\n budgetMs: number;\n}): number {\n if (input.override !== undefined && input.override !== null) {\n return input.override;\n }\n const derived = Math.ceil(input.budgetMs / 1000) + STORYBOOK_URL_TTL_GRACE;\n return Math.min(MAX_STORYBOOK_URL_TTL, Math.max(DEFAULT_STORYBOOK_URL_TTL, derived));\n}\n\n/** The reason a project's lifetime override is refused, or `undefined` when it is acceptable. */\nexport function storybookUrlTtlOverrideError(seconds: number): string | undefined {\n if (\n Number.isInteger(seconds) &&\n seconds >= MIN_STORYBOOK_URL_TTL_OVERRIDE &&\n seconds <= MAX_STORYBOOK_URL_TTL\n ) {\n return undefined;\n }\n return `The signed URL lifetime is a whole number of seconds from ${MIN_STORYBOOK_URL_TTL_OVERRIDE} (one slice of ${MIN_CAPTURES_PER_SLICE} captures plus the ${STORYBOOK_URL_TTL_GRACE} s grace) to ${MAX_STORYBOOK_URL_TTL}`;\n}\n\n/** Throws unless `seconds` is an integer within the allowed lifetime range. */\nexport function assertStorybookUrlTtl(seconds: number): void {\n if (\n !Number.isInteger(seconds) ||\n seconds < MIN_STORYBOOK_URL_TTL ||\n seconds > MAX_STORYBOOK_URL_TTL\n ) {\n throw new Error(\n `A Storybook URL lifetime must be a whole number of seconds from ${MIN_STORYBOOK_URL_TTL} to ${MAX_STORYBOOK_URL_TTL}, got ${String(seconds)}`,\n );\n }\n}\n\nfunction claimsOf(payload: string, nowSeconds: number): StorybookTokenResult {\n const match = CLAIMS_SHAPE.exec(Buffer.from(payload, \"base64url\").toString(\"utf8\"));\n if (!match) {\n return { ok: false, reason: \"malformed\" };\n }\n const [, projectId = \"\", buildId = \"\", exp = \"\"] = match;\n const expiresAt = Number(exp);\n if (expiresAt <= nowSeconds) {\n return { ok: false, reason: \"expired\" };\n }\n if (expiresAt > nowSeconds + MAX_STORYBOOK_URL_TTL) {\n return { ok: false, reason: \"scope\" };\n }\n return { ok: true, claims: { projectId, buildId, expiresAt } };\n}\n\n/** The key tokens are signed with; throws when none is configured. */\nfunction currentSecret(secret: SecretInput): string {\n const current = typeof secret === \"object\" ? secret.current : secret;\n if (!current) {\n throw new Error(\"Cannot sign a Storybook URL: ShelfConfig.secret is not configured\");\n }\n return current;\n}\n\n/** Every key a token may verify with: the current secret and, during a rotation, the previous one. */\nfunction secretsOf(secret: SecretInput): string[] {\n const keys = typeof secret === \"object\" ? [secret.current, secret.previous] : [secret];\n return keys.filter((key): key is string => key !== undefined && key !== \"\");\n}\n\nfunction signature(secret: string, payload: string): string {\n const key = createHmac(\"sha256\", secret).update(KEY_CONTEXT).digest();\n return createHmac(\"sha256\", key).update(payload).digest(\"base64url\");\n}\n"],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ import __tsdown_shims_path from 'node:path';
2
+ import __tsdown_shims_url from 'node:url';
3
+ //#region src/utils/storybook-url.d.ts
4
+ /** The Storybook URL a runner opens for a `reach: "url"` target (ADR 0007, decision 5). */
5
+ /**
6
+ * The root of a build's Storybook from the URL core hands a runner, without its trailing slash. The
7
+ * URL is the directory root `<origin>/_s/<token>/` and nothing else: no `iframe.html`, no query, no
8
+ * fragment, and a path ending in a slash. Anything else throws, so a runner never silently opens a
9
+ * different page than the one core signed. The URL carries a token and is never logged, nor is it
10
+ * quoted in the error.
11
+ */
12
+ declare function storybookBase(storybookUrl: string): string;
13
+ //#endregion
14
+ export { storybookBase };
15
+ //# sourceMappingURL=storybook-url.d.mts.map
@@ -0,0 +1,21 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ //#region src/utils/storybook-url.ts
5
+ /** The Storybook URL a runner opens for a `reach: "url"` target (ADR 0007, decision 5). */
6
+ /**
7
+ * The root of a build's Storybook from the URL core hands a runner, without its trailing slash. The
8
+ * URL is the directory root `<origin>/_s/<token>/` and nothing else: no `iframe.html`, no query, no
9
+ * fragment, and a path ending in a slash. Anything else throws, so a runner never silently opens a
10
+ * different page than the one core signed. The URL carries a token and is never logged, nor is it
11
+ * quoted in the error.
12
+ */
13
+ function storybookBase(storybookUrl) {
14
+ const url = new URL(storybookUrl);
15
+ if (url.search !== "" || url.hash !== "" || !url.pathname.endsWith("/")) throw new Error("The Storybook URL must be the build's directory root (ending in a slash, no query or fragment)");
16
+ return `${url.origin}${url.pathname.replace(/\/+$/u, "")}`;
17
+ }
18
+ //#endregion
19
+ export { storybookBase };
20
+
21
+ //# sourceMappingURL=storybook-url.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storybook-url.mjs","names":[],"sources":["../../src/utils/storybook-url.ts"],"sourcesContent":["/** The Storybook URL a runner opens for a `reach: \"url\"` target (ADR 0007, decision 5). */\n\n/**\n * The root of a build's Storybook from the URL core hands a runner, without its trailing slash. The\n * URL is the directory root `<origin>/_s/<token>/` and nothing else: no `iframe.html`, no query, no\n * fragment, and a path ending in a slash. Anything else throws, so a runner never silently opens a\n * different page than the one core signed. The URL carries a token and is never logged, nor is it\n * quoted in the error.\n */\nexport function storybookBase(storybookUrl: string): string {\n const url = new URL(storybookUrl);\n if (url.search !== \"\" || url.hash !== \"\" || !url.pathname.endsWith(\"/\")) {\n throw new Error(\n \"The Storybook URL must be the build's directory root (ending in a slash, no query or fragment)\",\n );\n }\n return `${url.origin}${url.pathname.replace(/\\/+$/u, \"\")}`;\n}\n"],"mappings":";;;;;;;;;;;;AASA,SAAgB,cAAc,cAA8B;CAC1D,MAAM,MAAM,IAAI,IAAI,YAAY;CAChC,IAAI,IAAI,WAAW,MAAM,IAAI,SAAS,MAAM,CAAC,IAAI,SAAS,SAAS,GAAG,GACpE,MAAM,IAAI,MACR,gGACF;CAEF,OAAO,GAAG,IAAI,SAAS,IAAI,SAAS,QAAQ,SAAS,EAAE;AACzD"}
@@ -7,7 +7,7 @@ import { AsyncLocalStorage } from "node:async_hooks";
7
7
  * Per-scope timing recorder: aggregate millisecond sums under shared names.
8
8
  *
9
9
  * Framework- and telemetry-free (ALS + numbers only) so any host — the Hono
10
- * app, the remote worker, or a third-party server — can open a scope. The
10
+ * app, the forked capture child, or a third-party server — can open a scope. The
11
11
  * app folds request scopes into `Server-Timing`; background scopes surface
12
12
  * as log lines and attempt history. Without an open scope every call is a
13
13
  * cheap no-op, keeping unit tests and sleeping workers silent.
@@ -1 +1 @@
1
- {"version":3,"file":"timing.mjs","names":[],"sources":["../../src/utils/timing.ts"],"sourcesContent":["/**\n * Per-scope timing recorder: aggregate millisecond sums under shared names.\n *\n * Framework- and telemetry-free (ALS + numbers only) so any host — the Hono\n * app, the remote worker, or a third-party server — can open a scope. The\n * app folds request scopes into `Server-Timing`; background scopes surface\n * as log lines and attempt history. Without an open scope every call is a\n * cheap no-op, keeping unit tests and sleeping workers silent.\n */\nimport { AsyncLocalStorage } from \"node:async_hooks\";\n\n/** Aggregated durations for one scope (family/phase name → total ms). */\nexport type TimingMap = Record<string, number>;\n\nconst scopes = new AsyncLocalStorage<TimingMap>();\n\n/** Run `fn` with a fresh timing scope, returning its result and aggregates. */\nexport async function runWithTimings<T>(\n fn: () => T | Promise<T>,\n): Promise<{ result: T; timings: TimingMap }> {\n const timings: TimingMap = {};\n const result = await scopes.run(timings, fn);\n return { result, timings };\n}\n\n/** Add `durationMs` to `name` in the active scope (no-op when scopeless). */\nexport function addTiming(name: string, durationMs: number): void {\n const timings = scopes.getStore();\n if (!timings) {\n return;\n }\n timings[name] = (timings[name] ?? 0) + durationMs;\n}\n\n/** Run `fn`, recording its wall duration under `name` in the active scope. */\nexport async function timed<T>(name: string, fn: () => T | Promise<T>): Promise<T> {\n const started = performance.now();\n try {\n return await fn();\n } finally {\n addTiming(name, performance.now() - started);\n }\n}\n\n/** Copy of aggregates with whole-millisecond values for logs and headers. */\nexport function roundedTimings(timings: TimingMap): TimingMap {\n return Object.fromEntries(Object.entries(timings).map(([name, ms]) => [name, Math.round(ms)]));\n}\n"],"mappings":";;;;;;;;;;;;;;AAcA,MAAM,SAAS,IAAI,kBAA6B;;AAGhD,eAAsB,eACpB,IAC4C;CAC5C,MAAM,UAAqB,CAAC;CAE5B,OAAO;EAAE,QAAA,MADY,OAAO,IAAI,SAAS,EAAE;EAC1B;CAAQ;AAC3B;;AAGA,SAAgB,UAAU,MAAc,YAA0B;CAChE,MAAM,UAAU,OAAO,SAAS;CAChC,IAAI,CAAC,SACH;CAEF,QAAQ,SAAS,QAAQ,SAAS,KAAK;AACzC;;AAGA,eAAsB,MAAS,MAAc,IAAsC;CACjF,MAAM,UAAU,YAAY,IAAI;CAChC,IAAI;EACF,OAAO,MAAM,GAAG;CAClB,UAAU;EACR,UAAU,MAAM,YAAY,IAAI,IAAI,OAAO;CAC7C;AACF;;AAGA,SAAgB,eAAe,SAA+B;CAC5D,OAAO,OAAO,YAAY,OAAO,QAAQ,OAAO,CAAC,CAAC,KAAK,CAAC,MAAM,QAAQ,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC,CAAC,CAAC;AAC/F"}
1
+ {"version":3,"file":"timing.mjs","names":[],"sources":["../../src/utils/timing.ts"],"sourcesContent":["/**\n * Per-scope timing recorder: aggregate millisecond sums under shared names.\n *\n * Framework- and telemetry-free (ALS + numbers only) so any host — the Hono\n * app, the forked capture child, or a third-party server — can open a scope. The\n * app folds request scopes into `Server-Timing`; background scopes surface\n * as log lines and attempt history. Without an open scope every call is a\n * cheap no-op, keeping unit tests and sleeping workers silent.\n */\nimport { AsyncLocalStorage } from \"node:async_hooks\";\n\n/** Aggregated durations for one scope (family/phase name → total ms). */\nexport type TimingMap = Record<string, number>;\n\nconst scopes = new AsyncLocalStorage<TimingMap>();\n\n/** Run `fn` with a fresh timing scope, returning its result and aggregates. */\nexport async function runWithTimings<T>(\n fn: () => T | Promise<T>,\n): Promise<{ result: T; timings: TimingMap }> {\n const timings: TimingMap = {};\n const result = await scopes.run(timings, fn);\n return { result, timings };\n}\n\n/** Add `durationMs` to `name` in the active scope (no-op when scopeless). */\nexport function addTiming(name: string, durationMs: number): void {\n const timings = scopes.getStore();\n if (!timings) {\n return;\n }\n timings[name] = (timings[name] ?? 0) + durationMs;\n}\n\n/** Run `fn`, recording its wall duration under `name` in the active scope. */\nexport async function timed<T>(name: string, fn: () => T | Promise<T>): Promise<T> {\n const started = performance.now();\n try {\n return await fn();\n } finally {\n addTiming(name, performance.now() - started);\n }\n}\n\n/** Copy of aggregates with whole-millisecond values for logs and headers. */\nexport function roundedTimings(timings: TimingMap): TimingMap {\n return Object.fromEntries(Object.entries(timings).map(([name, ms]) => [name, Math.round(ms)]));\n}\n"],"mappings":";;;;;;;;;;;;;;AAcA,MAAM,SAAS,IAAI,kBAA6B;;AAGhD,eAAsB,eACpB,IAC4C;CAC5C,MAAM,UAAqB,CAAC;CAE5B,OAAO;EAAE,QAAA,MADY,OAAO,IAAI,SAAS,EAAE;EAC1B;CAAQ;AAC3B;;AAGA,SAAgB,UAAU,MAAc,YAA0B;CAChE,MAAM,UAAU,OAAO,SAAS;CAChC,IAAI,CAAC,SACH;CAEF,QAAQ,SAAS,QAAQ,SAAS,KAAK;AACzC;;AAGA,eAAsB,MAAS,MAAc,IAAsC;CACjF,MAAM,UAAU,YAAY,IAAI;CAChC,IAAI;EACF,OAAO,MAAM,GAAG;CAClB,UAAU;EACR,UAAU,MAAM,YAAY,IAAI,IAAI,OAAO;CAC7C;AACF;;AAGA,SAAgB,eAAe,SAA+B;CAC5D,OAAO,OAAO,YAAY,OAAO,QAAQ,OAAO,CAAC,CAAC,KAAK,CAAC,MAAM,QAAQ,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC,CAAC,CAAC;AAC/F"}