@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
@@ -1,10 +1,12 @@
1
1
  import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
+ import { credentialKeys } from "../credential-keys.mjs";
4
5
  import { getStore } from "../store.mjs";
5
6
  import { notFound, resolveAuthorizedProject } from "./helpers.mjs";
6
- import { BaselineModel, BuildModel, SnapshotModel } from "@storyshelf/core/models";
7
- //#region src/routers/media.ts
7
+ import { readComparisonImage, validFrozenReference } from "./comparison-image.mjs";
8
+ import { BaselineModel, BuildModel, DesignCredentialModel, DesignNodeModel, SnapshotModel, baselineStatus } from "@storyshelf/core/models";
9
+ // src/routers/media.ts
8
10
  const VIEW_ROLES = [
9
11
  "viewer",
10
12
  "developer",
@@ -12,12 +14,14 @@ const VIEW_ROLES = [
12
14
  "admin"
13
15
  ];
14
16
  const PNG = "image/png";
15
- function imageResponse(buffer) {
17
+ function imageResponse(buffer, cacheControl = "private, max-age=3600") {
16
18
  return new Response(new Uint8Array(buffer), { headers: {
17
19
  "content-type": PNG,
18
- "cache-control": "private, max-age=3600"
20
+ "cache-control": cacheControl
19
21
  } });
20
22
  }
23
+ /** Design renders are addressed by file version (`?v=`): immutable once the version matches the row. */
24
+ const RENDER_IMMUTABLE = "private, max-age=31536000, immutable";
21
25
  /** Register the PNG image, diff, and baseline media endpoints for snapshots. */
22
26
  function registerMedia(app) {
23
27
  app.get("/api/v1/projects/:slug/builds/:buildId/snapshots/:snapshotId/image", async (c) => {
@@ -31,15 +35,35 @@ function registerMedia(app) {
31
35
  if (!snapshot.diffPath) notFound("No diff for this snapshot");
32
36
  return imageResponse(await getStore().storage.read(snapshot.diffPath));
33
37
  });
38
+ app.get("/api/v1/projects/:slug/design/renders/:renderId", async (c) => {
39
+ const project = await resolveAuthorizedProject(c, c.req.param("slug"), ...VIEW_ROLES);
40
+ const { db, config, storage, design } = getStore();
41
+ const credentials = new DesignCredentialModel(db, credentialKeys(config));
42
+ const nodes = new DesignNodeModel(db, credentials, design, { storage });
43
+ const found = await nodes.get(project.id, c.req.param("renderId"));
44
+ const node = found && (found.viewerId === "" || found.viewerId === getStore().user?.id) ? found : null;
45
+ const png = node ? await nodes.readRender(node) : null;
46
+ if (!png) notFound("No design render");
47
+ return imageResponse(png, node?.renderVersion && c.req.query("v") === node.renderVersion ? RENDER_IMMUTABLE : void 0);
48
+ });
34
49
  app.get("/api/v1/projects/:slug/builds/:buildId/snapshots/:snapshotId/baseline", async (c) => {
35
50
  const project = await resolveAuthorizedProject(c, c.req.param("slug"), ...VIEW_ROLES);
36
51
  const snapshot = await findSnapshot(c.req.param("buildId"), c.req.param("snapshotId"), project.id);
37
52
  const build = await new BuildModel(getStore().db).get(snapshot.buildId);
38
53
  if (!build) notFound("Build not found");
54
+ if (c.req.query("reference") !== "default" && (snapshot.comparisonImagePath || snapshot.comparisonMetadata)) {
55
+ const frozen = validFrozenReference(snapshot, project.id, build.id);
56
+ if (!frozen) notFound("Original baseline is unavailable");
57
+ const image = await readComparisonImage(frozen.path, frozen.reference.imageSha256);
58
+ if (!image) notFound("Original baseline image is unavailable");
59
+ return imageResponse(image, "private, no-store");
60
+ }
39
61
  const baselines = new BaselineModel(getStore().db, void 0, getStore().storage);
40
- const baseline = await baselines.resolve(project.id, snapshot.storyId, snapshot.viewportName, build.gitBranch, project.gitDefaultBranch);
62
+ const baseline = c.req.query("reference") === "default" ? await baselines.getFor(project.id, snapshot.storyId, snapshot.viewportName, project.gitDefaultBranch, snapshot.browser) : await baselines.resolve(project.id, snapshot.storyId, snapshot.viewportName, build.gitBranch, project.gitDefaultBranch, snapshot.browser);
63
+ if (c.req.query("reference") === "default" && (baseline?.id !== c.req.query("id") || baseline?.updatedAt !== c.req.query("version"))) notFound("Default baseline changed; reload the comparison");
41
64
  if (!baseline) notFound("No baseline for this snapshot");
42
- return imageResponse(await baselines.read(baseline));
65
+ if (c.req.query("reference") !== "default" && snapshot.baselineVersion !== null && baselineStatus(snapshot, baseline) !== "current") notFound("Original baseline is no longer available");
66
+ return imageResponse(await baselines.read(baseline), "private, no-store");
43
67
  });
44
68
  }
45
69
  async function findSnapshot(buildId, snapshotId, projectId) {
@@ -47,7 +71,6 @@ async function findSnapshot(buildId, snapshotId, projectId) {
47
71
  if (!snapshot || snapshot.buildId !== buildId || snapshot.projectId !== projectId) notFound("Snapshot not found");
48
72
  return snapshot;
49
73
  }
50
- //#endregion
51
74
  export { registerMedia };
52
75
 
53
76
  //# sourceMappingURL=media.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"media.mjs","names":[],"sources":["../../src/routers/media.ts"],"sourcesContent":["import { BaselineModel } from \"@storyshelf/core/models\";\nimport { BuildModel } from \"@storyshelf/core/models\";\nimport { SnapshotModel } from \"@storyshelf/core/models\";\nimport type { Snapshot } from \"@storyshelf/core/schema\";\nimport type { ProjectRole } from \"@storyshelf/core/types\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { getStore } from \"../store.ts\";\nimport { notFound, resolveAuthorizedProject } from \"./helpers.ts\";\nconst VIEW_ROLES: readonly ProjectRole[] = [\"viewer\", \"developer\", \"approver\", \"admin\"];\n\nconst PNG = \"image/png\";\n\nfunction imageResponse(buffer: Buffer): Response {\n return new Response(new Uint8Array(buffer), {\n headers: { \"content-type\": PNG, \"cache-control\": \"private, max-age=3600\" },\n });\n}\n\n/** Register the PNG image, diff, and baseline media endpoints for snapshots. */\nexport function registerMedia(app: ShelfRouter): void {\n app.get(\"/api/v1/projects/:slug/builds/:buildId/snapshots/:snapshotId/image\", async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.param(\"slug\"), ...VIEW_ROLES);\n const snapshot = await findSnapshot(\n c.req.param(\"buildId\"),\n c.req.param(\"snapshotId\"),\n project.id,\n );\n return imageResponse(await getStore().storage.read(snapshot.screenshotPath));\n });\n\n app.get(\"/api/v1/projects/:slug/builds/:buildId/snapshots/:snapshotId/diff\", async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.param(\"slug\"), ...VIEW_ROLES);\n const snapshot = await findSnapshot(\n c.req.param(\"buildId\"),\n c.req.param(\"snapshotId\"),\n project.id,\n );\n if (!snapshot.diffPath) {\n notFound(\"No diff for this snapshot\");\n }\n return imageResponse(await getStore().storage.read(snapshot.diffPath));\n });\n\n app.get(\"/api/v1/projects/:slug/builds/:buildId/snapshots/:snapshotId/baseline\", async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.param(\"slug\"), ...VIEW_ROLES);\n const snapshot = await findSnapshot(\n c.req.param(\"buildId\"),\n c.req.param(\"snapshotId\"),\n project.id,\n );\n const build = await new BuildModel(getStore().db).get(snapshot.buildId);\n if (!build) {\n notFound(\"Build not found\");\n }\n const baselines = new BaselineModel(getStore().db, undefined, getStore().storage);\n const baseline = await baselines.resolve(\n project.id,\n snapshot.storyId,\n snapshot.viewportName,\n build.gitBranch,\n project.gitDefaultBranch,\n );\n if (!baseline) {\n notFound(\"No baseline for this snapshot\");\n }\n return imageResponse(await baselines.read(baseline));\n });\n}\n\nasync function findSnapshot(\n buildId: string,\n snapshotId: string,\n projectId: string,\n): Promise<Snapshot> {\n const snapshot = await new SnapshotModel(getStore().db).get(snapshotId);\n if (!snapshot || snapshot.buildId !== buildId || snapshot.projectId !== projectId) {\n notFound(\"Snapshot not found\");\n }\n return snapshot;\n}\n"],"mappings":";;;;;;;AAQA,MAAM,aAAqC;CAAC;CAAU;CAAa;CAAY;AAAO;AAEtF,MAAM,MAAM;AAEZ,SAAS,cAAc,QAA0B;CAC/C,OAAO,IAAI,SAAS,IAAI,WAAW,MAAM,GAAG,EAC1C,SAAS;EAAE,gBAAgB;EAAK,iBAAiB;CAAwB,EAC3E,CAAC;AACH;;AAGA,SAAgB,cAAc,KAAwB;CACpD,IAAI,IAAI,sEAAsE,OAAO,MAAM;EACzF,MAAM,UAAU,MAAM,yBAAyB,GAAG,EAAE,IAAI,MAAM,MAAM,GAAG,GAAG,UAAU;EACpF,MAAM,WAAW,MAAM,aACrB,EAAE,IAAI,MAAM,SAAS,GACrB,EAAE,IAAI,MAAM,YAAY,GACxB,QAAQ,EACV;EACA,OAAO,cAAc,MAAM,SAAS,CAAC,CAAC,QAAQ,KAAK,SAAS,cAAc,CAAC;CAC7E,CAAC;CAED,IAAI,IAAI,qEAAqE,OAAO,MAAM;EACxF,MAAM,UAAU,MAAM,yBAAyB,GAAG,EAAE,IAAI,MAAM,MAAM,GAAG,GAAG,UAAU;EACpF,MAAM,WAAW,MAAM,aACrB,EAAE,IAAI,MAAM,SAAS,GACrB,EAAE,IAAI,MAAM,YAAY,GACxB,QAAQ,EACV;EACA,IAAI,CAAC,SAAS,UACZ,SAAS,2BAA2B;EAEtC,OAAO,cAAc,MAAM,SAAS,CAAC,CAAC,QAAQ,KAAK,SAAS,QAAQ,CAAC;CACvE,CAAC;CAED,IAAI,IAAI,yEAAyE,OAAO,MAAM;EAC5F,MAAM,UAAU,MAAM,yBAAyB,GAAG,EAAE,IAAI,MAAM,MAAM,GAAG,GAAG,UAAU;EACpF,MAAM,WAAW,MAAM,aACrB,EAAE,IAAI,MAAM,SAAS,GACrB,EAAE,IAAI,MAAM,YAAY,GACxB,QAAQ,EACV;EACA,MAAM,QAAQ,MAAM,IAAI,WAAW,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,SAAS,OAAO;EACtE,IAAI,CAAC,OACH,SAAS,iBAAiB;EAE5B,MAAM,YAAY,IAAI,cAAc,SAAS,CAAC,CAAC,IAAI,KAAA,GAAW,SAAS,CAAC,CAAC,OAAO;EAChF,MAAM,WAAW,MAAM,UAAU,QAC/B,QAAQ,IACR,SAAS,SACT,SAAS,cACT,MAAM,WACN,QAAQ,gBACV;EACA,IAAI,CAAC,UACH,SAAS,+BAA+B;EAE1C,OAAO,cAAc,MAAM,UAAU,KAAK,QAAQ,CAAC;CACrD,CAAC;AACH;AAEA,eAAe,aACb,SACA,YACA,WACmB;CACnB,MAAM,WAAW,MAAM,IAAI,cAAc,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,UAAU;CACtE,IAAI,CAAC,YAAY,SAAS,YAAY,WAAW,SAAS,cAAc,WACtE,SAAS,oBAAoB;CAE/B,OAAO;AACT"}
1
+ {"version":3,"file":"media.mjs","names":[],"sources":["../../src/routers/media.ts"],"sourcesContent":["import {\n BaselineModel,\n DesignCredentialModel,\n DesignNodeModel,\n baselineStatus,\n} from \"@storyshelf/core/models\";\nimport { BuildModel } from \"@storyshelf/core/models\";\nimport { SnapshotModel } from \"@storyshelf/core/models\";\nimport type { Snapshot } from \"@storyshelf/core/schema\";\nimport type { ProjectRole } from \"@storyshelf/core/types\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { credentialKeys } from \"../credential-keys.ts\";\nimport { getStore } from \"../store.ts\";\nimport { readComparisonImage, validFrozenReference } from \"./comparison-image.ts\";\nimport { notFound, resolveAuthorizedProject } from \"./helpers.ts\";\nconst VIEW_ROLES: readonly ProjectRole[] = [\"viewer\", \"developer\", \"approver\", \"admin\"];\n\nconst PNG = \"image/png\";\n\nfunction imageResponse(buffer: Buffer, cacheControl = \"private, max-age=3600\"): Response {\n return new Response(new Uint8Array(buffer), {\n headers: { \"content-type\": PNG, \"cache-control\": cacheControl },\n });\n}\n\n/** Design renders are addressed by file version (`?v=`): immutable once the version matches the row. */\nconst RENDER_IMMUTABLE = \"private, max-age=31536000, immutable\";\n\n/** Register the PNG image, diff, and baseline media endpoints for snapshots. */\nexport function registerMedia(app: ShelfRouter): void {\n app.get(\"/api/v1/projects/:slug/builds/:buildId/snapshots/:snapshotId/image\", async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.param(\"slug\"), ...VIEW_ROLES);\n const snapshot = await findSnapshot(\n c.req.param(\"buildId\"),\n c.req.param(\"snapshotId\"),\n project.id,\n );\n return imageResponse(await getStore().storage.read(snapshot.screenshotPath));\n });\n\n app.get(\"/api/v1/projects/:slug/builds/:buildId/snapshots/:snapshotId/diff\", async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.param(\"slug\"), ...VIEW_ROLES);\n const snapshot = await findSnapshot(\n c.req.param(\"buildId\"),\n c.req.param(\"snapshotId\"),\n project.id,\n );\n if (!snapshot.diffPath) {\n notFound(\"No diff for this snapshot\");\n }\n return imageResponse(await getStore().storage.read(snapshot.diffPath));\n });\n\n // `:renderId` is the internal design_nodes row id (a ULID), not a provider node id.\n app.get(\"/api/v1/projects/:slug/design/renders/:renderId\", async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.param(\"slug\"), ...VIEW_ROLES);\n const { db, config, storage, design } = getStore();\n const credentials = new DesignCredentialModel(db, credentialKeys(config));\n const nodes = new DesignNodeModel(db, credentials, design, { storage });\n const found = await nodes.get(project.id, c.req.param(\"renderId\"));\n // A render fetched with a member's own connection is theirs alone (it may show files others cannot open).\n const node =\n found && (found.viewerId === \"\" || found.viewerId === getStore().user?.id) ? found : null;\n const png = node ? await nodes.readRender(node) : null;\n if (!png) {\n notFound(\"No design render\");\n }\n const versioned = node?.renderVersion && c.req.query(\"v\") === node.renderVersion;\n return imageResponse(png, versioned ? RENDER_IMMUTABLE : undefined);\n });\n\n app.get(\"/api/v1/projects/:slug/builds/:buildId/snapshots/:snapshotId/baseline\", async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.param(\"slug\"), ...VIEW_ROLES);\n const snapshot = await findSnapshot(\n c.req.param(\"buildId\"),\n c.req.param(\"snapshotId\"),\n project.id,\n );\n const build = await new BuildModel(getStore().db).get(snapshot.buildId);\n if (!build) {\n notFound(\"Build not found\");\n }\n if (\n c.req.query(\"reference\") !== \"default\" &&\n (snapshot.comparisonImagePath || snapshot.comparisonMetadata)\n ) {\n const frozen = validFrozenReference(snapshot, project.id, build.id);\n if (!frozen) {\n notFound(\"Original baseline is unavailable\");\n }\n const image = await readComparisonImage(frozen.path, frozen.reference.imageSha256);\n if (!image) {\n notFound(\"Original baseline image is unavailable\");\n }\n return imageResponse(image, \"private, no-store\");\n }\n const baselines = new BaselineModel(getStore().db, undefined, getStore().storage);\n const baseline =\n c.req.query(\"reference\") === \"default\"\n ? await baselines.getFor(\n project.id,\n snapshot.storyId,\n snapshot.viewportName,\n project.gitDefaultBranch,\n snapshot.browser,\n )\n : await baselines.resolve(\n project.id,\n snapshot.storyId,\n snapshot.viewportName,\n build.gitBranch,\n project.gitDefaultBranch,\n snapshot.browser,\n );\n if (\n c.req.query(\"reference\") === \"default\" &&\n (baseline?.id !== c.req.query(\"id\") || baseline?.updatedAt !== c.req.query(\"version\"))\n ) {\n notFound(\"Default baseline changed; reload the comparison\");\n }\n if (!baseline) {\n notFound(\"No baseline for this snapshot\");\n }\n if (\n c.req.query(\"reference\") !== \"default\" &&\n snapshot.baselineVersion !== null &&\n baselineStatus(snapshot, baseline) !== \"current\"\n ) {\n notFound(\"Original baseline is no longer available\");\n }\n return imageResponse(await baselines.read(baseline), \"private, no-store\");\n });\n}\n\nasync function findSnapshot(\n buildId: string,\n snapshotId: string,\n projectId: string,\n): Promise<Snapshot> {\n const snapshot = await new SnapshotModel(getStore().db).get(snapshotId);\n if (!snapshot || snapshot.buildId !== buildId || snapshot.projectId !== projectId) {\n notFound(\"Snapshot not found\");\n }\n return snapshot;\n}\n"],"mappings":"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"}
@@ -7,7 +7,7 @@ import { groupMappingCreateSchema, groupMappingSchema, memberRoleSchema, memberS
7
7
  import { createRoute, z } from "@hono/zod-openapi";
8
8
  import { MemberModel, ProjectGroupMappingModel } from "@storyshelf/core/models";
9
9
  import { HTTPException } from "hono/http-exception";
10
- //#region src/routers/members.ts
10
+ // src/routers/members.ts
11
11
  const VIEW_ROLES = [
12
12
  "viewer",
13
13
  "developer",
@@ -161,7 +161,8 @@ function registerGroupMappingRoutes(app) {
161
161
  app.openapi(createGroupMappingRoute, async (c) => {
162
162
  const project = await resolveAuthorizedProject(c, c.req.valid("param").slug, ...ADMIN_ROLES);
163
163
  const body = c.req.valid("json");
164
- if (body.groupName.includes("*")) throw new HTTPException(400, { message: "Group names match exactly; wildcards are not expanded. A broad pattern can silently grant org-wide access." });
164
+ const inputError = ProjectGroupMappingModel.inputError(body.groupName, body.role);
165
+ if (inputError) throw new HTTPException(400, { message: inputError });
165
166
  return c.json(await new ProjectGroupMappingModel(getStore().db).create(project.id, body.groupName, body.role), 201);
166
167
  });
167
168
  app.openapi(deleteGroupMappingRoute, async (c) => {
@@ -171,7 +172,6 @@ function registerGroupMappingRoutes(app) {
171
172
  return c.body(null, 204);
172
173
  });
173
174
  }
174
- //#endregion
175
175
  export { registerMembers };
176
176
 
177
177
  //# sourceMappingURL=members.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"members.mjs","names":[],"sources":["../../src/routers/members.ts"],"sourcesContent":["import { createRoute, z } from \"@hono/zod-openapi\";\nimport { MemberModel, ProjectGroupMappingModel } from \"@storyshelf/core/models\";\nimport type { ProjectRole } from \"@storyshelf/core/types\";\nimport { HTTPException } from \"hono/http-exception\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { getStore } from \"../store.ts\";\nimport { resolveAuthorizedProject } from \"./helpers.ts\";\nimport {\n groupMappingCreateSchema,\n groupMappingSchema,\n memberRoleSchema,\n memberSchema,\n memberSetSchema,\n notFound,\n unauthorized,\n} from \"./schemas.ts\";\nconst VIEW_ROLES: readonly ProjectRole[] = [\"viewer\", \"developer\", \"approver\", \"admin\"];\nconst ADMIN_ROLES: readonly ProjectRole[] = [\"admin\"];\n\nconst listMembersRoute = createRoute({\n method: \"get\",\n tags: [\"Members\"],\n summary: \"List members\",\n path: \"/api/v1/projects/{slug}/members\",\n request: { params: z.object({ slug: z.string() }) },\n responses: {\n 200: {\n content: { \"application/json\": { schema: memberSchema.array() } },\n description: \"List project members\",\n },\n ...notFound,\n ...unauthorized,\n },\n});\n\nconst setMemberRoute = createRoute({\n method: \"post\",\n tags: [\"Members\"],\n summary: \"Add or update a member\",\n path: \"/api/v1/projects/{slug}/members\",\n request: {\n params: z.object({ slug: z.string() }),\n body: { content: { \"application/json\": { schema: memberSetSchema } } },\n },\n responses: {\n 201: {\n content: { \"application/json\": { schema: memberSchema } },\n description: \"Added or updated member\",\n },\n ...notFound,\n },\n});\n\nconst updateMemberRoute = createRoute({\n method: \"patch\",\n tags: [\"Members\"],\n summary: \"Change a member's role\",\n path: \"/api/v1/projects/{slug}/members/{userId}\",\n request: {\n params: z.object({ slug: z.string(), userId: z.string() }),\n body: { content: { \"application/json\": { schema: memberRoleSchema } } },\n },\n responses: {\n 200: {\n content: { \"application/json\": { schema: memberSchema } },\n description: \"Updated member role\",\n },\n ...notFound,\n },\n});\n\nconst deleteMemberRoute = createRoute({\n method: \"delete\",\n tags: [\"Members\"],\n summary: \"Remove a member\",\n path: \"/api/v1/projects/{slug}/members/{userId}\",\n request: { params: z.object({ slug: z.string(), userId: z.string() }) },\n responses: {\n 204: { description: \"Removed member\" },\n ...notFound,\n },\n});\n\nconst listGroupMappingsRoute = createRoute({\n method: \"get\",\n tags: [\"Members\"],\n summary: \"List group mappings\",\n path: \"/api/v1/projects/{slug}/group-mappings\",\n request: { params: z.object({ slug: z.string() }) },\n responses: {\n 200: {\n content: { \"application/json\": { schema: groupMappingSchema.array() } },\n description: \"List identity-provider group mappings\",\n },\n ...notFound,\n ...unauthorized,\n },\n});\n\nconst createGroupMappingRoute = createRoute({\n method: \"post\",\n tags: [\"Members\"],\n summary: \"Create a group mapping\",\n path: \"/api/v1/projects/{slug}/group-mappings\",\n request: {\n params: z.object({ slug: z.string() }),\n body: { content: { \"application/json\": { schema: groupMappingCreateSchema } } },\n },\n responses: {\n 201: {\n content: { \"application/json\": { schema: groupMappingSchema } },\n description: \"Created group mapping (exact group-name match)\",\n },\n ...notFound,\n },\n});\n\nconst deleteGroupMappingRoute = createRoute({\n method: \"delete\",\n tags: [\"Members\"],\n summary: \"Remove a group mapping\",\n path: \"/api/v1/projects/{slug}/group-mappings/{mappingId}\",\n request: { params: z.object({ slug: z.string(), mappingId: z.string() }) },\n responses: {\n 204: { description: \"Removed group mapping\" },\n ...notFound,\n },\n});\n\n/** Register the project member list, upsert, and removal endpoints. */\nexport function registerMembers(app: ShelfRouter): void {\n app.openapi(listMembersRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...VIEW_ROLES);\n return c.json(await new MemberModel(getStore().db).list(project.id));\n });\n\n app.openapi(setMemberRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...ADMIN_ROLES);\n const body = c.req.valid(\"json\");\n return c.json(\n await new MemberModel(getStore().db).set(project.id, body.userId, body.role),\n 201,\n );\n });\n\n app.openapi(updateMemberRoute, async (c) => {\n const { slug, userId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...ADMIN_ROLES);\n const body = c.req.valid(\"json\");\n return c.json(await new MemberModel(getStore().db).set(project.id, userId, body.role));\n });\n\n app.openapi(deleteMemberRoute, async (c) => {\n const { slug, userId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...ADMIN_ROLES);\n await new MemberModel(getStore().db).remove(project.id, userId);\n return c.body(null, 204);\n });\n\n registerGroupMappingRoutes(app);\n}\n\n/** Register identity-provider group mapping endpoints. */\nfunction registerGroupMappingRoutes(app: ShelfRouter): void {\n app.openapi(listGroupMappingsRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...VIEW_ROLES);\n return c.json(await new ProjectGroupMappingModel(getStore().db).list(project.id));\n });\n\n app.openapi(createGroupMappingRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...ADMIN_ROLES);\n const body = c.req.valid(\"json\");\n if (body.groupName.includes(\"*\")) {\n throw new HTTPException(400, {\n message:\n \"Group names match exactly; wildcards are not expanded. A broad pattern can silently grant org-wide access.\",\n });\n }\n return c.json(\n await new ProjectGroupMappingModel(getStore().db).create(\n project.id,\n body.groupName,\n body.role,\n ),\n 201,\n );\n });\n\n app.openapi(deleteGroupMappingRoute, async (c) => {\n const { slug, mappingId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...ADMIN_ROLES);\n await new ProjectGroupMappingModel(getStore().db).remove(project.id, mappingId);\n return c.body(null, 204);\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"members.mjs","names":[],"sources":["../../src/routers/members.ts"],"sourcesContent":["import { createRoute, z } from \"@hono/zod-openapi\";\nimport { MemberModel, ProjectGroupMappingModel } from \"@storyshelf/core/models\";\nimport type { ProjectRole } from \"@storyshelf/core/types\";\nimport { HTTPException } from \"hono/http-exception\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { getStore } from \"../store.ts\";\nimport { resolveAuthorizedProject } from \"./helpers.ts\";\nimport {\n groupMappingCreateSchema,\n groupMappingSchema,\n memberRoleSchema,\n memberSchema,\n memberSetSchema,\n notFound,\n unauthorized,\n} from \"./schemas.ts\";\nconst VIEW_ROLES: readonly ProjectRole[] = [\"viewer\", \"developer\", \"approver\", \"admin\"];\nconst ADMIN_ROLES: readonly ProjectRole[] = [\"admin\"];\n\nconst listMembersRoute = createRoute({\n method: \"get\",\n tags: [\"Members\"],\n summary: \"List members\",\n path: \"/api/v1/projects/{slug}/members\",\n request: { params: z.object({ slug: z.string() }) },\n responses: {\n 200: {\n content: { \"application/json\": { schema: memberSchema.array() } },\n description: \"List project members\",\n },\n ...notFound,\n ...unauthorized,\n },\n});\n\nconst setMemberRoute = createRoute({\n method: \"post\",\n tags: [\"Members\"],\n summary: \"Add or update a member\",\n path: \"/api/v1/projects/{slug}/members\",\n request: {\n params: z.object({ slug: z.string() }),\n body: { content: { \"application/json\": { schema: memberSetSchema } } },\n },\n responses: {\n 201: {\n content: { \"application/json\": { schema: memberSchema } },\n description: \"Added or updated member\",\n },\n ...notFound,\n },\n});\n\nconst updateMemberRoute = createRoute({\n method: \"patch\",\n tags: [\"Members\"],\n summary: \"Change a member's role\",\n path: \"/api/v1/projects/{slug}/members/{userId}\",\n request: {\n params: z.object({ slug: z.string(), userId: z.string() }),\n body: { content: { \"application/json\": { schema: memberRoleSchema } } },\n },\n responses: {\n 200: {\n content: { \"application/json\": { schema: memberSchema } },\n description: \"Updated member role\",\n },\n ...notFound,\n },\n});\n\nconst deleteMemberRoute = createRoute({\n method: \"delete\",\n tags: [\"Members\"],\n summary: \"Remove a member\",\n path: \"/api/v1/projects/{slug}/members/{userId}\",\n request: { params: z.object({ slug: z.string(), userId: z.string() }) },\n responses: {\n 204: { description: \"Removed member\" },\n ...notFound,\n },\n});\n\nconst listGroupMappingsRoute = createRoute({\n method: \"get\",\n tags: [\"Members\"],\n summary: \"List group mappings\",\n path: \"/api/v1/projects/{slug}/group-mappings\",\n request: { params: z.object({ slug: z.string() }) },\n responses: {\n 200: {\n content: { \"application/json\": { schema: groupMappingSchema.array() } },\n description: \"List identity-provider group mappings\",\n },\n ...notFound,\n ...unauthorized,\n },\n});\n\nconst createGroupMappingRoute = createRoute({\n method: \"post\",\n tags: [\"Members\"],\n summary: \"Create a group mapping\",\n path: \"/api/v1/projects/{slug}/group-mappings\",\n request: {\n params: z.object({ slug: z.string() }),\n body: { content: { \"application/json\": { schema: groupMappingCreateSchema } } },\n },\n responses: {\n 201: {\n content: { \"application/json\": { schema: groupMappingSchema } },\n description: \"Created group mapping (exact group-name match)\",\n },\n ...notFound,\n },\n});\n\nconst deleteGroupMappingRoute = createRoute({\n method: \"delete\",\n tags: [\"Members\"],\n summary: \"Remove a group mapping\",\n path: \"/api/v1/projects/{slug}/group-mappings/{mappingId}\",\n request: { params: z.object({ slug: z.string(), mappingId: z.string() }) },\n responses: {\n 204: { description: \"Removed group mapping\" },\n ...notFound,\n },\n});\n\n/** Register the project member list, upsert, and removal endpoints. */\nexport function registerMembers(app: ShelfRouter): void {\n app.openapi(listMembersRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...VIEW_ROLES);\n return c.json(await new MemberModel(getStore().db).list(project.id));\n });\n\n app.openapi(setMemberRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...ADMIN_ROLES);\n const body = c.req.valid(\"json\");\n return c.json(\n await new MemberModel(getStore().db).set(project.id, body.userId, body.role),\n 201,\n );\n });\n\n app.openapi(updateMemberRoute, async (c) => {\n const { slug, userId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...ADMIN_ROLES);\n const body = c.req.valid(\"json\");\n return c.json(await new MemberModel(getStore().db).set(project.id, userId, body.role));\n });\n\n app.openapi(deleteMemberRoute, async (c) => {\n const { slug, userId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...ADMIN_ROLES);\n await new MemberModel(getStore().db).remove(project.id, userId);\n return c.body(null, 204);\n });\n\n registerGroupMappingRoutes(app);\n}\n\n/** Register identity-provider group mapping endpoints. */\nfunction registerGroupMappingRoutes(app: ShelfRouter): void {\n app.openapi(listGroupMappingsRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...VIEW_ROLES);\n return c.json(await new ProjectGroupMappingModel(getStore().db).list(project.id));\n });\n\n app.openapi(createGroupMappingRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...ADMIN_ROLES);\n const body = c.req.valid(\"json\");\n const inputError = ProjectGroupMappingModel.inputError(body.groupName, body.role);\n if (inputError) {\n throw new HTTPException(400, { message: inputError });\n }\n return c.json(\n await new ProjectGroupMappingModel(getStore().db).create(\n project.id,\n body.groupName,\n body.role,\n ),\n 201,\n );\n });\n\n app.openapi(deleteGroupMappingRoute, async (c) => {\n const { slug, mappingId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...ADMIN_ROLES);\n await new ProjectGroupMappingModel(getStore().db).remove(project.id, mappingId);\n return c.body(null, 204);\n });\n}\n"],"mappings":"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"}
@@ -1,13 +1,13 @@
1
1
  import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
- import { credentialKeys } from "../credential-keys.mjs";
5
4
  import { getStore } from "../store.mjs";
6
5
  import { notFound, requireSiteAdmin, resolveAuthorizedProject } from "./helpers.mjs";
7
6
  import { notFound as notFound$1, notificationChannelCreateSchema, notificationChannelSchema, unauthorized } from "./schemas.mjs";
7
+ import { notificationChannelModel } from "./scoped-models.mjs";
8
8
  import { createRoute, z } from "@hono/zod-openapi";
9
9
  import { NotificationChannelModel } from "@storyshelf/core/models";
10
- //#region src/routers/notification-channels.ts
10
+ // src/routers/notification-channels.ts
11
11
  const ADMIN_ROLES = ["admin"];
12
12
  /** Public channel view (secret redacted, config/events decoded). */
13
13
  function toPublic(row) {
@@ -19,24 +19,12 @@ function toPublic(row) {
19
19
  hasSecret: row.secretEncrypted !== null,
20
20
  events: NotificationChannelModel.eventsOf(row),
21
21
  enabled: row.enabled,
22
+ lastError: row.lastError,
23
+ lastErrorAt: row.lastErrorAt,
22
24
  createdAt: row.createdAt,
23
25
  updatedAt: row.updatedAt
24
26
  };
25
27
  }
26
- /** Validate channel input against the provider descriptor. */
27
- function validateChannelInput(provider, config) {
28
- const descriptor = getStore().notifiers.find((candidate) => candidate.metadata.kind === provider);
29
- if (!descriptor) return {
30
- ok: false,
31
- message: `Unknown provider: ${provider}`
32
- };
33
- const parsed = descriptor.metadata.schema.safeParse(config);
34
- if (!parsed.success) return {
35
- ok: false,
36
- message: parsed.error.message
37
- };
38
- return { ok: true };
39
- }
40
28
  const listChannelsRoute = createRoute({
41
29
  method: "get",
42
30
  tags: ["Notifications"],
@@ -91,15 +79,18 @@ const deleteChannelRoute = createRoute({
91
79
  function registerNotificationChannels(app) {
92
80
  app.openapi(listChannelsRoute, async (c) => {
93
81
  const project = await resolveAuthorizedProject(c, c.req.valid("param").slug, ...ADMIN_ROLES);
94
- const rows = await new NotificationChannelModel(getStore().db, void 0, credentialKeys(getStore().config)).list(project.id);
82
+ const rows = await notificationChannelModel().list(project.id);
95
83
  return c.json(rows.map((row) => toPublic(row)));
96
84
  });
97
85
  app.openapi(createChannelRoute, async (c) => {
98
86
  const project = await resolveAuthorizedProject(c, c.req.valid("param").slug, ...ADMIN_ROLES);
99
87
  const body = c.req.valid("json");
100
- const valid = validateChannelInput(body.provider, body.config);
88
+ const valid = NotificationChannelModel.validateInput(getStore().notifiers, body.provider, body.config, {
89
+ projectId: project.id,
90
+ hasSecret: body.secret !== void 0
91
+ });
101
92
  if (!valid.ok) return c.json({ message: valid.message }, 400);
102
- const row = await new NotificationChannelModel(getStore().db, void 0, credentialKeys(getStore().config)).create({
93
+ const row = await notificationChannelModel().create({
103
94
  projectId: project.id,
104
95
  ...body
105
96
  });
@@ -108,7 +99,7 @@ function registerNotificationChannels(app) {
108
99
  app.openapi(deleteChannelRoute, async (c) => {
109
100
  const { slug, channelId } = c.req.valid("param");
110
101
  const project = await resolveAuthorizedProject(c, slug, ...ADMIN_ROLES);
111
- const model = new NotificationChannelModel(getStore().db, void 0, credentialKeys(getStore().config));
102
+ const model = notificationChannelModel();
112
103
  if ((await model.get(channelId))?.projectId !== project.id) notFound("Notification channel not found");
113
104
  await model.remove(channelId);
114
105
  return c.body(null, 204);
@@ -161,15 +152,18 @@ const deleteSystemRoute = createRoute({
161
152
  function registerSystemChannels(app) {
162
153
  app.openapi(listSystemRoute, async (c) => {
163
154
  requireSiteAdmin(c);
164
- const rows = await new NotificationChannelModel(getStore().db, void 0, credentialKeys(getStore().config)).listSystem();
155
+ const rows = await notificationChannelModel().listSystem();
165
156
  return c.json(rows.map((row) => toPublic(row)));
166
157
  });
167
158
  app.openapi(createSystemRoute, async (c) => {
168
159
  requireSiteAdmin(c);
169
160
  const body = c.req.valid("json");
170
- const valid = validateChannelInput(body.provider, body.config);
161
+ const valid = NotificationChannelModel.validateInput(getStore().notifiers, body.provider, body.config, {
162
+ projectId: null,
163
+ hasSecret: body.secret !== void 0
164
+ });
171
165
  if (!valid.ok) return c.json({ message: valid.message }, 400);
172
- const row = await new NotificationChannelModel(getStore().db, void 0, credentialKeys(getStore().config)).create({
166
+ const row = await notificationChannelModel().create({
173
167
  projectId: null,
174
168
  ...body
175
169
  });
@@ -178,13 +172,12 @@ function registerSystemChannels(app) {
178
172
  app.openapi(deleteSystemRoute, async (c) => {
179
173
  requireSiteAdmin(c);
180
174
  const { channelId } = c.req.valid("param");
181
- const model = new NotificationChannelModel(getStore().db, void 0, credentialKeys(getStore().config));
175
+ const model = notificationChannelModel();
182
176
  if ((await model.get(channelId))?.projectId !== null) notFound("Notification channel not found");
183
177
  await model.remove(channelId);
184
178
  return c.body(null, 204);
185
179
  });
186
180
  }
187
- //#endregion
188
181
  export { registerNotificationChannels, registerSystemChannels };
189
182
 
190
183
  //# sourceMappingURL=notification-channels.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"notification-channels.mjs","names":["notFoundResponse"],"sources":["../../src/routers/notification-channels.ts"],"sourcesContent":["import { createRoute, z } from \"@hono/zod-openapi\";\nimport { NotificationChannelModel } from \"@storyshelf/core/models\";\nimport type { NotificationChannelRow } from \"@storyshelf/core/schema\";\nimport type { ProjectRole } from \"@storyshelf/core/types\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { credentialKeys } from \"../credential-keys.ts\";\nimport { getStore } from \"../store.ts\";\nimport { notFound, requireSiteAdmin, resolveAuthorizedProject } from \"./helpers.ts\";\nimport {\n notFound as notFoundResponse,\n notificationChannelCreateSchema,\n notificationChannelSchema,\n unauthorized,\n} from \"./schemas.ts\";\nconst ADMIN_ROLES: readonly ProjectRole[] = [\"admin\"];\n\n/** Public channel view (secret redacted, config/events decoded). */\nfunction toPublic(row: NotificationChannelRow): {\n id: string;\n projectId: string | null;\n provider: string;\n config: Record<string, unknown>;\n hasSecret: boolean;\n events: string[];\n enabled: boolean;\n createdAt: string;\n updatedAt: string;\n} {\n return {\n id: row.id,\n projectId: row.projectId,\n provider: row.provider,\n config: NotificationChannelModel.configOf(row),\n hasSecret: row.secretEncrypted !== null,\n events: NotificationChannelModel.eventsOf(row),\n enabled: row.enabled,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n };\n}\n\n/** Validate channel input against the provider descriptor. */\nfunction validateChannelInput(\n provider: string,\n config: unknown,\n): { ok: true } | { ok: false; message: string } {\n const descriptor = getStore().notifiers.find((candidate) => candidate.metadata.kind === provider);\n if (!descriptor) {\n return { ok: false, message: `Unknown provider: ${provider}` };\n }\n const parsed = descriptor.metadata.schema.safeParse(config);\n if (!parsed.success) {\n return { ok: false, message: parsed.error.message };\n }\n return { ok: true };\n}\n\nconst listChannelsRoute = createRoute({\n method: \"get\",\n tags: [\"Notifications\"],\n summary: \"List notification channels\",\n path: \"/api/v1/projects/{slug}/notification-channels\",\n request: { params: z.object({ slug: z.string() }) },\n responses: {\n 200: {\n content: { \"application/json\": { schema: notificationChannelSchema.array() } },\n description: \"List notification channels\",\n },\n ...notFoundResponse,\n ...unauthorized,\n },\n});\n\nconst createChannelRoute = createRoute({\n method: \"post\",\n tags: [\"Notifications\"],\n summary: \"Create a notification channel\",\n path: \"/api/v1/projects/{slug}/notification-channels\",\n request: {\n params: z.object({ slug: z.string() }),\n body: { content: { \"application/json\": { schema: notificationChannelCreateSchema } } },\n },\n responses: {\n 201: {\n content: { \"application/json\": { schema: notificationChannelSchema } },\n description: \"Created notification channel\",\n },\n 400: {\n content: { \"application/json\": { schema: z.object({ message: z.string() }) } },\n description: \"Bad request\",\n },\n ...notFoundResponse,\n },\n});\n\nconst deleteChannelRoute = createRoute({\n method: \"delete\",\n tags: [\"Notifications\"],\n summary: \"Delete a notification channel\",\n path: \"/api/v1/projects/{slug}/notification-channels/{channelId}\",\n request: { params: z.object({ slug: z.string(), channelId: z.string() }) },\n responses: {\n 204: { description: \"Deleted notification channel\" },\n ...notFoundResponse,\n },\n});\n\n/** Register the project notification-channel list, create, and delete endpoints. */\nexport function registerNotificationChannels(app: ShelfRouter): void {\n app.openapi(listChannelsRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...ADMIN_ROLES);\n const rows = await new NotificationChannelModel(\n getStore().db,\n undefined,\n credentialKeys(getStore().config),\n ).list(project.id);\n return c.json(rows.map((row) => toPublic(row)));\n });\n\n app.openapi(createChannelRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...ADMIN_ROLES);\n const body = c.req.valid(\"json\");\n const valid = validateChannelInput(body.provider, body.config);\n if (!valid.ok) {\n return c.json({ message: valid.message }, 400);\n }\n const row = await new NotificationChannelModel(\n getStore().db,\n undefined,\n credentialKeys(getStore().config),\n ).create({ projectId: project.id, ...body });\n return c.json(toPublic(row), 201);\n });\n\n app.openapi(deleteChannelRoute, async (c) => {\n const { slug, channelId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...ADMIN_ROLES);\n const model = new NotificationChannelModel(\n getStore().db,\n undefined,\n credentialKeys(getStore().config),\n );\n const existing = await model.get(channelId);\n if (existing?.projectId !== project.id) {\n notFound(\"Notification channel not found\");\n }\n await model.remove(channelId);\n return c.body(null, 204);\n });\n}\n\nconst listSystemRoute = createRoute({\n method: \"get\",\n tags: [\"Notifications\"],\n summary: \"List site-wide channels\",\n path: \"/api/v1/admin/notification-channels\",\n request: {},\n responses: {\n 200: {\n content: { \"application/json\": { schema: notificationChannelSchema.array() } },\n description: \"List site-wide notification channels\",\n },\n ...unauthorized,\n },\n});\n\nconst createSystemRoute = createRoute({\n method: \"post\",\n tags: [\"Notifications\"],\n summary: \"Create a site-wide channel\",\n path: \"/api/v1/admin/notification-channels\",\n request: {\n body: { content: { \"application/json\": { schema: notificationChannelCreateSchema } } },\n },\n responses: {\n 201: {\n content: { \"application/json\": { schema: notificationChannelSchema } },\n description: \"Created site-wide notification channel\",\n },\n 400: {\n content: { \"application/json\": { schema: z.object({ message: z.string() }) } },\n description: \"Bad request\",\n },\n ...unauthorized,\n },\n});\n\nconst deleteSystemRoute = createRoute({\n method: \"delete\",\n tags: [\"Notifications\"],\n summary: \"Delete a site-wide channel\",\n path: \"/api/v1/admin/notification-channels/{channelId}\",\n request: { params: z.object({ channelId: z.string() }) },\n responses: {\n 204: { description: \"Deleted site-wide notification channel\" },\n ...notFoundResponse,\n },\n});\n\n/** Register the site-admin (project-less, `sys:*`) channel endpoints. */\nexport function registerSystemChannels(app: ShelfRouter): void {\n app.openapi(listSystemRoute, async (c) => {\n requireSiteAdmin(c);\n const rows = await new NotificationChannelModel(\n getStore().db,\n undefined,\n credentialKeys(getStore().config),\n ).listSystem();\n return c.json(rows.map((row) => toPublic(row)));\n });\n\n app.openapi(createSystemRoute, async (c) => {\n requireSiteAdmin(c);\n const body = c.req.valid(\"json\");\n const valid = validateChannelInput(body.provider, body.config);\n if (!valid.ok) {\n return c.json({ message: valid.message }, 400);\n }\n const row = await new NotificationChannelModel(\n getStore().db,\n undefined,\n credentialKeys(getStore().config),\n ).create({ projectId: null, ...body });\n return c.json(toPublic(row), 201);\n });\n\n app.openapi(deleteSystemRoute, async (c) => {\n requireSiteAdmin(c);\n const { channelId } = c.req.valid(\"param\");\n const model = new NotificationChannelModel(\n getStore().db,\n undefined,\n credentialKeys(getStore().config),\n );\n const existing = await model.get(channelId);\n if (existing?.projectId !== null) {\n notFound(\"Notification channel not found\");\n }\n await model.remove(channelId);\n return c.body(null, 204);\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"notification-channels.mjs","names":["notFoundResponse"],"sources":["../../src/routers/notification-channels.ts"],"sourcesContent":["import { createRoute, z } from \"@hono/zod-openapi\";\nimport { NotificationChannelModel } from \"@storyshelf/core/models\";\nimport type { NotificationChannelRow } from \"@storyshelf/core/schema\";\nimport type { ProjectRole } from \"@storyshelf/core/types\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { getStore } from \"../store.ts\";\nimport { notFound, requireSiteAdmin, resolveAuthorizedProject } from \"./helpers.ts\";\nimport {\n notFound as notFoundResponse,\n notificationChannelCreateSchema,\n notificationChannelSchema,\n unauthorized,\n} from \"./schemas.ts\";\nimport { notificationChannelModel } from \"./scoped-models.ts\";\nconst ADMIN_ROLES: readonly ProjectRole[] = [\"admin\"];\n\n/** Public channel view (secret redacted, config/events decoded). */\nfunction toPublic(row: NotificationChannelRow): {\n id: string;\n projectId: string | null;\n provider: string;\n config: Record<string, unknown>;\n hasSecret: boolean;\n events: string[];\n enabled: boolean;\n lastError: string | null;\n lastErrorAt: string | null;\n createdAt: string;\n updatedAt: string;\n} {\n return {\n id: row.id,\n projectId: row.projectId,\n provider: 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 createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n };\n}\n\nconst listChannelsRoute = createRoute({\n method: \"get\",\n tags: [\"Notifications\"],\n summary: \"List notification channels\",\n path: \"/api/v1/projects/{slug}/notification-channels\",\n request: { params: z.object({ slug: z.string() }) },\n responses: {\n 200: {\n content: { \"application/json\": { schema: notificationChannelSchema.array() } },\n description: \"List notification channels\",\n },\n ...notFoundResponse,\n ...unauthorized,\n },\n});\n\nconst createChannelRoute = createRoute({\n method: \"post\",\n tags: [\"Notifications\"],\n summary: \"Create a notification channel\",\n path: \"/api/v1/projects/{slug}/notification-channels\",\n request: {\n params: z.object({ slug: z.string() }),\n body: { content: { \"application/json\": { schema: notificationChannelCreateSchema } } },\n },\n responses: {\n 201: {\n content: { \"application/json\": { schema: notificationChannelSchema } },\n description: \"Created notification channel\",\n },\n 400: {\n content: { \"application/json\": { schema: z.object({ message: z.string() }) } },\n description: \"Bad request\",\n },\n ...notFoundResponse,\n },\n});\n\nconst deleteChannelRoute = createRoute({\n method: \"delete\",\n tags: [\"Notifications\"],\n summary: \"Delete a notification channel\",\n path: \"/api/v1/projects/{slug}/notification-channels/{channelId}\",\n request: { params: z.object({ slug: z.string(), channelId: z.string() }) },\n responses: {\n 204: { description: \"Deleted notification channel\" },\n ...notFoundResponse,\n },\n});\n\n/** Register the project notification-channel list, create, and delete endpoints. */\nexport function registerNotificationChannels(app: ShelfRouter): void {\n app.openapi(listChannelsRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...ADMIN_ROLES);\n const rows = await notificationChannelModel().list(project.id);\n return c.json(rows.map((row) => toPublic(row)));\n });\n\n app.openapi(createChannelRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...ADMIN_ROLES);\n const body = c.req.valid(\"json\");\n const valid = NotificationChannelModel.validateInput(\n getStore().notifiers,\n body.provider,\n body.config,\n { projectId: project.id, hasSecret: body.secret !== undefined },\n );\n if (!valid.ok) {\n return c.json({ message: valid.message }, 400);\n }\n const row = await notificationChannelModel().create({ projectId: project.id, ...body });\n return c.json(toPublic(row), 201);\n });\n\n app.openapi(deleteChannelRoute, async (c) => {\n const { slug, channelId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...ADMIN_ROLES);\n const model = notificationChannelModel();\n const existing = await model.get(channelId);\n if (existing?.projectId !== project.id) {\n notFound(\"Notification channel not found\");\n }\n await model.remove(channelId);\n return c.body(null, 204);\n });\n}\n\nconst listSystemRoute = createRoute({\n method: \"get\",\n tags: [\"Notifications\"],\n summary: \"List site-wide channels\",\n path: \"/api/v1/admin/notification-channels\",\n request: {},\n responses: {\n 200: {\n content: { \"application/json\": { schema: notificationChannelSchema.array() } },\n description: \"List site-wide notification channels\",\n },\n ...unauthorized,\n },\n});\n\nconst createSystemRoute = createRoute({\n method: \"post\",\n tags: [\"Notifications\"],\n summary: \"Create a site-wide channel\",\n path: \"/api/v1/admin/notification-channels\",\n request: {\n body: { content: { \"application/json\": { schema: notificationChannelCreateSchema } } },\n },\n responses: {\n 201: {\n content: { \"application/json\": { schema: notificationChannelSchema } },\n description: \"Created site-wide notification channel\",\n },\n 400: {\n content: { \"application/json\": { schema: z.object({ message: z.string() }) } },\n description: \"Bad request\",\n },\n ...unauthorized,\n },\n});\n\nconst deleteSystemRoute = createRoute({\n method: \"delete\",\n tags: [\"Notifications\"],\n summary: \"Delete a site-wide channel\",\n path: \"/api/v1/admin/notification-channels/{channelId}\",\n request: { params: z.object({ channelId: z.string() }) },\n responses: {\n 204: { description: \"Deleted site-wide notification channel\" },\n ...notFoundResponse,\n },\n});\n\n/** Register the site-admin (project-less, `sys:*`) channel endpoints. */\nexport function registerSystemChannels(app: ShelfRouter): void {\n app.openapi(listSystemRoute, async (c) => {\n requireSiteAdmin(c);\n const rows = await notificationChannelModel().listSystem();\n return c.json(rows.map((row) => toPublic(row)));\n });\n\n app.openapi(createSystemRoute, async (c) => {\n requireSiteAdmin(c);\n const body = c.req.valid(\"json\");\n const valid = NotificationChannelModel.validateInput(\n getStore().notifiers,\n body.provider,\n body.config,\n { projectId: null, hasSecret: body.secret !== undefined },\n );\n if (!valid.ok) {\n return c.json({ message: valid.message }, 400);\n }\n const row = await notificationChannelModel().create({ projectId: null, ...body });\n return c.json(toPublic(row), 201);\n });\n\n app.openapi(deleteSystemRoute, async (c) => {\n requireSiteAdmin(c);\n const { channelId } = c.req.valid(\"param\");\n const model = notificationChannelModel();\n const existing = await model.get(channelId);\n if (existing?.projectId !== null) {\n notFound(\"Notification channel not found\");\n }\n await model.remove(channelId);\n return c.body(null, 204);\n });\n}\n"],"mappings":"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"}
@@ -6,7 +6,7 @@ import { resolveAuthorizedProject, unauthorized } from "./helpers.mjs";
6
6
  import { notFound, notificationSubscriptionPutSchema, notificationSubscriptionSchema, unauthorized as unauthorized$1 } from "./schemas.mjs";
7
7
  import { createRoute, z } from "@hono/zod-openapi";
8
8
  import { NotificationSubscriptionModel } from "@storyshelf/core/models";
9
- //#region src/routers/notification-subscriptions.ts
9
+ // src/routers/notification-subscriptions.ts
10
10
  const VIEW_ROLES = [
11
11
  "viewer",
12
12
  "developer",
@@ -114,7 +114,6 @@ function registerNotificationSubscriptions(app) {
114
114
  return c.json(rows.map((row) => toPublic(row)));
115
115
  });
116
116
  }
117
- //#endregion
118
117
  export { registerNotificationSubscriptions };
119
118
 
120
119
  //# sourceMappingURL=notification-subscriptions.mjs.map
@@ -0,0 +1,44 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { timingSafeEqualString } from "@storyshelf/core/utils";
5
+ import { createHmac, randomBytes } from "node:crypto";
6
+ // src/routers/oauth-state.ts
7
+ /** Signed, expiring OAuth `state` for "connect your account" flows (binds user, service and browser). */
8
+ /** State lifetime: long enough to sign in at the service, short enough to limit replay. */
9
+ const STATE_TTL_MS = 6e5;
10
+ /** Issue a signed state for a new flow, with a fresh nonce. */
11
+ function signState(secret, fields, now = Date.now()) {
12
+ const nonce = randomBytes(16).toString("hex");
13
+ const body = {
14
+ ...fields,
15
+ nonce,
16
+ expiresAt: now + STATE_TTL_MS
17
+ };
18
+ const payload = Buffer.from(JSON.stringify(body)).toString("base64url");
19
+ return {
20
+ state: `${payload}.${mac(secret, payload)}`,
21
+ nonce
22
+ };
23
+ }
24
+ /** Parse and verify a state; null when forged, malformed or expired. */
25
+ function verifyState(secret, state, now = Date.now()) {
26
+ const [payload, signature] = state.split(".");
27
+ if (!payload || !signature || !timingSafeEqualString(signature, mac(secret, payload))) return null;
28
+ try {
29
+ const body = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
30
+ return body.expiresAt > now ? body : null;
31
+ } catch {
32
+ return null;
33
+ }
34
+ }
35
+ /** A same-site path (never an absolute or protocol-relative URL), else the fallback. */
36
+ function safeReturnPath(value, fallback) {
37
+ return value && /^\/(?![/\\])/u.test(value) ? value : fallback;
38
+ }
39
+ function mac(secret, payload) {
40
+ return createHmac("sha256", secret).update(`oauth-connect:${payload}`).digest("base64url");
41
+ }
42
+ export { safeReturnPath, signState, verifyState };
43
+
44
+ //# sourceMappingURL=oauth-state.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth-state.mjs","names":[],"sources":["../../src/routers/oauth-state.ts"],"sourcesContent":["/** Signed, expiring OAuth `state` for \"connect your account\" flows (binds user, service and browser). */\nimport { timingSafeEqualString } from \"@storyshelf/core/utils\";\nimport { createHmac, randomBytes } from \"node:crypto\";\n\n/** State lifetime: long enough to sign in at the service, short enough to limit replay. */\nconst STATE_TTL_MS = 10 * 60_000;\n\n/** What a state asserts about the flow that issued it. */\nexport interface OAuthState {\n /** User who started the flow. */\n userId: string;\n /** Service `metadata.kind`. */\n service: string;\n /** Local path to return to afterwards. */\n returnTo: string;\n /** Random value also kept in a cookie, binding the state to one browser. */\n nonce: string;\n expiresAt: number;\n}\n\n/** Issue a signed state for a new flow, with a fresh nonce. */\nexport function signState(\n secret: string,\n fields: Omit<OAuthState, \"nonce\" | \"expiresAt\">,\n now = Date.now(),\n): { state: string; nonce: string } {\n const nonce = randomBytes(16).toString(\"hex\");\n const body: OAuthState = { ...fields, nonce, expiresAt: now + STATE_TTL_MS };\n const payload = Buffer.from(JSON.stringify(body)).toString(\"base64url\");\n return { state: `${payload}.${mac(secret, payload)}`, nonce };\n}\n\n/** Parse and verify a state; null when forged, malformed or expired. */\nexport function verifyState(secret: string, state: string, now = Date.now()): OAuthState | null {\n const [payload, signature] = state.split(\".\");\n if (!payload || !signature || !timingSafeEqualString(signature, mac(secret, payload))) {\n return null;\n }\n try {\n const body = JSON.parse(Buffer.from(payload, \"base64url\").toString(\"utf8\")) as OAuthState;\n return body.expiresAt > now ? body : null;\n } catch {\n return null;\n }\n}\n\n/** A same-site path (never an absolute or protocol-relative URL), else the fallback. */\nexport function safeReturnPath(value: string | undefined, fallback: string): string {\n return value && /^\\/(?![/\\\\])/u.test(value) ? value : fallback;\n}\n\nfunction mac(secret: string, payload: string): string {\n return createHmac(\"sha256\", secret).update(`oauth-connect:${payload}`).digest(\"base64url\");\n}\n"],"mappings":";;;;;;;;AAKA,MAAM,eAAe;;AAgBrB,SAAgB,UACd,QACA,QACA,MAAM,KAAK,IAAI,GACmB;CAClC,MAAM,QAAQ,YAAY,EAAE,CAAC,CAAC,SAAS,KAAK;CAC5C,MAAM,OAAmB;EAAE,GAAG;EAAQ;EAAO,WAAW,MAAM;CAAa;CAC3E,MAAM,UAAU,OAAO,KAAK,KAAK,UAAU,IAAI,CAAC,CAAC,CAAC,SAAS,WAAW;CACtE,OAAO;EAAE,OAAO,GAAG,QAAQ,GAAG,IAAI,QAAQ,OAAO;EAAK;CAAM;AAC9D;;AAGA,SAAgB,YAAY,QAAgB,OAAe,MAAM,KAAK,IAAI,GAAsB;CAC9F,MAAM,CAAC,SAAS,aAAa,MAAM,MAAM,GAAG;CAC5C,IAAI,CAAC,WAAW,CAAC,aAAa,CAAC,sBAAsB,WAAW,IAAI,QAAQ,OAAO,CAAC,GAClF,OAAO;CAET,IAAI;EACF,MAAM,OAAO,KAAK,MAAM,OAAO,KAAK,SAAS,WAAW,CAAC,CAAC,SAAS,MAAM,CAAC;EAC1E,OAAO,KAAK,YAAY,MAAM,OAAO;CACvC,QAAQ;EACN,OAAO;CACT;AACF;;AAGA,SAAgB,eAAe,OAA2B,UAA0B;CAClF,OAAO,SAAS,gBAAgB,KAAK,KAAK,IAAI,QAAQ;AACxD;AAEA,SAAS,IAAI,QAAgB,SAAyB;CACpD,OAAO,WAAW,UAAU,MAAM,CAAC,CAAC,OAAO,iBAAiB,SAAS,CAAC,CAAC,OAAO,WAAW;AAC3F"}
@@ -0,0 +1,37 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { credentialKeys } from "../credential-keys.mjs";
5
+ import { getStore, testingEnabled } from "../store.mjs";
6
+ import { isAnonymouslyViewable, publishedAccess } from "./published-access.mjs";
7
+ import { STORYBOOK_TOKEN_PREFIX, signStorybookToken } from "@storyshelf/core/utils";
8
+ import { createUrlBuilder } from "@storyshelf/core/urls";
9
+ // src/routers/preview-base.ts
10
+ /** Lifetime of the token in a page: long enough to open the frame, short enough to be harmless. */
11
+ const PREVIEW_TOKEN_TTL = 3600;
12
+ /**
13
+ * How often a page asks for a fresh frame token (seconds): well inside {@link PREVIEW_TOKEN_TTL},
14
+ * so a frame on a page left open never holds an expired token. The token stays short-lived.
15
+ */
16
+ const PREVIEW_TOKEN_REFRESH = 2400;
17
+ /** Whether a frame root is a signed `/_s/<token>` URL, the only kind that expires. */
18
+ function isSignedPreviewBase(base) {
19
+ return base.startsWith(STORYBOOK_TOKEN_PREFIX);
20
+ }
21
+ /** The frame's root URL (no trailing slash), or null when this viewer cannot be given one. */
22
+ async function previewBaseOf(project, build) {
23
+ const { config } = getStore();
24
+ if (isAnonymouslyViewable(build, project)) return createUrlBuilder("/", config.publishedBaseDomain).storybookBuild(project.slug, build.id);
25
+ const secret = credentialKeys(config);
26
+ if (!testingEnabled() || !secret || !await publishedAccess(build, project)) return null;
27
+ const token = signStorybookToken({
28
+ secret,
29
+ projectId: project.id,
30
+ buildId: build.id,
31
+ ttlSeconds: PREVIEW_TOKEN_TTL
32
+ });
33
+ return `${STORYBOOK_TOKEN_PREFIX}${token}`;
34
+ }
35
+ export { PREVIEW_TOKEN_REFRESH, PREVIEW_TOKEN_TTL, isSignedPreviewBase, previewBaseOf };
36
+
37
+ //# sourceMappingURL=preview-base.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"preview-base.mjs","names":[],"sources":["../../src/routers/preview-base.ts"],"sourcesContent":["/**\n * The root a component page's preview frame loads a build's Storybook from (ADR 0006, decision 9).\n * A sandboxed frame has no cookies, so only a URL that needs none works in it: the build's own URL\n * when anyone who reaches the app may open it, otherwise a signed `/_s/<token>` URL minted for a\n * viewer who already has access (ADR 0007, decision 5; needs `testing` and a `secret`).\n */\nimport type { Build, Project } from \"@storyshelf/core/schema\";\nimport { createUrlBuilder } from \"@storyshelf/core/urls\";\nimport {\n signStorybookToken,\n STORYBOOK_TOKEN_PREFIX,\n type SecretInput,\n} from \"@storyshelf/core/utils\";\nimport { credentialKeys } from \"../credential-keys.ts\";\nimport { getStore, testingEnabled } from \"../store.ts\";\nimport { isAnonymouslyViewable, publishedAccess } from \"./published-access.ts\";\n\n/** Lifetime of the token in a page: long enough to open the frame, short enough to be harmless. */\nexport const PREVIEW_TOKEN_TTL = 3600;\n\n/**\n * How often a page asks for a fresh frame token (seconds): well inside {@link PREVIEW_TOKEN_TTL},\n * so a frame on a page left open never holds an expired token. The token stays short-lived.\n */\nexport const PREVIEW_TOKEN_REFRESH = 2400;\n\n/** Whether a frame root is a signed `/_s/<token>` URL, the only kind that expires. */\nexport function isSignedPreviewBase(base: string): boolean {\n return base.startsWith(STORYBOOK_TOKEN_PREFIX);\n}\n\n/** The frame's root URL (no trailing slash), or null when this viewer cannot be given one. */\nexport async function previewBaseOf(project: Project, build: Build): Promise<string | null> {\n const { config } = getStore();\n if (isAnonymouslyViewable(build, project)) {\n return createUrlBuilder(\"/\", config.publishedBaseDomain).storybookBuild(project.slug, build.id);\n }\n const secret: SecretInput = credentialKeys(config);\n if (!testingEnabled() || !secret || !(await publishedAccess(build, project))) {\n return null;\n }\n const token = signStorybookToken({\n secret,\n projectId: project.id,\n buildId: build.id,\n ttlSeconds: PREVIEW_TOKEN_TTL,\n });\n return `${STORYBOOK_TOKEN_PREFIX}${token}`;\n}\n"],"mappings":";;;;;;;;;;AAkBA,MAAa,oBAAoB;;;;;AAMjC,MAAa,wBAAwB;;AAGrC,SAAgB,oBAAoB,MAAuB;CACzD,OAAO,KAAK,WAAW,sBAAsB;AAC/C;;AAGA,eAAsB,cAAc,SAAkB,OAAsC;CAC1F,MAAM,EAAE,WAAW,SAAS;CAC5B,IAAI,sBAAsB,OAAO,OAAO,GACtC,OAAO,iBAAiB,KAAK,OAAO,mBAAmB,CAAC,CAAC,eAAe,QAAQ,MAAM,MAAM,EAAE;CAEhG,MAAM,SAAsB,eAAe,MAAM;CACjD,IAAI,CAAC,eAAe,KAAK,CAAC,UAAU,CAAE,MAAM,gBAAgB,OAAO,OAAO,GACxE,OAAO;CAET,MAAM,QAAQ,mBAAmB;EAC/B;EACA,WAAW,QAAQ;EACnB,SAAS,MAAM;EACf,YAAY;CACd,CAAC;CACD,OAAO,GAAG,yBAAyB;AACrC"}
@@ -0,0 +1,50 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { getStore } from "../store.mjs";
5
+ import { isSignedPreviewBase, previewBaseOf } from "./preview-base.mjs";
6
+ import { SIGNED_URL_HEADERS } from "./published-files.mjs";
7
+ import { createRoute, z } from "@hono/zod-openapi";
8
+ import { BuildModel, ProjectModel } from "@storyshelf/core/models";
9
+ // src/routers/preview-token.ts
10
+ /**
11
+ * `GET /api/v1/projects/:slug/builds/:buildId/preview-token`: a fresh root for a component page's preview frame
12
+ * (ADR 0006, decision 9). A frame holding a signed `/_s/<token>` URL (ADR 0007, decision 5) outlives
13
+ * the token's hour on a page left open, so the page's script asks for a new root before then and
14
+ * re-points the frame. Answers `{ base }` for a viewer who may open the build, 404 otherwise or when
15
+ * the build needs no token (anyone may open it). Never cached: the answer carries a token.
16
+ */
17
+ const previewTokenRoute = createRoute({
18
+ method: "get",
19
+ tags: ["Review"],
20
+ summary: "Get a fresh signed root for a build's preview frame",
21
+ path: "/api/v1/projects/{slug}/builds/{buildId}/preview-token",
22
+ request: { params: z.object({
23
+ slug: z.string(),
24
+ buildId: z.string()
25
+ }) },
26
+ responses: {
27
+ 200: {
28
+ content: { "application/json": { schema: z.object({ base: z.string() }) } },
29
+ description: "The signed root (`/_s/<token>`) to frame the build's Storybook under"
30
+ },
31
+ 404: {
32
+ content: { "text/plain": { schema: z.string() } },
33
+ description: "Not found, or the build needs no signed URL"
34
+ }
35
+ }
36
+ });
37
+ /** Register the preview token refresh route. */
38
+ function registerPreviewToken(app) {
39
+ app.openapi(previewTokenRoute, async (c) => {
40
+ const { db } = getStore();
41
+ const project = await new ProjectModel(db).getBySlug(c.req.param("slug"));
42
+ const build = await new BuildModel(db).get(c.req.param("buildId"));
43
+ const base = project && build?.projectId === project.id ? await previewBaseOf(project, build) : null;
44
+ if (base === null || !isSignedPreviewBase(base)) return c.text("Not found", 404, SIGNED_URL_HEADERS);
45
+ return c.json({ base }, 200, SIGNED_URL_HEADERS);
46
+ });
47
+ }
48
+ export { registerPreviewToken };
49
+
50
+ //# sourceMappingURL=preview-token.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"preview-token.mjs","names":[],"sources":["../../src/routers/preview-token.ts"],"sourcesContent":["/**\n * `GET /api/v1/projects/:slug/builds/:buildId/preview-token`: a fresh root for a component page's preview frame\n * (ADR 0006, decision 9). A frame holding a signed `/_s/<token>` URL (ADR 0007, decision 5) outlives\n * the token's hour on a page left open, so the page's script asks for a new root before then and\n * re-points the frame. Answers `{ base }` for a viewer who may open the build, 404 otherwise or when\n * the build needs no token (anyone may open it). Never cached: the answer carries a token.\n */\nimport { createRoute, z } from \"@hono/zod-openapi\";\nimport { BuildModel, ProjectModel } from \"@storyshelf/core/models\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { getStore } from \"../store.ts\";\nimport { isSignedPreviewBase, previewBaseOf } from \"./preview-base.ts\";\nimport { SIGNED_URL_HEADERS } from \"./published-files.ts\";\n\nconst previewTokenRoute = createRoute({\n method: \"get\",\n tags: [\"Review\"],\n summary: \"Get a fresh signed root for a build's preview frame\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/preview-token\",\n request: { params: z.object({ slug: z.string(), buildId: z.string() }) },\n responses: {\n 200: {\n content: { \"application/json\": { schema: z.object({ base: z.string() }) } },\n description: \"The signed root (`/_s/<token>`) to frame the build's Storybook under\",\n },\n 404: {\n content: { \"text/plain\": { schema: z.string() } },\n description: \"Not found, or the build needs no signed URL\",\n },\n },\n});\n\n/** Register the preview token refresh route. */\nexport function registerPreviewToken(app: ShelfRouter): void {\n app.openapi(previewTokenRoute, async (c) => {\n const { db } = getStore();\n const project = await new ProjectModel(db).getBySlug(c.req.param(\"slug\"));\n const build = await new BuildModel(db).get(c.req.param(\"buildId\"));\n const base =\n project && build?.projectId === project.id ? await previewBaseOf(project, build) : null;\n if (base === null || !isSignedPreviewBase(base)) {\n return c.text(\"Not found\", 404, SIGNED_URL_HEADERS);\n }\n return c.json({ base }, 200, SIGNED_URL_HEADERS);\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAcA,MAAM,oBAAoB,YAAY;CACpC,QAAQ;CACR,MAAM,CAAC,QAAQ;CACf,SAAS;CACT,MAAM;CACN,SAAS,EAAE,QAAQ,EAAE,OAAO;EAAE,MAAM,EAAE,OAAO;EAAG,SAAS,EAAE,OAAO;CAAE,CAAC,EAAE;CACvE,WAAW;EACT,KAAK;GACH,SAAS,EAAE,oBAAoB,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,EAAE,EAAE;GAC1E,aAAa;EACf;EACA,KAAK;GACH,SAAS,EAAE,cAAc,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE;GAChD,aAAa;EACf;CACF;AACF,CAAC;;AAGD,SAAgB,qBAAqB,KAAwB;CAC3D,IAAI,QAAQ,mBAAmB,OAAO,MAAM;EAC1C,MAAM,EAAE,OAAO,SAAS;EACxB,MAAM,UAAU,MAAM,IAAI,aAAa,EAAE,CAAC,CAAC,UAAU,EAAE,IAAI,MAAM,MAAM,CAAC;EACxE,MAAM,QAAQ,MAAM,IAAI,WAAW,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,MAAM,SAAS,CAAC;EACjE,MAAM,OACJ,WAAW,OAAO,cAAc,QAAQ,KAAK,MAAM,cAAc,SAAS,KAAK,IAAI;EACrF,IAAI,SAAS,QAAQ,CAAC,oBAAoB,IAAI,GAC5C,OAAO,EAAE,KAAK,aAAa,KAAK,kBAAkB;EAEpD,OAAO,EAAE,KAAK,EAAE,KAAK,GAAG,KAAK,kBAAkB;CACjD,CAAC;AACH"}
@@ -2,6 +2,7 @@ import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
4
  import { getStore } from "../store.mjs";
5
+ import { notificationsPaused } from "../notifications-pause.mjs";
5
6
  import { hxRedirect } from "./htmx.mjs";
6
7
  import { flash } from "./flash.mjs";
7
8
  import { forwardToEngine } from "./auth.mjs";
@@ -10,7 +11,7 @@ import { NotificationSubscriptionModel, ProjectModel, UserModel } from "@storysh
10
11
  import { SESSION_COOKIE } from "@storyshelf/core/types";
11
12
  import { MIN_PASSWORD_LENGTH, unsignedToken } from "@storyshelf/core/auth";
12
13
  import { eq, getTableColumns } from "@storyshelf/core/orm";
13
- //#region src/routers/profile.ts
14
+ // src/routers/profile.ts
14
15
  function field(form, name) {
15
16
  const raw = form.get(name);
16
17
  return typeof raw === "string" ? raw : "";
@@ -89,7 +90,8 @@ async function profileView(c, user, auth, extra) {
89
90
  subscribedSlugs,
90
91
  isLocal: extra?.local ?? security.hasPassword,
91
92
  security,
92
- error: extra?.error
93
+ error: extra?.error,
94
+ notificationsPaused: getStore().notifiers.length > 0 && await notificationsPaused()
93
95
  });
94
96
  if (extra?.success) flash(c, extra.success);
95
97
  c.header("Cache-Control", "no-store");
@@ -270,7 +272,6 @@ function readPasswordChange(form) {
270
272
  next
271
273
  };
272
274
  }
273
- //#endregion
274
275
  export { registerProfile };
275
276
 
276
277
  //# sourceMappingURL=profile.mjs.map