@storyshelf/app 0.6.2 → 0.8.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/README.md +42 -4
  2. package/dist/app-types.d.mts +2 -6
  3. package/dist/asset-manifest.mjs +14 -7
  4. package/dist/asset-manifest.mjs.map +1 -1
  5. package/dist/assets/{htmx.min.js → htmx.mjs} +9 -1
  6. package/dist/assets/htmx.mjs.map +1 -0
  7. package/dist/assets/mark.mjs +9 -0
  8. package/dist/assets/mark.mjs.map +1 -0
  9. package/dist/boot-tasks.mjs +79 -0
  10. package/dist/boot-tasks.mjs.map +1 -0
  11. package/dist/build-state-flags.mjs +17 -0
  12. package/dist/build-state-flags.mjs.map +1 -0
  13. package/dist/build-state-setup.mjs +74 -0
  14. package/dist/build-state-setup.mjs.map +1 -0
  15. package/dist/capture-alerts.mjs +45 -0
  16. package/dist/capture-alerts.mjs.map +1 -0
  17. package/dist/capture-process.mjs +58 -0
  18. package/dist/capture-process.mjs.map +1 -0
  19. package/dist/capture-setup.mjs +96 -119
  20. package/dist/capture-setup.mjs.map +1 -1
  21. package/dist/connections.mjs +33 -0
  22. package/dist/connections.mjs.map +1 -0
  23. package/dist/consumer-setup.mjs +57 -0
  24. package/dist/consumer-setup.mjs.map +1 -0
  25. package/dist/credential-keys.mjs +1 -2
  26. package/dist/credentials.mjs +1 -2
  27. package/dist/git-status.mjs +45 -0
  28. package/dist/git-status.mjs.map +1 -0
  29. package/dist/hidden-data.mjs +23 -0
  30. package/dist/hidden-data.mjs.map +1 -0
  31. package/dist/index.d.mts +3 -5
  32. package/dist/index.mjs +204 -127
  33. package/dist/index.mjs.map +1 -1
  34. package/dist/insights/budget.mjs +48 -0
  35. package/dist/insights/budget.mjs.map +1 -0
  36. package/dist/insights/deps.mjs +20 -0
  37. package/dist/insights/deps.mjs.map +1 -0
  38. package/dist/insights/errors.mjs +21 -0
  39. package/dist/insights/errors.mjs.map +1 -0
  40. package/dist/insights/health-panel.mjs +39 -0
  41. package/dist/insights/health-panel.mjs.map +1 -0
  42. package/dist/insights/job.mjs +102 -0
  43. package/dist/insights/job.mjs.map +1 -0
  44. package/dist/insights/load-health.mjs +58 -0
  45. package/dist/insights/load-health.mjs.map +1 -0
  46. package/dist/insights/load-triage.mjs +97 -0
  47. package/dist/insights/load-triage.mjs.map +1 -0
  48. package/dist/insights/panel.mjs +27 -0
  49. package/dist/insights/panel.mjs.map +1 -0
  50. package/dist/insights/request.mjs +112 -0
  51. package/dist/insights/request.mjs.map +1 -0
  52. package/dist/insights/stale.mjs +20 -0
  53. package/dist/insights/stale.mjs.map +1 -0
  54. package/dist/insights/usage-report.mjs +59 -0
  55. package/dist/insights/usage-report.mjs.map +1 -0
  56. package/dist/insights/view.mjs +47 -0
  57. package/dist/insights/view.mjs.map +1 -0
  58. package/dist/job-canceller.mjs +13 -0
  59. package/dist/job-canceller.mjs.map +1 -0
  60. package/dist/job-queue.mjs +50 -0
  61. package/dist/job-queue.mjs.map +1 -0
  62. package/dist/jobs-setup.mjs +131 -0
  63. package/dist/jobs-setup.mjs.map +1 -0
  64. package/dist/lifecycle.mjs +93 -22
  65. package/dist/lifecycle.mjs.map +1 -1
  66. package/dist/middleware/auth-gate.mjs +5 -4
  67. package/dist/middleware/auth-gate.mjs.map +1 -1
  68. package/dist/middleware/csrf.mjs +1 -2
  69. package/dist/middleware/rate-limit.mjs +42 -35
  70. package/dist/middleware/rate-limit.mjs.map +1 -1
  71. package/dist/middleware/request-log.mjs +4 -4
  72. package/dist/middleware/request-log.mjs.map +1 -1
  73. package/dist/middleware/require-review.mjs +50 -0
  74. package/dist/middleware/require-review.mjs.map +1 -0
  75. package/dist/middleware/require-testing.mjs +40 -0
  76. package/dist/middleware/require-testing.mjs.map +1 -0
  77. package/dist/middleware/server-timing.mjs +1 -2
  78. package/dist/middleware/setup-gate.mjs +1 -2
  79. package/dist/middleware/setup-gate.mjs.map +1 -1
  80. package/dist/middleware/store-scope.mjs +13 -4
  81. package/dist/middleware/store-scope.mjs.map +1 -1
  82. package/dist/notifications-pause.mjs +32 -0
  83. package/dist/notifications-pause.mjs.map +1 -0
  84. package/dist/notify.d.mts +3 -4
  85. package/dist/notify.mjs +79 -41
  86. package/dist/notify.mjs.map +1 -1
  87. package/dist/pages/a11y-violations.mjs +115 -0
  88. package/dist/pages/a11y-violations.mjs.map +1 -0
  89. package/dist/pages/admin-ai.mjs +77 -0
  90. package/dist/pages/admin-ai.mjs.map +1 -0
  91. package/dist/pages/admin-system-origin.mjs +132 -0
  92. package/dist/pages/admin-system-origin.mjs.map +1 -0
  93. package/dist/pages/admin-system-targets.mjs +52 -0
  94. package/dist/pages/admin-system-targets.mjs.map +1 -0
  95. package/dist/pages/admin-system.mjs +64 -15
  96. package/dist/pages/admin-system.mjs.map +1 -1
  97. package/dist/pages/build-detail.mjs +105 -78
  98. package/dist/pages/build-detail.mjs.map +1 -1
  99. package/dist/pages/build-diff-actions.mjs +88 -35
  100. package/dist/pages/build-diff-actions.mjs.map +1 -1
  101. package/dist/pages/build-diff-comments.mjs +17 -16
  102. package/dist/pages/build-diff-comments.mjs.map +1 -1
  103. package/dist/pages/build-diff-default.mjs +53 -0
  104. package/dist/pages/build-diff-default.mjs.map +1 -0
  105. package/dist/pages/build-diff-environment.mjs +81 -0
  106. package/dist/pages/build-diff-environment.mjs.map +1 -0
  107. package/dist/pages/build-diff-header.mjs +23 -6
  108. package/dist/pages/build-diff-header.mjs.map +1 -1
  109. package/dist/pages/build-diff-insight.mjs +112 -0
  110. package/dist/pages/build-diff-insight.mjs.map +1 -0
  111. package/dist/pages/build-diff-model.mjs +74 -12
  112. package/dist/pages/build-diff-model.mjs.map +1 -1
  113. package/dist/pages/build-diff-nav.mjs +29 -14
  114. package/dist/pages/build-diff-nav.mjs.map +1 -1
  115. package/dist/pages/build-diff-reference.mjs +84 -0
  116. package/dist/pages/build-diff-reference.mjs.map +1 -0
  117. package/dist/pages/build-diff-shortcuts.mjs +6 -3
  118. package/dist/pages/build-diff-shortcuts.mjs.map +1 -1
  119. package/dist/pages/build-diff-stale.mjs +1 -2
  120. package/dist/pages/build-diff-usage.mjs +62 -0
  121. package/dist/pages/build-diff-usage.mjs.map +1 -0
  122. package/dist/pages/build-diff-viewer.mjs +97 -14
  123. package/dist/pages/build-diff-viewer.mjs.map +1 -1
  124. package/dist/pages/build-diff.mjs +37 -22
  125. package/dist/pages/build-diff.mjs.map +1 -1
  126. package/dist/pages/build-tests.mjs +73 -0
  127. package/dist/pages/build-tests.mjs.map +1 -0
  128. package/dist/pages/component-detail.mjs +248 -0
  129. package/dist/pages/component-detail.mjs.map +1 -0
  130. package/dist/pages/component-stories.mjs +167 -0
  131. package/dist/pages/component-stories.mjs.map +1 -0
  132. package/dist/pages/compute-jobs.mjs +14 -28
  133. package/dist/pages/compute-jobs.mjs.map +1 -1
  134. package/dist/pages/design-links.mjs +181 -0
  135. package/dist/pages/design-links.mjs.map +1 -0
  136. package/dist/pages/environment-change.mjs +52 -0
  137. package/dist/pages/environment-change.mjs.map +1 -0
  138. package/dist/pages/health-panel.mjs +125 -0
  139. package/dist/pages/health-panel.mjs.map +1 -0
  140. package/dist/pages/invite.mjs +1 -2
  141. package/dist/pages/label-detail.mjs +1 -2
  142. package/dist/pages/library-grid.mjs +104 -0
  143. package/dist/pages/library-grid.mjs.map +1 -0
  144. package/dist/pages/library-model.mjs +46 -0
  145. package/dist/pages/library-model.mjs.map +1 -0
  146. package/dist/pages/library-snapshots.mjs +129 -0
  147. package/dist/pages/library-snapshots.mjs.map +1 -0
  148. package/dist/pages/library.mjs +113 -220
  149. package/dist/pages/library.mjs.map +1 -1
  150. package/dist/pages/login.mjs +1 -2
  151. package/dist/pages/notifications-paused.mjs +21 -0
  152. package/dist/pages/notifications-paused.mjs.map +1 -0
  153. package/dist/pages/passkey-ceremony.mjs +1 -2
  154. package/dist/pages/profile.mjs +54 -50
  155. package/dist/pages/profile.mjs.map +1 -1
  156. package/dist/pages/project-builds-table.mjs +3 -4
  157. package/dist/pages/project-builds-table.mjs.map +1 -1
  158. package/dist/pages/project-builds.mjs +52 -23
  159. package/dist/pages/project-builds.mjs.map +1 -1
  160. package/dist/pages/project-create.mjs +1 -2
  161. package/dist/pages/project-settings.mjs +52 -14
  162. package/dist/pages/project-settings.mjs.map +1 -1
  163. package/dist/pages/projects-card.mjs +15 -3
  164. package/dist/pages/projects-card.mjs.map +1 -1
  165. package/dist/pages/projects.mjs +7 -4
  166. package/dist/pages/projects.mjs.map +1 -1
  167. package/dist/pages/review-changeset.mjs +115 -0
  168. package/dist/pages/review-changeset.mjs.map +1 -0
  169. package/dist/pages/review-comments.mjs +102 -0
  170. package/dist/pages/review-comments.mjs.map +1 -0
  171. package/dist/pages/review-verdict.mjs +101 -0
  172. package/dist/pages/review-verdict.mjs.map +1 -0
  173. package/dist/pages/root.mjs +1 -2
  174. package/dist/pages/settings-ai.mjs +68 -0
  175. package/dist/pages/settings-ai.mjs.map +1 -0
  176. package/dist/pages/settings-capture.mjs +123 -0
  177. package/dist/pages/settings-capture.mjs.map +1 -0
  178. package/dist/pages/settings-design-credential.mjs +98 -0
  179. package/dist/pages/settings-design-credential.mjs.map +1 -0
  180. package/dist/pages/settings-design-links.mjs +178 -0
  181. package/dist/pages/settings-design-links.mjs.map +1 -0
  182. package/dist/pages/settings-design.mjs +47 -0
  183. package/dist/pages/settings-design.mjs.map +1 -0
  184. package/dist/pages/settings-general.mjs +47 -23
  185. package/dist/pages/settings-general.mjs.map +1 -1
  186. package/dist/pages/settings-labels.mjs +44 -4
  187. package/dist/pages/settings-labels.mjs.map +1 -1
  188. package/dist/pages/settings-links.mjs +111 -0
  189. package/dist/pages/settings-links.mjs.map +1 -0
  190. package/dist/pages/settings-members.mjs +1 -2
  191. package/dist/pages/settings-notifications-git.mjs +127 -0
  192. package/dist/pages/settings-notifications-git.mjs.map +1 -0
  193. package/dist/pages/settings-notifications.mjs +80 -44
  194. package/dist/pages/settings-notifications.mjs.map +1 -1
  195. package/dist/pages/settings-tests.mjs +34 -4
  196. package/dist/pages/settings-tests.mjs.map +1 -1
  197. package/dist/pages/settings-tokens.mjs +1 -2
  198. package/dist/pages/settings-webhooks.mjs +1 -2
  199. package/dist/pages/storybook.mjs +1 -2
  200. package/dist/pages/target-choices.mjs +38 -0
  201. package/dist/pages/target-choices.mjs.map +1 -0
  202. package/dist/publish-setup.mjs +88 -0
  203. package/dist/publish-setup.mjs.map +1 -0
  204. package/dist/retention-timer.mjs +12 -12
  205. package/dist/retention-timer.mjs.map +1 -1
  206. package/dist/review-events.mjs +47 -0
  207. package/dist/review-events.mjs.map +1 -0
  208. package/dist/routers/admin-pages.mjs +46 -5
  209. package/dist/routers/admin-pages.mjs.map +1 -1
  210. package/dist/routers/admin-public-origin.mjs +92 -0
  211. package/dist/routers/admin-public-origin.mjs.map +1 -0
  212. package/dist/routers/admin.mjs +8 -4
  213. package/dist/routers/admin.mjs.map +1 -1
  214. package/dist/routers/assets.mjs +10 -4
  215. package/dist/routers/assets.mjs.map +1 -1
  216. package/dist/routers/attempts.mjs +1 -2
  217. package/dist/routers/auth-sync.mjs +1 -2
  218. package/dist/routers/auth.mjs +2 -3
  219. package/dist/routers/baseline-guard.mjs +4 -5
  220. package/dist/routers/baseline-guard.mjs.map +1 -1
  221. package/dist/routers/baseline-reference.mjs +72 -0
  222. package/dist/routers/baseline-reference.mjs.map +1 -0
  223. package/dist/routers/baselines.handlers.mjs +63 -0
  224. package/dist/routers/baselines.handlers.mjs.map +1 -0
  225. package/dist/routers/baselines.mjs +124 -0
  226. package/dist/routers/baselines.mjs.map +1 -0
  227. package/dist/routers/build-cancel.mjs +67 -0
  228. package/dist/routers/build-cancel.mjs.map +1 -0
  229. package/dist/routers/build-changeset.mjs +69 -0
  230. package/dist/routers/build-changeset.mjs.map +1 -0
  231. package/dist/routers/build-reviews.handlers.mjs +85 -0
  232. package/dist/routers/build-reviews.handlers.mjs.map +1 -0
  233. package/dist/routers/build-reviews.mjs +73 -0
  234. package/dist/routers/build-reviews.mjs.map +1 -0
  235. package/dist/routers/builds.handlers.mjs +65 -60
  236. package/dist/routers/builds.handlers.mjs.map +1 -1
  237. package/dist/routers/builds.mjs +77 -19
  238. package/dist/routers/builds.mjs.map +1 -1
  239. package/dist/routers/capture-cache.mjs +29 -0
  240. package/dist/routers/capture-cache.mjs.map +1 -0
  241. package/dist/routers/capture-config.mjs +34 -0
  242. package/dist/routers/capture-config.mjs.map +1 -0
  243. package/dist/routers/comments.mjs +42 -17
  244. package/dist/routers/comments.mjs.map +1 -1
  245. package/dist/routers/comparison-image.mjs +29 -0
  246. package/dist/routers/comparison-image.mjs.map +1 -0
  247. package/dist/routers/component-catalog.handlers.mjs +70 -0
  248. package/dist/routers/component-catalog.handlers.mjs.map +1 -0
  249. package/dist/routers/component.handlers.mjs +120 -0
  250. package/dist/routers/component.handlers.mjs.map +1 -0
  251. package/dist/routers/connections.mjs +117 -0
  252. package/dist/routers/connections.mjs.map +1 -0
  253. package/dist/routers/design-connect.handlers.mjs +43 -0
  254. package/dist/routers/design-connect.handlers.mjs.map +1 -0
  255. package/dist/routers/design.handlers.mjs +113 -0
  256. package/dist/routers/design.handlers.mjs.map +1 -0
  257. package/dist/routers/flash.mjs +1 -2
  258. package/dist/routers/health-report.mjs +3 -5
  259. package/dist/routers/health-report.mjs.map +1 -1
  260. package/dist/routers/health.mjs +5 -5
  261. package/dist/routers/health.mjs.map +1 -1
  262. package/dist/routers/helpers.mjs +20 -5
  263. package/dist/routers/helpers.mjs.map +1 -1
  264. package/dist/routers/htmx.mjs +1 -2
  265. package/dist/routers/insights-ui.mjs +63 -0
  266. package/dist/routers/insights-ui.mjs.map +1 -0
  267. package/dist/routers/insights.handlers.mjs +44 -0
  268. package/dist/routers/insights.handlers.mjs.map +1 -0
  269. package/dist/routers/insights.mjs +188 -0
  270. package/dist/routers/insights.mjs.map +1 -0
  271. package/dist/routers/labels.mjs +1 -2
  272. package/dist/routers/links.handlers.mjs +79 -0
  273. package/dist/routers/links.handlers.mjs.map +1 -0
  274. package/dist/routers/links.mjs +138 -0
  275. package/dist/routers/links.mjs.map +1 -0
  276. package/dist/routers/media.mjs +30 -7
  277. package/dist/routers/media.mjs.map +1 -1
  278. package/dist/routers/members.mjs +3 -3
  279. package/dist/routers/members.mjs.map +1 -1
  280. package/dist/routers/notification-channels.mjs +18 -25
  281. package/dist/routers/notification-channels.mjs.map +1 -1
  282. package/dist/routers/notification-subscriptions.mjs +1 -2
  283. package/dist/routers/oauth-state.mjs +44 -0
  284. package/dist/routers/oauth-state.mjs.map +1 -0
  285. package/dist/routers/preview-base.mjs +37 -0
  286. package/dist/routers/preview-base.mjs.map +1 -0
  287. package/dist/routers/preview-token.mjs +50 -0
  288. package/dist/routers/preview-token.mjs.map +1 -0
  289. package/dist/routers/profile.mjs +4 -3
  290. package/dist/routers/profile.mjs.map +1 -1
  291. package/dist/routers/projects.mjs +18 -3
  292. package/dist/routers/projects.mjs.map +1 -1
  293. package/dist/routers/publish-upload.mjs +85 -0
  294. package/dist/routers/publish-upload.mjs.map +1 -0
  295. package/dist/routers/published-access.mjs +51 -0
  296. package/dist/routers/published-access.mjs.map +1 -0
  297. package/dist/routers/published-compression.mjs +109 -0
  298. package/dist/routers/published-compression.mjs.map +1 -0
  299. package/dist/routers/published-files.mjs +69 -0
  300. package/dist/routers/published-files.mjs.map +1 -0
  301. package/dist/routers/review-forms.mjs +71 -0
  302. package/dist/routers/review-forms.mjs.map +1 -0
  303. package/dist/routers/review-next.mjs +1 -2
  304. package/dist/routers/review-section.handlers.mjs +84 -0
  305. package/dist/routers/review-section.handlers.mjs.map +1 -0
  306. package/dist/routers/review-switch-off.mjs +28 -0
  307. package/dist/routers/review-switch-off.mjs.map +1 -0
  308. package/dist/routers/schemas.mjs +141 -24
  309. package/dist/routers/schemas.mjs.map +1 -1
  310. package/dist/routers/scoped-models.mjs +21 -0
  311. package/dist/routers/scoped-models.mjs.map +1 -0
  312. package/dist/routers/settings-ai.mjs +34 -0
  313. package/dist/routers/settings-ai.mjs.map +1 -0
  314. package/dist/routers/settings-capture.mjs +76 -0
  315. package/dist/routers/settings-capture.mjs.map +1 -0
  316. package/dist/routers/settings-design.mjs +102 -0
  317. package/dist/routers/settings-design.mjs.map +1 -0
  318. package/dist/routers/settings-general.mjs +56 -12
  319. package/dist/routers/settings-general.mjs.map +1 -1
  320. package/dist/routers/settings-git-channel.mjs +76 -0
  321. package/dist/routers/settings-git-channel.mjs.map +1 -0
  322. package/dist/routers/settings-labels.mjs +28 -3
  323. package/dist/routers/settings-labels.mjs.map +1 -1
  324. package/dist/routers/settings-links.mjs +62 -0
  325. package/dist/routers/settings-links.mjs.map +1 -0
  326. package/dist/routers/settings-members.mjs +5 -9
  327. package/dist/routers/settings-members.mjs.map +1 -1
  328. package/dist/routers/settings-notifications.mjs +16 -12
  329. package/dist/routers/settings-notifications.mjs.map +1 -1
  330. package/dist/routers/settings-tokens.mjs +3 -7
  331. package/dist/routers/settings-tokens.mjs.map +1 -1
  332. package/dist/routers/settings-webhooks.mjs +8 -26
  333. package/dist/routers/settings-webhooks.mjs.map +1 -1
  334. package/dist/routers/settings.handlers.mjs +108 -28
  335. package/dist/routers/settings.handlers.mjs.map +1 -1
  336. package/dist/routers/settings.mjs +17 -6
  337. package/dist/routers/settings.mjs.map +1 -1
  338. package/dist/routers/signed-storybook.mjs +55 -0
  339. package/dist/routers/signed-storybook.mjs.map +1 -0
  340. package/dist/routers/snapshot-groups.mjs +62 -0
  341. package/dist/routers/snapshot-groups.mjs.map +1 -0
  342. package/dist/routers/snapshots.mjs +104 -7
  343. package/dist/routers/snapshots.mjs.map +1 -1
  344. package/dist/routers/storybook.mjs +57 -104
  345. package/dist/routers/storybook.mjs.map +1 -1
  346. package/dist/routers/tokens.mjs +3 -7
  347. package/dist/routers/tokens.mjs.map +1 -1
  348. package/dist/routers/ui.mjs +38 -7
  349. package/dist/routers/ui.mjs.map +1 -1
  350. package/dist/routers/webhooks.mjs +6 -13
  351. package/dist/routers/webhooks.mjs.map +1 -1
  352. package/dist/routers/well-known.mjs +12 -6
  353. package/dist/routers/well-known.mjs.map +1 -1
  354. package/dist/routes/api-routes.mjs +92 -0
  355. package/dist/routes/api-routes.mjs.map +1 -0
  356. package/dist/routes/classification.mjs +37 -0
  357. package/dist/routes/classification.mjs.map +1 -0
  358. package/dist/routes/openapi-classes.mjs +21 -0
  359. package/dist/routes/openapi-classes.mjs.map +1 -0
  360. package/dist/routes/page-routes.mjs +103 -0
  361. package/dist/routes/page-routes.mjs.map +1 -0
  362. package/dist/runtime.mjs +19 -15
  363. package/dist/runtime.mjs.map +1 -1
  364. package/dist/serverless.mjs +4 -0
  365. package/dist/store.mjs +20 -3
  366. package/dist/store.mjs.map +1 -1
  367. package/dist/targets.mjs +45 -0
  368. package/dist/targets.mjs.map +1 -0
  369. package/dist/ui/avatar.mjs +1 -2
  370. package/dist/ui/buttons.mjs +17 -5
  371. package/dist/ui/buttons.mjs.map +1 -1
  372. package/dist/ui/code-block.mjs +1 -2
  373. package/dist/ui/compare-stage.mjs +86 -54
  374. package/dist/ui/compare-stage.mjs.map +1 -1
  375. package/dist/ui/csrf-field.mjs +1 -2
  376. package/dist/ui/css.mjs +1 -2
  377. package/dist/ui/design-reference.mjs +96 -0
  378. package/dist/ui/design-reference.mjs.map +1 -0
  379. package/dist/ui/document.mjs +2 -4
  380. package/dist/ui/document.mjs.map +1 -1
  381. package/dist/ui/dropdown.mjs +1 -2
  382. package/dist/ui/feedback.mjs +1 -2
  383. package/dist/ui/filter-input.mjs +1 -2
  384. package/dist/ui/forms.mjs +7 -7
  385. package/dist/ui/forms.mjs.map +1 -1
  386. package/dist/ui/icons/icon.mjs +1 -2
  387. package/dist/ui/icons/paths.mjs +2 -2
  388. package/dist/ui/icons/paths.mjs.map +1 -1
  389. package/dist/ui/icons/sprite.mjs +1 -2
  390. package/dist/ui/kbd.mjs +1 -2
  391. package/dist/ui/layout.mjs +1 -2
  392. package/dist/ui/preview-embed.mjs +81 -0
  393. package/dist/ui/preview-embed.mjs.map +1 -0
  394. package/dist/ui/progress.mjs +1 -2
  395. package/dist/ui/segmented.mjs +1 -2
  396. package/dist/ui/shell/account.mjs +1 -2
  397. package/dist/ui/shell/bare.mjs +1 -2
  398. package/dist/ui/shell/boot-script.mjs +1 -2
  399. package/dist/ui/shell/client-script.mjs +86 -2
  400. package/dist/ui/shell/client-script.mjs.map +1 -1
  401. package/dist/ui/shell/nav.mjs +13 -10
  402. package/dist/ui/shell/nav.mjs.map +1 -1
  403. package/dist/ui/shell/review-script.mjs +25 -3
  404. package/dist/ui/shell/review-script.mjs.map +1 -1
  405. package/dist/ui/shell/sidebar.mjs +3 -4
  406. package/dist/ui/shell/sidebar.mjs.map +1 -1
  407. package/dist/ui/shell/topbar.mjs +1 -2
  408. package/dist/ui/stacks.mjs +1 -2
  409. package/dist/ui/styles/base.mjs +1 -2
  410. package/dist/ui/styles/behavior.mjs +1 -2
  411. package/dist/ui/styles/review-layout.mjs +1 -2
  412. package/dist/ui/styles/review.mjs +1 -2
  413. package/dist/ui/styles/tokens.mjs +1 -2
  414. package/dist/ui/styles.mjs +1 -2
  415. package/dist/ui/subnav.mjs +1 -2
  416. package/dist/ui/table.mjs +1 -2
  417. package/dist/ui/theme.mjs +1 -2
  418. package/dist/ui/thumbnail.mjs +1 -2
  419. package/dist/ui/time.mjs +1 -2
  420. package/package.json +14 -12
  421. package/dist/asset-paths.mjs +0 -23
  422. package/dist/asset-paths.mjs.map +0 -1
  423. package/dist/assets/mark.svg +0 -1
  424. package/dist/pages/settings-status.mjs +0 -137
  425. package/dist/pages/settings-status.mjs.map +0 -1
  426. package/dist/routers/settings-status.mjs +0 -77
  427. package/dist/routers/settings-status.mjs.map +0 -1
  428. package/dist/routers/status-configs.mjs +0 -108
  429. package/dist/routers/status-configs.mjs.map +0 -1
@@ -0,0 +1,73 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { resolveAuthorizedProject, resolveReviewer } from "./helpers.mjs";
5
+ import { hxRefresh } from "./htmx.mjs";
6
+ import { flashHx } from "./flash.mjs";
7
+ import { buildReviewCreateSchema, buildReviewSchema, buildReviewStateSchema, forbidden, notFound, unauthorized } from "./schemas.mjs";
8
+ import { APPROVER_ROLES, VIEW_ROLES, buildForProject } from "./builds.handlers.mjs";
9
+ import { giveVerdict, reviewStateOf } from "./build-reviews.handlers.mjs";
10
+ import { createRoute, z } from "@hono/zod-openapi";
11
+ // src/routers/build-reviews.ts
12
+ const params = z.object({
13
+ slug: z.string(),
14
+ buildId: z.string()
15
+ });
16
+ const getReviewsRoute = createRoute({
17
+ method: "get",
18
+ tags: ["Review"],
19
+ summary: "Get a build's review state",
20
+ description: "The verdicts reviewers gave the build and what they make of it: each reviewer's latest verdict, the approvals against the project's requirement, and the full history. 404 when the project has review off.",
21
+ path: "/api/v1/projects/{slug}/builds/{buildId}/reviews",
22
+ request: { params },
23
+ responses: {
24
+ 200: {
25
+ content: { "application/json": { schema: buildReviewStateSchema } },
26
+ description: "Review state of the build"
27
+ },
28
+ ...notFound,
29
+ ...unauthorized
30
+ }
31
+ });
32
+ const createReviewRoute = createRoute({
33
+ method: "post",
34
+ tags: ["Review"],
35
+ summary: "Approve or reject a build",
36
+ description: "Give the build a verdict (approver and admin). A reviewer rewrites their verdict by posting again; the newest counts. With `testing` off the verdicts decide the build: `approved` once the project's required approvals are present and no reviewer's latest verdict is `rejected`, `rejected` when any is. With `testing` on the verdict is a sign-off and the snapshot rollup keeps deciding. The verdict is attributed to the signed-in user, or to the user who owns the API token; a caller that is no user (the bootstrap admin token) gets 403, and only an instance with no authentication at all records an anonymous verdict, which counts when the project needs one approval (409 otherwise). 404 when the project has review off; 409 for a failed or cancelled build, while the build is still publishing, or for an anonymous verdict the project cannot count.",
37
+ path: "/api/v1/projects/{slug}/builds/{buildId}/reviews",
38
+ request: {
39
+ params,
40
+ body: { content: { "application/json": { schema: buildReviewCreateSchema } } }
41
+ },
42
+ responses: {
43
+ 201: {
44
+ content: { "application/json": { schema: buildReviewSchema } },
45
+ description: "Verdict recorded"
46
+ },
47
+ ...forbidden,
48
+ ...notFound,
49
+ ...unauthorized
50
+ }
51
+ });
52
+ /** Register the build-level verdict endpoints (`review` class). */
53
+ function registerBuildReviews(app) {
54
+ app.openapi(getReviewsRoute, async (c) => {
55
+ const { slug, buildId } = c.req.valid("param");
56
+ const project = await resolveAuthorizedProject(c, slug, ...VIEW_ROLES);
57
+ const build = await buildForProject(project.id, buildId);
58
+ return c.json(await reviewStateOf(project, build));
59
+ });
60
+ app.openapi(createReviewRoute, async (c) => {
61
+ const { slug, buildId } = c.req.valid("param");
62
+ const project = await resolveAuthorizedProject(c, slug, ...APPROVER_ROLES);
63
+ const build = await buildForProject(project.id, buildId);
64
+ const userId = await resolveReviewer(c);
65
+ const review = await giveVerdict(project, build, userId, c.req.valid("json"));
66
+ hxRefresh(c);
67
+ flashHx(c, "Verdict recorded");
68
+ return c.json(review, 201);
69
+ });
70
+ }
71
+ export { registerBuildReviews };
72
+
73
+ //# sourceMappingURL=build-reviews.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build-reviews.mjs","names":["forbiddenResponse"],"sources":["../../src/routers/build-reviews.ts"],"sourcesContent":["import { createRoute, z } from \"@hono/zod-openapi\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { giveVerdict, reviewStateOf } from \"./build-reviews.handlers.ts\";\nimport { APPROVER_ROLES, VIEW_ROLES, buildForProject } from \"./builds.handlers.ts\";\nimport { flashHx } from \"./flash.ts\";\nimport { resolveAuthorizedProject, resolveReviewer } from \"./helpers.ts\";\nimport { hxRefresh } from \"./htmx.ts\";\nimport {\n buildReviewCreateSchema,\n buildReviewSchema,\n buildReviewStateSchema,\n forbidden as forbiddenResponse,\n notFound,\n unauthorized,\n} from \"./schemas.ts\";\n\nconst params = z.object({ slug: z.string(), buildId: z.string() });\n\nconst getReviewsRoute = createRoute({\n method: \"get\",\n tags: [\"Review\"],\n summary: \"Get a build's review state\",\n description:\n \"The verdicts reviewers gave the build and what they make of it: each reviewer's latest verdict, the approvals against the project's requirement, and the full history. 404 when the project has review off.\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/reviews\",\n request: { params },\n responses: {\n 200: {\n content: { \"application/json\": { schema: buildReviewStateSchema } },\n description: \"Review state of the build\",\n },\n ...notFound,\n ...unauthorized,\n },\n});\n\nconst createReviewRoute = createRoute({\n method: \"post\",\n tags: [\"Review\"],\n summary: \"Approve or reject a build\",\n description:\n \"Give the build a verdict (approver and admin). A reviewer rewrites their verdict by posting again; the newest counts. With `testing` off the verdicts decide the build: `approved` once the project's required approvals are present and no reviewer's latest verdict is `rejected`, `rejected` when any is. With `testing` on the verdict is a sign-off and the snapshot rollup keeps deciding. The verdict is attributed to the signed-in user, or to the user who owns the API token; a caller that is no user (the bootstrap admin token) gets 403, and only an instance with no authentication at all records an anonymous verdict, which counts when the project needs one approval (409 otherwise). 404 when the project has review off; 409 for a failed or cancelled build, while the build is still publishing, or for an anonymous verdict the project cannot count.\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/reviews\",\n request: {\n params,\n body: { content: { \"application/json\": { schema: buildReviewCreateSchema } } },\n },\n responses: {\n 201: {\n content: { \"application/json\": { schema: buildReviewSchema } },\n description: \"Verdict recorded\",\n },\n ...forbiddenResponse,\n ...notFound,\n ...unauthorized,\n },\n});\n\n/** Register the build-level verdict endpoints (`review` class). */\nexport function registerBuildReviews(app: ShelfRouter): void {\n app.openapi(getReviewsRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...VIEW_ROLES);\n const build = await buildForProject(project.id, buildId);\n return c.json(await reviewStateOf(project, build));\n });\n\n app.openapi(createReviewRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...APPROVER_ROLES);\n const build = await buildForProject(project.id, buildId);\n const userId = await resolveReviewer(c);\n const review = await giveVerdict(project, build, userId, c.req.valid(\"json\"));\n hxRefresh(c);\n flashHx(c, \"Verdict recorded\");\n return c.json(review, 201);\n });\n}\n"],"mappings":";;;;;;;;;;;AAgBA,MAAM,SAAS,EAAE,OAAO;CAAE,MAAM,EAAE,OAAO;CAAG,SAAS,EAAE,OAAO;AAAE,CAAC;AAEjE,MAAM,kBAAkB,YAAY;CAClC,QAAQ;CACR,MAAM,CAAC,QAAQ;CACf,SAAS;CACT,aACE;CACF,MAAM;CACN,SAAS,EAAE,OAAO;CAClB,WAAW;EACT,KAAK;GACH,SAAS,EAAE,oBAAoB,EAAE,QAAQ,uBAAuB,EAAE;GAClE,aAAa;EACf;EACA,GAAG;EACH,GAAG;CACL;AACF,CAAC;AAED,MAAM,oBAAoB,YAAY;CACpC,QAAQ;CACR,MAAM,CAAC,QAAQ;CACf,SAAS;CACT,aACE;CACF,MAAM;CACN,SAAS;EACP;EACA,MAAM,EAAE,SAAS,EAAE,oBAAoB,EAAE,QAAQ,wBAAwB,EAAE,EAAE;CAC/E;CACA,WAAW;EACT,KAAK;GACH,SAAS,EAAE,oBAAoB,EAAE,QAAQ,kBAAkB,EAAE;GAC7D,aAAa;EACf;EACA,GAAGA;EACH,GAAG;EACH,GAAG;CACL;AACF,CAAC;;AAGD,SAAgB,qBAAqB,KAAwB;CAC3D,IAAI,QAAQ,iBAAiB,OAAO,MAAM;EACxC,MAAM,EAAE,MAAM,YAAY,EAAE,IAAI,MAAM,OAAO;EAC7C,MAAM,UAAU,MAAM,yBAAyB,GAAG,MAAM,GAAG,UAAU;EACrE,MAAM,QAAQ,MAAM,gBAAgB,QAAQ,IAAI,OAAO;EACvD,OAAO,EAAE,KAAK,MAAM,cAAc,SAAS,KAAK,CAAC;CACnD,CAAC;CAED,IAAI,QAAQ,mBAAmB,OAAO,MAAM;EAC1C,MAAM,EAAE,MAAM,YAAY,EAAE,IAAI,MAAM,OAAO;EAC7C,MAAM,UAAU,MAAM,yBAAyB,GAAG,MAAM,GAAG,cAAc;EACzE,MAAM,QAAQ,MAAM,gBAAgB,QAAQ,IAAI,OAAO;EACvD,MAAM,SAAS,MAAM,gBAAgB,CAAC;EACtC,MAAM,SAAS,MAAM,YAAY,SAAS,OAAO,QAAQ,EAAE,IAAI,MAAM,MAAM,CAAC;EAC5E,UAAU,CAAC;EACX,QAAQ,GAAG,kBAAkB;EAC7B,OAAO,EAAE,KAAK,QAAQ,GAAG;CAC3B,CAAC;AACH"}
@@ -5,15 +5,16 @@ import { credentialKeys } from "../credential-keys.mjs";
5
5
  import { getStore } from "../store.mjs";
6
6
  import { notifyProject } from "../notify.mjs";
7
7
  import { notFound } from "./helpers.mjs";
8
- import { assertBaselineCurrent } from "./baseline-guard.mjs";
8
+ import { requestBuildState } from "../git-status.mjs";
9
+ import { assertBaselineCurrent, baselineChanged } from "./baseline-guard.mjs";
9
10
  import { z } from "@hono/zod-openapi";
10
- import { extractStorybookToScratch, persistStorybookStatics } from "@storyshelf/core/capture";
11
- import { BaselineModel, BuildModel, LabelModel, ProjectModel, SnapshotModel } from "@storyshelf/core/models";
12
- import { BUILD_STATUSES } from "@storyshelf/core/types";
11
+ import { BaselineApprovalConflict, BaselineModel, BaselineWriteConflict, BuildModel, LabelModel, ProjectModel, SnapshotModel } from "@storyshelf/core/models";
13
12
  import { HTTPException } from "hono/http-exception";
14
13
  import { emitWebhookEvent } from "@storyshelf/core/adapter/webhook-events";
14
+ import { BUILD_STATUSES } from "@storyshelf/core/types";
15
+ import { ReferenceGenerationConflict } from "@storyshelf/core/adapter/database";
15
16
  import { Readable, Transform } from "node:stream";
16
- //#region src/routers/builds.handlers.ts
17
+ // src/routers/builds.handlers.ts
17
18
  /**
18
19
  * Create a build record with labels and the `build:created` webhook.
19
20
  * Shared by the build-creation route.
@@ -48,10 +49,12 @@ async function createBuildRecord(project, meta) {
48
49
  authorName: meta.authorName,
49
50
  message: meta.message
50
51
  }, `/builds/${build.id}`);
52
+ await requestBuildState(build.id);
51
53
  return build;
52
54
  }
53
55
  /**
54
- * Stream a raw request body into storage with a byte cap.
56
+ * Stream a raw request body into storage with a byte cap, counting the bytes as they pass (a
57
+ * chunked upload has no `Content-Length`). Resolves to the bytes stored.
55
58
  * Throws 400 for a missing body, 413 once `maxBytes` is exceeded.
56
59
  */
57
60
  async function storeUploadStream(storage, path, body, maxBytes) {
@@ -65,34 +68,12 @@ async function storeUploadStream(storage, path, body, maxBytes) {
65
68
  try {
66
69
  const nodeBody = Readable.fromWeb(body);
67
70
  await storage.writeStream(path, nodeBody.pipe(limiter));
71
+ return seen;
68
72
  } catch (error) {
69
73
  if (seen > maxBytes) throw new HTTPException(413, { message: "Upload exceeds size limit" });
70
74
  throw error;
71
75
  }
72
76
  }
73
- /**
74
- * Best-effort inline statics extraction for small uploads (see
75
- * `maxInlineUnzipSize`). Never throws: failures are logged and the capture
76
- * job backfills the same keys idempotently.
77
- */
78
- async function persistInlineStatics(storage, scratchDir, maxInlineBytes, contentLength, projectId, buildId, logger) {
79
- if (!scratchDir || !maxInlineBytes) return;
80
- const size = parseContentLength(contentLength);
81
- if (size === void 0 || size > maxInlineBytes) return;
82
- try {
83
- const extractedDir = await extractStorybookToScratch(storage, scratchDir, projectId, buildId);
84
- await persistStorybookStatics(storage, extractedDir, projectId, buildId);
85
- logger.info({
86
- buildId,
87
- size
88
- }, "upload statics persisted inline");
89
- } catch (error) {
90
- logger.error({
91
- err: error,
92
- buildId
93
- }, "inline statics persist failed; capture will backfill");
94
- }
95
- }
96
77
  /** Query filters accepted by the build list endpoint. */
97
78
  const buildListQuery = z.object({
98
79
  status: z.enum(BUILD_STATUSES).optional(),
@@ -112,34 +93,46 @@ async function snapshotForBuild(build, snapshotId) {
112
93
  if (!snapshot || snapshot.buildId !== build.id) notFound("Snapshot not found");
113
94
  return snapshot;
114
95
  }
96
+ /** Roll snapshot statuses up into the build's review status. */
97
+ function rollupStatus(snapshots) {
98
+ if (snapshots.some((s) => s.status === "new" || s.status === "changed")) return "reviewing";
99
+ return snapshots.some((s) => s.status === "rejected") ? "rejected" : "approved";
100
+ }
115
101
  /** Recompute a build's counts and roll its status up from its snapshots. */
116
102
  async function refreshBuild(buildId) {
117
- const { db, config } = getStore();
103
+ const { db } = getStore();
118
104
  await new BuildModel(db).updateCounts(buildId);
119
105
  const snapshots = await new SnapshotModel(db).listByBuild(buildId);
120
- const unresolved = snapshots.some((s) => s.status === "new" || s.status === "changed");
121
- let status;
122
- if (unresolved) status = "reviewing";
123
- else status = snapshots.some((s) => s.status === "rejected") ? "rejected" : "approved";
106
+ const status = rollupStatus(snapshots);
124
107
  const build = await new BuildModel(db).get(buildId);
125
- if (build) {
126
- await new BuildModel(db).setStatus(buildId, status);
127
- await emitWebhookEvent(db, { webhooks: getStore().db.tables.webhooks }, build.projectId, `build:${status}`, {
128
- buildId,
129
- status,
130
- snapshotCount: snapshots.length
131
- }, credentialKeys(config));
132
- const project = await new ProjectModel(db).get(build.projectId);
133
- if (project) await notifyProject(project, `build:${status}`, {
134
- buildId,
135
- status,
136
- snapshotCount: snapshots.length
137
- }, `/builds/${buildId}`);
138
- }
108
+ if (!build || build.status === "cancelled") return;
109
+ await new BuildModel(db).setStatus(buildId, status);
110
+ await announceBuildStatus(buildId, status, { snapshotCount: snapshots.length });
111
+ }
112
+ /**
113
+ * Tell the world a build reached `status`: its git channels, the `build:<status>` webhook and the
114
+ * project's notification channels. Every route that moves a build to a decided status ends here.
115
+ */
116
+ async function announceBuildStatus(buildId, status, extra = {}) {
117
+ const { db, config } = getStore();
118
+ const build = await new BuildModel(db).get(buildId);
119
+ if (!build) return;
120
+ await requestBuildState(buildId);
121
+ const payload = {
122
+ buildId,
123
+ status,
124
+ ...extra
125
+ };
126
+ await emitWebhookEvent(db, { webhooks: getStore().db.tables.webhooks }, build.projectId, `build:${status}`, payload, credentialKeys(config));
127
+ const project = await new ProjectModel(db).get(build.projectId);
128
+ if (!project) return;
129
+ await notifyProject(project, `build:${status}`, payload, `/builds/${buildId}`);
139
130
  }
140
131
  /**
141
132
  * Approve a snapshot, promote its screenshot to baseline, and refresh the build.
142
133
  * Refuses with 409 when the baseline changed since the diff, unless `force` is set.
134
+ * Bulk callers pass `deferRefresh` and call {@link refreshBuild} once, so the build's
135
+ * outcome (and its git status) is announced a single time.
143
136
  */
144
137
  async function approveSnapshot(snapshotId, userId, options = {}) {
145
138
  const { db, config } = getStore();
@@ -149,19 +142,38 @@ async function approveSnapshot(snapshotId, userId, options = {}) {
149
142
  const project = await new ProjectModel(db).get(snapshot.projectId);
150
143
  const build = await new BuildModel(db).get(snapshot.buildId);
151
144
  if (!project || !build) notFound("Project or build not found");
145
+ if (build.status === "cancelled") throw new HTTPException(409, { message: "The build was cancelled; retry it to review it" });
152
146
  await assertBaselineCurrent(project, build, snapshot, options.force ?? false);
153
- await snapshots.review(snapshotId, "approved", userId);
147
+ if (!db.buildReferenceLifecycle) await snapshots.review(snapshotId, "approved", userId);
154
148
  const baselines = new BaselineModel(db, void 0, getStore().storage, credentialKeys(config));
155
- const prior = await baselines.getFor(project.id, snapshot.storyId, snapshot.viewportName, build.gitBranch);
156
- await baselines.upsert(project.id, snapshot.storyId, snapshot.viewportName, build.gitBranch, snapshot.id, snapshot.screenshotPath);
149
+ const prior = await baselines.getFor(project.id, snapshot.storyId, snapshot.viewportName, build.gitBranch, snapshot.browser);
150
+ try {
151
+ await baselines.upsert(project.id, snapshot.storyId, snapshot.viewportName, build.gitBranch, snapshot.id, snapshot.screenshotPath, void 0, {
152
+ browser: snapshot.browser,
153
+ mode: snapshot.mode,
154
+ approval: db.buildReferenceLifecycle ? {
155
+ snapshot,
156
+ reviewedBy: userId,
157
+ force: options.force ?? false,
158
+ branch: build.gitBranch
159
+ } : void 0
160
+ });
161
+ } catch (error) {
162
+ if (error instanceof BaselineWriteConflict || error instanceof ReferenceGenerationConflict) {
163
+ if (error instanceof BaselineApprovalConflict && error.driftStatus) throw baselineChanged(snapshot.id, error.driftStatus);
164
+ throw new HTTPException(409, { message: error.message });
165
+ }
166
+ throw error;
167
+ }
157
168
  await notifyProject(project, prior ? "baseline:updated" : "baseline:created", {
158
169
  storyId: snapshot.storyId,
159
170
  viewport: snapshot.viewportName,
171
+ browser: snapshot.browser ?? "chromium",
160
172
  branch: build.gitBranch,
161
173
  snapshotId: snapshot.id,
162
174
  buildId: build.id
163
175
  }, `/builds/${build.id}`);
164
- await refreshBuild(build.id);
176
+ if (!options.deferRefresh) await refreshBuild(build.id);
165
177
  }
166
178
  /** Roles permitted to view builds, snapshots, and comments. */
167
179
  const VIEW_ROLES = [
@@ -205,13 +217,6 @@ async function attachLabels(db, projectId, buildId, labels) {
205
217
  await models.attach(projectId, buildId, key, value);
206
218
  }
207
219
  }
208
- /** Parse a Content-Length header value into bytes, or undefined when unknown. */
209
- function parseContentLength(value) {
210
- if (!value) return;
211
- const parsed = Math.trunc(Number(value));
212
- return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : void 0;
213
- }
214
- //#endregion
215
- export { APPROVER_ROLES, DEVELOPER_ROLES, VIEW_ROLES, approveSnapshot, buildCreateJsonSchema, buildForProject, buildListQuery, createBuildRecord, persistInlineStatics, refreshBuild, snapshotForBuild, storeUploadStream };
220
+ export { APPROVER_ROLES, DEVELOPER_ROLES, VIEW_ROLES, announceBuildStatus, approveSnapshot, buildCreateJsonSchema, buildForProject, buildListQuery, createBuildRecord, refreshBuild, snapshotForBuild, storeUploadStream };
216
221
 
217
222
  //# sourceMappingURL=builds.handlers.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"builds.handlers.mjs","names":[],"sources":["../../src/routers/builds.handlers.ts"],"sourcesContent":["import { z } from \"@hono/zod-openapi\";\nimport type { DatabaseAdapter } from \"@storyshelf/core/adapter/database\";\nimport type { StorageAdapter } from \"@storyshelf/core/adapter/storage\";\nimport { emitWebhookEvent } from \"@storyshelf/core/adapter/webhook-events\";\nimport { extractStorybookToScratch, persistStorybookStatics } from \"@storyshelf/core/capture\";\nimport type { Logger } from \"@storyshelf/core/logger\";\nimport { BaselineModel } from \"@storyshelf/core/models\";\nimport { BuildModel } from \"@storyshelf/core/models\";\nimport { LabelModel } from \"@storyshelf/core/models\";\nimport { ProjectModel } from \"@storyshelf/core/models\";\nimport { SnapshotModel } from \"@storyshelf/core/models\";\nimport type { Project } from \"@storyshelf/core/schema\";\nimport type { Build, Snapshot } from \"@storyshelf/core/schema\";\nimport { type ProjectRole, BUILD_STATUSES } from \"@storyshelf/core/types\";\nimport { HTTPException } from \"hono/http-exception\";\nimport { Readable, Transform } from \"node:stream\";\nimport type { ReadableStream as NodeWebStream } from \"node:stream/web\";\nimport { credentialKeys } from \"../credential-keys.ts\";\nimport { notifyProject } from \"../notify.ts\";\nimport { getStore } from \"../store.ts\";\nimport { assertBaselineCurrent } from \"./baseline-guard.ts\";\nimport { notFound } from \"./helpers.ts\";\n/**\n * Create a build record with labels and the `build:created` webhook.\n * Shared by the build-creation route.\n */\nexport async function createBuildRecord(\n project: Project,\n meta: BuildCreateMetadata,\n): Promise<Build> {\n const { db, config } = getStore();\n if (!meta.gitSha || !meta.gitBranch) {\n throw new HTTPException(400, {\n message: \"gitSha and gitBranch are required (revision SHA and baseline namespace)\",\n });\n }\n const build = await new BuildModel(db).create(project.id, {\n gitSha: meta.gitSha,\n gitBranch: meta.gitBranch,\n isDefault: meta.gitBranch === project.gitDefaultBranch,\n authorEmail: meta.authorEmail,\n authorName: meta.authorName,\n message: meta.message,\n affectedOnly: meta.affectedOnly,\n baselineSha: await resolveBaselineSha(db, project.id, meta.gitBranch, meta.gitSha),\n });\n if (meta.labels && meta.labels.length > 0) {\n await attachLabels(db, project.id, build.id, meta.labels);\n }\n await emitWebhookEvent(\n db,\n { webhooks: getStore().db.tables.webhooks },\n project.id,\n \"build:created\",\n {\n buildId: build.id,\n gitSha: meta.gitSha,\n gitBranch: meta.gitBranch,\n authorEmail: meta.authorEmail,\n authorName: meta.authorName,\n message: meta.message,\n },\n credentialKeys(config),\n );\n await notifyProject(\n project,\n \"build:created\",\n {\n buildId: build.id,\n gitSha: meta.gitSha,\n gitBranch: meta.gitBranch,\n authorEmail: meta.authorEmail,\n authorName: meta.authorName,\n message: meta.message,\n },\n `/builds/${build.id}`,\n );\n return build;\n}\n\n/**\n * Stream a raw request body into storage with a byte cap.\n * Throws 400 for a missing body, 413 once `maxBytes` is exceeded.\n */\nexport async function storeUploadStream(\n storage: StorageAdapter,\n path: string,\n body: ReadableStream<Uint8Array> | null,\n maxBytes: number,\n): Promise<void> {\n if (!body) {\n throw new HTTPException(400, { message: \"Missing request body\" });\n }\n let seen = 0;\n const limiter = new Transform({\n transform(\n chunk: Buffer,\n _encoding: string,\n callback: (error?: Error | null, data?: Buffer) => void,\n ): void {\n seen += chunk.length;\n if (seen > maxBytes) {\n callback(new Error(`Upload exceeds ${maxBytes} byte limit`));\n } else {\n callback(null, chunk);\n }\n },\n });\n try {\n const nodeBody = Readable.fromWeb(body as unknown as NodeWebStream);\n await storage.writeStream(path, nodeBody.pipe(limiter));\n } catch (error) {\n if (seen > maxBytes) {\n throw new HTTPException(413, { message: \"Upload exceeds size limit\" });\n }\n throw error;\n }\n}\n\n/**\n * Best-effort inline statics extraction for small uploads (see\n * `maxInlineUnzipSize`). Never throws: failures are logged and the capture\n * job backfills the same keys idempotently.\n */\nexport async function persistInlineStatics(\n storage: StorageAdapter,\n scratchDir: string | undefined,\n maxInlineBytes: number | undefined,\n contentLength: string | undefined,\n projectId: string,\n buildId: string,\n logger: Logger,\n): Promise<void> {\n if (!scratchDir || !maxInlineBytes) {\n return;\n }\n const size = parseContentLength(contentLength);\n if (size === undefined || size > maxInlineBytes) {\n return;\n }\n try {\n const extractedDir = await extractStorybookToScratch(storage, scratchDir, projectId, buildId);\n await persistStorybookStatics(storage, extractedDir, projectId, buildId);\n logger.info({ buildId, size }, \"upload statics persisted inline\");\n } catch (error) {\n logger.error({ err: error, buildId }, \"inline statics persist failed; capture will backfill\");\n }\n}\n\n/** Query filters accepted by the build list endpoint. */\nexport const buildListQuery = z.object({\n status: z.enum(BUILD_STATUSES).optional(),\n branch: z.string().optional(),\n labelKey: z.string().optional(),\n labelValue: z.string().optional(),\n});\n\n/** Fetch a build scoped to its project, throwing 404 when it does not belong. */\nexport async function buildForProject(projectId: string, buildId: string): Promise<Build> {\n const build = await new BuildModel(getStore().db).get(buildId);\n if (!build || build.projectId !== projectId) {\n notFound(\"Build not found\");\n }\n return build;\n}\n\n/** Fetch a snapshot scoped to its build, throwing 404 when it does not belong. */\nexport async function snapshotForBuild(\n build: { id: string },\n snapshotId: string,\n): Promise<Snapshot> {\n const snapshot = await new SnapshotModel(getStore().db).get(snapshotId);\n if (!snapshot || snapshot.buildId !== build.id) {\n notFound(\"Snapshot not found\");\n }\n return snapshot;\n}\n\n/** Recompute a build's counts and roll its status up from its snapshots. */\nexport async function refreshBuild(buildId: string): Promise<void> {\n const { db, config } = getStore();\n await new BuildModel(db).updateCounts(buildId);\n const snapshots = await new SnapshotModel(db).listByBuild(buildId);\n const unresolved = snapshots.some((s) => s.status === \"new\" || s.status === \"changed\");\n let status: \"reviewing\" | \"rejected\" | \"approved\";\n if (unresolved) {\n status = \"reviewing\";\n } else {\n const rejected = snapshots.some((s) => s.status === \"rejected\");\n status = rejected ? \"rejected\" : \"approved\";\n }\n const build = await new BuildModel(db).get(buildId);\n if (build) {\n await new BuildModel(db).setStatus(buildId, status);\n await emitWebhookEvent(\n db,\n { webhooks: getStore().db.tables.webhooks },\n build.projectId,\n `build:${status}`,\n {\n buildId,\n status,\n snapshotCount: snapshots.length,\n },\n credentialKeys(config),\n );\n const project = await new ProjectModel(db).get(build.projectId);\n if (project) {\n await notifyProject(\n project,\n `build:${status}`,\n { buildId, status, snapshotCount: snapshots.length },\n `/builds/${buildId}`,\n );\n }\n }\n}\n\n/**\n * Approve a snapshot, promote its screenshot to baseline, and refresh the build.\n * Refuses with 409 when the baseline changed since the diff, unless `force` is set.\n */\nexport async function approveSnapshot(\n snapshotId: string,\n userId: string | null,\n options: { force?: boolean } = {},\n): Promise<void> {\n const { db, config } = getStore();\n const snapshots = new SnapshotModel(db);\n const snapshot = await snapshots.get(snapshotId);\n if (!snapshot) {\n notFound(\"Snapshot not found\");\n }\n const project = await new ProjectModel(db).get(snapshot.projectId);\n const build = await new BuildModel(db).get(snapshot.buildId);\n if (!project || !build) {\n notFound(\"Project or build not found\");\n }\n await assertBaselineCurrent(project, build, snapshot, options.force ?? false);\n await snapshots.review(snapshotId, \"approved\", userId);\n const baselines = new BaselineModel(db, undefined, getStore().storage, credentialKeys(config));\n const prior = await baselines.getFor(\n project.id,\n snapshot.storyId,\n snapshot.viewportName,\n build.gitBranch,\n );\n await baselines.upsert(\n project.id,\n snapshot.storyId,\n snapshot.viewportName,\n build.gitBranch,\n snapshot.id,\n snapshot.screenshotPath,\n );\n await notifyProject(\n project,\n prior ? \"baseline:updated\" : \"baseline:created\",\n {\n storyId: snapshot.storyId,\n viewport: snapshot.viewportName,\n branch: build.gitBranch,\n snapshotId: snapshot.id,\n buildId: build.id,\n },\n `/builds/${build.id}`,\n );\n await refreshBuild(build.id);\n}\n\n/** Roles permitted to view builds, snapshots, and comments. */\nexport const VIEW_ROLES: readonly ProjectRole[] = [\"viewer\", \"developer\", \"approver\", \"admin\"];\n/** Roles permitted to upload builds and write comments. */\nexport const DEVELOPER_ROLES: readonly ProjectRole[] = [\"developer\", \"approver\", \"admin\"];\n/** Roles permitted to approve or reject snapshots and delete builds. */\nexport const APPROVER_ROLES: readonly ProjectRole[] = [\"approver\", \"admin\"];\n\n/** JSON input schema for creating a build before streaming its bundle. */\nexport const buildCreateJsonSchema = z\n .object({\n gitSha: z.string().min(1),\n gitBranch: z.string().min(1),\n authorEmail: z.string().optional(),\n authorName: z.string().optional(),\n message: z.string().optional(),\n affectedOnly: z.boolean().optional(),\n labels: z.array(z.object({ key: z.string().min(1), value: z.string() })).optional(),\n })\n .openapi(\"BuildCreate\");\n\n/** Metadata accepted by either build-creation route. */\nexport interface BuildCreateMetadata {\n gitSha: string;\n gitBranch: string;\n authorEmail?: string;\n authorName?: string;\n message?: string;\n affectedOnly?: boolean;\n labels?: { key: string; value: string }[];\n}\n\n/** Latest prior revision on a branch with a build, or null for first builds. */\nasync function resolveBaselineSha(\n db: DatabaseAdapter,\n projectId: string,\n branch: string,\n sha: string,\n): Promise<string | null> {\n const builds = await new BuildModel(db).list(projectId, { branch });\n return builds.find((build) => build.gitSha !== sha)?.gitSha ?? null;\n}\n\n/* oxlint-disable eslint/no-await-in-loop -- label attach is intentionally sequential */\nasync function attachLabels(\n db: DatabaseAdapter,\n projectId: string,\n buildId: string,\n labels: { key: string; value: string }[],\n): Promise<void> {\n const models = new LabelModel(db);\n for (const { key, value } of labels) {\n const existing = await models.getType(projectId, key);\n if (!existing) {\n await models.createType(projectId, { key, name: key });\n }\n await models.attach(projectId, buildId, key, value);\n }\n}\n/* oxlint-enable eslint/no-await-in-loop */\n\n/** Parse a Content-Length header value into bytes, or undefined when unknown. */\nfunction parseContentLength(value: string | undefined): number | undefined {\n if (!value) {\n return undefined;\n }\n const parsed = Math.trunc(Number(value));\n return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : undefined;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AA0BA,eAAsB,kBACpB,SACA,MACgB;CAChB,MAAM,EAAE,IAAI,WAAW,SAAS;CAChC,IAAI,CAAC,KAAK,UAAU,CAAC,KAAK,WACxB,MAAM,IAAI,cAAc,KAAK,EAC3B,SAAS,0EACX,CAAC;CAEH,MAAM,QAAQ,MAAM,IAAI,WAAW,EAAE,CAAC,CAAC,OAAO,QAAQ,IAAI;EACxD,QAAQ,KAAK;EACb,WAAW,KAAK;EAChB,WAAW,KAAK,cAAc,QAAQ;EACtC,aAAa,KAAK;EAClB,YAAY,KAAK;EACjB,SAAS,KAAK;EACd,cAAc,KAAK;EACnB,aAAa,MAAM,mBAAmB,IAAI,QAAQ,IAAI,KAAK,WAAW,KAAK,MAAM;CACnF,CAAC;CACD,IAAI,KAAK,UAAU,KAAK,OAAO,SAAS,GACtC,MAAM,aAAa,IAAI,QAAQ,IAAI,MAAM,IAAI,KAAK,MAAM;CAE1D,MAAM,iBACJ,IACA,EAAE,UAAU,SAAS,CAAC,CAAC,GAAG,OAAO,SAAS,GAC1C,QAAQ,IACR,iBACA;EACE,SAAS,MAAM;EACf,QAAQ,KAAK;EACb,WAAW,KAAK;EAChB,aAAa,KAAK;EAClB,YAAY,KAAK;EACjB,SAAS,KAAK;CAChB,GACA,eAAe,MAAM,CACvB;CACA,MAAM,cACJ,SACA,iBACA;EACE,SAAS,MAAM;EACf,QAAQ,KAAK;EACb,WAAW,KAAK;EAChB,aAAa,KAAK;EAClB,YAAY,KAAK;EACjB,SAAS,KAAK;CAChB,GACA,WAAW,MAAM,IACnB;CACA,OAAO;AACT;;;;;AAMA,eAAsB,kBACpB,SACA,MACA,MACA,UACe;CACf,IAAI,CAAC,MACH,MAAM,IAAI,cAAc,KAAK,EAAE,SAAS,uBAAuB,CAAC;CAElE,IAAI,OAAO;CACX,MAAM,UAAU,IAAI,UAAU,EAC5B,UACE,OACA,WACA,UACM;EACN,QAAQ,MAAM;EACd,IAAI,OAAO,UACT,yBAAS,IAAI,MAAM,kBAAkB,SAAS,YAAY,CAAC;OAE3D,SAAS,MAAM,KAAK;CAExB,EACF,CAAC;CACD,IAAI;EACF,MAAM,WAAW,SAAS,QAAQ,IAAgC;EAClE,MAAM,QAAQ,YAAY,MAAM,SAAS,KAAK,OAAO,CAAC;CACxD,SAAS,OAAO;EACd,IAAI,OAAO,UACT,MAAM,IAAI,cAAc,KAAK,EAAE,SAAS,4BAA4B,CAAC;EAEvE,MAAM;CACR;AACF;;;;;;AAOA,eAAsB,qBACpB,SACA,YACA,gBACA,eACA,WACA,SACA,QACe;CACf,IAAI,CAAC,cAAc,CAAC,gBAClB;CAEF,MAAM,OAAO,mBAAmB,aAAa;CAC7C,IAAI,SAAS,KAAA,KAAa,OAAO,gBAC/B;CAEF,IAAI;EACF,MAAM,eAAe,MAAM,0BAA0B,SAAS,YAAY,WAAW,OAAO;EAC5F,MAAM,wBAAwB,SAAS,cAAc,WAAW,OAAO;EACvE,OAAO,KAAK;GAAE;GAAS;EAAK,GAAG,iCAAiC;CAClE,SAAS,OAAO;EACd,OAAO,MAAM;GAAE,KAAK;GAAO;EAAQ,GAAG,sDAAsD;CAC9F;AACF;;AAGA,MAAa,iBAAiB,EAAE,OAAO;CACrC,QAAQ,EAAE,KAAK,cAAc,CAAC,CAAC,SAAS;CACxC,QAAQ,EAAE,OAAO,CAAC,CAAC,SAAS;CAC5B,UAAU,EAAE,OAAO,CAAC,CAAC,SAAS;CAC9B,YAAY,EAAE,OAAO,CAAC,CAAC,SAAS;AAClC,CAAC;;AAGD,eAAsB,gBAAgB,WAAmB,SAAiC;CACxF,MAAM,QAAQ,MAAM,IAAI,WAAW,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,OAAO;CAC7D,IAAI,CAAC,SAAS,MAAM,cAAc,WAChC,SAAS,iBAAiB;CAE5B,OAAO;AACT;;AAGA,eAAsB,iBACpB,OACA,YACmB;CACnB,MAAM,WAAW,MAAM,IAAI,cAAc,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,UAAU;CACtE,IAAI,CAAC,YAAY,SAAS,YAAY,MAAM,IAC1C,SAAS,oBAAoB;CAE/B,OAAO;AACT;;AAGA,eAAsB,aAAa,SAAgC;CACjE,MAAM,EAAE,IAAI,WAAW,SAAS;CAChC,MAAM,IAAI,WAAW,EAAE,CAAC,CAAC,aAAa,OAAO;CAC7C,MAAM,YAAY,MAAM,IAAI,cAAc,EAAE,CAAC,CAAC,YAAY,OAAO;CACjE,MAAM,aAAa,UAAU,MAAM,MAAM,EAAE,WAAW,SAAS,EAAE,WAAW,SAAS;CACrF,IAAI;CACJ,IAAI,YACF,SAAS;MAGT,SADiB,UAAU,MAAM,MAAM,EAAE,WAAW,UACpC,IAAI,aAAa;CAEnC,MAAM,QAAQ,MAAM,IAAI,WAAW,EAAE,CAAC,CAAC,IAAI,OAAO;CAClD,IAAI,OAAO;EACT,MAAM,IAAI,WAAW,EAAE,CAAC,CAAC,UAAU,SAAS,MAAM;EAClD,MAAM,iBACJ,IACA,EAAE,UAAU,SAAS,CAAC,CAAC,GAAG,OAAO,SAAS,GAC1C,MAAM,WACN,SAAS,UACT;GACE;GACA;GACA,eAAe,UAAU;EAC3B,GACA,eAAe,MAAM,CACvB;EACA,MAAM,UAAU,MAAM,IAAI,aAAa,EAAE,CAAC,CAAC,IAAI,MAAM,SAAS;EAC9D,IAAI,SACF,MAAM,cACJ,SACA,SAAS,UACT;GAAE;GAAS;GAAQ,eAAe,UAAU;EAAO,GACnD,WAAW,SACb;CAEJ;AACF;;;;;AAMA,eAAsB,gBACpB,YACA,QACA,UAA+B,CAAC,GACjB;CACf,MAAM,EAAE,IAAI,WAAW,SAAS;CAChC,MAAM,YAAY,IAAI,cAAc,EAAE;CACtC,MAAM,WAAW,MAAM,UAAU,IAAI,UAAU;CAC/C,IAAI,CAAC,UACH,SAAS,oBAAoB;CAE/B,MAAM,UAAU,MAAM,IAAI,aAAa,EAAE,CAAC,CAAC,IAAI,SAAS,SAAS;CACjE,MAAM,QAAQ,MAAM,IAAI,WAAW,EAAE,CAAC,CAAC,IAAI,SAAS,OAAO;CAC3D,IAAI,CAAC,WAAW,CAAC,OACf,SAAS,4BAA4B;CAEvC,MAAM,sBAAsB,SAAS,OAAO,UAAU,QAAQ,SAAS,KAAK;CAC5E,MAAM,UAAU,OAAO,YAAY,YAAY,MAAM;CACrD,MAAM,YAAY,IAAI,cAAc,IAAI,KAAA,GAAW,SAAS,CAAC,CAAC,SAAS,eAAe,MAAM,CAAC;CAC7F,MAAM,QAAQ,MAAM,UAAU,OAC5B,QAAQ,IACR,SAAS,SACT,SAAS,cACT,MAAM,SACR;CACA,MAAM,UAAU,OACd,QAAQ,IACR,SAAS,SACT,SAAS,cACT,MAAM,WACN,SAAS,IACT,SAAS,cACX;CACA,MAAM,cACJ,SACA,QAAQ,qBAAqB,oBAC7B;EACE,SAAS,SAAS;EAClB,UAAU,SAAS;EACnB,QAAQ,MAAM;EACd,YAAY,SAAS;EACrB,SAAS,MAAM;CACjB,GACA,WAAW,MAAM,IACnB;CACA,MAAM,aAAa,MAAM,EAAE;AAC7B;;AAGA,MAAa,aAAqC;CAAC;CAAU;CAAa;CAAY;AAAO;;AAE7F,MAAa,kBAA0C;CAAC;CAAa;CAAY;AAAO;;AAExF,MAAa,iBAAyC,CAAC,YAAY,OAAO;;AAG1E,MAAa,wBAAwB,EAClC,OAAO;CACN,QAAQ,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC;CACxB,WAAW,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC;CAC3B,aAAa,EAAE,OAAO,CAAC,CAAC,SAAS;CACjC,YAAY,EAAE,OAAO,CAAC,CAAC,SAAS;CAChC,SAAS,EAAE,OAAO,CAAC,CAAC,SAAS;CAC7B,cAAc,EAAE,QAAQ,CAAC,CAAC,SAAS;CACnC,QAAQ,EAAE,MAAM,EAAE,OAAO;EAAE,KAAK,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC;EAAG,OAAO,EAAE,OAAO;CAAE,CAAC,CAAC,CAAC,CAAC,SAAS;AACpF,CAAC,CAAC,CACD,QAAQ,aAAa;;AAcxB,eAAe,mBACb,IACA,WACA,QACA,KACwB;CAExB,QAAO,MADc,IAAI,WAAW,EAAE,CAAC,CAAC,KAAK,WAAW,EAAE,OAAO,CAAC,EAAA,CACpD,MAAM,UAAU,MAAM,WAAW,GAAG,CAAC,EAAE,UAAU;AACjE;AAGA,eAAe,aACb,IACA,WACA,SACA,QACe;CACf,MAAM,SAAS,IAAI,WAAW,EAAE;CAChC,KAAK,MAAM,EAAE,KAAK,WAAW,QAAQ;EAEnC,IAAI,CAAC,MADkB,OAAO,QAAQ,WAAW,GAAG,GAElD,MAAM,OAAO,WAAW,WAAW;GAAE;GAAK,MAAM;EAAI,CAAC;EAEvD,MAAM,OAAO,OAAO,WAAW,SAAS,KAAK,KAAK;CACpD;AACF;;AAIA,SAAS,mBAAmB,OAA+C;CACzE,IAAI,CAAC,OACH;CAEF,MAAM,SAAS,KAAK,MAAM,OAAO,KAAK,CAAC;CACvC,OAAO,OAAO,cAAc,MAAM,KAAK,SAAS,IAAI,SAAS,KAAA;AAC/D"}
1
+ {"version":3,"file":"builds.handlers.mjs","names":[],"sources":["../../src/routers/builds.handlers.ts"],"sourcesContent":["import { z } from \"@hono/zod-openapi\";\nimport {\n ReferenceGenerationConflict,\n type DatabaseAdapter,\n} from \"@storyshelf/core/adapter/database\";\nimport type { StorageAdapter } from \"@storyshelf/core/adapter/storage\";\nimport { emitWebhookEvent } from \"@storyshelf/core/adapter/webhook-events\";\nimport {\n BaselineApprovalConflict,\n BaselineModel,\n BaselineWriteConflict,\n} from \"@storyshelf/core/models\";\nimport { BuildModel } from \"@storyshelf/core/models\";\nimport { LabelModel } from \"@storyshelf/core/models\";\nimport { ProjectModel } from \"@storyshelf/core/models\";\nimport { SnapshotModel } from \"@storyshelf/core/models\";\nimport type { Project } from \"@storyshelf/core/schema\";\nimport type { Build, Snapshot } from \"@storyshelf/core/schema\";\nimport { type BuildStatus, type ProjectRole, BUILD_STATUSES } from \"@storyshelf/core/types\";\nimport { HTTPException } from \"hono/http-exception\";\nimport { Readable, Transform } from \"node:stream\";\nimport type { ReadableStream as NodeWebStream } from \"node:stream/web\";\nimport { credentialKeys } from \"../credential-keys.ts\";\nimport { requestBuildState } from \"../git-status.ts\";\nimport { notifyProject } from \"../notify.ts\";\nimport { getStore } from \"../store.ts\";\nimport { assertBaselineCurrent, baselineChanged } from \"./baseline-guard.ts\";\nimport { notFound } from \"./helpers.ts\";\n/**\n * Create a build record with labels and the `build:created` webhook.\n * Shared by the build-creation route.\n */\nexport async function createBuildRecord(\n project: Project,\n meta: BuildCreateMetadata,\n): Promise<Build> {\n const { db, config } = getStore();\n if (!meta.gitSha || !meta.gitBranch) {\n throw new HTTPException(400, {\n message: \"gitSha and gitBranch are required (revision SHA and baseline namespace)\",\n });\n }\n const build = await new BuildModel(db).create(project.id, {\n gitSha: meta.gitSha,\n gitBranch: meta.gitBranch,\n isDefault: meta.gitBranch === project.gitDefaultBranch,\n authorEmail: meta.authorEmail,\n authorName: meta.authorName,\n message: meta.message,\n affectedOnly: meta.affectedOnly,\n baselineSha: await resolveBaselineSha(db, project.id, meta.gitBranch, meta.gitSha),\n });\n if (meta.labels && meta.labels.length > 0) {\n await attachLabels(db, project.id, build.id, meta.labels);\n }\n await emitWebhookEvent(\n db,\n { webhooks: getStore().db.tables.webhooks },\n project.id,\n \"build:created\",\n {\n buildId: build.id,\n gitSha: meta.gitSha,\n gitBranch: meta.gitBranch,\n authorEmail: meta.authorEmail,\n authorName: meta.authorName,\n message: meta.message,\n },\n credentialKeys(config),\n );\n await notifyProject(\n project,\n \"build:created\",\n {\n buildId: build.id,\n gitSha: meta.gitSha,\n gitBranch: meta.gitBranch,\n authorEmail: meta.authorEmail,\n authorName: meta.authorName,\n message: meta.message,\n },\n `/builds/${build.id}`,\n );\n await requestBuildState(build.id);\n return build;\n}\n\n/**\n * Stream a raw request body into storage with a byte cap, counting the bytes as they pass (a\n * chunked upload has no `Content-Length`). Resolves to the bytes stored.\n * Throws 400 for a missing body, 413 once `maxBytes` is exceeded.\n */\nexport async function storeUploadStream(\n storage: StorageAdapter,\n path: string,\n body: ReadableStream<Uint8Array> | null,\n maxBytes: number,\n): Promise<number> {\n if (!body) {\n throw new HTTPException(400, { message: \"Missing request body\" });\n }\n let seen = 0;\n const limiter = new Transform({\n transform(\n chunk: Buffer,\n _encoding: string,\n callback: (error?: Error | null, data?: Buffer) => void,\n ): void {\n seen += chunk.length;\n if (seen > maxBytes) {\n callback(new Error(`Upload exceeds ${maxBytes} byte limit`));\n } else {\n callback(null, chunk);\n }\n },\n });\n try {\n const nodeBody = Readable.fromWeb(body as unknown as NodeWebStream);\n await storage.writeStream(path, nodeBody.pipe(limiter));\n return seen;\n } catch (error) {\n if (seen > maxBytes) {\n throw new HTTPException(413, { message: \"Upload exceeds size limit\" });\n }\n throw error;\n }\n}\n\n/** Query filters accepted by the build list endpoint. */\nexport const buildListQuery = z.object({\n status: z.enum(BUILD_STATUSES).optional(),\n branch: z.string().optional(),\n labelKey: z.string().optional(),\n labelValue: z.string().optional(),\n});\n\n/** Fetch a build scoped to its project, throwing 404 when it does not belong. */\nexport async function buildForProject(projectId: string, buildId: string): Promise<Build> {\n const build = await new BuildModel(getStore().db).get(buildId);\n if (!build || build.projectId !== projectId) {\n notFound(\"Build not found\");\n }\n return build;\n}\n\n/** Fetch a snapshot scoped to its build, throwing 404 when it does not belong. */\nexport async function snapshotForBuild(\n build: { id: string },\n snapshotId: string,\n): Promise<Snapshot> {\n const snapshot = await new SnapshotModel(getStore().db).get(snapshotId);\n if (!snapshot || snapshot.buildId !== build.id) {\n notFound(\"Snapshot not found\");\n }\n return snapshot;\n}\n\n/** Roll snapshot statuses up into the build's review status. */\nfunction rollupStatus(snapshots: Snapshot[]): \"reviewing\" | \"rejected\" | \"approved\" {\n if (snapshots.some((s) => s.status === \"new\" || s.status === \"changed\")) {\n return \"reviewing\";\n }\n return snapshots.some((s) => s.status === \"rejected\") ? \"rejected\" : \"approved\";\n}\n\n/** Recompute a build's counts and roll its status up from its snapshots. */\nexport async function refreshBuild(buildId: string): Promise<void> {\n const { db } = getStore();\n await new BuildModel(db).updateCounts(buildId);\n const snapshots = await new SnapshotModel(db).listByBuild(buildId);\n const status = rollupStatus(snapshots);\n const build = await new BuildModel(db).get(buildId);\n // A cancelled build is terminal: its snapshots may still be touched, but no verdict is announced.\n if (!build || build.status === \"cancelled\") return;\n await new BuildModel(db).setStatus(buildId, status);\n await announceBuildStatus(buildId, status, { snapshotCount: snapshots.length });\n}\n\n/**\n * Tell the world a build reached `status`: its git channels, the `build:<status>` webhook and the\n * project's notification channels. Every route that moves a build to a decided status ends here.\n */\nexport async function announceBuildStatus(\n buildId: string,\n status: BuildStatus,\n extra: { snapshotCount?: number } = {},\n): Promise<void> {\n const { db, config } = getStore();\n const build = await new BuildModel(db).get(buildId);\n if (!build) return;\n await requestBuildState(buildId);\n const payload = { buildId, status, ...extra };\n await emitWebhookEvent(\n db,\n { webhooks: getStore().db.tables.webhooks },\n build.projectId,\n `build:${status}`,\n payload,\n credentialKeys(config),\n );\n const project = await new ProjectModel(db).get(build.projectId);\n if (!project) return;\n await notifyProject(project, `build:${status}`, payload, `/builds/${buildId}`);\n}\n\n/**\n * Approve a snapshot, promote its screenshot to baseline, and refresh the build.\n * Refuses with 409 when the baseline changed since the diff, unless `force` is set.\n * Bulk callers pass `deferRefresh` and call {@link refreshBuild} once, so the build's\n * outcome (and its git status) is announced a single time.\n */\nexport async function approveSnapshot(\n snapshotId: string,\n userId: string | null,\n options: { force?: boolean; deferRefresh?: boolean } = {},\n): Promise<void> {\n const { db, config } = getStore();\n const snapshots = new SnapshotModel(db);\n const snapshot = await snapshots.get(snapshotId);\n if (!snapshot) {\n notFound(\"Snapshot not found\");\n }\n const project = await new ProjectModel(db).get(snapshot.projectId);\n const build = await new BuildModel(db).get(snapshot.buildId);\n if (!project || !build) {\n notFound(\"Project or build not found\");\n }\n if (build.status === \"cancelled\") {\n throw new HTTPException(409, { message: \"The build was cancelled; retry it to review it\" });\n }\n await assertBaselineCurrent(project, build, snapshot, options.force ?? false);\n if (!db.buildReferenceLifecycle) await snapshots.review(snapshotId, \"approved\", userId);\n const baselines = new BaselineModel(db, undefined, getStore().storage, credentialKeys(config));\n const prior = await baselines.getFor(\n project.id,\n snapshot.storyId,\n snapshot.viewportName,\n build.gitBranch,\n snapshot.browser,\n );\n try {\n await baselines.upsert(\n project.id,\n snapshot.storyId,\n snapshot.viewportName,\n build.gitBranch,\n snapshot.id,\n snapshot.screenshotPath,\n undefined,\n {\n browser: snapshot.browser,\n mode: snapshot.mode,\n approval: db.buildReferenceLifecycle\n ? { snapshot, reviewedBy: userId, force: options.force ?? false, branch: build.gitBranch }\n : undefined,\n },\n );\n } catch (error) {\n if (error instanceof BaselineWriteConflict || error instanceof ReferenceGenerationConflict) {\n if (error instanceof BaselineApprovalConflict && error.driftStatus)\n throw baselineChanged(snapshot.id, error.driftStatus);\n throw new HTTPException(409, { message: error.message });\n }\n throw error;\n }\n await notifyProject(\n project,\n prior ? \"baseline:updated\" : \"baseline:created\",\n {\n storyId: snapshot.storyId,\n viewport: snapshot.viewportName,\n browser: snapshot.browser ?? \"chromium\",\n branch: build.gitBranch,\n snapshotId: snapshot.id,\n buildId: build.id,\n },\n `/builds/${build.id}`,\n );\n if (!options.deferRefresh) {\n await refreshBuild(build.id);\n }\n}\n\n/** Roles permitted to view builds, snapshots, and comments. */\nexport const VIEW_ROLES: readonly ProjectRole[] = [\"viewer\", \"developer\", \"approver\", \"admin\"];\n/** Roles permitted to upload builds and write comments. */\nexport const DEVELOPER_ROLES: readonly ProjectRole[] = [\"developer\", \"approver\", \"admin\"];\n/** Roles permitted to approve or reject snapshots and delete builds. */\nexport const APPROVER_ROLES: readonly ProjectRole[] = [\"approver\", \"admin\"];\n\n/** JSON input schema for creating a build before streaming its bundle. */\nexport const buildCreateJsonSchema = z\n .object({\n gitSha: z.string().min(1),\n gitBranch: z.string().min(1),\n authorEmail: z.string().optional(),\n authorName: z.string().optional(),\n message: z.string().optional(),\n affectedOnly: z.boolean().optional(),\n labels: z.array(z.object({ key: z.string().min(1), value: z.string() })).optional(),\n })\n .openapi(\"BuildCreate\");\n\n/** Metadata accepted by either build-creation route. */\nexport interface BuildCreateMetadata {\n gitSha: string;\n gitBranch: string;\n authorEmail?: string;\n authorName?: string;\n message?: string;\n affectedOnly?: boolean;\n labels?: { key: string; value: string }[];\n}\n\n/** Latest prior revision on a branch with a build, or null for first builds. */\nasync function resolveBaselineSha(\n db: DatabaseAdapter,\n projectId: string,\n branch: string,\n sha: string,\n): Promise<string | null> {\n const builds = await new BuildModel(db).list(projectId, { branch });\n return builds.find((build) => build.gitSha !== sha)?.gitSha ?? null;\n}\n\n/* oxlint-disable eslint/no-await-in-loop -- label attach is intentionally sequential */\nasync function attachLabels(\n db: DatabaseAdapter,\n projectId: string,\n buildId: string,\n labels: { key: string; value: string }[],\n): Promise<void> {\n const models = new LabelModel(db);\n for (const { key, value } of labels) {\n const existing = await models.getType(projectId, key);\n if (!existing) {\n await models.createType(projectId, { key, name: key });\n }\n await models.attach(projectId, buildId, key, value);\n }\n}\n/* oxlint-enable eslint/no-await-in-loop */\n"],"mappings":"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"}
@@ -2,20 +2,33 @@ import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
4
  import { getStore } from "../store.mjs";
5
+ import { resolveAuthorizedProject } from "./helpers.mjs";
5
6
  import { hxRefresh } from "./htmx.mjs";
6
7
  import { flashHx } from "./flash.mjs";
7
- import { resolveAuthorizedProject } from "./helpers.mjs";
8
8
  import { badRequest, buildSchema, forbidden, notFound, unauthorized } from "./schemas.mjs";
9
- import { APPROVER_ROLES, DEVELOPER_ROLES, VIEW_ROLES, buildCreateJsonSchema, buildForProject, buildListQuery, createBuildRecord, persistInlineStatics, storeUploadStream } from "./builds.handlers.mjs";
9
+ import { APPROVER_ROLES, DEVELOPER_ROLES, VIEW_ROLES, buildCreateJsonSchema, buildForProject, buildListQuery, createBuildRecord, storeUploadStream } from "./builds.handlers.mjs";
10
10
  import { registerAttempts } from "./attempts.mjs";
11
+ import { discardZip, planUpload, publishUpload } from "./publish-upload.mjs";
12
+ import { registerBuildCancel } from "./build-cancel.mjs";
13
+ import { registerBuildChangeset } from "./build-changeset.mjs";
14
+ import { registerBuildReviews } from "./build-reviews.mjs";
11
15
  import { registerComments } from "./comments.mjs";
12
16
  import { registerSnapshots } from "./snapshots.mjs";
13
17
  import { createRoute, z } from "@hono/zod-openapi";
14
- import { BuildModel } from "@storyshelf/core/models";
18
+ import { DEFAULT_MAX_UPLOAD_BYTES } from "@storyshelf/core/config";
15
19
  import { storybookZipPath } from "@storyshelf/core/utils";
20
+ import { BuildModel, PackageUsageModel } from "@storyshelf/core/models";
16
21
  import { HTTPException } from "hono/http-exception";
17
- import { DEFAULT_MAX_UPLOAD_BYTES } from "@storyshelf/core/config";
18
- //#region src/routers/builds.ts
22
+ // src/routers/builds.ts
23
+ /** Decide how the stored zip is published; a zip this host cannot publish is removed and refused. */
24
+ async function planOrDiscard(zipBytes, projectId, buildId) {
25
+ try {
26
+ return planUpload(zipBytes);
27
+ } catch (error) {
28
+ await discardZip(projectId, buildId);
29
+ throw error;
30
+ }
31
+ }
19
32
  async function upsertContentRef(hash) {
20
33
  try {
21
34
  const contentRefs = getStore().db.tables.contentRefs;
@@ -48,14 +61,13 @@ function registerBuilds(app) {
48
61
  });
49
62
  app.openapi(createBuildRoute, async (c) => {
50
63
  const project = await resolveAuthorizedProject(c, c.req.valid("param").slug, ...DEVELOPER_ROLES);
51
- const parsed = buildCreateJsonSchema.safeParse(await c.req.json());
52
- if (!parsed.success) throw new HTTPException(400, { message: parsed.error.message });
53
- const build = await createBuildRecord(project, parsed.data);
64
+ const build = await createBuildRecord(project, c.req.valid("json"));
54
65
  const uploadUrl = `/api/v1/projects/${project.slug}/builds/${build.id}/zip`;
55
66
  return c.json({
56
67
  build,
57
68
  uploadUrl,
58
- baselineSha: build.baselineSha
69
+ baselineSha: build.baselineSha,
70
+ testing: getStore().testing
59
71
  }, 202);
60
72
  });
61
73
  app.openapi(affectedRoute, async (c) => {
@@ -70,17 +82,24 @@ function registerBuilds(app) {
70
82
  });
71
83
  return c.json(updated);
72
84
  });
85
+ app.openapi(usageRoute, async (c) => {
86
+ const { slug, buildId } = c.req.valid("param");
87
+ const project = await resolveAuthorizedProject(c, slug, ...DEVELOPER_ROLES);
88
+ const build = await buildForProject(project.id, buildId);
89
+ const { usage } = c.req.valid("json");
90
+ const stored = await new PackageUsageModel(getStore().db).replaceForBuild(project.id, build.id, usage);
91
+ return c.json({ stored });
92
+ });
73
93
  app.openapi(uploadZipRoute, async (c) => {
74
94
  const { slug, buildId } = c.req.valid("param");
75
95
  const project = await resolveAuthorizedProject(c, slug, ...DEVELOPER_ROLES);
76
96
  const build = await buildForProject(project.id, buildId);
77
97
  const maxBytes = getStore().config.maxUploadBytes ?? DEFAULT_MAX_UPLOAD_BYTES;
78
- await storeUploadStream(getStore().storage, storybookZipPath(project.id, build.id), c.req.raw.body, maxBytes);
98
+ const route = await planOrDiscard(await storeUploadStream(getStore().storage, storybookZipPath(project.id, build.id), c.req.raw.body, maxBytes), project.id, build.id);
79
99
  const reqId = c.get("requestId");
100
+ const published = await publishUpload(route, build, reqId);
80
101
  await getStore().enqueueCapture?.(build.id, reqId);
81
- const { storage, config, logger } = getStore();
82
- await persistInlineStatics(storage, config.scratchDir, config.maxInlineUnzipSize, c.req.header("content-length"), project.id, build.id, logger);
83
- return c.json(build, 202);
102
+ return c.json(published, 202);
84
103
  });
85
104
  app.openapi(dedupRoute, async (c) => {
86
105
  const { slug, buildId } = c.req.valid("param");
@@ -130,6 +149,7 @@ function registerBuilds(app) {
130
149
  });
131
150
  else await getStore().db.update(contentRefs, f.hash, { lastSeenAt: now });
132
151
  } catch {}
152
+ await getStore().publish?.completer.complete(build.id, { ok: true });
133
153
  const reqId = c.get("requestId");
134
154
  await getStore().enqueueCapture?.(build.id, reqId);
135
155
  return c.json({ ok: true });
@@ -144,7 +164,7 @@ function registerBuilds(app) {
144
164
  const { slug, buildId } = c.req.valid("param");
145
165
  const project = await resolveAuthorizedProject(c, slug, ...DEVELOPER_ROLES);
146
166
  const build = await buildForProject(project.id, buildId);
147
- const updated = await new BuildModel(getStore().db).setStatus(build.id, "pending");
167
+ const updated = await new BuildModel(getStore().db).resetStatus(build.id, "pending");
148
168
  await getStore().enqueueCapture?.(build.id, c.get("requestId"));
149
169
  hxRefresh(c);
150
170
  flashHx(c, "Capture queued");
@@ -159,6 +179,9 @@ function registerBuilds(app) {
159
179
  });
160
180
  registerSnapshots(app);
161
181
  registerComments(app);
182
+ registerBuildReviews(app);
183
+ registerBuildChangeset(app);
184
+ registerBuildCancel(app);
162
185
  registerAttempts(app);
163
186
  }
164
187
  const listBuildsRoute = createRoute({
@@ -182,7 +205,8 @@ const listBuildsRoute = createRoute({
182
205
  const buildCreatedSchema = z.object({
183
206
  build: buildSchema,
184
207
  uploadUrl: z.string(),
185
- baselineSha: z.string().nullable()
208
+ baselineSha: z.string().nullable(),
209
+ testing: z.boolean().openapi({ description: "False when the server has no `testing` option: the build is published but never captured or compared." })
186
210
  }).openapi("BuildCreated");
187
211
  const createBuildRoute = createRoute({
188
212
  method: "post",
@@ -196,7 +220,7 @@ const createBuildRoute = createRoute({
196
220
  responses: {
197
221
  202: {
198
222
  content: { "application/json": { schema: buildCreatedSchema } },
199
- description: "Build created. PUT the Storybook zip to uploadUrl to queue capture."
223
+ description: "Build created. PUT the Storybook zip to uploadUrl to publish it (and queue capture when testing is on)."
200
224
  },
201
225
  ...badRequest,
202
226
  ...forbidden,
@@ -221,11 +245,15 @@ const uploadZipRoute = createRoute({
221
245
  responses: {
222
246
  202: {
223
247
  content: { "application/json": { schema: buildSchema } },
224
- description: "Zip stored and capture queued"
248
+ description: "Zip stored. A zip up to maxInlineUnzipSize (default 50 MB) is extracted inline in this request; a larger one is published by a background job. With testing off, the build becomes approved when publishing finishes on the project's default branch (failed if it cannot be published) and stays pending on other branches; with testing on, capture sets the status."
249
+ },
250
+ 422: {
251
+ content: { "text/plain": { schema: z.string() } },
252
+ description: "The inline zip is invalid, unsafe, or exceeds the extraction limits"
225
253
  },
226
254
  413: {
227
255
  content: { "application/json": { schema: z.object({ message: z.string() }).openapi("Error") } },
228
- description: "Upload exceeds the configured size limit"
256
+ description: "Upload exceeds the configured size limit, or the zip is over the inline limit and this host (serverless) runs no publish consumer: upload file by file instead (POST /dedup, PUT /content, POST /manifest)"
229
257
  },
230
258
  ...badRequest,
231
259
  ...forbidden,
@@ -238,6 +266,7 @@ const dedupRoute = createRoute({
238
266
  method: "post",
239
267
  tags: ["Builds"],
240
268
  summary: "Check which files need uploading",
269
+ description: "File-by-file upload, step 1 (dedup, content, manifest): needs no zip extraction and works on any host, serverless included. Send the content hashes; the response lists those the server does not hold yet.",
241
270
  path: "/api/v1/projects/{slug}/builds/{buildId}/dedup",
242
271
  request: {
243
272
  params: z.object({
@@ -258,6 +287,7 @@ const contentUploadRoute = createRoute({
258
287
  method: "post",
259
288
  tags: ["Builds"],
260
289
  summary: "Upload build content",
290
+ description: "File-by-file upload, step 2: store the missing files content-addressed (multipart, or one file with an X-Content-Hash header).",
261
291
  path: "/api/v1/projects/{slug}/builds/{buildId}/content",
262
292
  request: {
263
293
  params: z.object({
@@ -283,6 +313,7 @@ const manifestRoute = createRoute({
283
313
  method: "post",
284
314
  tags: ["Builds"],
285
315
  summary: "Store the build manifest",
316
+ description: "File-by-file upload, step 3: store the path-to-hash manifest. This completes publishing. With testing off the build becomes approved when it is on the project's default branch and stays pending on other branches; with testing on, a capture is queued and sets the status.",
286
317
  path: "/api/v1/projects/{slug}/builds/{buildId}/manifest",
287
318
  request: {
288
319
  params: z.object({
@@ -326,6 +357,34 @@ const affectedRoute = createRoute({
326
357
  ...notFound
327
358
  }
328
359
  });
360
+ const usagePayloadSchema = z.object({ usage: z.array(z.object({
361
+ storyImportPath: z.string().min(1).max(1024),
362
+ packageName: z.string().min(1).max(214),
363
+ modulePath: z.string().min(1).max(2048),
364
+ version: z.string().max(128).nullable().optional()
365
+ })).max(2e4) });
366
+ const usageRoute = createRoute({
367
+ method: "post",
368
+ tags: ["Builds"],
369
+ summary: "Record the dependency packages each story uses",
370
+ path: "/api/v1/projects/{slug}/builds/{buildId}/usage",
371
+ request: {
372
+ params: z.object({
373
+ slug: z.string(),
374
+ buildId: z.string()
375
+ }),
376
+ body: { content: { "application/json": { schema: usagePayloadSchema } } }
377
+ },
378
+ responses: {
379
+ 200: {
380
+ content: { "application/json": { schema: z.object({ stored: z.number() }) } },
381
+ description: "Usage recorded (replaces any earlier report for the build)"
382
+ },
383
+ ...badRequest,
384
+ ...forbidden,
385
+ ...notFound
386
+ }
387
+ });
329
388
  const getBuildRoute = createRoute({
330
389
  method: "get",
331
390
  tags: ["Builds"],
@@ -375,7 +434,6 @@ const deleteBuildRoute = createRoute({
375
434
  ...notFound
376
435
  }
377
436
  });
378
- //#endregion
379
437
  export { registerBuilds };
380
438
 
381
439
  //# sourceMappingURL=builds.mjs.map