@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
@@ -3,10 +3,13 @@ import "node:url";
3
3
  import.meta.url;
4
4
  import { credentialKeys } from "../credential-keys.mjs";
5
5
  import { getStore } from "../store.mjs";
6
+ import { notificationsPaused } from "../notifications-pause.mjs";
6
7
  import { notFound } from "./helpers.mjs";
8
+ import { runnerTargets } from "../targets.mjs";
9
+ import { isSiteAdmin } from "./insights.handlers.mjs";
7
10
  import { renderProjectSettingsPage } from "../pages/project-settings.mjs";
8
- import { LabelModel, MemberModel, NotificationChannelModel, NotificationSubscriptionModel, ProjectGroupMappingModel, ProjectModel, StatusConfigModel, TokenModel, WebhookModel } from "@storyshelf/core/models";
9
- //#region src/routers/settings.handlers.ts
11
+ import { DesignCredentialModel, DesignModel, LabelModel, MemberModel, NotificationChannelModel, NotificationSubscriptionModel, PackageUsageModel, ProjectGroupMappingModel, ProjectModel, TokenModel, WebhookModel, credentialDaysLeft } from "@storyshelf/core/models";
12
+ // src/routers/settings.handlers.ts
10
13
  /** Tokens without hashes for the settings UI. */
11
14
  async function loadTokenSummaries(db, projectId) {
12
15
  return (await new TokenModel(db).list(projectId)).map(({ hash: _hash, ...rest }) => rest);
@@ -19,17 +22,6 @@ async function loadWebhookSummaries(db, projectId) {
19
22
  events: WebhookModel.eventsOf(webhook)
20
23
  }));
21
24
  }
22
- /** Status-config summaries for the settings UI. */
23
- async function loadStatusConfigSummaries(db, secret, projectId) {
24
- return (await new StatusConfigModel(db, void 0, secret).list(projectId)).map((row) => ({
25
- id: row.id,
26
- provider: row.provider,
27
- config: JSON.parse(row.config),
28
- hasToken: row.tokenEncrypted.length > 0,
29
- createdAt: row.createdAt,
30
- updatedAt: row.updatedAt
31
- }));
32
- }
33
25
  /** True when the user may administer the project. */
34
26
  function isProjectAdmin(authEnabled, user, members) {
35
27
  return !authEnabled || user?.role === "admin" || members.some((member) => member.userId === user?.id && member.role === "admin");
@@ -54,15 +46,22 @@ async function loadMembersSection(db, projectId) {
54
46
  async function loadGroupMappings(db, projectId) {
55
47
  return await new ProjectGroupMappingModel(db).list(projectId);
56
48
  }
49
+ /** Delivery kind of a stored channel's provider (unwired providers read as messages). */
50
+ function deliveryOf(notifiers, provider) {
51
+ return NotificationChannelModel.isStateKind(notifiers, provider) ? "state" : "message";
52
+ }
57
53
  /** Channel summaries for the settings UI. */
58
- async function loadNotificationChannels(db, secret, projectId) {
54
+ async function loadNotificationChannels(db, secret, projectId, notifiers) {
59
55
  return (await new NotificationChannelModel(db, void 0, secret).list(projectId)).map((row) => ({
60
56
  id: row.id,
61
57
  provider: row.provider,
58
+ delivery: deliveryOf(notifiers, row.provider),
62
59
  config: NotificationChannelModel.configOf(row),
63
60
  hasSecret: row.secretEncrypted !== null,
64
61
  events: NotificationChannelModel.eventsOf(row),
65
- enabled: row.enabled
62
+ enabled: row.enabled,
63
+ lastError: row.lastError,
64
+ lastErrorAt: row.lastErrorAt
66
65
  }));
67
66
  }
68
67
  /** The viewer's own subscription for the settings UI (null when opted out). */
@@ -78,38 +77,106 @@ async function loadMySubscription(db, projectId, userId) {
78
77
  }
79
78
  /** Notification channels, providers, and my subscription for the UI. */
80
79
  async function loadNotificationSection(db, secret, projectId, userId, notifiers) {
81
- const notificationChannels = await loadNotificationChannels(db, secret, projectId);
80
+ const notificationChannels = await loadNotificationChannels(db, secret, projectId, notifiers);
82
81
  const mySubscription = await loadMySubscription(db, projectId, userId);
83
82
  return {
84
83
  notificationChannels,
85
- notifyProviders: notifiers.map((candidate) => candidate.metadata.kind),
84
+ notifyProviders: notifiers.map((candidate) => ({
85
+ kind: candidate.metadata.kind,
86
+ name: candidate.metadata.name,
87
+ delivery: deliveryOf(notifiers, candidate.metadata.kind)
88
+ })),
86
89
  mySubscription
87
90
  };
88
91
  }
89
- /** Roster, tokens, webhooks, and status configs for the settings UI. */
90
- async function loadInventorySection(db, secret, projectId) {
92
+ /** Roster, tokens and webhooks for the settings UI. */
93
+ async function loadInventorySection(db, projectId) {
91
94
  const { labelTypes, members, groupMappings } = await loadMembersSection(db, projectId);
92
95
  return {
93
96
  labelTypes,
94
97
  members,
95
98
  groupMappings,
96
99
  tokens: await loadTokenSummaries(db, projectId),
97
- webhooks: await loadWebhookSummaries(db, projectId),
98
- statusConfigs: await loadStatusConfigSummaries(db, secret, projectId)
100
+ webhooks: await loadWebhookSummaries(db, projectId)
99
101
  };
100
102
  }
103
+ /** Package links plus the other projects that can be picked as upstream. */
104
+ async function loadLinksSection(db, project) {
105
+ const projects = await new ProjectModel(db).list();
106
+ const byId = new Map(projects.map((candidate) => [candidate.id, candidate]));
107
+ return {
108
+ links: (await new PackageUsageModel(db).listLinks(project.id)).flatMap((row) => {
109
+ const upstream = byId.get(row.upstreamId);
110
+ return upstream ? [{
111
+ packageName: row.packageName,
112
+ upstreamSlug: upstream.slug,
113
+ upstreamName: upstream.name
114
+ }] : [];
115
+ }),
116
+ linkCandidates: projects.filter((candidate) => candidate.id !== project.id).map((candidate) => ({
117
+ slug: candidate.slug,
118
+ name: candidate.name
119
+ }))
120
+ };
121
+ }
122
+ /** Design files, component mappings and per-provider credential health for the Design tab (never a token). */
123
+ async function loadDesignSection(db, secret, providers, projectId) {
124
+ const model = new DesignModel(db, providers);
125
+ const files = (await model.projectLinks(projectId)).map((link) => ({
126
+ id: link.id,
127
+ label: link.label ?? "Design file",
128
+ url: link.url
129
+ }));
130
+ const components = (await model.componentLinks(projectId)).map((link) => ({
131
+ id: link.id,
132
+ importPath: link.importPath ?? "",
133
+ url: link.url
134
+ }));
135
+ const credentials = new DesignCredentialModel(db, secret);
136
+ return {
137
+ files,
138
+ components,
139
+ providers: await Promise.all(providers.map(async (provider) => await providerSettings(credentials, provider, projectId)))
140
+ };
141
+ }
142
+ async function providerSettings(credentials, provider, projectId) {
143
+ const { metadata } = provider;
144
+ const maxAgeDays = metadata.credentialMaxAgeDays;
145
+ const row = await credentials.info(projectId, metadata.kind);
146
+ return {
147
+ kind: metadata.kind,
148
+ name: metadata.name,
149
+ credentialLabel: metadata.credentialLabel,
150
+ credentialHelp: metadata.credentialHelp,
151
+ ...metadata.credentialPlaceholder ? { credentialPlaceholder: metadata.credentialPlaceholder } : {},
152
+ urlHint: metadata.urlHint,
153
+ ...maxAgeDays === void 0 ? {} : { maxAgeDays },
154
+ credential: row ? {
155
+ status: credentialState(row.status),
156
+ daysLeft: maxAgeDays === void 0 ? null : credentialDaysLeft(row.createdAt, maxAgeDays),
157
+ checkedAt: row.checkedAt
158
+ } : null
159
+ };
160
+ }
161
+ /** The stored status column as a known state (anything unrecognized reads as unknown). */
162
+ function credentialState(status) {
163
+ return status === "ok" || status === "unauthorized" || status === "forbidden" ? status : "unknown";
164
+ }
101
165
  async function loadSettingsData(slug) {
102
166
  const project = await new ProjectModel(getStore().db).getBySlug(slug);
103
167
  if (!project) return null;
104
- const { db, config, user, authEnabled, gitHosts, notifiers } = getStore();
105
- const inventory = await loadInventorySection(db, credentialKeys(config), project.id);
168
+ const { db, config, user, authEnabled, design: designProviders, notifiers } = getStore();
169
+ const inventory = await loadInventorySection(db, project.id);
106
170
  const notifications = await loadNotificationSection(db, credentialKeys(config), project.id, user?.id, notifiers);
107
171
  const isAdmin = isProjectAdmin(authEnabled, user, inventory.members);
172
+ const linksSection = await loadLinksSection(db, project);
173
+ const design = await loadDesignSection(db, credentialKeys(config), designProviders, project.id);
108
174
  return {
109
175
  project,
110
176
  ...inventory,
111
177
  ...notifications,
112
- gitHosts,
178
+ ...linksSection,
179
+ design,
113
180
  isAdmin
114
181
  };
115
182
  }
@@ -121,6 +188,9 @@ async function renderSettingsPage(c, tab, formState) {
121
188
  project: data.project,
122
189
  activeTab: tab,
123
190
  labelTypes: data.labelTypes,
191
+ links: data.links,
192
+ linkCandidates: data.linkCandidates,
193
+ design: data.design,
124
194
  tokens: data.tokens,
125
195
  members: data.members,
126
196
  groupMappings: data.groupMappings,
@@ -128,11 +198,22 @@ async function renderSettingsPage(c, tab, formState) {
128
198
  notificationChannels: data.notificationChannels,
129
199
  notifyProviders: data.notifyProviders,
130
200
  mySubscription: data.mySubscription,
131
- statusConfigs: data.statusConfigs,
132
- gitHosts: data.gitHosts,
133
- isAdmin: data.isAdmin
201
+ isAdmin: data.isAdmin,
202
+ notificationsPaused: data.notifyProviders.length > 0 && await notificationsPaused(),
203
+ ai: aiSettingsData(c),
204
+ targets: await runnerTargets()
134
205
  }, formState);
135
206
  }
207
+ /** AI availability and profile choices for the settings AI tab. */
208
+ function aiSettingsData(c) {
209
+ const { ai } = getStore();
210
+ return {
211
+ available: ai !== void 0,
212
+ profiles: ai?.profileNames() ?? [],
213
+ defaultProfile: ai?.defaultProfile() ?? "",
214
+ isSiteAdmin: isSiteAdmin(c)
215
+ };
216
+ }
136
217
  /** Find a project by slug or throw 404. */
137
218
  async function findProject(slug) {
138
219
  const project = await new ProjectModel(getStore().db).getBySlug(slug);
@@ -143,7 +224,6 @@ async function findProject(slug) {
143
224
  function asString(value) {
144
225
  return typeof value === "string" ? value.trim() : void 0;
145
226
  }
146
- //#endregion
147
227
  export { asString, findProject, renderSettingsPage };
148
228
 
149
229
  //# sourceMappingURL=settings.handlers.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"settings.handlers.mjs","names":[],"sources":["../../src/routers/settings.handlers.ts"],"sourcesContent":["import type { DatabaseAdapter } from \"@storyshelf/core/adapter/database\";\nimport type { GitHostProvider } from \"@storyshelf/core/adapter/git-host\";\nimport { LabelModel } from \"@storyshelf/core/models\";\nimport { MemberModel } from \"@storyshelf/core/models\";\nimport { NotificationChannelModel } from \"@storyshelf/core/models\";\nimport { NotificationSubscriptionModel } from \"@storyshelf/core/models\";\nimport { ProjectGroupMappingModel } from \"@storyshelf/core/models\";\nimport { ProjectModel } from \"@storyshelf/core/models\";\nimport { StatusConfigModel } from \"@storyshelf/core/models\";\nimport { TokenModel } from \"@storyshelf/core/models\";\nimport { WebhookModel } from \"@storyshelf/core/models\";\nimport type { LabelType } from \"@storyshelf/core/schema\";\nimport type { Project } from \"@storyshelf/core/schema\";\nimport type { ProjectGroupMapping } from \"@storyshelf/core/schema\";\nimport type { Token } from \"@storyshelf/core/schema\";\nimport type { AuthUser } from \"@storyshelf/core/types\";\nimport type { SecretInput } from \"@storyshelf/core/utils\";\nimport type { Context } from \"hono\";\nimport { credentialKeys } from \"../credential-keys.ts\";\nimport {\n renderProjectSettingsPage,\n type SettingsFormState,\n type SettingsTab,\n} from \"../pages/project-settings.tsx\";\nimport type { SettingsMember } from \"../pages/settings-members.tsx\";\nimport type {\n SettingsMySubscription,\n SettingsNotificationChannel,\n} from \"../pages/settings-notifications.tsx\";\nimport type { SettingsStatusConfig } from \"../pages/settings-status.tsx\";\nimport type { SettingsWebhook } from \"../pages/settings-webhooks.tsx\";\nimport { getStore } from \"../store.ts\";\nimport { notFound } from \"./helpers.ts\";\n/** Aggregated data for rendering a settings tab. */\nexport interface SettingsData {\n project: Project;\n labelTypes: LabelType[];\n tokens: Omit<Token, \"hash\">[];\n members: SettingsMember[];\n groupMappings: ProjectGroupMapping[];\n webhooks: SettingsWebhook[];\n notificationChannels: SettingsNotificationChannel[];\n notifyProviders: string[];\n mySubscription: SettingsMySubscription | null;\n statusConfigs: SettingsStatusConfig[];\n gitHosts: GitHostProvider[];\n isAdmin: boolean;\n}\n\n/** Tokens without hashes for the settings UI. */\nasync function loadTokenSummaries(\n db: DatabaseAdapter,\n projectId: string,\n): Promise<Omit<Token, \"hash\">[]> {\n const tokensDb = await new TokenModel(db).list(projectId);\n return tokensDb.map(({ hash: _hash, ...rest }) => rest);\n}\n\n/** Webhook summaries for the settings UI. */\nasync function loadWebhookSummaries(\n db: DatabaseAdapter,\n projectId: string,\n): Promise<SettingsWebhook[]> {\n const webhooksDb = await new WebhookModel(db).list(projectId);\n return webhooksDb.map((webhook) => ({\n id: webhook.id,\n url: webhook.url,\n events: WebhookModel.eventsOf(webhook),\n }));\n}\n\n/** Status-config summaries for the settings UI. */\nasync function loadStatusConfigSummaries(\n db: DatabaseAdapter,\n secret: SecretInput,\n projectId: string,\n): Promise<SettingsStatusConfig[]> {\n const statusConfigsDb = await new StatusConfigModel(db, undefined, secret).list(projectId);\n return statusConfigsDb.map((row) => ({\n id: row.id,\n provider: row.provider,\n config: JSON.parse(row.config) as Record<string, unknown>,\n hasToken: row.tokenEncrypted.length > 0,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n }));\n}\n\n/** True when the user may administer the project. */\nfunction isProjectAdmin(\n authEnabled: boolean,\n user: AuthUser | null,\n members: SettingsMember[],\n): boolean {\n return (\n !authEnabled ||\n user?.role === \"admin\" ||\n members.some((member) => member.userId === user?.id && member.role === \"admin\")\n );\n}\n\n/** Label types + member roster for the settings UI. */\nasync function loadProjectRoster(\n db: DatabaseAdapter,\n projectId: string,\n): Promise<{ labelTypes: LabelType[]; members: SettingsMember[] }> {\n const labelTypes = await new LabelModel(db).listTypes(projectId);\n const members = await new MemberModel(db).list(projectId);\n return { labelTypes, members };\n}\n\n/** Label types + member roster + group mappings for the settings UI. */\nasync function loadMembersSection(\n db: DatabaseAdapter,\n projectId: string,\n): Promise<{\n labelTypes: LabelType[];\n members: SettingsMember[];\n groupMappings: ProjectGroupMapping[];\n}> {\n const { labelTypes, members } = await loadProjectRoster(db, projectId);\n const groupMappings = await loadGroupMappings(db, projectId);\n return { labelTypes, members, groupMappings };\n}\n\n/** Group mappings for the members settings tab. */\nasync function loadGroupMappings(\n db: DatabaseAdapter,\n projectId: string,\n): Promise<ProjectGroupMapping[]> {\n return await new ProjectGroupMappingModel(db).list(projectId);\n}\n\n/** Channel summaries for the settings UI. */\nasync function loadNotificationChannels(\n db: DatabaseAdapter,\n secret: SecretInput,\n projectId: string,\n): Promise<SettingsNotificationChannel[]> {\n const rows = await new NotificationChannelModel(db, undefined, secret).list(projectId);\n return rows.map((row) => ({\n id: row.id,\n provider: row.provider,\n config: NotificationChannelModel.configOf(row),\n hasSecret: row.secretEncrypted !== null,\n events: NotificationChannelModel.eventsOf(row),\n enabled: row.enabled,\n }));\n}\n\n/** The viewer's own subscription for the settings UI (null when opted out). */\nasync function loadMySubscription(\n db: DatabaseAdapter,\n projectId: string,\n userId: string | undefined,\n): Promise<SettingsMySubscription | null> {\n if (!userId) {\n return null;\n }\n const row = await new NotificationSubscriptionModel(db).getFor(projectId, userId);\n if (!row) {\n return null;\n }\n return {\n events: NotificationSubscriptionModel.eventsOf(row),\n via: NotificationSubscriptionModel.viaOf(row),\n enabled: row.enabled,\n };\n}\n\n/** Notification channels, providers, and my subscription for the UI. */\nasync function loadNotificationSection(\n db: DatabaseAdapter,\n secret: SecretInput,\n projectId: string,\n userId: string | undefined,\n notifiers: { metadata: { kind: string } }[],\n): Promise<{\n notificationChannels: SettingsNotificationChannel[];\n notifyProviders: string[];\n mySubscription: SettingsMySubscription | null;\n}> {\n const notificationChannels = await loadNotificationChannels(db, secret, projectId);\n const mySubscription = await loadMySubscription(db, projectId, userId);\n return {\n notificationChannels,\n notifyProviders: notifiers.map((candidate) => candidate.metadata.kind),\n mySubscription,\n };\n}\n\n/** Roster, tokens, webhooks, and status configs for the settings UI. */\nasync function loadInventorySection(\n db: DatabaseAdapter,\n secret: SecretInput,\n projectId: string,\n): Promise<{\n labelTypes: LabelType[];\n members: SettingsMember[];\n groupMappings: ProjectGroupMapping[];\n tokens: Omit<Token, \"hash\">[];\n webhooks: SettingsWebhook[];\n statusConfigs: SettingsStatusConfig[];\n}> {\n const { labelTypes, members, groupMappings } = await loadMembersSection(db, projectId);\n const tokens = await loadTokenSummaries(db, projectId);\n const webhooks = await loadWebhookSummaries(db, projectId);\n const statusConfigs = await loadStatusConfigSummaries(db, secret, projectId);\n return { labelTypes, members, groupMappings, tokens, webhooks, statusConfigs };\n}\n\nasync function loadSettingsData(slug: string): Promise<SettingsData | null> {\n const project = await new ProjectModel(getStore().db).getBySlug(slug);\n if (!project) {\n return null;\n }\n const { db, config, user, authEnabled, gitHosts, notifiers } = getStore();\n const inventory = await loadInventorySection(db, credentialKeys(config), project.id);\n const notifications = await loadNotificationSection(\n db,\n credentialKeys(config),\n project.id,\n user?.id,\n notifiers,\n );\n const isAdmin = isProjectAdmin(authEnabled, user, inventory.members);\n return { project, ...inventory, ...notifications, gitHosts, isAdmin };\n}\n\n/** Render the project settings page for the given tab, optionally with form state. */\nexport async function renderSettingsPage(\n c: Context,\n tab: SettingsTab,\n formState?: SettingsFormState,\n): Promise<string> {\n const slug = c.req.param(\"slug\") ?? \"\";\n const data = await loadSettingsData(slug);\n if (!data) {\n notFound(\"Project not found\");\n }\n return await renderProjectSettingsPage(\n {\n project: data.project,\n activeTab: tab,\n labelTypes: data.labelTypes,\n tokens: data.tokens,\n members: data.members,\n groupMappings: data.groupMappings,\n webhooks: data.webhooks,\n notificationChannels: data.notificationChannels,\n notifyProviders: data.notifyProviders,\n mySubscription: data.mySubscription,\n statusConfigs: data.statusConfigs,\n gitHosts: data.gitHosts,\n isAdmin: data.isAdmin,\n },\n formState,\n );\n}\n\n/** Find a project by slug or throw 404. */\nexport async function findProject(slug: string): Promise<Project> {\n const project = await new ProjectModel(getStore().db).getBySlug(slug);\n if (!project) {\n notFound(\"Project not found\");\n }\n return project;\n}\n\n/** Trim a form value to a string, dropping files and blanks. */\nexport function asString(value: FormDataEntryValue | null): string | undefined {\n return typeof value === \"string\" ? value.trim() : undefined;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"settings.handlers.mjs","names":[],"sources":["../../src/routers/settings.handlers.ts"],"sourcesContent":["import type { DatabaseAdapter } from \"@storyshelf/core/adapter/database\";\nimport type { DesignProvider } from \"@storyshelf/core/adapter/design\";\nimport type { NotifierProvider } from \"@storyshelf/core/adapter/notifier\";\nimport {\n credentialDaysLeft,\n DesignCredentialModel,\n DesignModel,\n LabelModel,\n} from \"@storyshelf/core/models\";\nimport { PackageUsageModel } from \"@storyshelf/core/models\";\nimport { MemberModel } from \"@storyshelf/core/models\";\nimport { NotificationChannelModel } from \"@storyshelf/core/models\";\nimport { NotificationSubscriptionModel } from \"@storyshelf/core/models\";\nimport { ProjectGroupMappingModel } from \"@storyshelf/core/models\";\nimport { ProjectModel } from \"@storyshelf/core/models\";\nimport { TokenModel } from \"@storyshelf/core/models\";\nimport { WebhookModel } from \"@storyshelf/core/models\";\nimport type { LabelType } from \"@storyshelf/core/schema\";\nimport type { Project } from \"@storyshelf/core/schema\";\nimport type { ProjectGroupMapping } from \"@storyshelf/core/schema\";\nimport type { Token } from \"@storyshelf/core/schema\";\nimport type { AuthUser } from \"@storyshelf/core/types\";\nimport type { SecretInput } from \"@storyshelf/core/utils\";\nimport type { Context } from \"hono\";\nimport { credentialKeys } from \"../credential-keys.ts\";\nimport { notificationsPaused } from \"../notifications-pause.ts\";\nimport {\n renderProjectSettingsPage,\n type SettingsFormState,\n type SettingsTab,\n} from \"../pages/project-settings.tsx\";\nimport type { SettingsAiData } from \"../pages/settings-ai.tsx\";\nimport type { DesignCredentialState } from \"../pages/settings-design-credential.tsx\";\nimport type { SettingsDesignData, SettingsDesignProvider } from \"../pages/settings-design.tsx\";\nimport type { SettingsLink, SettingsLinkCandidate } from \"../pages/settings-links.tsx\";\nimport type { SettingsMember } from \"../pages/settings-members.tsx\";\nimport type {\n SettingsMySubscription,\n SettingsNotificationChannel,\n SettingsNotifyProvider,\n} from \"../pages/settings-notifications.tsx\";\nimport type { SettingsWebhook } from \"../pages/settings-webhooks.tsx\";\nimport { getStore } from \"../store.ts\";\nimport { runnerTargets } from \"../targets.ts\";\nimport { notFound } from \"./helpers.ts\";\nimport { isSiteAdmin } from \"./insights.handlers.ts\";\n/** Aggregated data for rendering a settings tab. */\nexport interface SettingsData {\n project: Project;\n links: SettingsLink[];\n linkCandidates: SettingsLinkCandidate[];\n design: SettingsDesignData;\n labelTypes: LabelType[];\n tokens: Omit<Token, \"hash\">[];\n members: SettingsMember[];\n groupMappings: ProjectGroupMapping[];\n webhooks: SettingsWebhook[];\n notificationChannels: SettingsNotificationChannel[];\n notifyProviders: SettingsNotifyProvider[];\n mySubscription: SettingsMySubscription | null;\n isAdmin: boolean;\n}\n\n/** Tokens without hashes for the settings UI. */\nasync function loadTokenSummaries(\n db: DatabaseAdapter,\n projectId: string,\n): Promise<Omit<Token, \"hash\">[]> {\n const tokensDb = await new TokenModel(db).list(projectId);\n return tokensDb.map(({ hash: _hash, ...rest }) => rest);\n}\n\n/** Webhook summaries for the settings UI. */\nasync function loadWebhookSummaries(\n db: DatabaseAdapter,\n projectId: string,\n): Promise<SettingsWebhook[]> {\n const webhooksDb = await new WebhookModel(db).list(projectId);\n return webhooksDb.map((webhook) => ({\n id: webhook.id,\n url: webhook.url,\n events: WebhookModel.eventsOf(webhook),\n }));\n}\n\n/** True when the user may administer the project. */\nfunction isProjectAdmin(\n authEnabled: boolean,\n user: AuthUser | null,\n members: SettingsMember[],\n): boolean {\n return (\n !authEnabled ||\n user?.role === \"admin\" ||\n members.some((member) => member.userId === user?.id && member.role === \"admin\")\n );\n}\n\n/** Label types + member roster for the settings UI. */\nasync function loadProjectRoster(\n db: DatabaseAdapter,\n projectId: string,\n): Promise<{ labelTypes: LabelType[]; members: SettingsMember[] }> {\n const labelTypes = await new LabelModel(db).listTypes(projectId);\n const members = await new MemberModel(db).list(projectId);\n return { labelTypes, members };\n}\n\n/** Label types + member roster + group mappings for the settings UI. */\nasync function loadMembersSection(\n db: DatabaseAdapter,\n projectId: string,\n): Promise<{\n labelTypes: LabelType[];\n members: SettingsMember[];\n groupMappings: ProjectGroupMapping[];\n}> {\n const { labelTypes, members } = await loadProjectRoster(db, projectId);\n const groupMappings = await loadGroupMappings(db, projectId);\n return { labelTypes, members, groupMappings };\n}\n\n/** Group mappings for the members settings tab. */\nasync function loadGroupMappings(\n db: DatabaseAdapter,\n projectId: string,\n): Promise<ProjectGroupMapping[]> {\n return await new ProjectGroupMappingModel(db).list(projectId);\n}\n\n/** Delivery kind of a stored channel's provider (unwired providers read as messages). */\nfunction deliveryOf(\n notifiers: readonly NotifierProvider[],\n provider: string,\n): SettingsNotificationChannel[\"delivery\"] {\n return NotificationChannelModel.isStateKind(notifiers, provider) ? \"state\" : \"message\";\n}\n\n/** Channel summaries for the settings UI. */\nasync function loadNotificationChannels(\n db: DatabaseAdapter,\n secret: SecretInput,\n projectId: string,\n notifiers: readonly NotifierProvider[],\n): Promise<SettingsNotificationChannel[]> {\n const rows = await new NotificationChannelModel(db, undefined, secret).list(projectId);\n return rows.map((row) => ({\n id: row.id,\n provider: row.provider,\n delivery: deliveryOf(notifiers, row.provider),\n config: NotificationChannelModel.configOf(row),\n hasSecret: row.secretEncrypted !== null,\n events: NotificationChannelModel.eventsOf(row),\n enabled: row.enabled,\n lastError: row.lastError,\n lastErrorAt: row.lastErrorAt,\n }));\n}\n\n/** The viewer's own subscription for the settings UI (null when opted out). */\nasync function loadMySubscription(\n db: DatabaseAdapter,\n projectId: string,\n userId: string | undefined,\n): Promise<SettingsMySubscription | null> {\n if (!userId) {\n return null;\n }\n const row = await new NotificationSubscriptionModel(db).getFor(projectId, userId);\n if (!row) {\n return null;\n }\n return {\n events: NotificationSubscriptionModel.eventsOf(row),\n via: NotificationSubscriptionModel.viaOf(row),\n enabled: row.enabled,\n };\n}\n\n/** Notification channels, providers, and my subscription for the UI. */\nasync function loadNotificationSection(\n db: DatabaseAdapter,\n secret: SecretInput,\n projectId: string,\n userId: string | undefined,\n notifiers: readonly NotifierProvider[],\n): Promise<{\n notificationChannels: SettingsNotificationChannel[];\n notifyProviders: SettingsNotifyProvider[];\n mySubscription: SettingsMySubscription | null;\n}> {\n const notificationChannels = await loadNotificationChannels(db, secret, projectId, notifiers);\n const mySubscription = await loadMySubscription(db, projectId, userId);\n const notifyProviders = notifiers.map((candidate) => ({\n kind: candidate.metadata.kind,\n name: candidate.metadata.name,\n delivery: deliveryOf(notifiers, candidate.metadata.kind),\n }));\n return { notificationChannels, notifyProviders, mySubscription };\n}\n\n/** Roster, tokens and webhooks for the settings UI. */\nasync function loadInventorySection(\n db: DatabaseAdapter,\n projectId: string,\n): Promise<{\n labelTypes: LabelType[];\n members: SettingsMember[];\n groupMappings: ProjectGroupMapping[];\n tokens: Omit<Token, \"hash\">[];\n webhooks: SettingsWebhook[];\n}> {\n const { labelTypes, members, groupMappings } = await loadMembersSection(db, projectId);\n const tokens = await loadTokenSummaries(db, projectId);\n const webhooks = await loadWebhookSummaries(db, projectId);\n return { labelTypes, members, groupMappings, tokens, webhooks };\n}\n\n/** Package links plus the other projects that can be picked as upstream. */\nasync function loadLinksSection(\n db: DatabaseAdapter,\n project: Project,\n): Promise<{ links: SettingsLink[]; linkCandidates: SettingsLinkCandidate[] }> {\n const projects = await new ProjectModel(db).list();\n const byId = new Map(projects.map((candidate) => [candidate.id, candidate]));\n const rows = await new PackageUsageModel(db).listLinks(project.id);\n const links = rows.flatMap((row) => {\n const upstream = byId.get(row.upstreamId);\n return upstream\n ? [{ packageName: row.packageName, upstreamSlug: upstream.slug, upstreamName: upstream.name }]\n : [];\n });\n const linkCandidates = projects\n .filter((candidate) => candidate.id !== project.id)\n .map((candidate) => ({ slug: candidate.slug, name: candidate.name }));\n return { links, linkCandidates };\n}\n\n/** Design files, component mappings and per-provider credential health for the Design tab (never a token). */\nasync function loadDesignSection(\n db: DatabaseAdapter,\n secret: SecretInput,\n providers: DesignProvider[],\n projectId: string,\n): Promise<SettingsDesignData> {\n const model = new DesignModel(db, providers);\n const files = (await model.projectLinks(projectId)).map((link) => ({\n id: link.id,\n label: link.label ?? \"Design file\",\n url: link.url,\n }));\n const components = (await model.componentLinks(projectId)).map((link) => ({\n id: link.id,\n importPath: link.importPath ?? \"\",\n url: link.url,\n }));\n const credentials = new DesignCredentialModel(db, secret);\n const rows = await Promise.all(\n providers.map(async (provider) => await providerSettings(credentials, provider, projectId)),\n );\n return { files, components, providers: rows };\n}\n\nasync function providerSettings(\n credentials: DesignCredentialModel,\n provider: DesignProvider,\n projectId: string,\n): Promise<SettingsDesignProvider> {\n const { metadata } = provider;\n const maxAgeDays = metadata.credentialMaxAgeDays;\n const row = await credentials.info(projectId, metadata.kind);\n return {\n kind: metadata.kind,\n name: metadata.name,\n credentialLabel: metadata.credentialLabel,\n credentialHelp: metadata.credentialHelp,\n ...(metadata.credentialPlaceholder\n ? { credentialPlaceholder: metadata.credentialPlaceholder }\n : {}),\n urlHint: metadata.urlHint,\n ...(maxAgeDays === undefined ? {} : { maxAgeDays }),\n credential: row\n ? {\n status: credentialState(row.status),\n daysLeft: maxAgeDays === undefined ? null : credentialDaysLeft(row.createdAt, maxAgeDays),\n checkedAt: row.checkedAt,\n }\n : null,\n };\n}\n\n/** The stored status column as a known state (anything unrecognized reads as unknown). */\nfunction credentialState(status: string): DesignCredentialState {\n return status === \"ok\" || status === \"unauthorized\" || status === \"forbidden\"\n ? status\n : \"unknown\";\n}\n\nasync function loadSettingsData(slug: string): Promise<SettingsData | null> {\n const project = await new ProjectModel(getStore().db).getBySlug(slug);\n if (!project) {\n return null;\n }\n const { db, config, user, authEnabled, design: designProviders, notifiers } = getStore();\n const inventory = await loadInventorySection(db, project.id);\n const notifications = await loadNotificationSection(\n db,\n credentialKeys(config),\n project.id,\n user?.id,\n notifiers,\n );\n const isAdmin = isProjectAdmin(authEnabled, user, inventory.members);\n const linksSection = await loadLinksSection(db, project);\n const design = await loadDesignSection(db, credentialKeys(config), designProviders, project.id);\n return { project, ...inventory, ...notifications, ...linksSection, design, isAdmin };\n}\n\n/** Render the project settings page for the given tab, optionally with form state. */\nexport async function renderSettingsPage(\n c: Context,\n tab: SettingsTab,\n formState?: SettingsFormState,\n): Promise<string> {\n const slug = c.req.param(\"slug\") ?? \"\";\n const data = await loadSettingsData(slug);\n if (!data) {\n notFound(\"Project not found\");\n }\n return await renderProjectSettingsPage(\n {\n project: data.project,\n activeTab: tab,\n labelTypes: data.labelTypes,\n links: data.links,\n linkCandidates: data.linkCandidates,\n design: data.design,\n tokens: data.tokens,\n members: data.members,\n groupMappings: data.groupMappings,\n webhooks: data.webhooks,\n notificationChannels: data.notificationChannels,\n notifyProviders: data.notifyProviders,\n mySubscription: data.mySubscription,\n isAdmin: data.isAdmin,\n notificationsPaused: data.notifyProviders.length > 0 && (await notificationsPaused()),\n ai: aiSettingsData(c),\n targets: await runnerTargets(),\n },\n formState,\n );\n}\n\n/** AI availability and profile choices for the settings AI tab. */\nfunction aiSettingsData(c: Context): SettingsAiData {\n const { ai } = getStore();\n return {\n available: ai !== undefined,\n profiles: ai?.profileNames() ?? [],\n defaultProfile: ai?.defaultProfile() ?? \"\",\n isSiteAdmin: isSiteAdmin(c),\n };\n}\n\n/** Find a project by slug or throw 404. */\nexport async function findProject(slug: string): Promise<Project> {\n const project = await new ProjectModel(getStore().db).getBySlug(slug);\n if (!project) {\n notFound(\"Project not found\");\n }\n return project;\n}\n\n/** Trim a form value to a string, dropping files and blanks. */\nexport function asString(value: FormDataEntryValue | null): string | undefined {\n return typeof value === \"string\" ? value.trim() : undefined;\n}\n"],"mappings":"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"}
@@ -1,25 +1,36 @@
1
1
  import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
+ import { registerAiSettings } from "./settings-ai.mjs";
5
+ import { registerCaptureSettings } from "./settings-capture.mjs";
6
+ import { registerDesignSettings } from "./settings-design.mjs";
4
7
  import { registerGeneralSettings } from "./settings-general.mjs";
5
8
  import { registerLabelSettings } from "./settings-labels.mjs";
9
+ import { registerLinkSettings } from "./settings-links.mjs";
6
10
  import { registerMemberSettings } from "./settings-members.mjs";
7
11
  import { registerNotificationSettings } from "./settings-notifications.mjs";
8
- import { registerStatusSettings } from "./settings-status.mjs";
9
12
  import { registerTokenSettings } from "./settings-tokens.mjs";
10
13
  import { registerWebhookSettings } from "./settings-webhooks.mjs";
11
- //#region src/routers/settings.ts
14
+ // src/routers/settings.ts
12
15
  /** Register the server-rendered project settings pages and their form handlers. */
13
16
  function registerSettingsPages(app) {
14
- registerGeneralSettings(app);
15
- registerLabelSettings(app);
17
+ registerProjectSettings(app);
18
+ registerCatalogSettings(app);
16
19
  registerTokenSettings(app);
17
20
  registerWebhookSettings(app);
18
21
  registerNotificationSettings(app);
19
22
  registerMemberSettings(app);
20
- registerStatusSettings(app);
23
+ registerAiSettings(app);
24
+ }
25
+ function registerProjectSettings(app) {
26
+ registerGeneralSettings(app);
27
+ registerCaptureSettings(app);
28
+ }
29
+ function registerCatalogSettings(app) {
30
+ registerLabelSettings(app);
31
+ registerLinkSettings(app);
32
+ registerDesignSettings(app);
21
33
  }
22
- //#endregion
23
34
  export { registerSettingsPages };
24
35
 
25
36
  //# sourceMappingURL=settings.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"settings.mjs","names":[],"sources":["../../src/routers/settings.ts"],"sourcesContent":["import type { ShelfRouter } from \"../app-types.ts\";\nimport { registerGeneralSettings } from \"./settings-general.ts\";\nimport { registerLabelSettings } from \"./settings-labels.ts\";\nimport { registerMemberSettings } from \"./settings-members.ts\";\nimport { registerNotificationSettings } from \"./settings-notifications.ts\";\nimport { registerStatusSettings } from \"./settings-status.ts\";\nimport { registerTokenSettings } from \"./settings-tokens.ts\";\nimport { registerWebhookSettings } from \"./settings-webhooks.ts\";\n\n/** Register the server-rendered project settings pages and their form handlers. */\nexport function registerSettingsPages(app: ShelfRouter): void {\n registerGeneralSettings(app);\n registerLabelSettings(app);\n registerTokenSettings(app);\n registerWebhookSettings(app);\n registerNotificationSettings(app);\n registerMemberSettings(app);\n registerStatusSettings(app);\n}\n"],"mappings":";;;;;;;;;;;;AAUA,SAAgB,sBAAsB,KAAwB;CAC5D,wBAAwB,GAAG;CAC3B,sBAAsB,GAAG;CACzB,sBAAsB,GAAG;CACzB,wBAAwB,GAAG;CAC3B,6BAA6B,GAAG;CAChC,uBAAuB,GAAG;CAC1B,uBAAuB,GAAG;AAC5B"}
1
+ {"version":3,"file":"settings.mjs","names":[],"sources":["../../src/routers/settings.ts"],"sourcesContent":["import type { ShelfRouter } from \"../app-types.ts\";\nimport { registerAiSettings } from \"./settings-ai.ts\";\nimport { registerCaptureSettings } from \"./settings-capture.ts\";\nimport { registerDesignSettings } from \"./settings-design.ts\";\nimport { registerGeneralSettings } from \"./settings-general.ts\";\nimport { registerLabelSettings } from \"./settings-labels.ts\";\nimport { registerLinkSettings } from \"./settings-links.ts\";\nimport { registerMemberSettings } from \"./settings-members.ts\";\nimport { registerNotificationSettings } from \"./settings-notifications.ts\";\nimport { registerTokenSettings } from \"./settings-tokens.ts\";\nimport { registerWebhookSettings } from \"./settings-webhooks.ts\";\n\n/** Register the server-rendered project settings pages and their form handlers. */\nexport function registerSettingsPages(app: ShelfRouter): void {\n registerProjectSettings(app);\n registerCatalogSettings(app);\n registerTokenSettings(app);\n registerWebhookSettings(app);\n registerNotificationSettings(app);\n registerMemberSettings(app);\n registerAiSettings(app);\n}\n\nfunction registerProjectSettings(app: ShelfRouter): void {\n registerGeneralSettings(app);\n registerCaptureSettings(app);\n}\n\nfunction registerCatalogSettings(app: ShelfRouter): void {\n registerLabelSettings(app);\n registerLinkSettings(app);\n registerDesignSettings(app);\n}\n"],"mappings":";;;;;;;;;;;;;;;AAaA,SAAgB,sBAAsB,KAAwB;CAC5D,wBAAwB,GAAG;CAC3B,wBAAwB,GAAG;CAC3B,sBAAsB,GAAG;CACzB,wBAAwB,GAAG;CAC3B,6BAA6B,GAAG;CAChC,uBAAuB,GAAG;CAC1B,mBAAmB,GAAG;AACxB;AAEA,SAAS,wBAAwB,KAAwB;CACvD,wBAAwB,GAAG;CAC3B,wBAAwB,GAAG;AAC7B;AAEA,SAAS,wBAAwB,KAAwB;CACvD,sBAAsB,GAAG;CACzB,qBAAqB,GAAG;CACxB,uBAAuB,GAAG;AAC5B"}
@@ -0,0 +1,55 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { credentialKeys } from "../credential-keys.mjs";
5
+ import { getStore } from "../store.mjs";
6
+ import { publishedAccess } from "./published-access.mjs";
7
+ import { SIGNED_URL_HEADERS, publishedRelativePath, servePublishedFile } from "./published-files.mjs";
8
+ import { STORYBOOK_TOKEN_PREFIX, storybookTokenPath, verifyStorybookToken } from "@storyshelf/core/utils";
9
+ import { BuildModel, ProjectModel } from "@storyshelf/core/models";
10
+ // src/routers/signed-storybook.ts
11
+ /**
12
+ * Signed Storybook URLs (ADR 0007, decision 5): `GET /_s/<token>/<file>` serves one build's static
13
+ * Storybook files to a remote browser, with no session. The HMAC token is the only credential; it
14
+ * names one project and one build and expires. The file itself is served by the same handler and
15
+ * access policy as the other published routes (`published-files.ts`, `published-access.ts`).
16
+ * Nothing else lives under `/_s/`: no wrapper page, no API, no other build, no listing.
17
+ * Answers are `private, no-store`, except a success for a capture-purpose grant when the server
18
+ * opted in (`captureAssets.cacheMaxAgeSeconds`, see `capture-cache.ts`).
19
+ */
20
+ /** Register the signed-URL file route. */
21
+ function registerSignedStorybook(app) {
22
+ app.get(`${STORYBOOK_TOKEN_PREFIX}:token/*`, async (c) => await serveSigned(c));
23
+ }
24
+ async function serveSigned(c) {
25
+ const token = c.req.param("token") ?? "";
26
+ const rel = publishedRelativePath(c.req.path, storybookTokenPath(token));
27
+ if (!rel) return refuse(c, "bad path");
28
+ return await serveGranted(c, token, rel) ?? refuse(c, "no such file");
29
+ }
30
+ /** The file the token grants as a response, or null (for the debug log only: why not). */
31
+ async function serveGranted(c, token, rel) {
32
+ const { config, db } = getStore();
33
+ const verified = verifyStorybookToken(token, credentialKeys(config));
34
+ if (!verified.ok) return refuse(c, verified.reason);
35
+ const { projectId, buildId } = verified.claims;
36
+ const [project, build] = await Promise.all([new ProjectModel(db).get(projectId), new BuildModel(db).get(buildId)]);
37
+ const access = project && build ? await publishedAccess(build, project, verified.claims) : null;
38
+ if (access !== "token") return refuse(c, "unknown build");
39
+ return await servePublishedFile(c, {
40
+ projectId,
41
+ buildId,
42
+ rel,
43
+ access,
44
+ cacheControl: "",
45
+ grant: verified.claims
46
+ });
47
+ }
48
+ /** The one answer for every refusal, so a caller cannot tell a bad signature from an expired token. */
49
+ function refuse(c, reason) {
50
+ getStore().logger.debug({ reason }, "signed storybook request refused");
51
+ return c.text("Not found", 404, SIGNED_URL_HEADERS);
52
+ }
53
+ export { registerSignedStorybook };
54
+
55
+ //# sourceMappingURL=signed-storybook.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signed-storybook.mjs","names":[],"sources":["../../src/routers/signed-storybook.ts"],"sourcesContent":["/**\n * Signed Storybook URLs (ADR 0007, decision 5): `GET /_s/<token>/<file>` serves one build's static\n * Storybook files to a remote browser, with no session. The HMAC token is the only credential; it\n * names one project and one build and expires. The file itself is served by the same handler and\n * access policy as the other published routes (`published-files.ts`, `published-access.ts`).\n * Nothing else lives under `/_s/`: no wrapper page, no API, no other build, no listing.\n * Answers are `private, no-store`, except a success for a capture-purpose grant when the server\n * opted in (`captureAssets.cacheMaxAgeSeconds`, see `capture-cache.ts`).\n */\nimport { BuildModel, ProjectModel } from \"@storyshelf/core/models\";\nimport {\n STORYBOOK_TOKEN_PREFIX,\n storybookTokenPath,\n verifyStorybookToken,\n} from \"@storyshelf/core/utils\";\nimport type { Context } from \"hono\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { credentialKeys } from \"../credential-keys.ts\";\nimport { getStore } from \"../store.ts\";\nimport { publishedAccess } from \"./published-access.ts\";\nimport {\n publishedRelativePath,\n servePublishedFile,\n SIGNED_URL_HEADERS,\n} from \"./published-files.ts\";\n\n/** Register the signed-URL file route. */\nexport function registerSignedStorybook(app: ShelfRouter): void {\n app.get(`${STORYBOOK_TOKEN_PREFIX}:token/*`, async (c) => await serveSigned(c));\n}\n\nasync function serveSigned(c: Context): Promise<Response> {\n const token = c.req.param(\"token\") ?? \"\";\n const rel = publishedRelativePath(c.req.path, storybookTokenPath(token));\n if (!rel) {\n return refuse(c, \"bad path\");\n }\n return (await serveGranted(c, token, rel)) ?? refuse(c, \"no such file\");\n}\n\n/** The file the token grants as a response, or null (for the debug log only: why not). */\nasync function serveGranted(c: Context, token: string, rel: string): Promise<Response | null> {\n const { config, db } = getStore();\n const verified = verifyStorybookToken(token, credentialKeys(config));\n if (!verified.ok) {\n return refuse(c, verified.reason);\n }\n const { projectId, buildId } = verified.claims;\n const [project, build] = await Promise.all([\n new ProjectModel(db).get(projectId),\n new BuildModel(db).get(buildId),\n ]);\n const access = project && build ? await publishedAccess(build, project, verified.claims) : null;\n if (access !== \"token\") {\n return refuse(c, \"unknown build\");\n }\n return await servePublishedFile(c, {\n projectId,\n buildId,\n rel,\n access,\n cacheControl: \"\",\n grant: verified.claims,\n });\n}\n\n/** The one answer for every refusal, so a caller cannot tell a bad signature from an expired token. */\nfunction refuse(c: Context, reason: string): Response {\n getStore().logger.debug({ reason }, \"signed storybook request refused\");\n return c.text(\"Not found\", 404, SIGNED_URL_HEADERS);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AA2BA,SAAgB,wBAAwB,KAAwB;CAC9D,IAAI,IAAI,GAAG,uBAAuB,WAAW,OAAO,MAAM,MAAM,YAAY,CAAC,CAAC;AAChF;AAEA,eAAe,YAAY,GAA+B;CACxD,MAAM,QAAQ,EAAE,IAAI,MAAM,OAAO,KAAK;CACtC,MAAM,MAAM,sBAAsB,EAAE,IAAI,MAAM,mBAAmB,KAAK,CAAC;CACvE,IAAI,CAAC,KACH,OAAO,OAAO,GAAG,UAAU;CAE7B,OAAQ,MAAM,aAAa,GAAG,OAAO,GAAG,KAAM,OAAO,GAAG,cAAc;AACxE;;AAGA,eAAe,aAAa,GAAY,OAAe,KAAuC;CAC5F,MAAM,EAAE,QAAQ,OAAO,SAAS;CAChC,MAAM,WAAW,qBAAqB,OAAO,eAAe,MAAM,CAAC;CACnE,IAAI,CAAC,SAAS,IACZ,OAAO,OAAO,GAAG,SAAS,MAAM;CAElC,MAAM,EAAE,WAAW,YAAY,SAAS;CACxC,MAAM,CAAC,SAAS,SAAS,MAAM,QAAQ,IAAI,CACzC,IAAI,aAAa,EAAE,CAAC,CAAC,IAAI,SAAS,GAClC,IAAI,WAAW,EAAE,CAAC,CAAC,IAAI,OAAO,CAChC,CAAC;CACD,MAAM,SAAS,WAAW,QAAQ,MAAM,gBAAgB,OAAO,SAAS,SAAS,MAAM,IAAI;CAC3F,IAAI,WAAW,SACb,OAAO,OAAO,GAAG,eAAe;CAElC,OAAO,MAAM,mBAAmB,GAAG;EACjC;EACA;EACA;EACA;EACA,cAAc;EACd,OAAO,SAAS;CAClB,CAAC;AACH;;AAGA,SAAS,OAAO,GAAY,QAA0B;CACpD,SAAS,CAAC,CAAC,OAAO,MAAM,EAAE,OAAO,GAAG,kCAAkC;CACtE,OAAO,EAAE,KAAK,aAAa,KAAK,kBAAkB;AACpD"}
@@ -0,0 +1,62 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { getStore } from "../store.mjs";
5
+ import { notFound } from "./helpers.mjs";
6
+ import { baselineChanged, findDriftedSnapshots, isOpenSnapshot } from "./baseline-guard.mjs";
7
+ import { approveSnapshot, refreshBuild } from "./builds.handlers.mjs";
8
+ import { SnapshotModel } from "@storyshelf/core/models";
9
+ // src/routers/snapshot-groups.ts
10
+ /** Shared (story, mode) review: one decision covers every browser's snapshot. */
11
+ /** Identity of a review group: the same story and mode across engines. */
12
+ function groupKey(snapshot) {
13
+ return `${snapshot.storyId}\u0000${snapshot.viewportName}`;
14
+ }
15
+ /** Open snapshots of one story/mode; 404 when the group has nothing left to decide. */
16
+ async function openMembers(build, storyId, modeKey) {
17
+ const members = (await new SnapshotModel(getStore().db).listByBuild(build.id)).filter((snapshot) => snapshot.storyId === storyId && snapshot.viewportName === modeKey && isOpenSnapshot(snapshot));
18
+ if (members.length === 0) notFound("No open snapshots for this story and mode");
19
+ return members;
20
+ }
21
+ /**
22
+ * Approve every open snapshot of a story/mode, promoting each engine's
23
+ * baseline. All-or-nothing on drift: when any member's baseline moved and
24
+ * `force` is off, nothing is written and the 409 names that snapshot.
25
+ *
26
+ * @returns The approved snapshot ids, primary engine first.
27
+ */
28
+ async function approveGroup(project, build, target, userId, options = {}) {
29
+ const members = await openMembers(build, target.storyId, target.modeKey);
30
+ if (!options.force) {
31
+ const first = [...(await findDriftedSnapshots(project, build, members)).entries()][0];
32
+ if (first) throw baselineChanged(first[0], first[1]);
33
+ }
34
+ for (const member of members) await approveSnapshot(member.id, userId, {
35
+ force: options.force ?? false,
36
+ deferRefresh: true
37
+ });
38
+ await refreshBuild(build.id);
39
+ return members.map((member) => member.id);
40
+ }
41
+ /** Reject every open snapshot of a story/mode and refresh the build once. */
42
+ async function rejectGroup(build, target, userId) {
43
+ const members = await openMembers(build, target.storyId, target.modeKey);
44
+ const snapshots = new SnapshotModel(getStore().db);
45
+ await Promise.all(members.map(async (member) => await snapshots.review(member.id, "rejected", userId)));
46
+ await refreshBuild(build.id);
47
+ return members.map((member) => member.id);
48
+ }
49
+ /**
50
+ * Split open snapshots into approvable groups and groups skipped because some
51
+ * member drifted (the whole group is skipped, never half-approved).
52
+ */
53
+ function splitByDrift(open, drifted) {
54
+ const blocked = new Set(open.filter((s) => drifted.has(s.id)).map((s) => groupKey(s)));
55
+ return {
56
+ approvable: open.filter((s) => !blocked.has(groupKey(s))),
57
+ skipped: open.filter((s) => blocked.has(groupKey(s)))
58
+ };
59
+ }
60
+ export { approveGroup, groupKey, rejectGroup, splitByDrift };
61
+
62
+ //# sourceMappingURL=snapshot-groups.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshot-groups.mjs","names":[],"sources":["../../src/routers/snapshot-groups.ts"],"sourcesContent":["/** Shared (story, mode) review: one decision covers every browser's snapshot. */\nimport { SnapshotModel } from \"@storyshelf/core/models\";\nimport type { Build, Project, Snapshot } from \"@storyshelf/core/schema\";\nimport { getStore } from \"../store.ts\";\nimport { baselineChanged, findDriftedSnapshots, isOpenSnapshot } from \"./baseline-guard.ts\";\nimport { approveSnapshot, refreshBuild } from \"./builds.handlers.ts\";\nimport { notFound } from \"./helpers.ts\";\n\n/** Identity of a review group: the same story and mode across engines. */\nexport function groupKey(snapshot: Pick<Snapshot, \"storyId\" | \"viewportName\">): string {\n return `${snapshot.storyId}\\u0000${snapshot.viewportName}`;\n}\n\n/** Open snapshots of one story/mode; 404 when the group has nothing left to decide. */\nasync function openMembers(build: Build, storyId: string, modeKey: string): Promise<Snapshot[]> {\n const all = await new SnapshotModel(getStore().db).listByBuild(build.id);\n const members = all.filter(\n (snapshot) =>\n snapshot.storyId === storyId && snapshot.viewportName === modeKey && isOpenSnapshot(snapshot),\n );\n if (members.length === 0) {\n notFound(\"No open snapshots for this story and mode\");\n }\n return members;\n}\n\n/**\n * Approve every open snapshot of a story/mode, promoting each engine's\n * baseline. All-or-nothing on drift: when any member's baseline moved and\n * `force` is off, nothing is written and the 409 names that snapshot.\n *\n * @returns The approved snapshot ids, primary engine first.\n */\nexport async function approveGroup(\n project: Project,\n build: Build,\n target: { storyId: string; modeKey: string },\n userId: string | null,\n options: { force?: boolean } = {},\n): Promise<string[]> {\n const members = await openMembers(build, target.storyId, target.modeKey);\n if (!options.force) {\n const drifted = await findDriftedSnapshots(project, build, members);\n const first = [...drifted.entries()][0];\n if (first) {\n throw baselineChanged(first[0], first[1]);\n }\n }\n for (const member of members) {\n // oxlint-disable-next-line no-await-in-loop -- baselines are promoted engine by engine\n await approveSnapshot(member.id, userId, {\n force: options.force ?? false,\n deferRefresh: true,\n });\n }\n await refreshBuild(build.id);\n return members.map((member) => member.id);\n}\n\n/** Reject every open snapshot of a story/mode and refresh the build once. */\nexport async function rejectGroup(\n build: Build,\n target: { storyId: string; modeKey: string },\n userId: string | null,\n): Promise<string[]> {\n const members = await openMembers(build, target.storyId, target.modeKey);\n const snapshots = new SnapshotModel(getStore().db);\n await Promise.all(\n members.map(async (member) => await snapshots.review(member.id, \"rejected\", userId)),\n );\n await refreshBuild(build.id);\n return members.map((member) => member.id);\n}\n\n/**\n * Split open snapshots into approvable groups and groups skipped because some\n * member drifted (the whole group is skipped, never half-approved).\n */\nexport function splitByDrift(\n open: Snapshot[],\n drifted: ReadonlyMap<string, unknown>,\n): { approvable: Snapshot[]; skipped: Snapshot[] } {\n const blocked = new Set(open.filter((s) => drifted.has(s.id)).map((s) => groupKey(s)));\n return {\n approvable: open.filter((s) => !blocked.has(groupKey(s))),\n skipped: open.filter((s) => blocked.has(groupKey(s))),\n };\n}\n"],"mappings":";;;;;;;;;;;AASA,SAAgB,SAAS,UAA8D;CACrF,OAAO,GAAG,SAAS,QAAQ,QAAQ,SAAS;AAC9C;;AAGA,eAAe,YAAY,OAAc,SAAiB,SAAsC;CAE9F,MAAM,WAAU,MADE,IAAI,cAAc,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,YAAY,MAAM,EAAE,EAAA,CACnD,QACjB,aACC,SAAS,YAAY,WAAW,SAAS,iBAAiB,WAAW,eAAe,QAAQ,CAChG;CACA,IAAI,QAAQ,WAAW,GACrB,SAAS,2CAA2C;CAEtD,OAAO;AACT;;;;;;;;AASA,eAAsB,aACpB,SACA,OACA,QACA,QACA,UAA+B,CAAC,GACb;CACnB,MAAM,UAAU,MAAM,YAAY,OAAO,OAAO,SAAS,OAAO,OAAO;CACvE,IAAI,CAAC,QAAQ,OAAO;EAElB,MAAM,QAAQ,CAAC,IAAG,MADI,qBAAqB,SAAS,OAAO,OAAO,EAAA,CACxC,QAAQ,CAAC,CAAC,CAAC;EACrC,IAAI,OACF,MAAM,gBAAgB,MAAM,IAAI,MAAM,EAAE;CAE5C;CACA,KAAK,MAAM,UAAU,SAEnB,MAAM,gBAAgB,OAAO,IAAI,QAAQ;EACvC,OAAO,QAAQ,SAAS;EACxB,cAAc;CAChB,CAAC;CAEH,MAAM,aAAa,MAAM,EAAE;CAC3B,OAAO,QAAQ,KAAK,WAAW,OAAO,EAAE;AAC1C;;AAGA,eAAsB,YACpB,OACA,QACA,QACmB;CACnB,MAAM,UAAU,MAAM,YAAY,OAAO,OAAO,SAAS,OAAO,OAAO;CACvE,MAAM,YAAY,IAAI,cAAc,SAAS,CAAC,CAAC,EAAE;CACjD,MAAM,QAAQ,IACZ,QAAQ,IAAI,OAAO,WAAW,MAAM,UAAU,OAAO,OAAO,IAAI,YAAY,MAAM,CAAC,CACrF;CACA,MAAM,aAAa,MAAM,EAAE;CAC3B,OAAO,QAAQ,KAAK,WAAW,OAAO,EAAE;AAC1C;;;;;AAMA,SAAgB,aACd,MACA,SACiD;CACjD,MAAM,UAAU,IAAI,IAAI,KAAK,QAAQ,MAAM,QAAQ,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,MAAM,SAAS,CAAC,CAAC,CAAC;CACrF,OAAO;EACL,YAAY,KAAK,QAAQ,MAAM,CAAC,QAAQ,IAAI,SAAS,CAAC,CAAC,CAAC;EACxD,SAAS,KAAK,QAAQ,MAAM,QAAQ,IAAI,SAAS,CAAC,CAAC,CAAC;CACtD;AACF"}
@@ -2,18 +2,19 @@ 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, isHxRequest } from "./htmx.mjs";
6
7
  import { flashHx } from "./flash.mjs";
7
- import { resolveAuthorizedProject } from "./helpers.mjs";
8
- import { approveAllResultSchema, baselineChangedSchema, errorSchema, notFound, okSchema, rediffResultSchema, snapshotSchema, unauthorized } from "./schemas.mjs";
8
+ import { approveAllResultSchema, baselineChangedSchema, errorSchema, groupDecisionResultSchema, notFound, okSchema, rediffResultSchema, snapshotSchema, unauthorized } from "./schemas.mjs";
9
9
  import { findDriftedSnapshots, isBaselineChanged, isOpenSnapshot } from "./baseline-guard.mjs";
10
10
  import { APPROVER_ROLES, VIEW_ROLES, approveSnapshot, buildForProject, refreshBuild, snapshotForBuild } from "./builds.handlers.mjs";
11
11
  import { hxAdvance } from "./review-next.mjs";
12
+ import { approveGroup, rejectGroup, splitByDrift } from "./snapshot-groups.mjs";
12
13
  import { createRoute, z } from "@hono/zod-openapi";
13
14
  import { REDIFFABLE_BUILD_STATUSES, rediffBuild } from "@storyshelf/core/capture";
14
15
  import { SnapshotModel } from "@storyshelf/core/models";
15
16
  import { HTTPException } from "hono/http-exception";
16
- //#region src/routers/snapshots.ts
17
+ // src/routers/snapshots.ts
17
18
  const listSnapshotsRoute = createRoute({
18
19
  method: "get",
19
20
  tags: ["Review"],
@@ -95,6 +96,47 @@ const approveAllRoute = createRoute({
95
96
  ...notFound
96
97
  }
97
98
  });
99
+ const groupParams = z.object({
100
+ slug: z.string(),
101
+ buildId: z.string(),
102
+ storyId: z.string(),
103
+ modeKey: z.string()
104
+ });
105
+ const approveGroupRoute = createRoute({
106
+ method: "post",
107
+ tags: ["Review"],
108
+ summary: "Approve a story/mode across all browsers",
109
+ path: "/api/v1/projects/{slug}/builds/{buildId}/stories/{storyId}/modes/{modeKey}/approve",
110
+ request: {
111
+ params: groupParams,
112
+ query: z.object({ force: z.enum(["true", "false"]).optional() })
113
+ },
114
+ responses: {
115
+ 200: {
116
+ content: { "application/json": { schema: groupDecisionResultSchema } },
117
+ description: "Every open snapshot of the story and mode approved and each engine's baseline promoted"
118
+ },
119
+ 409: {
120
+ content: { "application/json": { schema: baselineChangedSchema } },
121
+ description: "A member's baseline changed after its diff; nothing was approved. Re-diff or pass force=true"
122
+ },
123
+ ...notFound
124
+ }
125
+ });
126
+ const rejectGroupRoute = createRoute({
127
+ method: "post",
128
+ tags: ["Review"],
129
+ summary: "Reject a story/mode across all browsers",
130
+ path: "/api/v1/projects/{slug}/builds/{buildId}/stories/{storyId}/modes/{modeKey}/reject",
131
+ request: { params: groupParams },
132
+ responses: {
133
+ 200: {
134
+ content: { "application/json": { schema: groupDecisionResultSchema } },
135
+ description: "Every open snapshot of the story and mode rejected"
136
+ },
137
+ ...notFound
138
+ }
139
+ });
98
140
  const rediffRoute = createRoute({
99
141
  method: "post",
100
142
  tags: ["Review"],
@@ -191,15 +233,71 @@ function registerSnapshots(app) {
191
233
  const userId = getStore().user?.id ?? null;
192
234
  const forced = force === "true";
193
235
  const drifted = forced ? /* @__PURE__ */ new Map() : await findDriftedSnapshots(project, build, snapshots);
194
- await Promise.all(snapshots.filter((snapshot) => isOpenSnapshot(snapshot) && !drifted.has(snapshot.id)).map(async (snapshot) => {
195
- await approveSnapshot(snapshot.id, userId, { force: forced });
236
+ const { approvable, skipped } = splitByDrift(snapshots.filter((snapshot) => isOpenSnapshot(snapshot)), drifted);
237
+ await Promise.all(approvable.map(async (snapshot) => {
238
+ await approveSnapshot(snapshot.id, userId, {
239
+ force: forced,
240
+ deferRefresh: true
241
+ });
196
242
  }));
243
+ await refreshBuild(build.id);
197
244
  hxRefresh(c);
198
245
  return c.json({
199
246
  ok: true,
200
- skipped: [...drifted.keys()]
247
+ skipped: skipped.map((snapshot) => snapshot.id)
201
248
  });
202
249
  });
250
+ app.openapi(approveGroupRoute, async (c) => {
251
+ const { slug, buildId, storyId, modeKey } = c.req.valid("param");
252
+ const { force } = c.req.valid("query");
253
+ const project = await resolveAuthorizedProject(c, slug, ...APPROVER_ROLES);
254
+ const build = await buildForProject(project.id, buildId);
255
+ const userId = getStore().user?.id ?? null;
256
+ let snapshotIds;
257
+ try {
258
+ snapshotIds = await approveGroup(project, build, {
259
+ storyId,
260
+ modeKey
261
+ }, userId, { force: force === "true" });
262
+ } catch (error) {
263
+ if (!isHxRequest(c) || !isBaselineChanged(error)) throw error;
264
+ c.header("HX-Redirect", `/projects/${slug}/builds/${buildId}/diff`);
265
+ return c.json({
266
+ ok: true,
267
+ snapshotIds: []
268
+ }, 200);
269
+ }
270
+ const first = snapshotIds[0];
271
+ if (first) await hxAdvance(c, {
272
+ slug,
273
+ buildId,
274
+ snapshotId: first
275
+ });
276
+ return c.json({
277
+ ok: true,
278
+ snapshotIds
279
+ }, 200);
280
+ });
281
+ app.openapi(rejectGroupRoute, async (c) => {
282
+ const { slug, buildId, storyId, modeKey } = c.req.valid("param");
283
+ const project = await resolveAuthorizedProject(c, slug, ...APPROVER_ROLES);
284
+ const build = await buildForProject(project.id, buildId);
285
+ const userId = getStore().user?.id ?? null;
286
+ const snapshotIds = await rejectGroup(build, {
287
+ storyId,
288
+ modeKey
289
+ }, userId);
290
+ const first = snapshotIds[0];
291
+ if (first) await hxAdvance(c, {
292
+ slug,
293
+ buildId,
294
+ snapshotId: first
295
+ });
296
+ return c.json({
297
+ ok: true,
298
+ snapshotIds
299
+ }, 200);
300
+ });
203
301
  app.openapi(rediffRoute, async (c) => {
204
302
  const { slug, buildId } = c.req.valid("param");
205
303
  const project = await resolveAuthorizedProject(c, slug, ...APPROVER_ROLES);
@@ -234,7 +332,6 @@ function registerSnapshots(app) {
234
332
  return c.json({ ok: true });
235
333
  });
236
334
  }
237
- //#endregion
238
335
  export { registerSnapshots };
239
336
 
240
337
  //# sourceMappingURL=snapshots.mjs.map