@storyshelf/app 0.6.2 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (361) hide show
  1. package/README.md +42 -4
  2. package/dist/app-types.d.mts +1 -3
  3. package/dist/asset-manifest.mjs +13 -5
  4. package/dist/asset-manifest.mjs.map +1 -1
  5. package/dist/assets/{htmx.min.js → htmx.mjs} +10 -1
  6. package/dist/assets/htmx.mjs.map +1 -0
  7. package/dist/assets/mark.mjs +10 -0
  8. package/dist/assets/mark.mjs.map +1 -0
  9. package/dist/boot-tasks.mjs +80 -0
  10. package/dist/boot-tasks.mjs.map +1 -0
  11. package/dist/build-state-flags.mjs +18 -0
  12. package/dist/build-state-flags.mjs.map +1 -0
  13. package/dist/build-state-setup.mjs +75 -0
  14. package/dist/build-state-setup.mjs.map +1 -0
  15. package/dist/capture-alerts.mjs +46 -0
  16. package/dist/capture-alerts.mjs.map +1 -0
  17. package/dist/capture-process.mjs +59 -0
  18. package/dist/capture-process.mjs.map +1 -0
  19. package/dist/capture-setup.mjs +95 -117
  20. package/dist/capture-setup.mjs.map +1 -1
  21. package/dist/connections.mjs +34 -0
  22. package/dist/connections.mjs.map +1 -0
  23. package/dist/consumer-setup.mjs +58 -0
  24. package/dist/consumer-setup.mjs.map +1 -0
  25. package/dist/git-status.mjs +46 -0
  26. package/dist/git-status.mjs.map +1 -0
  27. package/dist/hidden-data.mjs +24 -0
  28. package/dist/hidden-data.mjs.map +1 -0
  29. package/dist/index.d.mts +3 -3
  30. package/dist/index.mjs +203 -125
  31. package/dist/index.mjs.map +1 -1
  32. package/dist/insights/budget.mjs +49 -0
  33. package/dist/insights/budget.mjs.map +1 -0
  34. package/dist/insights/deps.mjs +21 -0
  35. package/dist/insights/deps.mjs.map +1 -0
  36. package/dist/insights/errors.mjs +22 -0
  37. package/dist/insights/errors.mjs.map +1 -0
  38. package/dist/insights/health-panel.mjs +40 -0
  39. package/dist/insights/health-panel.mjs.map +1 -0
  40. package/dist/insights/job.mjs +103 -0
  41. package/dist/insights/job.mjs.map +1 -0
  42. package/dist/insights/load-health.mjs +59 -0
  43. package/dist/insights/load-health.mjs.map +1 -0
  44. package/dist/insights/load-triage.mjs +98 -0
  45. package/dist/insights/load-triage.mjs.map +1 -0
  46. package/dist/insights/panel.mjs +28 -0
  47. package/dist/insights/panel.mjs.map +1 -0
  48. package/dist/insights/request.mjs +113 -0
  49. package/dist/insights/request.mjs.map +1 -0
  50. package/dist/insights/stale.mjs +21 -0
  51. package/dist/insights/stale.mjs.map +1 -0
  52. package/dist/insights/usage-report.mjs +60 -0
  53. package/dist/insights/usage-report.mjs.map +1 -0
  54. package/dist/insights/view.mjs +48 -0
  55. package/dist/insights/view.mjs.map +1 -0
  56. package/dist/job-canceller.mjs +14 -0
  57. package/dist/job-canceller.mjs.map +1 -0
  58. package/dist/job-queue.mjs +51 -0
  59. package/dist/job-queue.mjs.map +1 -0
  60. package/dist/jobs-setup.mjs +132 -0
  61. package/dist/jobs-setup.mjs.map +1 -0
  62. package/dist/lifecycle.mjs +91 -19
  63. package/dist/lifecycle.mjs.map +1 -1
  64. package/dist/middleware/auth-gate.mjs +4 -2
  65. package/dist/middleware/auth-gate.mjs.map +1 -1
  66. package/dist/middleware/rate-limit.mjs +41 -33
  67. package/dist/middleware/rate-limit.mjs.map +1 -1
  68. package/dist/middleware/request-log.mjs +3 -2
  69. package/dist/middleware/request-log.mjs.map +1 -1
  70. package/dist/middleware/require-review.mjs +51 -0
  71. package/dist/middleware/require-review.mjs.map +1 -0
  72. package/dist/middleware/require-testing.mjs +41 -0
  73. package/dist/middleware/require-testing.mjs.map +1 -0
  74. package/dist/middleware/setup-gate.mjs.map +1 -1
  75. package/dist/middleware/store-scope.mjs +12 -2
  76. package/dist/middleware/store-scope.mjs.map +1 -1
  77. package/dist/notifications-pause.mjs +33 -0
  78. package/dist/notifications-pause.mjs.map +1 -0
  79. package/dist/notify.d.mts +2 -1
  80. package/dist/notify.mjs +78 -39
  81. package/dist/notify.mjs.map +1 -1
  82. package/dist/pages/a11y-violations.mjs +116 -0
  83. package/dist/pages/a11y-violations.mjs.map +1 -0
  84. package/dist/pages/admin-ai.mjs +78 -0
  85. package/dist/pages/admin-ai.mjs.map +1 -0
  86. package/dist/pages/admin-system-origin.mjs +123 -0
  87. package/dist/pages/admin-system-origin.mjs.map +1 -0
  88. package/dist/pages/admin-system-targets.mjs +53 -0
  89. package/dist/pages/admin-system-targets.mjs.map +1 -0
  90. package/dist/pages/admin-system.mjs +63 -13
  91. package/dist/pages/admin-system.mjs.map +1 -1
  92. package/dist/pages/build-detail.mjs +104 -76
  93. package/dist/pages/build-detail.mjs.map +1 -1
  94. package/dist/pages/build-diff-actions.mjs +87 -33
  95. package/dist/pages/build-diff-actions.mjs.map +1 -1
  96. package/dist/pages/build-diff-comments.mjs +16 -14
  97. package/dist/pages/build-diff-comments.mjs.map +1 -1
  98. package/dist/pages/build-diff-environment.mjs +82 -0
  99. package/dist/pages/build-diff-environment.mjs.map +1 -0
  100. package/dist/pages/build-diff-header.mjs +22 -4
  101. package/dist/pages/build-diff-header.mjs.map +1 -1
  102. package/dist/pages/build-diff-insight.mjs +113 -0
  103. package/dist/pages/build-diff-insight.mjs.map +1 -0
  104. package/dist/pages/build-diff-model.mjs +55 -10
  105. package/dist/pages/build-diff-model.mjs.map +1 -1
  106. package/dist/pages/build-diff-nav.mjs +28 -12
  107. package/dist/pages/build-diff-nav.mjs.map +1 -1
  108. package/dist/pages/build-diff-shortcuts.mjs +5 -1
  109. package/dist/pages/build-diff-shortcuts.mjs.map +1 -1
  110. package/dist/pages/build-diff-usage.mjs +63 -0
  111. package/dist/pages/build-diff-usage.mjs.map +1 -0
  112. package/dist/pages/build-diff-viewer.mjs +62 -5
  113. package/dist/pages/build-diff-viewer.mjs.map +1 -1
  114. package/dist/pages/build-diff.mjs +34 -13
  115. package/dist/pages/build-diff.mjs.map +1 -1
  116. package/dist/pages/build-tests.mjs +74 -0
  117. package/dist/pages/build-tests.mjs.map +1 -0
  118. package/dist/pages/component-detail.mjs +249 -0
  119. package/dist/pages/component-detail.mjs.map +1 -0
  120. package/dist/pages/component-stories.mjs +168 -0
  121. package/dist/pages/component-stories.mjs.map +1 -0
  122. package/dist/pages/compute-jobs.mjs +13 -26
  123. package/dist/pages/compute-jobs.mjs.map +1 -1
  124. package/dist/pages/design-links.mjs +182 -0
  125. package/dist/pages/design-links.mjs.map +1 -0
  126. package/dist/pages/environment-change.mjs +53 -0
  127. package/dist/pages/environment-change.mjs.map +1 -0
  128. package/dist/pages/health-panel.mjs +126 -0
  129. package/dist/pages/health-panel.mjs.map +1 -0
  130. package/dist/pages/library-grid.mjs +105 -0
  131. package/dist/pages/library-grid.mjs.map +1 -0
  132. package/dist/pages/library-model.mjs +47 -0
  133. package/dist/pages/library-model.mjs.map +1 -0
  134. package/dist/pages/library-snapshots.mjs +130 -0
  135. package/dist/pages/library-snapshots.mjs.map +1 -0
  136. package/dist/pages/library.mjs +112 -218
  137. package/dist/pages/library.mjs.map +1 -1
  138. package/dist/pages/notifications-paused.mjs +22 -0
  139. package/dist/pages/notifications-paused.mjs.map +1 -0
  140. package/dist/pages/profile.mjs +53 -48
  141. package/dist/pages/profile.mjs.map +1 -1
  142. package/dist/pages/project-builds-table.mjs +2 -2
  143. package/dist/pages/project-builds-table.mjs.map +1 -1
  144. package/dist/pages/project-builds.mjs +51 -21
  145. package/dist/pages/project-builds.mjs.map +1 -1
  146. package/dist/pages/project-settings.mjs +51 -12
  147. package/dist/pages/project-settings.mjs.map +1 -1
  148. package/dist/pages/projects-card.mjs +14 -1
  149. package/dist/pages/projects-card.mjs.map +1 -1
  150. package/dist/pages/projects.mjs +6 -2
  151. package/dist/pages/projects.mjs.map +1 -1
  152. package/dist/pages/review-changeset.mjs +116 -0
  153. package/dist/pages/review-changeset.mjs.map +1 -0
  154. package/dist/pages/review-comments.mjs +103 -0
  155. package/dist/pages/review-comments.mjs.map +1 -0
  156. package/dist/pages/review-verdict.mjs +102 -0
  157. package/dist/pages/review-verdict.mjs.map +1 -0
  158. package/dist/pages/settings-ai.mjs +69 -0
  159. package/dist/pages/settings-ai.mjs.map +1 -0
  160. package/dist/pages/settings-capture.mjs +124 -0
  161. package/dist/pages/settings-capture.mjs.map +1 -0
  162. package/dist/pages/settings-design-credential.mjs +99 -0
  163. package/dist/pages/settings-design-credential.mjs.map +1 -0
  164. package/dist/pages/settings-design-links.mjs +179 -0
  165. package/dist/pages/settings-design-links.mjs.map +1 -0
  166. package/dist/pages/settings-design.mjs +48 -0
  167. package/dist/pages/settings-design.mjs.map +1 -0
  168. package/dist/pages/settings-general.mjs +46 -21
  169. package/dist/pages/settings-general.mjs.map +1 -1
  170. package/dist/pages/settings-labels.mjs +43 -2
  171. package/dist/pages/settings-labels.mjs.map +1 -1
  172. package/dist/pages/settings-links.mjs +112 -0
  173. package/dist/pages/settings-links.mjs.map +1 -0
  174. package/dist/pages/settings-notifications-git.mjs +128 -0
  175. package/dist/pages/settings-notifications-git.mjs.map +1 -0
  176. package/dist/pages/settings-notifications.mjs +79 -42
  177. package/dist/pages/settings-notifications.mjs.map +1 -1
  178. package/dist/pages/settings-tests.mjs +33 -2
  179. package/dist/pages/settings-tests.mjs.map +1 -1
  180. package/dist/pages/target-choices.mjs +39 -0
  181. package/dist/pages/target-choices.mjs.map +1 -0
  182. package/dist/publish-setup.mjs +89 -0
  183. package/dist/publish-setup.mjs.map +1 -0
  184. package/dist/retention-timer.mjs +2 -1
  185. package/dist/retention-timer.mjs.map +1 -1
  186. package/dist/review-events.mjs +48 -0
  187. package/dist/review-events.mjs.map +1 -0
  188. package/dist/routers/admin-pages.mjs +45 -3
  189. package/dist/routers/admin-pages.mjs.map +1 -1
  190. package/dist/routers/admin-public-origin.mjs +85 -0
  191. package/dist/routers/admin-public-origin.mjs.map +1 -0
  192. package/dist/routers/assets.mjs +9 -2
  193. package/dist/routers/assets.mjs.map +1 -1
  194. package/dist/routers/auth.mjs +1 -1
  195. package/dist/routers/baseline-guard.mjs +2 -2
  196. package/dist/routers/baseline-guard.mjs.map +1 -1
  197. package/dist/routers/baselines.handlers.mjs +64 -0
  198. package/dist/routers/baselines.handlers.mjs.map +1 -0
  199. package/dist/routers/baselines.mjs +125 -0
  200. package/dist/routers/baselines.mjs.map +1 -0
  201. package/dist/routers/build-cancel.mjs +68 -0
  202. package/dist/routers/build-cancel.mjs.map +1 -0
  203. package/dist/routers/build-changeset.mjs +70 -0
  204. package/dist/routers/build-changeset.mjs.map +1 -0
  205. package/dist/routers/build-reviews.handlers.mjs +86 -0
  206. package/dist/routers/build-reviews.handlers.mjs.map +1 -0
  207. package/dist/routers/build-reviews.mjs +74 -0
  208. package/dist/routers/build-reviews.mjs.map +1 -0
  209. package/dist/routers/builds.handlers.mjs +46 -55
  210. package/dist/routers/builds.handlers.mjs.map +1 -1
  211. package/dist/routers/builds.mjs +71 -17
  212. package/dist/routers/builds.mjs.map +1 -1
  213. package/dist/routers/capture-config.mjs +35 -0
  214. package/dist/routers/capture-config.mjs.map +1 -0
  215. package/dist/routers/comments.mjs +41 -15
  216. package/dist/routers/comments.mjs.map +1 -1
  217. package/dist/routers/component-catalog.handlers.mjs +71 -0
  218. package/dist/routers/component-catalog.handlers.mjs.map +1 -0
  219. package/dist/routers/component.handlers.mjs +121 -0
  220. package/dist/routers/component.handlers.mjs.map +1 -0
  221. package/dist/routers/connections.mjs +118 -0
  222. package/dist/routers/connections.mjs.map +1 -0
  223. package/dist/routers/design-connect.handlers.mjs +44 -0
  224. package/dist/routers/design-connect.handlers.mjs.map +1 -0
  225. package/dist/routers/design.handlers.mjs +114 -0
  226. package/dist/routers/design.handlers.mjs.map +1 -0
  227. package/dist/routers/health-report.mjs +2 -3
  228. package/dist/routers/health-report.mjs.map +1 -1
  229. package/dist/routers/health.mjs +4 -3
  230. package/dist/routers/health.mjs.map +1 -1
  231. package/dist/routers/helpers.mjs +19 -3
  232. package/dist/routers/helpers.mjs.map +1 -1
  233. package/dist/routers/insights-ui.mjs +64 -0
  234. package/dist/routers/insights-ui.mjs.map +1 -0
  235. package/dist/routers/insights.handlers.mjs +45 -0
  236. package/dist/routers/insights.handlers.mjs.map +1 -0
  237. package/dist/routers/insights.mjs +189 -0
  238. package/dist/routers/insights.mjs.map +1 -0
  239. package/dist/routers/links.handlers.mjs +80 -0
  240. package/dist/routers/links.handlers.mjs.map +1 -0
  241. package/dist/routers/links.mjs +139 -0
  242. package/dist/routers/links.mjs.map +1 -0
  243. package/dist/routers/media.mjs +18 -4
  244. package/dist/routers/media.mjs.map +1 -1
  245. package/dist/routers/members.mjs +2 -1
  246. package/dist/routers/members.mjs.map +1 -1
  247. package/dist/routers/notification-channels.mjs +17 -23
  248. package/dist/routers/notification-channels.mjs.map +1 -1
  249. package/dist/routers/oauth-state.mjs +45 -0
  250. package/dist/routers/oauth-state.mjs.map +1 -0
  251. package/dist/routers/preview-base.mjs +38 -0
  252. package/dist/routers/preview-base.mjs.map +1 -0
  253. package/dist/routers/preview-token.mjs +51 -0
  254. package/dist/routers/preview-token.mjs.map +1 -0
  255. package/dist/routers/profile.mjs +3 -1
  256. package/dist/routers/profile.mjs.map +1 -1
  257. package/dist/routers/projects.mjs +17 -1
  258. package/dist/routers/projects.mjs.map +1 -1
  259. package/dist/routers/publish-upload.mjs +78 -0
  260. package/dist/routers/publish-upload.mjs.map +1 -0
  261. package/dist/routers/published-access.mjs +52 -0
  262. package/dist/routers/published-access.mjs.map +1 -0
  263. package/dist/routers/published-files.mjs +52 -0
  264. package/dist/routers/published-files.mjs.map +1 -0
  265. package/dist/routers/review-forms.mjs +72 -0
  266. package/dist/routers/review-forms.mjs.map +1 -0
  267. package/dist/routers/review-section.handlers.mjs +85 -0
  268. package/dist/routers/review-section.handlers.mjs.map +1 -0
  269. package/dist/routers/review-switch-off.mjs +29 -0
  270. package/dist/routers/review-switch-off.mjs.map +1 -0
  271. package/dist/routers/schemas.mjs +140 -22
  272. package/dist/routers/schemas.mjs.map +1 -1
  273. package/dist/routers/scoped-models.mjs +22 -0
  274. package/dist/routers/scoped-models.mjs.map +1 -0
  275. package/dist/routers/settings-ai.mjs +35 -0
  276. package/dist/routers/settings-ai.mjs.map +1 -0
  277. package/dist/routers/settings-capture.mjs +77 -0
  278. package/dist/routers/settings-capture.mjs.map +1 -0
  279. package/dist/routers/settings-design.mjs +103 -0
  280. package/dist/routers/settings-design.mjs.map +1 -0
  281. package/dist/routers/settings-general.mjs +55 -10
  282. package/dist/routers/settings-general.mjs.map +1 -1
  283. package/dist/routers/settings-git-channel.mjs +77 -0
  284. package/dist/routers/settings-git-channel.mjs.map +1 -0
  285. package/dist/routers/settings-labels.mjs +27 -1
  286. package/dist/routers/settings-labels.mjs.map +1 -1
  287. package/dist/routers/settings-links.mjs +63 -0
  288. package/dist/routers/settings-links.mjs.map +1 -0
  289. package/dist/routers/settings-members.mjs +4 -7
  290. package/dist/routers/settings-members.mjs.map +1 -1
  291. package/dist/routers/settings-notifications.mjs +15 -10
  292. package/dist/routers/settings-notifications.mjs.map +1 -1
  293. package/dist/routers/settings-tokens.mjs +2 -5
  294. package/dist/routers/settings-tokens.mjs.map +1 -1
  295. package/dist/routers/settings-webhooks.mjs +7 -24
  296. package/dist/routers/settings-webhooks.mjs.map +1 -1
  297. package/dist/routers/settings.handlers.mjs +107 -26
  298. package/dist/routers/settings.handlers.mjs.map +1 -1
  299. package/dist/routers/settings.mjs +16 -4
  300. package/dist/routers/settings.mjs.map +1 -1
  301. package/dist/routers/signed-storybook.mjs +53 -0
  302. package/dist/routers/signed-storybook.mjs.map +1 -0
  303. package/dist/routers/snapshot-groups.mjs +63 -0
  304. package/dist/routers/snapshot-groups.mjs.map +1 -0
  305. package/dist/routers/snapshots.mjs +103 -5
  306. package/dist/routers/snapshots.mjs.map +1 -1
  307. package/dist/routers/storybook.mjs +37 -88
  308. package/dist/routers/storybook.mjs.map +1 -1
  309. package/dist/routers/tokens.mjs +2 -5
  310. package/dist/routers/tokens.mjs.map +1 -1
  311. package/dist/routers/ui.mjs +34 -5
  312. package/dist/routers/ui.mjs.map +1 -1
  313. package/dist/routers/webhooks.mjs +5 -11
  314. package/dist/routers/webhooks.mjs.map +1 -1
  315. package/dist/routers/well-known.mjs +11 -4
  316. package/dist/routers/well-known.mjs.map +1 -1
  317. package/dist/routes/api-routes.mjs +93 -0
  318. package/dist/routes/api-routes.mjs.map +1 -0
  319. package/dist/routes/classification.mjs +38 -0
  320. package/dist/routes/classification.mjs.map +1 -0
  321. package/dist/routes/openapi-classes.mjs +22 -0
  322. package/dist/routes/openapi-classes.mjs.map +1 -0
  323. package/dist/routes/page-routes.mjs +104 -0
  324. package/dist/routes/page-routes.mjs.map +1 -0
  325. package/dist/runtime.mjs +18 -13
  326. package/dist/runtime.mjs.map +1 -1
  327. package/dist/serverless.mjs +5 -0
  328. package/dist/store.mjs +19 -1
  329. package/dist/store.mjs.map +1 -1
  330. package/dist/targets.mjs +46 -0
  331. package/dist/targets.mjs.map +1 -0
  332. package/dist/ui/buttons.mjs +16 -3
  333. package/dist/ui/buttons.mjs.map +1 -1
  334. package/dist/ui/design-reference.mjs +97 -0
  335. package/dist/ui/design-reference.mjs.map +1 -0
  336. package/dist/ui/document.mjs +1 -2
  337. package/dist/ui/document.mjs.map +1 -1
  338. package/dist/ui/forms.mjs +6 -5
  339. package/dist/ui/forms.mjs.map +1 -1
  340. package/dist/ui/icons/paths.mjs +1 -0
  341. package/dist/ui/icons/paths.mjs.map +1 -1
  342. package/dist/ui/preview-embed.mjs +82 -0
  343. package/dist/ui/preview-embed.mjs.map +1 -0
  344. package/dist/ui/shell/client-script.mjs +85 -0
  345. package/dist/ui/shell/client-script.mjs.map +1 -1
  346. package/dist/ui/shell/nav.mjs +12 -8
  347. package/dist/ui/shell/nav.mjs.map +1 -1
  348. package/dist/ui/shell/review-script.mjs +8 -0
  349. package/dist/ui/shell/review-script.mjs.map +1 -1
  350. package/dist/ui/shell/sidebar.mjs +2 -2
  351. package/dist/ui/shell/sidebar.mjs.map +1 -1
  352. package/package.json +11 -9
  353. package/dist/asset-paths.mjs +0 -23
  354. package/dist/asset-paths.mjs.map +0 -1
  355. package/dist/assets/mark.svg +0 -1
  356. package/dist/pages/settings-status.mjs +0 -137
  357. package/dist/pages/settings-status.mjs.map +0 -1
  358. package/dist/routers/settings-status.mjs +0 -77
  359. package/dist/routers/settings-status.mjs.map +0 -1
  360. package/dist/routers/status-configs.mjs +0 -108
  361. package/dist/routers/status-configs.mjs.map +0 -1
@@ -0,0 +1,189 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { failWith } from "../insights/errors.mjs";
5
+ import { notFound } from "./helpers.mjs";
6
+ import { badRequest, errorSchema, healthRequestSchema, insightRequestSchema, insightSchema, notFound as notFound$1, unauthorized } from "./schemas.mjs";
7
+ import { buildForProject } from "./builds.handlers.mjs";
8
+ import { freshen } from "../insights/stale.mjs";
9
+ import { toInsightView } from "../insights/view.mjs";
10
+ import { requestHealth, requestTriage } from "../insights/request.mjs";
11
+ import { isSiteAdmin, projectForRegenerate, projectForView } from "./insights.handlers.mjs";
12
+ import { createRoute, z } from "@hono/zod-openapi";
13
+ import { InsightModel } from "@storyshelf/core/models";
14
+ //#region src/routers/insights.ts
15
+ const BUILD_PARAMS = z.object({
16
+ slug: z.string(),
17
+ buildId: z.string()
18
+ });
19
+ const json = (schema) => ({ content: { "application/json": { schema } } });
20
+ const guardResponses = {
21
+ 501: {
22
+ ...json(errorSchema),
23
+ description: "AI is not configured on this server (ai-disabled)"
24
+ },
25
+ 409: {
26
+ ...json(errorSchema),
27
+ description: "AI is disabled for this project"
28
+ },
29
+ ...notFound$1,
30
+ ...unauthorized
31
+ };
32
+ const latestRoute = createRoute({
33
+ method: "get",
34
+ tags: ["Insights"],
35
+ summary: "Latest build triage",
36
+ path: "/api/v1/projects/{slug}/builds/{buildId}/insights/latest",
37
+ request: { params: BUILD_PARAMS },
38
+ responses: {
39
+ 200: {
40
+ ...json(insightSchema),
41
+ description: "Latest triage"
42
+ },
43
+ ...guardResponses
44
+ }
45
+ });
46
+ const listRoute = createRoute({
47
+ method: "get",
48
+ tags: ["Insights"],
49
+ summary: "Build triage history",
50
+ path: "/api/v1/projects/{slug}/builds/{buildId}/insights",
51
+ request: {
52
+ params: BUILD_PARAMS,
53
+ query: z.object({ limit: z.coerce.number().int().min(1).max(50).optional() })
54
+ },
55
+ responses: {
56
+ 200: {
57
+ ...json(insightSchema.array()),
58
+ description: "Triage history"
59
+ },
60
+ ...guardResponses
61
+ }
62
+ });
63
+ const generateRoute = createRoute({
64
+ method: "post",
65
+ tags: ["Insights"],
66
+ summary: "Generate a build triage",
67
+ path: "/api/v1/projects/{slug}/builds/{buildId}/insights",
68
+ request: {
69
+ params: BUILD_PARAMS,
70
+ body: { content: { "application/json": { schema: insightRequestSchema } } }
71
+ },
72
+ responses: {
73
+ 200: {
74
+ ...json(insightSchema),
75
+ description: "Cached insight (unbilled)"
76
+ },
77
+ 202: {
78
+ ...json(insightSchema),
79
+ description: "Generation started; poll the latest endpoint"
80
+ },
81
+ 429: {
82
+ ...json(errorSchema),
83
+ description: "Budget or rate limit reached (see Retry-After)"
84
+ },
85
+ ...guardResponses
86
+ }
87
+ });
88
+ const healthGetRoute = createRoute({
89
+ method: "get",
90
+ tags: ["Insights"],
91
+ summary: "Project health digest",
92
+ path: "/api/v1/projects/{slug}/insights/health",
93
+ request: {
94
+ params: z.object({ slug: z.string() }),
95
+ query: z.object({ window: z.string().optional() })
96
+ },
97
+ responses: {
98
+ 200: {
99
+ ...json(insightSchema),
100
+ description: "Cached digest"
101
+ },
102
+ ...guardResponses
103
+ }
104
+ });
105
+ const healthPostRoute = createRoute({
106
+ method: "post",
107
+ tags: ["Insights"],
108
+ summary: "Generate a project health digest",
109
+ path: "/api/v1/projects/{slug}/insights/health",
110
+ request: {
111
+ params: z.object({ slug: z.string() }),
112
+ body: { content: { "application/json": { schema: healthRequestSchema } } }
113
+ },
114
+ responses: {
115
+ 200: {
116
+ ...json(insightSchema),
117
+ description: "Cached digest (unbilled)"
118
+ },
119
+ 202: {
120
+ ...json(insightSchema),
121
+ description: "Generation started"
122
+ },
123
+ 429: {
124
+ ...json(errorSchema),
125
+ description: "Budget or rate limit reached (see Retry-After)"
126
+ },
127
+ ...badRequest,
128
+ ...guardResponses
129
+ }
130
+ });
131
+ /** Register the build triage endpoints. */
132
+ function registerBuildInsights(app) {
133
+ app.openapi(latestRoute, async (c) => {
134
+ const { slug, buildId } = c.req.valid("param");
135
+ const { deps, project } = await projectForView(c, slug);
136
+ const build = await buildForProject(project.id, buildId);
137
+ const [latest] = await new InsightModel(deps.db).listForBuild(build.id, 1);
138
+ return latest ? c.json(toInsightView(await freshen(deps, latest)), 200) : notFound("never-generated: POST to this build's insights to create one");
139
+ });
140
+ app.openapi(listRoute, async (c) => {
141
+ const { slug, buildId } = c.req.valid("param");
142
+ const { limit } = c.req.valid("query");
143
+ const { deps, project } = await projectForView(c, slug);
144
+ const build = await buildForProject(project.id, buildId);
145
+ const rows = await new InsightModel(deps.db).listForBuild(build.id, limit ?? 20);
146
+ return c.json(await Promise.all(rows.map(async (row) => toInsightView(await freshen(deps, row)))), 200);
147
+ });
148
+ app.openapi(generateRoute, async (c) => {
149
+ const { slug, buildId } = c.req.valid("param");
150
+ const body = c.req.valid("json");
151
+ const { deps, project } = await projectForRegenerate(c, slug);
152
+ const build = await buildForProject(project.id, buildId);
153
+ const outcome = await requestTriage(deps, project, build, {
154
+ force: body.force ?? false,
155
+ profile: isSiteAdmin(c) ? body.profile : void 0
156
+ });
157
+ const view = toInsightView(outcome.row);
158
+ return outcome.status === 200 ? c.json(view, 200) : c.json(view, 202, { location: `/api/v1/projects/${slug}/builds/${build.id}/insights/latest` });
159
+ });
160
+ }
161
+ /** Register the project-health endpoints. */
162
+ function registerHealthInsights(app) {
163
+ app.openapi(healthGetRoute, async (c) => {
164
+ const { slug } = c.req.valid("param");
165
+ const { window } = c.req.valid("query");
166
+ const { deps, project } = await projectForView(c, slug);
167
+ const row = await new InsightModel(deps.db).getHealth(project.id, window ?? "30d");
168
+ return row ? c.json(toInsightView(await freshen(deps, row)), 200) : notFound("never-generated: POST to create a health digest");
169
+ });
170
+ app.openapi(healthPostRoute, async (c) => {
171
+ const { slug } = c.req.valid("param");
172
+ const body = c.req.valid("json");
173
+ const { deps, project } = await projectForRegenerate(c, slug);
174
+ const window = body.window ?? "30d";
175
+ const outcome = await requestHealth(deps, project, window, { force: body.force ?? false });
176
+ if (!outcome) failWith(400, "window must look like 30d (1-365 days)");
177
+ const view = toInsightView(outcome.row);
178
+ return outcome.status === 200 ? c.json(view, 200) : c.json(view, 202, { location: `/api/v1/projects/${slug}/insights/health?window=${window}` });
179
+ });
180
+ }
181
+ /** Register the AI insights endpoints (build triage and project health). */
182
+ function registerInsights(app) {
183
+ registerBuildInsights(app);
184
+ registerHealthInsights(app);
185
+ }
186
+ //#endregion
187
+ export { registerInsights };
188
+
189
+ //# sourceMappingURL=insights.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"insights.mjs","names":["notFound","throwNotFound"],"sources":["../../src/routers/insights.ts"],"sourcesContent":["import { createRoute, z } from \"@hono/zod-openapi\";\nimport { InsightModel } from \"@storyshelf/core/models\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { failWith } from \"../insights/errors.ts\";\nimport { requestHealth, requestTriage } from \"../insights/request.ts\";\nimport { freshen } from \"../insights/stale.ts\";\nimport { toInsightView } from \"../insights/view.ts\";\nimport { buildForProject } from \"./builds.handlers.ts\";\nimport { notFound as throwNotFound } from \"./helpers.ts\";\nimport { isSiteAdmin, projectForRegenerate, projectForView } from \"./insights.handlers.ts\";\nimport {\n badRequest,\n errorSchema,\n healthRequestSchema,\n insightRequestSchema,\n insightSchema,\n notFound,\n unauthorized,\n} from \"./schemas.ts\";\n\nconst BUILD_PARAMS = z.object({ slug: z.string(), buildId: z.string() });\nconst json = (schema: z.ZodType): { content: { \"application/json\": { schema: z.ZodType } } } => ({\n content: { \"application/json\": { schema } },\n});\nconst guardResponses = {\n 501: { ...json(errorSchema), description: \"AI is not configured on this server (ai-disabled)\" },\n 409: { ...json(errorSchema), description: \"AI is disabled for this project\" },\n ...notFound,\n ...unauthorized,\n} as const;\n\nconst latestRoute = createRoute({\n method: \"get\",\n tags: [\"Insights\"],\n summary: \"Latest build triage\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/insights/latest\",\n request: { params: BUILD_PARAMS },\n responses: { 200: { ...json(insightSchema), description: \"Latest triage\" }, ...guardResponses },\n});\n\nconst listRoute = createRoute({\n method: \"get\",\n tags: [\"Insights\"],\n summary: \"Build triage history\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/insights\",\n request: {\n params: BUILD_PARAMS,\n query: z.object({ limit: z.coerce.number().int().min(1).max(50).optional() }),\n },\n responses: {\n 200: { ...json(insightSchema.array()), description: \"Triage history\" },\n ...guardResponses,\n },\n});\n\nconst generateRoute = createRoute({\n method: \"post\",\n tags: [\"Insights\"],\n summary: \"Generate a build triage\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/insights\",\n request: {\n params: BUILD_PARAMS,\n body: { content: { \"application/json\": { schema: insightRequestSchema } } },\n },\n responses: {\n 200: { ...json(insightSchema), description: \"Cached insight (unbilled)\" },\n 202: { ...json(insightSchema), description: \"Generation started; poll the latest endpoint\" },\n 429: { ...json(errorSchema), description: \"Budget or rate limit reached (see Retry-After)\" },\n ...guardResponses,\n },\n});\n\nconst healthGetRoute = createRoute({\n method: \"get\",\n tags: [\"Insights\"],\n summary: \"Project health digest\",\n path: \"/api/v1/projects/{slug}/insights/health\",\n request: {\n params: z.object({ slug: z.string() }),\n query: z.object({ window: z.string().optional() }),\n },\n responses: { 200: { ...json(insightSchema), description: \"Cached digest\" }, ...guardResponses },\n});\n\nconst healthPostRoute = createRoute({\n method: \"post\",\n tags: [\"Insights\"],\n summary: \"Generate a project health digest\",\n path: \"/api/v1/projects/{slug}/insights/health\",\n request: {\n params: z.object({ slug: z.string() }),\n body: { content: { \"application/json\": { schema: healthRequestSchema } } },\n },\n responses: {\n 200: { ...json(insightSchema), description: \"Cached digest (unbilled)\" },\n 202: { ...json(insightSchema), description: \"Generation started\" },\n 429: { ...json(errorSchema), description: \"Budget or rate limit reached (see Retry-After)\" },\n ...badRequest,\n ...guardResponses,\n },\n});\n\n/** Register the build triage endpoints. */\nfunction registerBuildInsights(app: ShelfRouter): void {\n app.openapi(latestRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const { deps, project } = await projectForView(c, slug);\n const build = await buildForProject(project.id, buildId);\n const [latest] = await new InsightModel(deps.db).listForBuild(build.id, 1);\n return latest\n ? c.json(toInsightView(await freshen(deps, latest)), 200)\n : throwNotFound(\"never-generated: POST to this build's insights to create one\");\n });\n\n app.openapi(listRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const { limit } = c.req.valid(\"query\");\n const { deps, project } = await projectForView(c, slug);\n const build = await buildForProject(project.id, buildId);\n const rows = await new InsightModel(deps.db).listForBuild(build.id, limit ?? 20);\n return c.json(\n await Promise.all(rows.map(async (row) => toInsightView(await freshen(deps, row)))),\n 200,\n );\n });\n\n app.openapi(generateRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const body = c.req.valid(\"json\");\n const { deps, project } = await projectForRegenerate(c, slug);\n const build = await buildForProject(project.id, buildId);\n const outcome = await requestTriage(deps, project, build, {\n force: body.force ?? false,\n profile: isSiteAdmin(c) ? body.profile : undefined,\n });\n const view = toInsightView(outcome.row);\n return outcome.status === 200\n ? c.json(view, 200)\n : c.json(view, 202, {\n location: `/api/v1/projects/${slug}/builds/${build.id}/insights/latest`,\n });\n });\n}\n\n/** Register the project-health endpoints. */\nfunction registerHealthInsights(app: ShelfRouter): void {\n app.openapi(healthGetRoute, async (c) => {\n const { slug } = c.req.valid(\"param\");\n const { window } = c.req.valid(\"query\");\n const { deps, project } = await projectForView(c, slug);\n const row = await new InsightModel(deps.db).getHealth(project.id, window ?? \"30d\");\n return row\n ? c.json(toInsightView(await freshen(deps, row)), 200)\n : throwNotFound(\"never-generated: POST to create a health digest\");\n });\n\n app.openapi(healthPostRoute, async (c) => {\n const { slug } = c.req.valid(\"param\");\n const body = c.req.valid(\"json\");\n const { deps, project } = await projectForRegenerate(c, slug);\n const window = body.window ?? \"30d\";\n const outcome = await requestHealth(deps, project, window, { force: body.force ?? false });\n if (!outcome) {\n failWith(400, \"window must look like 30d (1-365 days)\");\n }\n const view = toInsightView(outcome.row);\n return outcome.status === 200\n ? c.json(view, 200)\n : c.json(view, 202, {\n location: `/api/v1/projects/${slug}/insights/health?window=${window}`,\n });\n });\n}\n\n/** Register the AI insights endpoints (build triage and project health). */\nexport function registerInsights(app: ShelfRouter): void {\n registerBuildInsights(app);\n registerHealthInsights(app);\n}\n"],"mappings":"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"}
@@ -0,0 +1,80 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { catalogOf, getStore } from "../store.mjs";
5
+ import { resolveAuthorizedProject } from "./helpers.mjs";
6
+ import { BuildModel, PackageUsageModel, ProjectModel, SnapshotModel, matchUpstreamStory } from "@storyshelf/core/models";
7
+ //#region src/routers/links.handlers.ts
8
+ const VIEW_ROLES = [
9
+ "viewer",
10
+ "developer",
11
+ "approver",
12
+ "admin"
13
+ ];
14
+ /**
15
+ * Upstream project resolved from its latest approved default build: its stories from the build's
16
+ * snapshots when it has them, from its Storybook catalog otherwise (a hosting-only build).
17
+ */
18
+ async function resolveUpstream(upstream) {
19
+ const build = await new BuildModel(getStore().db).latestApprovedDefault(upstream);
20
+ const stories = (build ? await new SnapshotModel(getStore().db).listByBuild(build.id) : []).flatMap((snapshot) => snapshot.storyImportPath ? [{
21
+ importPath: snapshot.storyImportPath,
22
+ storyId: snapshot.storyId
23
+ }] : []);
24
+ return {
25
+ slug: upstream.slug,
26
+ buildId: build?.id ?? null,
27
+ stories: stories.length > 0 || !build ? stories : await catalogStories(upstream, build.id)
28
+ };
29
+ }
30
+ async function catalogStories(upstream, buildId) {
31
+ return ((await catalogOf().load(upstream.id, buildId).catch(() => null))?.entries ?? []).flatMap((entry) => entry.type === "story" && entry.importPath ? [{
32
+ importPath: entry.importPath,
33
+ storyId: entry.id
34
+ }] : []);
35
+ }
36
+ /** Resolve each linked package once; upstreams the viewer cannot open are omitted. */
37
+ async function resolveLinkedUpstreams(c, project, packages) {
38
+ const links = (await new PackageUsageModel(getStore().db).listLinks(project.id)).filter((link) => packages.has(link.packageName));
39
+ const resolved = await Promise.all(links.map(async (link) => {
40
+ const upstream = await new ProjectModel(getStore().db).get(link.upstreamId);
41
+ const visible = upstream ? await resolveAuthorizedProject(c, upstream.slug, ...VIEW_ROLES).catch(() => null) : null;
42
+ return upstream && visible ? [link.packageName, await resolveUpstream(upstream)] : null;
43
+ }));
44
+ return new Map(resolved.filter((entry) => entry !== null));
45
+ }
46
+ /**
47
+ * Linked-package usage grouped by story file, resolved with one pass over the
48
+ * build's usage rows and one lookup per linked upstream (never per story).
49
+ * Omits upstreams the viewer cannot open; pass `storyImportPath` to narrow to one story.
50
+ */
51
+ async function linkedUsageByStory(c, project, buildId, storyImportPath) {
52
+ const model = new PackageUsageModel(getStore().db);
53
+ const rows = storyImportPath ? await model.listForStory(buildId, storyImportPath) : await model.listForBuild(buildId);
54
+ const upstreams = await resolveLinkedUpstreams(c, project, new Set(rows.map((row) => row.packageName)));
55
+ const byStory = /* @__PURE__ */ new Map();
56
+ for (const row of rows) {
57
+ const upstream = upstreams.get(row.packageName);
58
+ if (upstream) {
59
+ const upstreamStoryId = matchUpstreamStory(row.modulePath, upstream.stories);
60
+ byStory.set(row.storyImportPath, [...byStory.get(row.storyImportPath) ?? [], {
61
+ package: row.packageName,
62
+ version: row.version,
63
+ upstreamSlug: upstream.slug,
64
+ upstreamBuildId: upstream.buildId,
65
+ upstreamStoryId,
66
+ upstreamImportPath: upstream.stories.find((story) => story.storyId === upstreamStoryId)?.importPath ?? null
67
+ }]);
68
+ }
69
+ }
70
+ return byStory;
71
+ }
72
+ /** Linked-package usage of one story, omitting upstreams the viewer cannot open. */
73
+ async function linkedUsageFor(c, project, buildId, storyImportPath) {
74
+ if (!storyImportPath) return [];
75
+ return (await linkedUsageByStory(c, project, buildId, storyImportPath)).get(storyImportPath) ?? [];
76
+ }
77
+ //#endregion
78
+ export { linkedUsageByStory, linkedUsageFor };
79
+
80
+ //# sourceMappingURL=links.handlers.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"links.handlers.mjs","names":[],"sources":["../../src/routers/links.handlers.ts"],"sourcesContent":["import {\n BuildModel,\n matchUpstreamStory,\n PackageUsageModel,\n ProjectModel,\n SnapshotModel,\n} from \"@storyshelf/core/models\";\nimport type { Project } from \"@storyshelf/core/schema\";\nimport type { ProjectRole } from \"@storyshelf/core/types\";\nimport type { LinkedUsageItem } from \"../pages/build-diff-usage.tsx\";\nimport { catalogOf, getStore } from \"../store.ts\";\nimport { resolveAuthorizedProject } from \"./helpers.ts\";\n\nconst VIEW_ROLES: readonly ProjectRole[] = [\"viewer\", \"developer\", \"approver\", \"admin\"];\n\ntype Caller = Parameters<typeof resolveAuthorizedProject>[0];\n\n/** What a link resolves to: the upstream project's latest published build and its stories. */\ninterface ResolvedUpstream {\n slug: string;\n buildId: string | null;\n stories: { importPath: string; storyId: string }[];\n}\n\n/**\n * Upstream project resolved from its latest approved default build: its stories from the build's\n * snapshots when it has them, from its Storybook catalog otherwise (a hosting-only build).\n */\nasync function resolveUpstream(upstream: Project): Promise<ResolvedUpstream> {\n const build = await new BuildModel(getStore().db).latestApprovedDefault(upstream);\n const snapshots = build ? await new SnapshotModel(getStore().db).listByBuild(build.id) : [];\n const stories = snapshots.flatMap((snapshot) =>\n snapshot.storyImportPath\n ? [{ importPath: snapshot.storyImportPath, storyId: snapshot.storyId }]\n : [],\n );\n return {\n slug: upstream.slug,\n buildId: build?.id ?? null,\n stories: stories.length > 0 || !build ? stories : await catalogStories(upstream, build.id),\n };\n}\n\nasync function catalogStories(\n upstream: Project,\n buildId: string,\n): Promise<ResolvedUpstream[\"stories\"]> {\n const catalog = await catalogOf()\n .load(upstream.id, buildId)\n .catch(() => null);\n return (catalog?.entries ?? []).flatMap((entry) =>\n entry.type === \"story\" && entry.importPath\n ? [{ importPath: entry.importPath, storyId: entry.id }]\n : [],\n );\n}\n\n/** Resolve each linked package once; upstreams the viewer cannot open are omitted. */\nasync function resolveLinkedUpstreams(\n c: Caller,\n project: Project,\n packages: Set<string>,\n): Promise<Map<string, ResolvedUpstream>> {\n const links = (await new PackageUsageModel(getStore().db).listLinks(project.id)).filter((link) =>\n packages.has(link.packageName),\n );\n const resolved = await Promise.all(\n links.map(async (link) => {\n const upstream = await new ProjectModel(getStore().db).get(link.upstreamId);\n const visible = upstream\n ? await resolveAuthorizedProject(c, upstream.slug, ...VIEW_ROLES).catch(() => null)\n : null;\n return upstream && visible\n ? ([link.packageName, await resolveUpstream(upstream)] as const)\n : null;\n }),\n );\n return new Map(resolved.filter((entry) => entry !== null));\n}\n\n/**\n * Linked-package usage grouped by story file, resolved with one pass over the\n * build's usage rows and one lookup per linked upstream (never per story).\n * Omits upstreams the viewer cannot open; pass `storyImportPath` to narrow to one story.\n */\nexport async function linkedUsageByStory(\n c: Caller,\n project: Project,\n buildId: string,\n storyImportPath?: string,\n): Promise<Map<string, LinkedUsageItem[]>> {\n const model = new PackageUsageModel(getStore().db);\n const rows = storyImportPath\n ? await model.listForStory(buildId, storyImportPath)\n : await model.listForBuild(buildId);\n const upstreams = await resolveLinkedUpstreams(\n c,\n project,\n new Set(rows.map((row) => row.packageName)),\n );\n const byStory = new Map<string, LinkedUsageItem[]>();\n for (const row of rows) {\n const upstream = upstreams.get(row.packageName);\n if (upstream) {\n const upstreamStoryId = matchUpstreamStory(row.modulePath, upstream.stories);\n byStory.set(row.storyImportPath, [\n ...(byStory.get(row.storyImportPath) ?? []),\n {\n package: row.packageName,\n version: row.version,\n upstreamSlug: upstream.slug,\n upstreamBuildId: upstream.buildId,\n upstreamStoryId,\n upstreamImportPath:\n upstream.stories.find((story) => story.storyId === upstreamStoryId)?.importPath ?? null,\n },\n ]);\n }\n }\n return byStory;\n}\n\n/** Linked-package usage of one story, omitting upstreams the viewer cannot open. */\nexport async function linkedUsageFor(\n c: Caller,\n project: Project,\n buildId: string,\n storyImportPath: string | null,\n): Promise<LinkedUsageItem[]> {\n if (!storyImportPath) {\n return [];\n }\n return (\n (await linkedUsageByStory(c, project, buildId, storyImportPath)).get(storyImportPath) ?? []\n );\n}\n"],"mappings":";;;;;;;AAaA,MAAM,aAAqC;CAAC;CAAU;CAAa;CAAY;AAAO;;;;;AAetF,eAAe,gBAAgB,UAA8C;CAC3E,MAAM,QAAQ,MAAM,IAAI,WAAW,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,sBAAsB,QAAQ;CAEhF,MAAM,WADY,QAAQ,MAAM,IAAI,cAAc,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,YAAY,MAAM,EAAE,IAAI,CAAC,EAAA,CAChE,SAAS,aACjC,SAAS,kBACL,CAAC;EAAE,YAAY,SAAS;EAAiB,SAAS,SAAS;CAAQ,CAAC,IACpE,CAAC,CACP;CACA,OAAO;EACL,MAAM,SAAS;EACf,SAAS,OAAO,MAAM;EACtB,SAAS,QAAQ,SAAS,KAAK,CAAC,QAAQ,UAAU,MAAM,eAAe,UAAU,MAAM,EAAE;CAC3F;AACF;AAEA,eAAe,eACb,UACA,SACsC;CAItC,SAAQ,MAHc,UAAU,CAAC,CAC9B,KAAK,SAAS,IAAI,OAAO,CAAC,CAC1B,YAAY,IAAI,EAAA,EACF,WAAW,CAAC,EAAA,CAAG,SAAS,UACvC,MAAM,SAAS,WAAW,MAAM,aAC5B,CAAC;EAAE,YAAY,MAAM;EAAY,SAAS,MAAM;CAAG,CAAC,IACpD,CAAC,CACP;AACF;;AAGA,eAAe,uBACb,GACA,SACA,UACwC;CACxC,MAAM,SAAS,MAAM,IAAI,kBAAkB,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,UAAU,QAAQ,EAAE,EAAA,CAAG,QAAQ,SACvF,SAAS,IAAI,KAAK,WAAW,CAC/B;CACA,MAAM,WAAW,MAAM,QAAQ,IAC7B,MAAM,IAAI,OAAO,SAAS;EACxB,MAAM,WAAW,MAAM,IAAI,aAAa,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,KAAK,UAAU;EAC1E,MAAM,UAAU,WACZ,MAAM,yBAAyB,GAAG,SAAS,MAAM,GAAG,UAAU,CAAC,CAAC,YAAY,IAAI,IAChF;EACJ,OAAO,YAAY,UACd,CAAC,KAAK,aAAa,MAAM,gBAAgB,QAAQ,CAAC,IACnD;CACN,CAAC,CACH;CACA,OAAO,IAAI,IAAI,SAAS,QAAQ,UAAU,UAAU,IAAI,CAAC;AAC3D;;;;;;AAOA,eAAsB,mBACpB,GACA,SACA,SACA,iBACyC;CACzC,MAAM,QAAQ,IAAI,kBAAkB,SAAS,CAAC,CAAC,EAAE;CACjD,MAAM,OAAO,kBACT,MAAM,MAAM,aAAa,SAAS,eAAe,IACjD,MAAM,MAAM,aAAa,OAAO;CACpC,MAAM,YAAY,MAAM,uBACtB,GACA,SACA,IAAI,IAAI,KAAK,KAAK,QAAQ,IAAI,WAAW,CAAC,CAC5C;CACA,MAAM,0BAAU,IAAI,IAA+B;CACnD,KAAK,MAAM,OAAO,MAAM;EACtB,MAAM,WAAW,UAAU,IAAI,IAAI,WAAW;EAC9C,IAAI,UAAU;GACZ,MAAM,kBAAkB,mBAAmB,IAAI,YAAY,SAAS,OAAO;GAC3E,QAAQ,IAAI,IAAI,iBAAiB,CAC/B,GAAI,QAAQ,IAAI,IAAI,eAAe,KAAK,CAAC,GACzC;IACE,SAAS,IAAI;IACb,SAAS,IAAI;IACb,cAAc,SAAS;IACvB,iBAAiB,SAAS;IAC1B;IACA,oBACE,SAAS,QAAQ,MAAM,UAAU,MAAM,YAAY,eAAe,CAAC,EAAE,cAAc;GACvF,CACF,CAAC;EACH;CACF;CACA,OAAO;AACT;;AAGA,eAAsB,eACpB,GACA,SACA,SACA,iBAC4B;CAC5B,IAAI,CAAC,iBACH,OAAO,CAAC;CAEV,QACG,MAAM,mBAAmB,GAAG,SAAS,SAAS,eAAe,EAAA,CAAG,IAAI,eAAe,KAAK,CAAC;AAE9F"}
@@ -0,0 +1,139 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { getStore } from "../store.mjs";
5
+ import { resolveAuthorizedProject } from "./helpers.mjs";
6
+ import { badRequest, forbidden, notFound, unauthorized } from "./schemas.mjs";
7
+ import { buildForProject, snapshotForBuild } from "./builds.handlers.mjs";
8
+ import { linkedUsageFor } from "./links.handlers.mjs";
9
+ import { createRoute, z } from "@hono/zod-openapi";
10
+ import { PackageUsageModel, ProjectModel } from "@storyshelf/core/models";
11
+ import { HTTPException } from "hono/http-exception";
12
+ //#region src/routers/links.ts
13
+ const VIEW_ROLES = [
14
+ "viewer",
15
+ "developer",
16
+ "approver",
17
+ "admin"
18
+ ];
19
+ const ADMIN_ROLES = ["admin"];
20
+ const linkSchema = z.object({
21
+ package: z.string().min(1).max(214),
22
+ upstreamSlug: z.string().min(1)
23
+ });
24
+ const linksBodySchema = z.object({ links: z.array(linkSchema).max(200) });
25
+ const usageItemSchema = z.object({
26
+ package: z.string(),
27
+ version: z.string().nullable(),
28
+ upstreamSlug: z.string(),
29
+ /** Latest approved default build of the upstream project; null when it has none. */
30
+ upstreamBuildId: z.string().nullable(),
31
+ /** Upstream story the package module maps to; null falls back to the library. */
32
+ upstreamStoryId: z.string().nullable(),
33
+ /** Story file of the matched upstream story; null when nothing matched. */
34
+ upstreamImportPath: z.string().nullable().optional()
35
+ });
36
+ const projectParams = z.object({ slug: z.string() });
37
+ const listLinksRoute = createRoute({
38
+ method: "get",
39
+ tags: ["Links"],
40
+ summary: "List the packages this project consumes from other projects",
41
+ path: "/api/v1/projects/{slug}/links",
42
+ request: { params: projectParams },
43
+ responses: {
44
+ 200: {
45
+ content: { "application/json": { schema: linksBodySchema } },
46
+ description: "Package links of the project"
47
+ },
48
+ ...notFound,
49
+ ...unauthorized
50
+ }
51
+ });
52
+ const putLinksRoute = createRoute({
53
+ method: "put",
54
+ tags: ["Links"],
55
+ summary: "Replace the packages this project consumes from other projects",
56
+ path: "/api/v1/projects/{slug}/links",
57
+ request: {
58
+ params: projectParams,
59
+ body: { content: { "application/json": { schema: linksBodySchema } } }
60
+ },
61
+ responses: {
62
+ 200: {
63
+ content: { "application/json": { schema: linksBodySchema } },
64
+ description: "Links replaced"
65
+ },
66
+ ...badRequest,
67
+ ...forbidden,
68
+ ...notFound
69
+ }
70
+ });
71
+ const usageRoute = createRoute({
72
+ method: "get",
73
+ tags: ["Links"],
74
+ summary: "Linked packages a snapshot's story uses",
75
+ path: "/api/v1/projects/{slug}/builds/{buildId}/snapshots/{snapshotId}/usage",
76
+ request: { params: z.object({
77
+ slug: z.string(),
78
+ buildId: z.string(),
79
+ snapshotId: z.string()
80
+ }) },
81
+ responses: {
82
+ 200: {
83
+ content: { "application/json": { schema: usageItemSchema.array() } },
84
+ description: "Usage of linked packages; empty when the build reported none"
85
+ },
86
+ ...notFound,
87
+ ...unauthorized
88
+ }
89
+ });
90
+ /** Resolve link slugs to projects, rejecting unknown, self, duplicate, or unviewable ones. */
91
+ async function resolveLinkTargets(c, project, links) {
92
+ if (new Set(links.map((link) => link.package)).size !== links.length) throw new HTTPException(400, { message: "Each package can be linked once" });
93
+ return await Promise.all(links.map(async (link) => {
94
+ if (link.upstreamSlug === project.slug) throw new HTTPException(400, { message: "A project cannot link to itself" });
95
+ return {
96
+ upstreamId: (await resolveAuthorizedProject(c, link.upstreamSlug, ...VIEW_ROLES).catch(() => {
97
+ throw new HTTPException(400, { message: `Unknown project: ${link.upstreamSlug}` });
98
+ })).id,
99
+ packageName: link.package
100
+ };
101
+ }));
102
+ }
103
+ /** Links of a project as API shape, dropping upstreams that no longer exist. */
104
+ async function linkViews(project) {
105
+ const db = getStore().db;
106
+ const links = await new PackageUsageModel(db).listLinks(project.id);
107
+ return (await Promise.all(links.map(async (link) => {
108
+ const upstream = await new ProjectModel(db).get(link.upstreamId);
109
+ return upstream ? {
110
+ package: link.packageName,
111
+ upstreamSlug: upstream.slug
112
+ } : null;
113
+ }))).filter((view) => view !== null);
114
+ }
115
+ /** Register the package-link and snapshot-usage endpoints. */
116
+ function registerLinks(app) {
117
+ app.openapi(listLinksRoute, async (c) => {
118
+ const project = await resolveAuthorizedProject(c, c.req.valid("param").slug, ...VIEW_ROLES);
119
+ return c.json({ links: await linkViews(project) });
120
+ });
121
+ app.openapi(putLinksRoute, async (c) => {
122
+ const project = await resolveAuthorizedProject(c, c.req.valid("param").slug, ...ADMIN_ROLES);
123
+ const { links } = c.req.valid("json");
124
+ const targets = await resolveLinkTargets(c, project, links);
125
+ await new PackageUsageModel(getStore().db).replaceLinks(project.id, targets);
126
+ return c.json({ links: await linkViews(project) });
127
+ });
128
+ app.openapi(usageRoute, async (c) => {
129
+ const { slug, buildId, snapshotId } = c.req.valid("param");
130
+ const project = await resolveAuthorizedProject(c, slug, ...VIEW_ROLES);
131
+ const build = await buildForProject(project.id, buildId);
132
+ const snapshot = await snapshotForBuild(build, snapshotId);
133
+ return c.json(await linkedUsageFor(c, project, build.id, snapshot.storyImportPath));
134
+ });
135
+ }
136
+ //#endregion
137
+ export { registerLinks };
138
+
139
+ //# sourceMappingURL=links.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"links.mjs","names":[],"sources":["../../src/routers/links.ts"],"sourcesContent":["import { createRoute, z } from \"@hono/zod-openapi\";\nimport { PackageUsageModel, ProjectModel } from \"@storyshelf/core/models\";\nimport type { Project } from \"@storyshelf/core/schema\";\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 { buildForProject, snapshotForBuild } from \"./builds.handlers.ts\";\nimport { resolveAuthorizedProject } from \"./helpers.ts\";\nimport { linkedUsageFor } from \"./links.handlers.ts\";\nimport { badRequest, forbidden, notFound, unauthorized } from \"./schemas.ts\";\n\nconst VIEW_ROLES: readonly ProjectRole[] = [\"viewer\", \"developer\", \"approver\", \"admin\"];\nconst ADMIN_ROLES: readonly ProjectRole[] = [\"admin\"];\n\nconst linkSchema = z.object({\n package: z.string().min(1).max(214),\n upstreamSlug: z.string().min(1),\n});\nconst linksBodySchema = z.object({ links: z.array(linkSchema).max(200) });\nconst usageItemSchema = z.object({\n package: z.string(),\n version: z.string().nullable(),\n upstreamSlug: z.string(),\n /** Latest approved default build of the upstream project; null when it has none. */\n upstreamBuildId: z.string().nullable(),\n /** Upstream story the package module maps to; null falls back to the library. */\n upstreamStoryId: z.string().nullable(),\n /** Story file of the matched upstream story; null when nothing matched. */\n upstreamImportPath: z.string().nullable().optional(),\n});\n\nconst projectParams = z.object({ slug: z.string() });\n\nconst listLinksRoute = createRoute({\n method: \"get\",\n tags: [\"Links\"],\n summary: \"List the packages this project consumes from other projects\",\n path: \"/api/v1/projects/{slug}/links\",\n request: { params: projectParams },\n responses: {\n 200: {\n content: { \"application/json\": { schema: linksBodySchema } },\n description: \"Package links of the project\",\n },\n ...notFound,\n ...unauthorized,\n },\n});\n\nconst putLinksRoute = createRoute({\n method: \"put\",\n tags: [\"Links\"],\n summary: \"Replace the packages this project consumes from other projects\",\n path: \"/api/v1/projects/{slug}/links\",\n request: {\n params: projectParams,\n body: { content: { \"application/json\": { schema: linksBodySchema } } },\n },\n responses: {\n 200: {\n content: { \"application/json\": { schema: linksBodySchema } },\n description: \"Links replaced\",\n },\n ...badRequest,\n ...forbidden,\n ...notFound,\n },\n});\n\nconst usageRoute = createRoute({\n method: \"get\",\n tags: [\"Links\"],\n summary: \"Linked packages a snapshot's story uses\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/snapshots/{snapshotId}/usage\",\n request: {\n params: z.object({ slug: z.string(), buildId: z.string(), snapshotId: z.string() }),\n },\n responses: {\n 200: {\n content: { \"application/json\": { schema: usageItemSchema.array() } },\n description: \"Usage of linked packages; empty when the build reported none\",\n },\n ...notFound,\n ...unauthorized,\n },\n});\n\n/** Resolve link slugs to projects, rejecting unknown, self, duplicate, or unviewable ones. */\nasync function resolveLinkTargets(\n c: Parameters<typeof resolveAuthorizedProject>[0],\n project: Project,\n links: z.infer<typeof linkSchema>[],\n): Promise<{ upstreamId: string; packageName: string }[]> {\n if (new Set(links.map((link) => link.package)).size !== links.length) {\n throw new HTTPException(400, { message: \"Each package can be linked once\" });\n }\n return await Promise.all(\n links.map(async (link) => {\n if (link.upstreamSlug === project.slug) {\n throw new HTTPException(400, { message: \"A project cannot link to itself\" });\n }\n const upstream = await resolveAuthorizedProject(c, link.upstreamSlug, ...VIEW_ROLES).catch(\n () => {\n throw new HTTPException(400, { message: `Unknown project: ${link.upstreamSlug}` });\n },\n );\n return { upstreamId: upstream.id, packageName: link.package };\n }),\n );\n}\n\n/** Links of a project as API shape, dropping upstreams that no longer exist. */\nasync function linkViews(project: Project): Promise<z.infer<typeof linkSchema>[]> {\n const db = getStore().db;\n const links = await new PackageUsageModel(db).listLinks(project.id);\n const views = await Promise.all(\n links.map(async (link) => {\n const upstream = await new ProjectModel(db).get(link.upstreamId);\n return upstream ? { package: link.packageName, upstreamSlug: upstream.slug } : null;\n }),\n );\n return views.filter((view): view is z.infer<typeof linkSchema> => view !== null);\n}\n\n/** Register the package-link and snapshot-usage endpoints. */\nexport function registerLinks(app: ShelfRouter): void {\n app.openapi(listLinksRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...VIEW_ROLES);\n return c.json({ links: await linkViews(project) });\n });\n\n app.openapi(putLinksRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...ADMIN_ROLES);\n const { links } = c.req.valid(\"json\");\n const targets = await resolveLinkTargets(c, project, links);\n await new PackageUsageModel(getStore().db).replaceLinks(project.id, targets);\n return c.json({ links: await linkViews(project) });\n });\n\n app.openapi(usageRoute, async (c) => {\n const { slug, buildId, snapshotId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...VIEW_ROLES);\n const build = await buildForProject(project.id, buildId);\n const snapshot = await snapshotForBuild(build, snapshotId);\n return c.json(await linkedUsageFor(c, project, build.id, snapshot.storyImportPath));\n });\n}\n"],"mappings":"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"}
@@ -1,9 +1,10 @@
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
+ import { BaselineModel, BuildModel, DesignCredentialModel, DesignNodeModel, SnapshotModel } from "@storyshelf/core/models";
7
8
  //#region src/routers/media.ts
8
9
  const VIEW_ROLES = [
9
10
  "viewer",
@@ -12,12 +13,14 @@ const VIEW_ROLES = [
12
13
  "admin"
13
14
  ];
14
15
  const PNG = "image/png";
15
- function imageResponse(buffer) {
16
+ function imageResponse(buffer, cacheControl = "private, max-age=3600") {
16
17
  return new Response(new Uint8Array(buffer), { headers: {
17
18
  "content-type": PNG,
18
- "cache-control": "private, max-age=3600"
19
+ "cache-control": cacheControl
19
20
  } });
20
21
  }
22
+ /** Design renders are addressed by file version (`?v=`): immutable once the version matches the row. */
23
+ const RENDER_IMMUTABLE = "private, max-age=31536000, immutable";
21
24
  /** Register the PNG image, diff, and baseline media endpoints for snapshots. */
22
25
  function registerMedia(app) {
23
26
  app.get("/api/v1/projects/:slug/builds/:buildId/snapshots/:snapshotId/image", async (c) => {
@@ -31,13 +34,24 @@ function registerMedia(app) {
31
34
  if (!snapshot.diffPath) notFound("No diff for this snapshot");
32
35
  return imageResponse(await getStore().storage.read(snapshot.diffPath));
33
36
  });
37
+ app.get("/api/v1/projects/:slug/design/renders/:renderId", async (c) => {
38
+ const project = await resolveAuthorizedProject(c, c.req.param("slug"), ...VIEW_ROLES);
39
+ const { db, config, storage, design } = getStore();
40
+ const credentials = new DesignCredentialModel(db, credentialKeys(config));
41
+ const nodes = new DesignNodeModel(db, credentials, design, { storage });
42
+ const found = await nodes.get(project.id, c.req.param("renderId"));
43
+ const node = found && (found.viewerId === "" || found.viewerId === getStore().user?.id) ? found : null;
44
+ const png = node ? await nodes.readRender(node) : null;
45
+ if (!png) notFound("No design render");
46
+ return imageResponse(png, node?.renderVersion && c.req.query("v") === node.renderVersion ? RENDER_IMMUTABLE : void 0);
47
+ });
34
48
  app.get("/api/v1/projects/:slug/builds/:buildId/snapshots/:snapshotId/baseline", async (c) => {
35
49
  const project = await resolveAuthorizedProject(c, c.req.param("slug"), ...VIEW_ROLES);
36
50
  const snapshot = await findSnapshot(c.req.param("buildId"), c.req.param("snapshotId"), project.id);
37
51
  const build = await new BuildModel(getStore().db).get(snapshot.buildId);
38
52
  if (!build) notFound("Build not found");
39
53
  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);
54
+ const baseline = await baselines.resolve(project.id, snapshot.storyId, snapshot.viewportName, build.gitBranch, project.gitDefaultBranch, snapshot.browser);
41
55
  if (!baseline) notFound("No baseline for this snapshot");
42
56
  return imageResponse(await baselines.read(baseline));
43
57
  });
@@ -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 { BaselineModel, DesignCredentialModel, DesignNodeModel } 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 { 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 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 snapshot.browser,\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":"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"}
@@ -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) => {
@@ -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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"}