@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 +1 @@
1
- {"version":3,"file":"settings-labels.mjs","names":[],"sources":["../../src/routers/settings-labels.ts"],"sourcesContent":["import { LabelModel } from \"@storyshelf/core/models\";\nimport type { Context } from \"hono\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { getStore } from \"../store.ts\";\nimport { flash } from \"./flash.ts\";\nimport { hxRedirect } from \"./htmx.ts\";\nimport { asString, findProject, renderSettingsPage } from \"./settings.handlers.ts\";\n/** Label-type settings (create custom types, delete removable ones). */\nexport function registerLabelSettings(app: ShelfRouter): void {\n app.get(\"/projects/:slug/settings/labels\", async (c) =>\n c.html(await renderSettingsPage(c, \"labels\")),\n );\n app.post(\"/projects/:slug/settings/labels\", handleCreateLabelType);\n app.post(\"/projects/:slug/settings/labels/:key/delete\", handleDeleteLabelType);\n}\n\nasync function handleCreateLabelType(c: Context): Promise<Response> {\n const project = await findProject(c.req.param(\"slug\") ?? \"\");\n const form = await c.req.formData();\n const key = asString(form.get(\"key\"));\n const labelName = asString(form.get(\"labelName\")) ?? asString(form.get(\"name\"));\n const linkTemplate = asString(form.get(\"linkTemplate\"));\n if (!key || !labelName) {\n return c.html(\n (await renderSettingsPage(c, \"labels\", { globalError: \"Key and name are required\" })) ?? \"\",\n 400,\n );\n }\n return await persistLabelType(c, project.id, project.slug, key, labelName, linkTemplate);\n}\n\nasync function persistLabelType(\n c: Context,\n projectId: string,\n projectSlug: string,\n key: string,\n labelName: string,\n linkTemplate: string | undefined,\n): Promise<Response> {\n try {\n await new LabelModel(getStore().db).createType(projectId, {\n key,\n name: labelName,\n linkTemplate,\n });\n flash(c, \"Label type added\");\n return hxRedirect(c, `/projects/${projectSlug}/settings/labels`);\n } catch (error) {\n const message = error instanceof Error ? error.message : \"Failed to create label\";\n return c.html((await renderSettingsPage(c, \"labels\", { globalError: message })) ?? \"\", 400);\n }\n}\n\nasync function handleDeleteLabelType(c: Context): Promise<Response> {\n const project = await findProject(c.req.param(\"slug\") ?? \"\");\n try {\n await new LabelModel(getStore().db).removeType(project.id, c.req.param(\"key\") ?? \"\");\n } catch {\n return c.html(\n (await renderSettingsPage(c, \"labels\", { globalError: \"Cannot delete built-in label\" })) ??\n \"\",\n 400,\n );\n }\n flash(c, \"Label type deleted\");\n return hxRedirect(c, `/projects/${project.slug}/settings/labels`);\n}\n"],"mappings":";;;;;;;;;;AAQA,SAAgB,sBAAsB,KAAwB;CAC5D,IAAI,IAAI,mCAAmC,OAAO,MAChD,EAAE,KAAK,MAAM,mBAAmB,GAAG,QAAQ,CAAC,CAC9C;CACA,IAAI,KAAK,mCAAmC,qBAAqB;CACjE,IAAI,KAAK,+CAA+C,qBAAqB;AAC/E;AAEA,eAAe,sBAAsB,GAA+B;CAClE,MAAM,UAAU,MAAM,YAAY,EAAE,IAAI,MAAM,MAAM,KAAK,EAAE;CAC3D,MAAM,OAAO,MAAM,EAAE,IAAI,SAAS;CAClC,MAAM,MAAM,SAAS,KAAK,IAAI,KAAK,CAAC;CACpC,MAAM,YAAY,SAAS,KAAK,IAAI,WAAW,CAAC,KAAK,SAAS,KAAK,IAAI,MAAM,CAAC;CAC9E,MAAM,eAAe,SAAS,KAAK,IAAI,cAAc,CAAC;CACtD,IAAI,CAAC,OAAO,CAAC,WACX,OAAO,EAAE,KACN,MAAM,mBAAmB,GAAG,UAAU,EAAE,aAAa,4BAA4B,CAAC,KAAM,IACzF,GACF;CAEF,OAAO,MAAM,iBAAiB,GAAG,QAAQ,IAAI,QAAQ,MAAM,KAAK,WAAW,YAAY;AACzF;AAEA,eAAe,iBACb,GACA,WACA,aACA,KACA,WACA,cACmB;CACnB,IAAI;EACF,MAAM,IAAI,WAAW,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,WAAW,WAAW;GACxD;GACA,MAAM;GACN;EACF,CAAC;EACD,MAAM,GAAG,kBAAkB;EAC3B,OAAO,WAAW,GAAG,aAAa,YAAY,iBAAiB;CACjE,SAAS,OAAO;EACd,MAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU;EACzD,OAAO,EAAE,KAAM,MAAM,mBAAmB,GAAG,UAAU,EAAE,aAAa,QAAQ,CAAC,KAAM,IAAI,GAAG;CAC5F;AACF;AAEA,eAAe,sBAAsB,GAA+B;CAClE,MAAM,UAAU,MAAM,YAAY,EAAE,IAAI,MAAM,MAAM,KAAK,EAAE;CAC3D,IAAI;EACF,MAAM,IAAI,WAAW,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,WAAW,QAAQ,IAAI,EAAE,IAAI,MAAM,KAAK,KAAK,EAAE;CACrF,QAAQ;EACN,OAAO,EAAE,KACN,MAAM,mBAAmB,GAAG,UAAU,EAAE,aAAa,+BAA+B,CAAC,KACpF,IACF,GACF;CACF;CACA,MAAM,GAAG,oBAAoB;CAC7B,OAAO,WAAW,GAAG,aAAa,QAAQ,KAAK,iBAAiB;AAClE"}
1
+ {"version":3,"file":"settings-labels.mjs","names":[],"sources":["../../src/routers/settings-labels.ts"],"sourcesContent":["import { LabelModel } from \"@storyshelf/core/models\";\nimport type { Context } from \"hono\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { getStore } from \"../store.ts\";\nimport { flash } from \"./flash.ts\";\nimport { hxRedirect } from \"./htmx.ts\";\nimport { asString, findProject, renderSettingsPage } from \"./settings.handlers.ts\";\n/** Label-type settings (create custom types, delete removable ones). */\nexport function registerLabelSettings(app: ShelfRouter): void {\n app.get(\"/projects/:slug/settings/labels\", async (c) =>\n c.html(await renderSettingsPage(c, \"labels\", { editKey: c.req.query(\"edit\") })),\n );\n app.post(\"/projects/:slug/settings/labels\", handleCreateLabelType);\n app.post(\"/projects/:slug/settings/labels/:key\", handleUpdateLabelType);\n app.post(\"/projects/:slug/settings/labels/:key/delete\", handleDeleteLabelType);\n}\n\nasync function handleCreateLabelType(c: Context): Promise<Response> {\n const project = await findProject(c.req.param(\"slug\") ?? \"\");\n const form = await c.req.formData();\n const key = asString(form.get(\"key\"));\n const labelName = asString(form.get(\"labelName\")) ?? asString(form.get(\"name\"));\n const linkTemplate = asString(form.get(\"linkTemplate\"));\n if (!key || !labelName) {\n return c.html(\n (await renderSettingsPage(c, \"labels\", { globalError: \"Key and name are required\" })) ?? \"\",\n 400,\n );\n }\n return await persistLabelType(c, project.id, project.slug, key, labelName, linkTemplate);\n}\n\nasync function persistLabelType(\n c: Context,\n projectId: string,\n projectSlug: string,\n key: string,\n labelName: string,\n linkTemplate: string | undefined,\n): Promise<Response> {\n try {\n await new LabelModel(getStore().db).createType(projectId, {\n key,\n name: labelName,\n linkTemplate,\n });\n flash(c, \"Label type added\");\n return hxRedirect(c, `/projects/${projectSlug}/settings/labels`);\n } catch (error) {\n const message = error instanceof Error ? error.message : \"Failed to create label\";\n return c.html((await renderSettingsPage(c, \"labels\", { globalError: message })) ?? \"\", 400);\n }\n}\n\nasync function handleUpdateLabelType(c: Context): Promise<Response> {\n const project = await findProject(c.req.param(\"slug\") ?? \"\");\n const key = c.req.param(\"key\") ?? \"\";\n const form = await c.req.formData();\n const name = asString(form.get(\"labelName\"));\n if (!name) {\n return c.html(\n (await renderSettingsPage(c, \"labels\", { globalError: \"Name is required\", editKey: key })) ??\n \"\",\n 400,\n );\n }\n const template = asString(form.get(\"linkTemplate\"));\n const linkTemplate = template === undefined || template === \"\" ? null : template;\n return await persistLabelUpdate(c, project, key, { name, linkTemplate });\n}\n\nasync function persistLabelUpdate(\n c: Context,\n project: { id: string; slug: string },\n key: string,\n input: { name: string; linkTemplate: string | null },\n): Promise<Response> {\n try {\n const updated = await new LabelModel(getStore().db).updateType(project.id, key, input);\n if (!updated) {\n throw new Error(\"Label type not found\");\n }\n } catch (error) {\n const message = error instanceof Error ? error.message : \"Failed to update label\";\n return c.html((await renderSettingsPage(c, \"labels\", { globalError: message })) ?? \"\", 400);\n }\n flash(c, \"Label type saved\");\n return hxRedirect(c, `/projects/${project.slug}/settings/labels`);\n}\n\nasync function handleDeleteLabelType(c: Context): Promise<Response> {\n const project = await findProject(c.req.param(\"slug\") ?? \"\");\n try {\n await new LabelModel(getStore().db).removeType(project.id, c.req.param(\"key\") ?? \"\");\n } catch {\n return c.html(\n (await renderSettingsPage(c, \"labels\", { globalError: \"Cannot delete built-in label\" })) ??\n \"\",\n 400,\n );\n }\n flash(c, \"Label type deleted\");\n return hxRedirect(c, `/projects/${project.slug}/settings/labels`);\n}\n"],"mappings":"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"}
@@ -0,0 +1,62 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { getStore } from "../store.mjs";
5
+ import { hxRedirect } from "./htmx.mjs";
6
+ import { flash } from "./flash.mjs";
7
+ import { asString, findProject, renderSettingsPage } from "./settings.handlers.mjs";
8
+ import { PackageUsageModel, ProjectModel } from "@storyshelf/core/models";
9
+ // src/routers/settings-links.ts
10
+ /** Package-link settings (link a consumed package to its upstream project, or unlink it). */
11
+ function registerLinkSettings(app) {
12
+ app.get("/projects/:slug/settings/links", async (c) => c.html(await renderSettingsPage(c, "links")));
13
+ app.post("/projects/:slug/settings/links", handleAddLink);
14
+ app.post("/projects/:slug/settings/links/delete", handleRemoveLink);
15
+ }
16
+ async function rejectLink(c, message) {
17
+ return c.html(await renderSettingsPage(c, "links", { globalError: message }) ?? "", 400);
18
+ }
19
+ /** Replace one package's link, keeping the project's other links. */
20
+ async function saveLink(project, packageName, upstreamId) {
21
+ const model = new PackageUsageModel(getStore().db);
22
+ const kept = (await model.listLinks(project.id)).filter((link) => link.packageName !== packageName);
23
+ await model.replaceLinks(project.id, [...kept.map((link) => ({
24
+ upstreamId: link.upstreamId,
25
+ packageName: link.packageName
26
+ })), {
27
+ upstreamId,
28
+ packageName
29
+ }]);
30
+ }
31
+ /** Read the add-link form fields. */
32
+ async function readLinkForm(c) {
33
+ const form = await c.req.formData();
34
+ return {
35
+ packageName: asString(form.get("package")),
36
+ upstreamSlug: asString(form.get("upstreamSlug"))
37
+ };
38
+ }
39
+ async function handleAddLink(c) {
40
+ const project = await findProject(c.req.param("slug") ?? "");
41
+ const { packageName, upstreamSlug } = await readLinkForm(c);
42
+ const upstream = upstreamSlug ? await new ProjectModel(getStore().db).getBySlug(upstreamSlug) : null;
43
+ if (!packageName || !upstream || upstream.id === project.id) return await rejectLink(c, "Enter a package and choose a different, existing upstream project");
44
+ await saveLink(project, packageName, upstream.id);
45
+ flash(c, "Package linked");
46
+ return hxRedirect(c, `/projects/${project.slug}/settings/links`);
47
+ }
48
+ async function handleRemoveLink(c) {
49
+ const project = await findProject(c.req.param("slug") ?? "");
50
+ const packageName = asString((await c.req.formData()).get("package"));
51
+ const model = new PackageUsageModel(getStore().db);
52
+ const kept = (await model.listLinks(project.id)).filter((link) => link.packageName !== packageName);
53
+ await model.replaceLinks(project.id, kept.map((link) => ({
54
+ upstreamId: link.upstreamId,
55
+ packageName: link.packageName
56
+ })));
57
+ flash(c, "Link removed");
58
+ return hxRedirect(c, `/projects/${project.slug}/settings/links`);
59
+ }
60
+ export { registerLinkSettings };
61
+
62
+ //# sourceMappingURL=settings-links.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settings-links.mjs","names":[],"sources":["../../src/routers/settings-links.ts"],"sourcesContent":["import { PackageUsageModel, ProjectModel } from \"@storyshelf/core/models\";\nimport type { Context } from \"hono\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { getStore } from \"../store.ts\";\nimport { flash } from \"./flash.ts\";\nimport { hxRedirect } from \"./htmx.ts\";\nimport { asString, findProject, renderSettingsPage } from \"./settings.handlers.ts\";\n\n/** Package-link settings (link a consumed package to its upstream project, or unlink it). */\nexport function registerLinkSettings(app: ShelfRouter): void {\n app.get(\"/projects/:slug/settings/links\", async (c) =>\n c.html(await renderSettingsPage(c, \"links\")),\n );\n app.post(\"/projects/:slug/settings/links\", handleAddLink);\n app.post(\"/projects/:slug/settings/links/delete\", handleRemoveLink);\n}\n\nasync function rejectLink(c: Context, message: string): Promise<Response> {\n return c.html((await renderSettingsPage(c, \"links\", { globalError: message })) ?? \"\", 400);\n}\n\n/** Replace one package's link, keeping the project's other links. */\nasync function saveLink(\n project: { id: string },\n packageName: string,\n upstreamId: string,\n): Promise<void> {\n const model = new PackageUsageModel(getStore().db);\n const kept = (await model.listLinks(project.id)).filter(\n (link) => link.packageName !== packageName,\n );\n await model.replaceLinks(project.id, [\n ...kept.map((link) => ({ upstreamId: link.upstreamId, packageName: link.packageName })),\n { upstreamId, packageName },\n ]);\n}\n\n/** Read the add-link form fields. */\nasync function readLinkForm(c: Context): Promise<{ packageName?: string; upstreamSlug?: string }> {\n const form = await c.req.formData();\n return {\n packageName: asString(form.get(\"package\")),\n upstreamSlug: asString(form.get(\"upstreamSlug\")),\n };\n}\n\nasync function handleAddLink(c: Context): Promise<Response> {\n const project = await findProject(c.req.param(\"slug\") ?? \"\");\n const { packageName, upstreamSlug } = await readLinkForm(c);\n const upstream = upstreamSlug\n ? await new ProjectModel(getStore().db).getBySlug(upstreamSlug)\n : null;\n if (!packageName || !upstream || upstream.id === project.id) {\n return await rejectLink(c, \"Enter a package and choose a different, existing upstream project\");\n }\n await saveLink(project, packageName, upstream.id);\n flash(c, \"Package linked\");\n return hxRedirect(c, `/projects/${project.slug}/settings/links`);\n}\n\nasync function handleRemoveLink(c: Context): Promise<Response> {\n const project = await findProject(c.req.param(\"slug\") ?? \"\");\n const packageName = asString((await c.req.formData()).get(\"package\"));\n const model = new PackageUsageModel(getStore().db);\n const kept = (await model.listLinks(project.id)).filter(\n (link) => link.packageName !== packageName,\n );\n await model.replaceLinks(\n project.id,\n kept.map((link) => ({ upstreamId: link.upstreamId, packageName: link.packageName })),\n );\n flash(c, \"Link removed\");\n return hxRedirect(c, `/projects/${project.slug}/settings/links`);\n}\n"],"mappings":";;;;;;;;;;AASA,SAAgB,qBAAqB,KAAwB;CAC3D,IAAI,IAAI,kCAAkC,OAAO,MAC/C,EAAE,KAAK,MAAM,mBAAmB,GAAG,OAAO,CAAC,CAC7C;CACA,IAAI,KAAK,kCAAkC,aAAa;CACxD,IAAI,KAAK,yCAAyC,gBAAgB;AACpE;AAEA,eAAe,WAAW,GAAY,SAAoC;CACxE,OAAO,EAAE,KAAM,MAAM,mBAAmB,GAAG,SAAS,EAAE,aAAa,QAAQ,CAAC,KAAM,IAAI,GAAG;AAC3F;;AAGA,eAAe,SACb,SACA,aACA,YACe;CACf,MAAM,QAAQ,IAAI,kBAAkB,SAAS,CAAC,CAAC,EAAE;CACjD,MAAM,QAAQ,MAAM,MAAM,UAAU,QAAQ,EAAE,EAAA,CAAG,QAC9C,SAAS,KAAK,gBAAgB,WACjC;CACA,MAAM,MAAM,aAAa,QAAQ,IAAI,CACnC,GAAG,KAAK,KAAK,UAAU;EAAE,YAAY,KAAK;EAAY,aAAa,KAAK;CAAY,EAAE,GACtF;EAAE;EAAY;CAAY,CAC5B,CAAC;AACH;;AAGA,eAAe,aAAa,GAAsE;CAChG,MAAM,OAAO,MAAM,EAAE,IAAI,SAAS;CAClC,OAAO;EACL,aAAa,SAAS,KAAK,IAAI,SAAS,CAAC;EACzC,cAAc,SAAS,KAAK,IAAI,cAAc,CAAC;CACjD;AACF;AAEA,eAAe,cAAc,GAA+B;CAC1D,MAAM,UAAU,MAAM,YAAY,EAAE,IAAI,MAAM,MAAM,KAAK,EAAE;CAC3D,MAAM,EAAE,aAAa,iBAAiB,MAAM,aAAa,CAAC;CAC1D,MAAM,WAAW,eACb,MAAM,IAAI,aAAa,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,UAAU,YAAY,IAC5D;CACJ,IAAI,CAAC,eAAe,CAAC,YAAY,SAAS,OAAO,QAAQ,IACvD,OAAO,MAAM,WAAW,GAAG,mEAAmE;CAEhG,MAAM,SAAS,SAAS,aAAa,SAAS,EAAE;CAChD,MAAM,GAAG,gBAAgB;CACzB,OAAO,WAAW,GAAG,aAAa,QAAQ,KAAK,gBAAgB;AACjE;AAEA,eAAe,iBAAiB,GAA+B;CAC7D,MAAM,UAAU,MAAM,YAAY,EAAE,IAAI,MAAM,MAAM,KAAK,EAAE;CAC3D,MAAM,cAAc,UAAU,MAAM,EAAE,IAAI,SAAS,EAAA,CAAG,IAAI,SAAS,CAAC;CACpE,MAAM,QAAQ,IAAI,kBAAkB,SAAS,CAAC,CAAC,EAAE;CACjD,MAAM,QAAQ,MAAM,MAAM,UAAU,QAAQ,EAAE,EAAA,CAAG,QAC9C,SAAS,KAAK,gBAAgB,WACjC;CACA,MAAM,MAAM,aACV,QAAQ,IACR,KAAK,KAAK,UAAU;EAAE,YAAY,KAAK;EAAY,aAAa,KAAK;CAAY,EAAE,CACrF;CACA,MAAM,GAAG,cAAc;CACvB,OAAO,WAAW,GAAG,aAAa,QAAQ,KAAK,gBAAgB;AACjE"}
@@ -6,7 +6,8 @@ import { hxRedirect } from "./htmx.mjs";
6
6
  import { flash } from "./flash.mjs";
7
7
  import { asString, findProject, renderSettingsPage } from "./settings.handlers.mjs";
8
8
  import { MemberModel, ProjectGroupMappingModel } from "@storyshelf/core/models";
9
- //#region src/routers/settings-members.ts
9
+ import { isProjectRole } from "@storyshelf/core/types";
10
+ // src/routers/settings-members.ts
10
11
  /** Project member settings (assign roles, remove members). */
11
12
  function registerMemberSettings(app) {
12
13
  app.get("/projects/:slug/settings/members", async (c) => c.html(await renderSettingsPage(c, "members")));
@@ -15,7 +16,7 @@ function registerMemberSettings(app) {
15
16
  const form = await c.req.formData();
16
17
  const userId = asString(form.get("userId"));
17
18
  const role = asString(form.get("role"));
18
- if (!userId || !role) return c.html(await renderSettingsPage(c, "members", { globalError: "User and role are required" }) ?? "", 400);
19
+ if (!userId || !isProjectRole(role)) return c.html(await renderSettingsPage(c, "members", { globalError: "User and role are required" }) ?? "", 400);
19
20
  await new MemberModel(getStore().db).set(project.id, userId, role);
20
21
  flash(c, "Member saved");
21
22
  return hxRedirect(c, `/projects/${project.slug}/settings/members`);
@@ -35,12 +36,8 @@ function registerGroupMappingSettings(app) {
35
36
  const form = await c.req.formData();
36
37
  const groupName = asString(form.get("groupName"));
37
38
  const role = asString(form.get("role"));
38
- if (!groupName || !role || ![
39
- "viewer",
40
- "developer",
41
- "approver",
42
- "admin"
43
- ].includes(role)) return c.html(await renderSettingsPage(c, "members", { globalError: "Group and role are required" }) ?? "", 400);
39
+ const inputError = ProjectGroupMappingModel.inputError(groupName, role);
40
+ if (inputError || !groupName || !isProjectRole(role)) return c.html(await renderSettingsPage(c, "members", { globalError: inputError ?? "Invalid input" }) ?? "", 400);
44
41
  await new ProjectGroupMappingModel(getStore().db).create(project.id, groupName, role);
45
42
  flash(c, "Group mapping added");
46
43
  return hxRedirect(c, `/projects/${project.slug}/settings/members`);
@@ -52,7 +49,6 @@ function registerGroupMappingSettings(app) {
52
49
  return hxRedirect(c, `/projects/${project.slug}/settings/members`);
53
50
  });
54
51
  }
55
- //#endregion
56
52
  export { registerMemberSettings };
57
53
 
58
54
  //# sourceMappingURL=settings-members.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"settings-members.mjs","names":[],"sources":["../../src/routers/settings-members.ts"],"sourcesContent":["import { MemberModel, ProjectGroupMappingModel } from \"@storyshelf/core/models\";\nimport type { ProjectRole } from \"@storyshelf/core/types\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { getStore } from \"../store.ts\";\nimport { flash } from \"./flash.ts\";\nimport { hxRedirect } from \"./htmx.ts\";\nimport { asString, findProject, renderSettingsPage } from \"./settings.handlers.ts\";\n/** Project member settings (assign roles, remove members). */\nexport function registerMemberSettings(app: ShelfRouter): void {\n app.get(\"/projects/:slug/settings/members\", async (c) =>\n c.html(await renderSettingsPage(c, \"members\")),\n );\n\n app.post(\"/projects/:slug/settings/members\", async (c) => {\n const project = await findProject(c.req.param(\"slug\") ?? \"\");\n const form = await c.req.formData();\n const userId = asString(form.get(\"userId\"));\n const role = asString(form.get(\"role\"));\n if (!userId || !role) {\n return c.html(\n (await renderSettingsPage(c, \"members\", { globalError: \"User and role are required\" })) ??\n \"\",\n 400,\n );\n }\n await new MemberModel(getStore().db).set(project.id, userId, role as ProjectRole);\n flash(c, \"Member saved\");\n return hxRedirect(c, `/projects/${project.slug}/settings/members`);\n });\n\n app.post(\"/projects/:slug/settings/members/:userId/remove\", async (c) => {\n const project = await findProject(c.req.param(\"slug\") ?? \"\");\n await new MemberModel(getStore().db).remove(project.id, c.req.param(\"userId\") ?? \"\");\n flash(c, \"Member removed\");\n return hxRedirect(c, `/projects/${project.slug}/settings/members`);\n });\n\n registerGroupMappingSettings(app);\n}\n\n/** Identity-provider group mapping settings (exact-match group to role). */\nfunction registerGroupMappingSettings(app: ShelfRouter): void {\n app.post(\"/projects/:slug/settings/members/groups\", async (c) => {\n const project = await findProject(c.req.param(\"slug\") ?? \"\");\n const form = await c.req.formData();\n const groupName = asString(form.get(\"groupName\"));\n const role = asString(form.get(\"role\")) as ProjectRole | undefined;\n if (!groupName || !role || ![\"viewer\", \"developer\", \"approver\", \"admin\"].includes(role)) {\n return c.html(\n (await renderSettingsPage(c, \"members\", { globalError: \"Group and role are required\" })) ??\n \"\",\n 400,\n );\n }\n await new ProjectGroupMappingModel(getStore().db).create(project.id, groupName, role);\n flash(c, \"Group mapping added\");\n return hxRedirect(c, `/projects/${project.slug}/settings/members`);\n });\n\n app.post(\"/projects/:slug/settings/members/groups/:mappingId/remove\", async (c) => {\n const project = await findProject(c.req.param(\"slug\") ?? \"\");\n await new ProjectGroupMappingModel(getStore().db).remove(\n project.id,\n c.req.param(\"mappingId\") ?? \"\",\n );\n flash(c, \"Group mapping removed\");\n return hxRedirect(c, `/projects/${project.slug}/settings/members`);\n });\n}\n"],"mappings":";;;;;;;;;;AAQA,SAAgB,uBAAuB,KAAwB;CAC7D,IAAI,IAAI,oCAAoC,OAAO,MACjD,EAAE,KAAK,MAAM,mBAAmB,GAAG,SAAS,CAAC,CAC/C;CAEA,IAAI,KAAK,oCAAoC,OAAO,MAAM;EACxD,MAAM,UAAU,MAAM,YAAY,EAAE,IAAI,MAAM,MAAM,KAAK,EAAE;EAC3D,MAAM,OAAO,MAAM,EAAE,IAAI,SAAS;EAClC,MAAM,SAAS,SAAS,KAAK,IAAI,QAAQ,CAAC;EAC1C,MAAM,OAAO,SAAS,KAAK,IAAI,MAAM,CAAC;EACtC,IAAI,CAAC,UAAU,CAAC,MACd,OAAO,EAAE,KACN,MAAM,mBAAmB,GAAG,WAAW,EAAE,aAAa,6BAA6B,CAAC,KACnF,IACF,GACF;EAEF,MAAM,IAAI,YAAY,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,QAAQ,IAAI,QAAQ,IAAmB;EAChF,MAAM,GAAG,cAAc;EACvB,OAAO,WAAW,GAAG,aAAa,QAAQ,KAAK,kBAAkB;CACnE,CAAC;CAED,IAAI,KAAK,mDAAmD,OAAO,MAAM;EACvE,MAAM,UAAU,MAAM,YAAY,EAAE,IAAI,MAAM,MAAM,KAAK,EAAE;EAC3D,MAAM,IAAI,YAAY,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,OAAO,QAAQ,IAAI,EAAE,IAAI,MAAM,QAAQ,KAAK,EAAE;EACnF,MAAM,GAAG,gBAAgB;EACzB,OAAO,WAAW,GAAG,aAAa,QAAQ,KAAK,kBAAkB;CACnE,CAAC;CAED,6BAA6B,GAAG;AAClC;;AAGA,SAAS,6BAA6B,KAAwB;CAC5D,IAAI,KAAK,2CAA2C,OAAO,MAAM;EAC/D,MAAM,UAAU,MAAM,YAAY,EAAE,IAAI,MAAM,MAAM,KAAK,EAAE;EAC3D,MAAM,OAAO,MAAM,EAAE,IAAI,SAAS;EAClC,MAAM,YAAY,SAAS,KAAK,IAAI,WAAW,CAAC;EAChD,MAAM,OAAO,SAAS,KAAK,IAAI,MAAM,CAAC;EACtC,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC;GAAC;GAAU;GAAa;GAAY;EAAO,CAAC,CAAC,SAAS,IAAI,GACpF,OAAO,EAAE,KACN,MAAM,mBAAmB,GAAG,WAAW,EAAE,aAAa,8BAA8B,CAAC,KACpF,IACF,GACF;EAEF,MAAM,IAAI,yBAAyB,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,OAAO,QAAQ,IAAI,WAAW,IAAI;EACpF,MAAM,GAAG,qBAAqB;EAC9B,OAAO,WAAW,GAAG,aAAa,QAAQ,KAAK,kBAAkB;CACnE,CAAC;CAED,IAAI,KAAK,6DAA6D,OAAO,MAAM;EACjF,MAAM,UAAU,MAAM,YAAY,EAAE,IAAI,MAAM,MAAM,KAAK,EAAE;EAC3D,MAAM,IAAI,yBAAyB,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,OAChD,QAAQ,IACR,EAAE,IAAI,MAAM,WAAW,KAAK,EAC9B;EACA,MAAM,GAAG,uBAAuB;EAChC,OAAO,WAAW,GAAG,aAAa,QAAQ,KAAK,kBAAkB;CACnE,CAAC;AACH"}
1
+ {"version":3,"file":"settings-members.mjs","names":[],"sources":["../../src/routers/settings-members.ts"],"sourcesContent":["import { MemberModel, ProjectGroupMappingModel } from \"@storyshelf/core/models\";\nimport { isProjectRole } from \"@storyshelf/core/types\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { getStore } from \"../store.ts\";\nimport { flash } from \"./flash.ts\";\nimport { hxRedirect } from \"./htmx.ts\";\nimport { asString, findProject, renderSettingsPage } from \"./settings.handlers.ts\";\n/** Project member settings (assign roles, remove members). */\nexport function registerMemberSettings(app: ShelfRouter): void {\n app.get(\"/projects/:slug/settings/members\", async (c) =>\n c.html(await renderSettingsPage(c, \"members\")),\n );\n\n app.post(\"/projects/:slug/settings/members\", async (c) => {\n const project = await findProject(c.req.param(\"slug\") ?? \"\");\n const form = await c.req.formData();\n const userId = asString(form.get(\"userId\"));\n const role = asString(form.get(\"role\"));\n if (!userId || !isProjectRole(role)) {\n return c.html(\n (await renderSettingsPage(c, \"members\", { globalError: \"User and role are required\" })) ??\n \"\",\n 400,\n );\n }\n await new MemberModel(getStore().db).set(project.id, userId, role);\n flash(c, \"Member saved\");\n return hxRedirect(c, `/projects/${project.slug}/settings/members`);\n });\n\n app.post(\"/projects/:slug/settings/members/:userId/remove\", async (c) => {\n const project = await findProject(c.req.param(\"slug\") ?? \"\");\n await new MemberModel(getStore().db).remove(project.id, c.req.param(\"userId\") ?? \"\");\n flash(c, \"Member removed\");\n return hxRedirect(c, `/projects/${project.slug}/settings/members`);\n });\n\n registerGroupMappingSettings(app);\n}\n\n/** Identity-provider group mapping settings (exact-match group to role). */\nfunction registerGroupMappingSettings(app: ShelfRouter): void {\n app.post(\"/projects/:slug/settings/members/groups\", async (c) => {\n const project = await findProject(c.req.param(\"slug\") ?? \"\");\n const form = await c.req.formData();\n const groupName = asString(form.get(\"groupName\"));\n const role = asString(form.get(\"role\"));\n const inputError = ProjectGroupMappingModel.inputError(groupName, role);\n if (inputError || !groupName || !isProjectRole(role)) {\n return c.html(\n (await renderSettingsPage(c, \"members\", { globalError: inputError ?? \"Invalid input\" })) ??\n \"\",\n 400,\n );\n }\n await new ProjectGroupMappingModel(getStore().db).create(project.id, groupName, role);\n flash(c, \"Group mapping added\");\n return hxRedirect(c, `/projects/${project.slug}/settings/members`);\n });\n\n app.post(\"/projects/:slug/settings/members/groups/:mappingId/remove\", async (c) => {\n const project = await findProject(c.req.param(\"slug\") ?? \"\");\n await new ProjectGroupMappingModel(getStore().db).remove(\n project.id,\n c.req.param(\"mappingId\") ?? \"\",\n );\n flash(c, \"Group mapping removed\");\n return hxRedirect(c, `/projects/${project.slug}/settings/members`);\n });\n}\n"],"mappings":";;;;;;;;;;;AAQA,SAAgB,uBAAuB,KAAwB;CAC7D,IAAI,IAAI,oCAAoC,OAAO,MACjD,EAAE,KAAK,MAAM,mBAAmB,GAAG,SAAS,CAAC,CAC/C;CAEA,IAAI,KAAK,oCAAoC,OAAO,MAAM;EACxD,MAAM,UAAU,MAAM,YAAY,EAAE,IAAI,MAAM,MAAM,KAAK,EAAE;EAC3D,MAAM,OAAO,MAAM,EAAE,IAAI,SAAS;EAClC,MAAM,SAAS,SAAS,KAAK,IAAI,QAAQ,CAAC;EAC1C,MAAM,OAAO,SAAS,KAAK,IAAI,MAAM,CAAC;EACtC,IAAI,CAAC,UAAU,CAAC,cAAc,IAAI,GAChC,OAAO,EAAE,KACN,MAAM,mBAAmB,GAAG,WAAW,EAAE,aAAa,6BAA6B,CAAC,KACnF,IACF,GACF;EAEF,MAAM,IAAI,YAAY,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,QAAQ,IAAI,QAAQ,IAAI;EACjE,MAAM,GAAG,cAAc;EACvB,OAAO,WAAW,GAAG,aAAa,QAAQ,KAAK,kBAAkB;CACnE,CAAC;CAED,IAAI,KAAK,mDAAmD,OAAO,MAAM;EACvE,MAAM,UAAU,MAAM,YAAY,EAAE,IAAI,MAAM,MAAM,KAAK,EAAE;EAC3D,MAAM,IAAI,YAAY,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,OAAO,QAAQ,IAAI,EAAE,IAAI,MAAM,QAAQ,KAAK,EAAE;EACnF,MAAM,GAAG,gBAAgB;EACzB,OAAO,WAAW,GAAG,aAAa,QAAQ,KAAK,kBAAkB;CACnE,CAAC;CAED,6BAA6B,GAAG;AAClC;;AAGA,SAAS,6BAA6B,KAAwB;CAC5D,IAAI,KAAK,2CAA2C,OAAO,MAAM;EAC/D,MAAM,UAAU,MAAM,YAAY,EAAE,IAAI,MAAM,MAAM,KAAK,EAAE;EAC3D,MAAM,OAAO,MAAM,EAAE,IAAI,SAAS;EAClC,MAAM,YAAY,SAAS,KAAK,IAAI,WAAW,CAAC;EAChD,MAAM,OAAO,SAAS,KAAK,IAAI,MAAM,CAAC;EACtC,MAAM,aAAa,yBAAyB,WAAW,WAAW,IAAI;EACtE,IAAI,cAAc,CAAC,aAAa,CAAC,cAAc,IAAI,GACjD,OAAO,EAAE,KACN,MAAM,mBAAmB,GAAG,WAAW,EAAE,aAAa,cAAc,gBAAgB,CAAC,KACpF,IACF,GACF;EAEF,MAAM,IAAI,yBAAyB,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,OAAO,QAAQ,IAAI,WAAW,IAAI;EACpF,MAAM,GAAG,qBAAqB;EAC9B,OAAO,WAAW,GAAG,aAAa,QAAQ,KAAK,kBAAkB;CACnE,CAAC;CAED,IAAI,KAAK,6DAA6D,OAAO,MAAM;EACjF,MAAM,UAAU,MAAM,YAAY,EAAE,IAAI,MAAM,MAAM,KAAK,EAAE;EAC3D,MAAM,IAAI,yBAAyB,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,OAChD,QAAQ,IACR,EAAE,IAAI,MAAM,WAAW,KAAK,EAC9B;EACA,MAAM,GAAG,uBAAuB;EAChC,OAAO,WAAW,GAAG,aAAa,QAAQ,KAAK,kBAAkB;CACnE,CAAC;AACH"}
@@ -1,13 +1,14 @@
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 { flash } from "./flash.mjs";
6
+ import { notificationChannelModel } from "./scoped-models.mjs";
7
7
  import { asString, findProject, renderSettingsPage } from "./settings.handlers.mjs";
8
+ import { createGitChannel } from "./settings-git-channel.mjs";
8
9
  import { NotificationChannelModel, NotificationSubscriptionModel } from "@storyshelf/core/models";
9
- //#region src/routers/settings-notifications.ts
10
- /** Notifications settings (channels plus my preferences). */
10
+ // src/routers/settings-notifications.ts
11
+ /** Notifications settings (message and git channels plus my preferences). */
11
12
  function registerNotificationSettings(app) {
12
13
  app.get("/projects/:slug/settings/notifications", async (c) => c.html(await renderSettingsPage(c, "notifications")));
13
14
  app.post("/projects/:slug/settings/notifications", handleCreateChannel);
@@ -28,6 +29,11 @@ function parseChecks(form, name) {
28
29
  async function handleCreateChannel(c) {
29
30
  const project = await findProject(c.req.param("slug") ?? "");
30
31
  const form = await c.req.formData();
32
+ const gitProvider = asString(form.get("gitProvider"));
33
+ return gitProvider ? await createGitChannel(c, project, gitProvider, form) : await createMessageChannel(c, project, form);
34
+ }
35
+ /** The message-channel form: a provider and a target (email address or webhook URL). */
36
+ async function createMessageChannel(c, project, form) {
31
37
  const provider = asString(form.get("provider"));
32
38
  const target = asString(form.get("target"));
33
39
  if (provider && target) return await createChannelRecord(c, project, provider, target, form);
@@ -44,11 +50,10 @@ async function targetError(c, message) {
44
50
  return c.html(await renderSettingsPage(c, "notifications", { errors: { target: message } }) ?? "", 400);
45
51
  }
46
52
  async function createChannelRecord(c, project, provider, target, form) {
47
- const descriptor = getStore().notifiers.find((candidate) => candidate.metadata.kind === provider);
48
- if (!descriptor) return await providerError(c, `Unknown provider: ${provider}`);
49
- const parsed = descriptor.metadata.schema.safeParse(channelConfig(provider, target, form));
50
- if (!parsed.success) return await targetError(c, parsed.error.message);
51
- await persistChannel(project, provider, target, parsed.data, form);
53
+ const config = channelConfig(provider, target, form);
54
+ const checked = NotificationChannelModel.validateInput(getStore().notifiers, provider, config);
55
+ if (!checked.ok) return checked.field === "provider" ? await providerError(c, checked.message) : await targetError(c, checked.message);
56
+ await persistChannel(project, provider, target, config, form);
52
57
  flash(c, "Notification channel added");
53
58
  return c.html(await renderSettingsPage(c, "notifications") ?? "", 201);
54
59
  }
@@ -63,7 +68,7 @@ function channelConfig(provider, target, form) {
63
68
  }
64
69
  /** Persist a validated channel (webhook URLs land in the secret column). */
65
70
  async function persistChannel(project, provider, target, config, form) {
66
- await new NotificationChannelModel(getStore().db, void 0, credentialKeys(getStore().config)).create({
71
+ await notificationChannelModel().create({
67
72
  projectId: project.id,
68
73
  provider,
69
74
  config,
@@ -73,11 +78,11 @@ async function persistChannel(project, provider, target, config, form) {
73
78
  }
74
79
  async function handleDeleteChannel(c) {
75
80
  const project = await findProject(c.req.param("slug") ?? "");
76
- const model = new NotificationChannelModel(getStore().db, void 0, credentialKeys(getStore().config));
81
+ const model = notificationChannelModel();
77
82
  const channel = await model.get(c.req.param("channelId") ?? "");
78
83
  if (!channel || channel.projectId !== project.id) return c.html(await renderSettingsPage(c, "notifications", { globalError: "Channel not found" }) ?? "", 404);
79
84
  await model.remove(channel.id);
80
- flash(c, "Notification channel removed");
85
+ flash(c, "Channel removed");
81
86
  return c.html(await renderSettingsPage(c, "notifications") ?? "");
82
87
  }
83
88
  async function handleSaveMine(c) {
@@ -102,7 +107,6 @@ async function saveSubscription(project, userId, form) {
102
107
  }
103
108
  await model.remove(project.id, userId);
104
109
  }
105
- //#endregion
106
110
  export { registerNotificationSettings };
107
111
 
108
112
  //# sourceMappingURL=settings-notifications.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"settings-notifications.mjs","names":[],"sources":["../../src/routers/settings-notifications.ts"],"sourcesContent":["import { NotificationChannelModel, NotificationSubscriptionModel } from \"@storyshelf/core/models\";\nimport type { Project } from \"@storyshelf/core/schema\";\nimport type { Context } from \"hono\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { credentialKeys } from \"../credential-keys.ts\";\nimport { getStore } from \"../store.ts\";\nimport { flash } from \"./flash.ts\";\nimport { asString, findProject, renderSettingsPage } from \"./settings.handlers.ts\";\n\n/** Notifications settings (channels plus my preferences). */\nexport function registerNotificationSettings(app: ShelfRouter): void {\n app.get(\"/projects/:slug/settings/notifications\", async (c) =>\n c.html(await renderSettingsPage(c, \"notifications\")),\n );\n app.post(\"/projects/:slug/settings/notifications\", handleCreateChannel);\n app.post(\"/projects/:slug/settings/notifications/:channelId/delete\", handleDeleteChannel);\n app.post(\"/projects/:slug/settings/notifications/me\", handleSaveMine);\n}\n\n/** Parse the comma-separated events field. */\nfunction parseEvents(form: FormData): string[] | undefined {\n const raw = asString(form.get(\"events\"));\n if (!raw) {\n return undefined;\n }\n const events = raw\n .split(\",\")\n .map((event) => event.trim())\n .filter((event) => event.length > 0);\n return events.length > 0 ? events : undefined;\n}\n\n/** Parse repeated checkbox values (topics). */\nfunction parseChecks(form: FormData, name: string): string[] {\n return form\n .getAll(name)\n .filter((entry): entry is string => typeof entry === \"string\")\n .map((entry) => entry.trim())\n .filter((entry) => entry.length > 0);\n}\n\nasync function handleCreateChannel(c: Context): Promise<Response> {\n const project = await findProject(c.req.param(\"slug\") ?? \"\");\n const form = await c.req.formData();\n const provider = asString(form.get(\"provider\"));\n const target = asString(form.get(\"target\"));\n if (provider && target) {\n return await createChannelRecord(c, project, provider, target, form);\n }\n const missing = provider ? \"target\" : \"provider\";\n const message = provider ? \"Enter a target\" : \"Choose a provider\";\n return c.html(\n (await renderSettingsPage(c, \"notifications\", { errors: { [missing]: message } })) ?? \"\",\n 400,\n );\n}\n\n/** Render the tab with a provider-field error. */\nasync function providerError(c: Context, message: string): Promise<Response> {\n return c.html(\n (await renderSettingsPage(c, \"notifications\", { errors: { provider: message } })) ?? \"\",\n 400,\n );\n}\n\n/** Render the tab with a target-field error. */\nasync function targetError(c: Context, message: string): Promise<Response> {\n return c.html(\n (await renderSettingsPage(c, \"notifications\", { errors: { target: message } })) ?? \"\",\n 400,\n );\n}\n\nasync function createChannelRecord(\n c: Context,\n project: Project,\n provider: string,\n target: string,\n form: FormData,\n): Promise<Response> {\n const descriptor = getStore().notifiers.find((candidate) => candidate.metadata.kind === provider);\n if (!descriptor) {\n return await providerError(c, `Unknown provider: ${provider}`);\n }\n const parsed = descriptor.metadata.schema.safeParse(channelConfig(provider, target, form));\n if (!parsed.success) {\n return await targetError(c, parsed.error.message);\n }\n await persistChannel(project, provider, target, parsed.data, form);\n flash(c, \"Notification channel added\");\n return c.html((await renderSettingsPage(c, \"notifications\")) ?? \"\", 201);\n}\n\n/** Build the provider config from the form (address vs secret per kind). */\nfunction channelConfig(provider: string, target: string, form: FormData): Record<string, unknown> {\n const style = asString(form.get(\"style\")) ?? \"compact\";\n if (provider === \"email\") {\n return { to: target, style };\n }\n return { style };\n}\n\n/** Persist a validated channel (webhook URLs land in the secret column). */\nasync function persistChannel(\n project: Project,\n provider: string,\n target: string,\n config: unknown,\n form: FormData,\n): Promise<void> {\n await new NotificationChannelModel(\n getStore().db,\n undefined,\n credentialKeys(getStore().config),\n ).create({\n projectId: project.id,\n provider,\n config: config as Record<string, unknown>,\n secret: provider === \"email\" ? undefined : target,\n events: parseEvents(form),\n });\n}\n\nasync function handleDeleteChannel(c: Context): Promise<Response> {\n const project = await findProject(c.req.param(\"slug\") ?? \"\");\n const model = new NotificationChannelModel(\n getStore().db,\n undefined,\n credentialKeys(getStore().config),\n );\n const channel = await model.get(c.req.param(\"channelId\") ?? \"\");\n if (!channel || channel.projectId !== project.id) {\n return c.html(\n (await renderSettingsPage(c, \"notifications\", {\n globalError: \"Channel not found\",\n })) ?? \"\",\n 404,\n );\n }\n await model.remove(channel.id);\n flash(c, \"Notification channel removed\");\n return c.html((await renderSettingsPage(c, \"notifications\")) ?? \"\");\n}\n\nasync function handleSaveMine(c: Context): Promise<Response> {\n const project = await findProject(c.req.param(\"slug\") ?? \"\");\n const user = getStore().user;\n if (user) {\n await saveSubscription(project, user.id, await c.req.formData());\n flash(c, \"Notification preferences saved\");\n return c.html((await renderSettingsPage(c, \"notifications\")) ?? \"\");\n }\n return c.html(\n (await renderSettingsPage(c, \"notifications\", {\n globalError: \"Sign in to save notification preferences\",\n })) ?? \"\",\n 401,\n );\n}\n\n/** Persist the viewer's own subscription (unchecked means opt out). */\nasync function saveSubscription(project: Project, userId: string, form: FormData): Promise<void> {\n const model = new NotificationSubscriptionModel(getStore().db);\n if (form.get(\"enabled\") === \"on\") {\n await model.upsert(project.id, userId, { events: parseChecks(form, \"events\"), via: [\"email\"] });\n return;\n }\n await model.remove(project.id, userId);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"settings-notifications.mjs","names":[],"sources":["../../src/routers/settings-notifications.ts"],"sourcesContent":["import { NotificationChannelModel, NotificationSubscriptionModel } from \"@storyshelf/core/models\";\nimport type { Project } from \"@storyshelf/core/schema\";\nimport type { Context } from \"hono\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { getStore } from \"../store.ts\";\nimport { flash } from \"./flash.ts\";\nimport { notificationChannelModel } from \"./scoped-models.ts\";\nimport { createGitChannel } from \"./settings-git-channel.ts\";\nimport { asString, findProject, renderSettingsPage } from \"./settings.handlers.ts\";\n\n/** Notifications settings (message and git channels plus my preferences). */\nexport function registerNotificationSettings(app: ShelfRouter): void {\n app.get(\"/projects/:slug/settings/notifications\", async (c) =>\n c.html(await renderSettingsPage(c, \"notifications\")),\n );\n app.post(\"/projects/:slug/settings/notifications\", handleCreateChannel);\n app.post(\"/projects/:slug/settings/notifications/:channelId/delete\", handleDeleteChannel);\n app.post(\"/projects/:slug/settings/notifications/me\", handleSaveMine);\n}\n\n/** Parse the comma-separated events field. */\nfunction parseEvents(form: FormData): string[] | undefined {\n const raw = asString(form.get(\"events\"));\n if (!raw) {\n return undefined;\n }\n const events = raw\n .split(\",\")\n .map((event) => event.trim())\n .filter((event) => event.length > 0);\n return events.length > 0 ? events : undefined;\n}\n\n/** Parse repeated checkbox values (topics). */\nfunction parseChecks(form: FormData, name: string): string[] {\n return form\n .getAll(name)\n .filter((entry): entry is string => typeof entry === \"string\")\n .map((entry) => entry.trim())\n .filter((entry) => entry.length > 0);\n}\n\nasync function handleCreateChannel(c: Context): Promise<Response> {\n const project = await findProject(c.req.param(\"slug\") ?? \"\");\n const form = await c.req.formData();\n const gitProvider = asString(form.get(\"gitProvider\"));\n return gitProvider\n ? await createGitChannel(c, project, gitProvider, form)\n : await createMessageChannel(c, project, form);\n}\n\n/** The message-channel form: a provider and a target (email address or webhook URL). */\nasync function createMessageChannel(\n c: Context,\n project: Project,\n form: FormData,\n): Promise<Response> {\n const provider = asString(form.get(\"provider\"));\n const target = asString(form.get(\"target\"));\n if (provider && target) {\n return await createChannelRecord(c, project, provider, target, form);\n }\n const missing = provider ? \"target\" : \"provider\";\n const message = provider ? \"Enter a target\" : \"Choose a provider\";\n return c.html(\n (await renderSettingsPage(c, \"notifications\", { errors: { [missing]: message } })) ?? \"\",\n 400,\n );\n}\n\n/** Render the tab with a provider-field error. */\nasync function providerError(c: Context, message: string): Promise<Response> {\n return c.html(\n (await renderSettingsPage(c, \"notifications\", { errors: { provider: message } })) ?? \"\",\n 400,\n );\n}\n\n/** Render the tab with a target-field error. */\nasync function targetError(c: Context, message: string): Promise<Response> {\n return c.html(\n (await renderSettingsPage(c, \"notifications\", { errors: { target: message } })) ?? \"\",\n 400,\n );\n}\n\nasync function createChannelRecord(\n c: Context,\n project: Project,\n provider: string,\n target: string,\n form: FormData,\n): Promise<Response> {\n const config = channelConfig(provider, target, form);\n const checked = NotificationChannelModel.validateInput(getStore().notifiers, provider, config);\n if (!checked.ok) {\n return checked.field === \"provider\"\n ? await providerError(c, checked.message)\n : await targetError(c, checked.message);\n }\n await persistChannel(project, provider, target, config, form);\n flash(c, \"Notification channel added\");\n return c.html((await renderSettingsPage(c, \"notifications\")) ?? \"\", 201);\n}\n\n/** Build the provider config from the form (address vs secret per kind). */\nfunction channelConfig(provider: string, target: string, form: FormData): Record<string, unknown> {\n const style = asString(form.get(\"style\")) ?? \"compact\";\n if (provider === \"email\") {\n return { to: target, style };\n }\n return { style };\n}\n\n/** Persist a validated channel (webhook URLs land in the secret column). */\nasync function persistChannel(\n project: Project,\n provider: string,\n target: string,\n config: unknown,\n form: FormData,\n): Promise<void> {\n await notificationChannelModel().create({\n projectId: project.id,\n provider,\n config: config as Record<string, unknown>,\n secret: provider === \"email\" ? undefined : target,\n events: parseEvents(form),\n });\n}\n\nasync function handleDeleteChannel(c: Context): Promise<Response> {\n const project = await findProject(c.req.param(\"slug\") ?? \"\");\n const model = notificationChannelModel();\n const channel = await model.get(c.req.param(\"channelId\") ?? \"\");\n if (!channel || channel.projectId !== project.id) {\n return c.html(\n (await renderSettingsPage(c, \"notifications\", {\n globalError: \"Channel not found\",\n })) ?? \"\",\n 404,\n );\n }\n await model.remove(channel.id);\n flash(c, \"Channel removed\");\n return c.html((await renderSettingsPage(c, \"notifications\")) ?? \"\");\n}\n\nasync function handleSaveMine(c: Context): Promise<Response> {\n const project = await findProject(c.req.param(\"slug\") ?? \"\");\n const user = getStore().user;\n if (user) {\n await saveSubscription(project, user.id, await c.req.formData());\n flash(c, \"Notification preferences saved\");\n return c.html((await renderSettingsPage(c, \"notifications\")) ?? \"\");\n }\n return c.html(\n (await renderSettingsPage(c, \"notifications\", {\n globalError: \"Sign in to save notification preferences\",\n })) ?? \"\",\n 401,\n );\n}\n\n/** Persist the viewer's own subscription (unchecked means opt out). */\nasync function saveSubscription(project: Project, userId: string, form: FormData): Promise<void> {\n const model = new NotificationSubscriptionModel(getStore().db);\n if (form.get(\"enabled\") === \"on\") {\n await model.upsert(project.id, userId, { events: parseChecks(form, \"events\"), via: [\"email\"] });\n return;\n }\n await model.remove(project.id, userId);\n}\n"],"mappings":";;;;;;;;;;;AAWA,SAAgB,6BAA6B,KAAwB;CACnE,IAAI,IAAI,0CAA0C,OAAO,MACvD,EAAE,KAAK,MAAM,mBAAmB,GAAG,eAAe,CAAC,CACrD;CACA,IAAI,KAAK,0CAA0C,mBAAmB;CACtE,IAAI,KAAK,4DAA4D,mBAAmB;CACxF,IAAI,KAAK,6CAA6C,cAAc;AACtE;;AAGA,SAAS,YAAY,MAAsC;CACzD,MAAM,MAAM,SAAS,KAAK,IAAI,QAAQ,CAAC;CACvC,IAAI,CAAC,KACH;CAEF,MAAM,SAAS,IACZ,MAAM,GAAG,CAAC,CACV,KAAK,UAAU,MAAM,KAAK,CAAC,CAAC,CAC5B,QAAQ,UAAU,MAAM,SAAS,CAAC;CACrC,OAAO,OAAO,SAAS,IAAI,SAAS,KAAA;AACtC;;AAGA,SAAS,YAAY,MAAgB,MAAwB;CAC3D,OAAO,KACJ,OAAO,IAAI,CAAC,CACZ,QAAQ,UAA2B,OAAO,UAAU,QAAQ,CAAC,CAC7D,KAAK,UAAU,MAAM,KAAK,CAAC,CAAC,CAC5B,QAAQ,UAAU,MAAM,SAAS,CAAC;AACvC;AAEA,eAAe,oBAAoB,GAA+B;CAChE,MAAM,UAAU,MAAM,YAAY,EAAE,IAAI,MAAM,MAAM,KAAK,EAAE;CAC3D,MAAM,OAAO,MAAM,EAAE,IAAI,SAAS;CAClC,MAAM,cAAc,SAAS,KAAK,IAAI,aAAa,CAAC;CACpD,OAAO,cACH,MAAM,iBAAiB,GAAG,SAAS,aAAa,IAAI,IACpD,MAAM,qBAAqB,GAAG,SAAS,IAAI;AACjD;;AAGA,eAAe,qBACb,GACA,SACA,MACmB;CACnB,MAAM,WAAW,SAAS,KAAK,IAAI,UAAU,CAAC;CAC9C,MAAM,SAAS,SAAS,KAAK,IAAI,QAAQ,CAAC;CAC1C,IAAI,YAAY,QACd,OAAO,MAAM,oBAAoB,GAAG,SAAS,UAAU,QAAQ,IAAI;CAErE,MAAM,UAAU,WAAW,WAAW;CACtC,MAAM,UAAU,WAAW,mBAAmB;CAC9C,OAAO,EAAE,KACN,MAAM,mBAAmB,GAAG,iBAAiB,EAAE,QAAQ,GAAG,UAAU,QAAQ,EAAE,CAAC,KAAM,IACtF,GACF;AACF;;AAGA,eAAe,cAAc,GAAY,SAAoC;CAC3E,OAAO,EAAE,KACN,MAAM,mBAAmB,GAAG,iBAAiB,EAAE,QAAQ,EAAE,UAAU,QAAQ,EAAE,CAAC,KAAM,IACrF,GACF;AACF;;AAGA,eAAe,YAAY,GAAY,SAAoC;CACzE,OAAO,EAAE,KACN,MAAM,mBAAmB,GAAG,iBAAiB,EAAE,QAAQ,EAAE,QAAQ,QAAQ,EAAE,CAAC,KAAM,IACnF,GACF;AACF;AAEA,eAAe,oBACb,GACA,SACA,UACA,QACA,MACmB;CACnB,MAAM,SAAS,cAAc,UAAU,QAAQ,IAAI;CACnD,MAAM,UAAU,yBAAyB,cAAc,SAAS,CAAC,CAAC,WAAW,UAAU,MAAM;CAC7F,IAAI,CAAC,QAAQ,IACX,OAAO,QAAQ,UAAU,aACrB,MAAM,cAAc,GAAG,QAAQ,OAAO,IACtC,MAAM,YAAY,GAAG,QAAQ,OAAO;CAE1C,MAAM,eAAe,SAAS,UAAU,QAAQ,QAAQ,IAAI;CAC5D,MAAM,GAAG,4BAA4B;CACrC,OAAO,EAAE,KAAM,MAAM,mBAAmB,GAAG,eAAe,KAAM,IAAI,GAAG;AACzE;;AAGA,SAAS,cAAc,UAAkB,QAAgB,MAAyC;CAChG,MAAM,QAAQ,SAAS,KAAK,IAAI,OAAO,CAAC,KAAK;CAC7C,IAAI,aAAa,SACf,OAAO;EAAE,IAAI;EAAQ;CAAM;CAE7B,OAAO,EAAE,MAAM;AACjB;;AAGA,eAAe,eACb,SACA,UACA,QACA,QACA,MACe;CACf,MAAM,yBAAyB,CAAC,CAAC,OAAO;EACtC,WAAW,QAAQ;EACnB;EACQ;EACR,QAAQ,aAAa,UAAU,KAAA,IAAY;EAC3C,QAAQ,YAAY,IAAI;CAC1B,CAAC;AACH;AAEA,eAAe,oBAAoB,GAA+B;CAChE,MAAM,UAAU,MAAM,YAAY,EAAE,IAAI,MAAM,MAAM,KAAK,EAAE;CAC3D,MAAM,QAAQ,yBAAyB;CACvC,MAAM,UAAU,MAAM,MAAM,IAAI,EAAE,IAAI,MAAM,WAAW,KAAK,EAAE;CAC9D,IAAI,CAAC,WAAW,QAAQ,cAAc,QAAQ,IAC5C,OAAO,EAAE,KACN,MAAM,mBAAmB,GAAG,iBAAiB,EAC5C,aAAa,oBACf,CAAC,KAAM,IACP,GACF;CAEF,MAAM,MAAM,OAAO,QAAQ,EAAE;CAC7B,MAAM,GAAG,iBAAiB;CAC1B,OAAO,EAAE,KAAM,MAAM,mBAAmB,GAAG,eAAe,KAAM,EAAE;AACpE;AAEA,eAAe,eAAe,GAA+B;CAC3D,MAAM,UAAU,MAAM,YAAY,EAAE,IAAI,MAAM,MAAM,KAAK,EAAE;CAC3D,MAAM,OAAO,SAAS,CAAC,CAAC;CACxB,IAAI,MAAM;EACR,MAAM,iBAAiB,SAAS,KAAK,IAAI,MAAM,EAAE,IAAI,SAAS,CAAC;EAC/D,MAAM,GAAG,gCAAgC;EACzC,OAAO,EAAE,KAAM,MAAM,mBAAmB,GAAG,eAAe,KAAM,EAAE;CACpE;CACA,OAAO,EAAE,KACN,MAAM,mBAAmB,GAAG,iBAAiB,EAC5C,aAAa,2CACf,CAAC,KAAM,IACP,GACF;AACF;;AAGA,eAAe,iBAAiB,SAAkB,QAAgB,MAA+B;CAC/F,MAAM,QAAQ,IAAI,8BAA8B,SAAS,CAAC,CAAC,EAAE;CAC7D,IAAI,KAAK,IAAI,SAAS,MAAM,MAAM;EAChC,MAAM,MAAM,OAAO,QAAQ,IAAI,QAAQ;GAAE,QAAQ,YAAY,MAAM,QAAQ;GAAG,KAAK,CAAC,OAAO;EAAE,CAAC;EAC9F;CACF;CACA,MAAM,MAAM,OAAO,QAAQ,IAAI,MAAM;AACvC"}
@@ -6,8 +6,7 @@ import { hxRedirect } from "./htmx.mjs";
6
6
  import { flash } from "./flash.mjs";
7
7
  import { asString, findProject, renderSettingsPage } from "./settings.handlers.mjs";
8
8
  import { TokenModel } from "@storyshelf/core/models";
9
- import { randomToken } from "@storyshelf/core/utils";
10
- //#region src/routers/settings-tokens.ts
9
+ // src/routers/settings-tokens.ts
11
10
  /** CI token settings (create with one-time display, delete). */
12
11
  function registerTokenSettings(app) {
13
12
  app.get("/projects/:slug/settings/tokens", async (c) => c.html(await renderSettingsPage(c, "tokens")));
@@ -19,14 +18,12 @@ async function handleCreateToken(c) {
19
18
  const form = await c.req.formData();
20
19
  const tokenName = asString(form.get("tokenName")) ?? asString(form.get("name"));
21
20
  if (!tokenName) return c.html(await renderSettingsPage(c, "tokens", { globalError: "Name is required" }) ?? "", 400);
22
- const token = randomToken("shelf_");
23
- await new TokenModel(getStore().db).create(project.id, {
21
+ const { value } = await new TokenModel(getStore().db).issue(project.id, {
24
22
  name: tokenName,
25
- hash: token.hash,
26
23
  userId: getStore().user?.id ?? null
27
24
  });
28
25
  flash(c, "Token created — copy it now, it is shown once");
29
- return c.html(await renderSettingsPage(c, "tokens", { secret: token.value }) ?? "");
26
+ return c.html(await renderSettingsPage(c, "tokens", { secret: value }) ?? "");
30
27
  }
31
28
  async function handleDeleteToken(c) {
32
29
  const project = await findProject(c.req.param("slug") ?? "");
@@ -34,7 +31,6 @@ async function handleDeleteToken(c) {
34
31
  flash(c, "Token deleted");
35
32
  return hxRedirect(c, `/projects/${project.slug}/settings/tokens`);
36
33
  }
37
- //#endregion
38
34
  export { registerTokenSettings };
39
35
 
40
36
  //# sourceMappingURL=settings-tokens.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"settings-tokens.mjs","names":[],"sources":["../../src/routers/settings-tokens.ts"],"sourcesContent":["import { TokenModel } from \"@storyshelf/core/models\";\nimport { randomToken } from \"@storyshelf/core/utils\";\nimport type { Context } from \"hono\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { getStore } from \"../store.ts\";\nimport { flash } from \"./flash.ts\";\nimport { hxRedirect } from \"./htmx.ts\";\nimport { asString, findProject, renderSettingsPage } from \"./settings.handlers.ts\";\n/** CI token settings (create with one-time display, delete). */\nexport function registerTokenSettings(app: ShelfRouter): void {\n app.get(\"/projects/:slug/settings/tokens\", async (c) =>\n c.html(await renderSettingsPage(c, \"tokens\")),\n );\n app.post(\"/projects/:slug/settings/tokens\", handleCreateToken);\n app.post(\"/projects/:slug/settings/tokens/:tokenId/delete\", handleDeleteToken);\n}\n\nasync function handleCreateToken(c: Context): Promise<Response> {\n const project = await findProject(c.req.param(\"slug\") ?? \"\");\n const form = await c.req.formData();\n const tokenName = asString(form.get(\"tokenName\")) ?? asString(form.get(\"name\"));\n if (!tokenName) {\n return c.html(\n (await renderSettingsPage(c, \"tokens\", { globalError: \"Name is required\" })) ?? \"\",\n 400,\n );\n }\n const token = randomToken(\"shelf_\");\n await new TokenModel(getStore().db).create(project.id, {\n name: tokenName,\n hash: token.hash,\n userId: getStore().user?.id ?? null,\n });\n flash(c, \"Token created — copy it now, it is shown once\");\n return c.html((await renderSettingsPage(c, \"tokens\", { secret: token.value })) ?? \"\");\n}\n\nasync function handleDeleteToken(c: Context): Promise<Response> {\n const project = await findProject(c.req.param(\"slug\") ?? \"\");\n await new TokenModel(getStore().db).remove(c.req.param(\"tokenId\") ?? \"\");\n flash(c, \"Token deleted\");\n return hxRedirect(c, `/projects/${project.slug}/settings/tokens`);\n}\n"],"mappings":";;;;;;;;;;;AASA,SAAgB,sBAAsB,KAAwB;CAC5D,IAAI,IAAI,mCAAmC,OAAO,MAChD,EAAE,KAAK,MAAM,mBAAmB,GAAG,QAAQ,CAAC,CAC9C;CACA,IAAI,KAAK,mCAAmC,iBAAiB;CAC7D,IAAI,KAAK,mDAAmD,iBAAiB;AAC/E;AAEA,eAAe,kBAAkB,GAA+B;CAC9D,MAAM,UAAU,MAAM,YAAY,EAAE,IAAI,MAAM,MAAM,KAAK,EAAE;CAC3D,MAAM,OAAO,MAAM,EAAE,IAAI,SAAS;CAClC,MAAM,YAAY,SAAS,KAAK,IAAI,WAAW,CAAC,KAAK,SAAS,KAAK,IAAI,MAAM,CAAC;CAC9E,IAAI,CAAC,WACH,OAAO,EAAE,KACN,MAAM,mBAAmB,GAAG,UAAU,EAAE,aAAa,mBAAmB,CAAC,KAAM,IAChF,GACF;CAEF,MAAM,QAAQ,YAAY,QAAQ;CAClC,MAAM,IAAI,WAAW,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,OAAO,QAAQ,IAAI;EACrD,MAAM;EACN,MAAM,MAAM;EACZ,QAAQ,SAAS,CAAC,CAAC,MAAM,MAAM;CACjC,CAAC;CACD,MAAM,GAAG,+CAA+C;CACxD,OAAO,EAAE,KAAM,MAAM,mBAAmB,GAAG,UAAU,EAAE,QAAQ,MAAM,MAAM,CAAC,KAAM,EAAE;AACtF;AAEA,eAAe,kBAAkB,GAA+B;CAC9D,MAAM,UAAU,MAAM,YAAY,EAAE,IAAI,MAAM,MAAM,KAAK,EAAE;CAC3D,MAAM,IAAI,WAAW,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,OAAO,EAAE,IAAI,MAAM,SAAS,KAAK,EAAE;CACvE,MAAM,GAAG,eAAe;CACxB,OAAO,WAAW,GAAG,aAAa,QAAQ,KAAK,iBAAiB;AAClE"}
1
+ {"version":3,"file":"settings-tokens.mjs","names":[],"sources":["../../src/routers/settings-tokens.ts"],"sourcesContent":["import { TokenModel } from \"@storyshelf/core/models\";\nimport type { Context } from \"hono\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { getStore } from \"../store.ts\";\nimport { flash } from \"./flash.ts\";\nimport { hxRedirect } from \"./htmx.ts\";\nimport { asString, findProject, renderSettingsPage } from \"./settings.handlers.ts\";\n/** CI token settings (create with one-time display, delete). */\nexport function registerTokenSettings(app: ShelfRouter): void {\n app.get(\"/projects/:slug/settings/tokens\", async (c) =>\n c.html(await renderSettingsPage(c, \"tokens\")),\n );\n app.post(\"/projects/:slug/settings/tokens\", handleCreateToken);\n app.post(\"/projects/:slug/settings/tokens/:tokenId/delete\", handleDeleteToken);\n}\n\nasync function handleCreateToken(c: Context): Promise<Response> {\n const project = await findProject(c.req.param(\"slug\") ?? \"\");\n const form = await c.req.formData();\n const tokenName = asString(form.get(\"tokenName\")) ?? asString(form.get(\"name\"));\n if (!tokenName) {\n return c.html(\n (await renderSettingsPage(c, \"tokens\", { globalError: \"Name is required\" })) ?? \"\",\n 400,\n );\n }\n const { value } = await new TokenModel(getStore().db).issue(project.id, {\n name: tokenName,\n userId: getStore().user?.id ?? null,\n });\n flash(c, \"Token created — copy it now, it is shown once\");\n return c.html((await renderSettingsPage(c, \"tokens\", { secret: value })) ?? \"\");\n}\n\nasync function handleDeleteToken(c: Context): Promise<Response> {\n const project = await findProject(c.req.param(\"slug\") ?? \"\");\n await new TokenModel(getStore().db).remove(c.req.param(\"tokenId\") ?? \"\");\n flash(c, \"Token deleted\");\n return hxRedirect(c, `/projects/${project.slug}/settings/tokens`);\n}\n"],"mappings":";;;;;;;;;;AAQA,SAAgB,sBAAsB,KAAwB;CAC5D,IAAI,IAAI,mCAAmC,OAAO,MAChD,EAAE,KAAK,MAAM,mBAAmB,GAAG,QAAQ,CAAC,CAC9C;CACA,IAAI,KAAK,mCAAmC,iBAAiB;CAC7D,IAAI,KAAK,mDAAmD,iBAAiB;AAC/E;AAEA,eAAe,kBAAkB,GAA+B;CAC9D,MAAM,UAAU,MAAM,YAAY,EAAE,IAAI,MAAM,MAAM,KAAK,EAAE;CAC3D,MAAM,OAAO,MAAM,EAAE,IAAI,SAAS;CAClC,MAAM,YAAY,SAAS,KAAK,IAAI,WAAW,CAAC,KAAK,SAAS,KAAK,IAAI,MAAM,CAAC;CAC9E,IAAI,CAAC,WACH,OAAO,EAAE,KACN,MAAM,mBAAmB,GAAG,UAAU,EAAE,aAAa,mBAAmB,CAAC,KAAM,IAChF,GACF;CAEF,MAAM,EAAE,UAAU,MAAM,IAAI,WAAW,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,MAAM,QAAQ,IAAI;EACtE,MAAM;EACN,QAAQ,SAAS,CAAC,CAAC,MAAM,MAAM;CACjC,CAAC;CACD,MAAM,GAAG,+CAA+C;CACxD,OAAO,EAAE,KAAM,MAAM,mBAAmB,GAAG,UAAU,EAAE,QAAQ,MAAM,CAAC,KAAM,EAAE;AAChF;AAEA,eAAe,kBAAkB,GAA+B;CAC9D,MAAM,UAAU,MAAM,YAAY,EAAE,IAAI,MAAM,MAAM,KAAK,EAAE;CAC3D,MAAM,IAAI,WAAW,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,OAAO,EAAE,IAAI,MAAM,SAAS,KAAK,EAAE;CACvE,MAAM,GAAG,eAAe;CACxB,OAAO,WAAW,GAAG,aAAa,QAAQ,KAAK,iBAAiB;AAClE"}
@@ -1,33 +1,19 @@
1
1
  import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
- import { credentialKeys } from "../credential-keys.mjs";
5
- import { getStore } from "../store.mjs";
4
+ import { notFound } from "./helpers.mjs";
6
5
  import { hxRedirect } from "./htmx.mjs";
7
6
  import { flash } from "./flash.mjs";
8
- import { notFound } from "./helpers.mjs";
7
+ import { webhookModel } from "./scoped-models.mjs";
9
8
  import { asString, findProject, renderSettingsPage } from "./settings.handlers.mjs";
10
9
  import { WebhookModel } from "@storyshelf/core/models";
11
- import { randomToken } from "@storyshelf/core/utils";
12
- //#region src/routers/settings-webhooks.ts
10
+ // src/routers/settings-webhooks.ts
13
11
  /** Webhook settings (create with one-time secret display, delete). */
14
12
  function registerWebhookSettings(app) {
15
13
  app.get("/projects/:slug/settings/webhooks", async (c) => c.html(await renderSettingsPage(c, "webhooks")));
16
14
  app.post("/projects/:slug/settings/webhooks", handleCreateWebhook);
17
15
  app.post("/projects/:slug/settings/webhooks/:webhookId/delete", handleDeleteWebhook);
18
16
  }
19
- /** Validate a webhook URL, returning the error message or null when valid. */
20
- function webhookUrlError(url) {
21
- if (!url) return "A valid URL is required";
22
- let protocol;
23
- try {
24
- protocol = new URL(url).protocol;
25
- } catch {
26
- return "Enter a valid URL";
27
- }
28
- if (protocol !== "https:" && protocol !== "http:") return "URL must use http or https";
29
- return null;
30
- }
31
17
  /** Parse the comma-separated events field. */
32
18
  function parseWebhookEvents(form) {
33
19
  const eventsRaw = asString(form.get("events"));
@@ -38,30 +24,26 @@ async function handleCreateWebhook(c) {
38
24
  const project = await findProject(c.req.param("slug") ?? "");
39
25
  const form = await c.req.formData();
40
26
  const url = asString(form.get("url"));
41
- const urlError = webhookUrlError(url);
27
+ const urlError = WebhookModel.urlError(url);
42
28
  if (urlError || !url) return c.html(await renderSettingsPage(c, "webhooks", { errors: { url: urlError ?? "Invalid URL" } }) ?? "", 400);
43
29
  return await createWebhookRecord(c, project, url, parseWebhookEvents(form));
44
30
  }
45
31
  async function createWebhookRecord(c, project, url, events) {
46
- const webhookModel = new WebhookModel(getStore().db, void 0, credentialKeys(getStore().config));
47
- const secret = randomToken("whsec_").value;
48
- await webhookModel.create(project.id, {
32
+ const { secret } = await webhookModel().issue(project.id, {
49
33
  url,
50
- events,
51
- secret
34
+ events
52
35
  });
53
36
  flash(c, "Webhook created — copy the signing secret now");
54
37
  return c.html(await renderSettingsPage(c, "webhooks", { secret }) ?? "", 201);
55
38
  }
56
39
  async function handleDeleteWebhook(c) {
57
40
  const project = await findProject(c.req.param("slug") ?? "");
58
- const webhook = await new WebhookModel(getStore().db, void 0, credentialKeys(getStore().config)).get(project.id, c.req.param("webhookId") ?? "");
41
+ const webhook = await webhookModel().get(project.id, c.req.param("webhookId") ?? "");
59
42
  if (!webhook) notFound("Webhook not found");
60
- await new WebhookModel(getStore().db, void 0, credentialKeys(getStore().config)).remove(project.id, webhook.id);
43
+ await webhookModel().remove(project.id, webhook.id);
61
44
  flash(c, "Webhook deleted");
62
45
  return hxRedirect(c, `/projects/${project.slug}/settings/webhooks`);
63
46
  }
64
- //#endregion
65
47
  export { registerWebhookSettings };
66
48
 
67
49
  //# sourceMappingURL=settings-webhooks.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"settings-webhooks.mjs","names":[],"sources":["../../src/routers/settings-webhooks.ts"],"sourcesContent":["import { WebhookModel } from \"@storyshelf/core/models\";\nimport type { Project } from \"@storyshelf/core/schema\";\nimport { randomToken } from \"@storyshelf/core/utils\";\nimport type { Context } from \"hono\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { credentialKeys } from \"../credential-keys.ts\";\nimport { getStore } from \"../store.ts\";\nimport { flash } from \"./flash.ts\";\nimport { notFound } from \"./helpers.ts\";\nimport { hxRedirect } from \"./htmx.ts\";\nimport { asString, findProject, renderSettingsPage } from \"./settings.handlers.ts\";\n/** Webhook settings (create with one-time secret display, delete). */\nexport function registerWebhookSettings(app: ShelfRouter): void {\n app.get(\"/projects/:slug/settings/webhooks\", async (c) =>\n c.html(await renderSettingsPage(c, \"webhooks\")),\n );\n app.post(\"/projects/:slug/settings/webhooks\", handleCreateWebhook);\n app.post(\"/projects/:slug/settings/webhooks/:webhookId/delete\", handleDeleteWebhook);\n}\n\n/** Validate a webhook URL, returning the error message or null when valid. */\nfunction webhookUrlError(url: string | undefined): string | null {\n if (!url) {\n return \"A valid URL is required\";\n }\n let protocol: string;\n try {\n protocol = new URL(url).protocol;\n } catch {\n return \"Enter a valid URL\";\n }\n if (protocol !== \"https:\" && protocol !== \"http:\") {\n return \"URL must use http or https\";\n }\n return null;\n}\n\n/** Parse the comma-separated events field. */\nfunction parseWebhookEvents(form: FormData): string[] | undefined {\n const eventsRaw = asString(form.get(\"events\"));\n if (!eventsRaw) {\n return undefined;\n }\n return eventsRaw\n .split(\",\")\n .map((event) => event.trim())\n .filter((event) => event.length > 0);\n}\n\nasync function handleCreateWebhook(c: Context): Promise<Response> {\n const project = await findProject(c.req.param(\"slug\") ?? \"\");\n const form = await c.req.formData();\n const url = asString(form.get(\"url\"));\n const urlError = webhookUrlError(url);\n if (urlError || !url) {\n return c.html(\n (await renderSettingsPage(c, \"webhooks\", { errors: { url: urlError ?? \"Invalid URL\" } })) ??\n \"\",\n 400,\n );\n }\n return await createWebhookRecord(c, project, url, parseWebhookEvents(form));\n}\n\nasync function createWebhookRecord(\n c: Context,\n project: Project,\n url: string,\n events: string[] | undefined,\n): Promise<Response> {\n const webhookModel = new WebhookModel(\n getStore().db,\n undefined,\n credentialKeys(getStore().config),\n );\n const secret = randomToken(\"whsec_\").value;\n await webhookModel.create(project.id, { url, events, secret });\n flash(c, \"Webhook created — copy the signing secret now\");\n return c.html((await renderSettingsPage(c, \"webhooks\", { secret })) ?? \"\", 201);\n}\n\nasync function handleDeleteWebhook(c: Context): Promise<Response> {\n const project = await findProject(c.req.param(\"slug\") ?? \"\");\n const webhook = await new WebhookModel(\n getStore().db,\n undefined,\n credentialKeys(getStore().config),\n ).get(project.id, c.req.param(\"webhookId\") ?? \"\");\n if (!webhook) {\n notFound(\"Webhook not found\");\n }\n await new WebhookModel(getStore().db, undefined, credentialKeys(getStore().config)).remove(\n project.id,\n webhook.id,\n );\n flash(c, \"Webhook deleted\");\n return hxRedirect(c, `/projects/${project.slug}/settings/webhooks`);\n}\n"],"mappings":";;;;;;;;;;;;;AAYA,SAAgB,wBAAwB,KAAwB;CAC9D,IAAI,IAAI,qCAAqC,OAAO,MAClD,EAAE,KAAK,MAAM,mBAAmB,GAAG,UAAU,CAAC,CAChD;CACA,IAAI,KAAK,qCAAqC,mBAAmB;CACjE,IAAI,KAAK,uDAAuD,mBAAmB;AACrF;;AAGA,SAAS,gBAAgB,KAAwC;CAC/D,IAAI,CAAC,KACH,OAAO;CAET,IAAI;CACJ,IAAI;EACF,WAAW,IAAI,IAAI,GAAG,CAAC,CAAC;CAC1B,QAAQ;EACN,OAAO;CACT;CACA,IAAI,aAAa,YAAY,aAAa,SACxC,OAAO;CAET,OAAO;AACT;;AAGA,SAAS,mBAAmB,MAAsC;CAChE,MAAM,YAAY,SAAS,KAAK,IAAI,QAAQ,CAAC;CAC7C,IAAI,CAAC,WACH;CAEF,OAAO,UACJ,MAAM,GAAG,CAAC,CACV,KAAK,UAAU,MAAM,KAAK,CAAC,CAAC,CAC5B,QAAQ,UAAU,MAAM,SAAS,CAAC;AACvC;AAEA,eAAe,oBAAoB,GAA+B;CAChE,MAAM,UAAU,MAAM,YAAY,EAAE,IAAI,MAAM,MAAM,KAAK,EAAE;CAC3D,MAAM,OAAO,MAAM,EAAE,IAAI,SAAS;CAClC,MAAM,MAAM,SAAS,KAAK,IAAI,KAAK,CAAC;CACpC,MAAM,WAAW,gBAAgB,GAAG;CACpC,IAAI,YAAY,CAAC,KACf,OAAO,EAAE,KACN,MAAM,mBAAmB,GAAG,YAAY,EAAE,QAAQ,EAAE,KAAK,YAAY,cAAc,EAAE,CAAC,KACrF,IACF,GACF;CAEF,OAAO,MAAM,oBAAoB,GAAG,SAAS,KAAK,mBAAmB,IAAI,CAAC;AAC5E;AAEA,eAAe,oBACb,GACA,SACA,KACA,QACmB;CACnB,MAAM,eAAe,IAAI,aACvB,SAAS,CAAC,CAAC,IACX,KAAA,GACA,eAAe,SAAS,CAAC,CAAC,MAAM,CAClC;CACA,MAAM,SAAS,YAAY,QAAQ,CAAC,CAAC;CACrC,MAAM,aAAa,OAAO,QAAQ,IAAI;EAAE;EAAK;EAAQ;CAAO,CAAC;CAC7D,MAAM,GAAG,+CAA+C;CACxD,OAAO,EAAE,KAAM,MAAM,mBAAmB,GAAG,YAAY,EAAE,OAAO,CAAC,KAAM,IAAI,GAAG;AAChF;AAEA,eAAe,oBAAoB,GAA+B;CAChE,MAAM,UAAU,MAAM,YAAY,EAAE,IAAI,MAAM,MAAM,KAAK,EAAE;CAC3D,MAAM,UAAU,MAAM,IAAI,aACxB,SAAS,CAAC,CAAC,IACX,KAAA,GACA,eAAe,SAAS,CAAC,CAAC,MAAM,CAClC,CAAC,CAAC,IAAI,QAAQ,IAAI,EAAE,IAAI,MAAM,WAAW,KAAK,EAAE;CAChD,IAAI,CAAC,SACH,SAAS,mBAAmB;CAE9B,MAAM,IAAI,aAAa,SAAS,CAAC,CAAC,IAAI,KAAA,GAAW,eAAe,SAAS,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAClF,QAAQ,IACR,QAAQ,EACV;CACA,MAAM,GAAG,iBAAiB;CAC1B,OAAO,WAAW,GAAG,aAAa,QAAQ,KAAK,mBAAmB;AACpE"}
1
+ {"version":3,"file":"settings-webhooks.mjs","names":[],"sources":["../../src/routers/settings-webhooks.ts"],"sourcesContent":["import { WebhookModel } from \"@storyshelf/core/models\";\nimport type { Project } from \"@storyshelf/core/schema\";\nimport type { Context } from \"hono\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { flash } from \"./flash.ts\";\nimport { notFound } from \"./helpers.ts\";\nimport { hxRedirect } from \"./htmx.ts\";\nimport { webhookModel } from \"./scoped-models.ts\";\nimport { asString, findProject, renderSettingsPage } from \"./settings.handlers.ts\";\n/** Webhook settings (create with one-time secret display, delete). */\nexport function registerWebhookSettings(app: ShelfRouter): void {\n app.get(\"/projects/:slug/settings/webhooks\", async (c) =>\n c.html(await renderSettingsPage(c, \"webhooks\")),\n );\n app.post(\"/projects/:slug/settings/webhooks\", handleCreateWebhook);\n app.post(\"/projects/:slug/settings/webhooks/:webhookId/delete\", handleDeleteWebhook);\n}\n\n/** Parse the comma-separated events field. */\nfunction parseWebhookEvents(form: FormData): string[] | undefined {\n const eventsRaw = asString(form.get(\"events\"));\n if (!eventsRaw) {\n return undefined;\n }\n return eventsRaw\n .split(\",\")\n .map((event) => event.trim())\n .filter((event) => event.length > 0);\n}\n\nasync function handleCreateWebhook(c: Context): Promise<Response> {\n const project = await findProject(c.req.param(\"slug\") ?? \"\");\n const form = await c.req.formData();\n const url = asString(form.get(\"url\"));\n const urlError = WebhookModel.urlError(url);\n if (urlError || !url) {\n return c.html(\n (await renderSettingsPage(c, \"webhooks\", { errors: { url: urlError ?? \"Invalid URL\" } })) ??\n \"\",\n 400,\n );\n }\n return await createWebhookRecord(c, project, url, parseWebhookEvents(form));\n}\n\nasync function createWebhookRecord(\n c: Context,\n project: Project,\n url: string,\n events: string[] | undefined,\n): Promise<Response> {\n const { secret } = await webhookModel().issue(project.id, { url, events });\n flash(c, \"Webhook created — copy the signing secret now\");\n return c.html((await renderSettingsPage(c, \"webhooks\", { secret })) ?? \"\", 201);\n}\n\nasync function handleDeleteWebhook(c: Context): Promise<Response> {\n const project = await findProject(c.req.param(\"slug\") ?? \"\");\n const webhook = await webhookModel().get(project.id, c.req.param(\"webhookId\") ?? \"\");\n if (!webhook) {\n notFound(\"Webhook not found\");\n }\n await webhookModel().remove(project.id, webhook.id);\n flash(c, \"Webhook deleted\");\n return hxRedirect(c, `/projects/${project.slug}/settings/webhooks`);\n}\n"],"mappings":";;;;;;;;;;;AAUA,SAAgB,wBAAwB,KAAwB;CAC9D,IAAI,IAAI,qCAAqC,OAAO,MAClD,EAAE,KAAK,MAAM,mBAAmB,GAAG,UAAU,CAAC,CAChD;CACA,IAAI,KAAK,qCAAqC,mBAAmB;CACjE,IAAI,KAAK,uDAAuD,mBAAmB;AACrF;;AAGA,SAAS,mBAAmB,MAAsC;CAChE,MAAM,YAAY,SAAS,KAAK,IAAI,QAAQ,CAAC;CAC7C,IAAI,CAAC,WACH;CAEF,OAAO,UACJ,MAAM,GAAG,CAAC,CACV,KAAK,UAAU,MAAM,KAAK,CAAC,CAAC,CAC5B,QAAQ,UAAU,MAAM,SAAS,CAAC;AACvC;AAEA,eAAe,oBAAoB,GAA+B;CAChE,MAAM,UAAU,MAAM,YAAY,EAAE,IAAI,MAAM,MAAM,KAAK,EAAE;CAC3D,MAAM,OAAO,MAAM,EAAE,IAAI,SAAS;CAClC,MAAM,MAAM,SAAS,KAAK,IAAI,KAAK,CAAC;CACpC,MAAM,WAAW,aAAa,SAAS,GAAG;CAC1C,IAAI,YAAY,CAAC,KACf,OAAO,EAAE,KACN,MAAM,mBAAmB,GAAG,YAAY,EAAE,QAAQ,EAAE,KAAK,YAAY,cAAc,EAAE,CAAC,KACrF,IACF,GACF;CAEF,OAAO,MAAM,oBAAoB,GAAG,SAAS,KAAK,mBAAmB,IAAI,CAAC;AAC5E;AAEA,eAAe,oBACb,GACA,SACA,KACA,QACmB;CACnB,MAAM,EAAE,WAAW,MAAM,aAAa,CAAC,CAAC,MAAM,QAAQ,IAAI;EAAE;EAAK;CAAO,CAAC;CACzE,MAAM,GAAG,+CAA+C;CACxD,OAAO,EAAE,KAAM,MAAM,mBAAmB,GAAG,YAAY,EAAE,OAAO,CAAC,KAAM,IAAI,GAAG;AAChF;AAEA,eAAe,oBAAoB,GAA+B;CAChE,MAAM,UAAU,MAAM,YAAY,EAAE,IAAI,MAAM,MAAM,KAAK,EAAE;CAC3D,MAAM,UAAU,MAAM,aAAa,CAAC,CAAC,IAAI,QAAQ,IAAI,EAAE,IAAI,MAAM,WAAW,KAAK,EAAE;CACnF,IAAI,CAAC,SACH,SAAS,mBAAmB;CAE9B,MAAM,aAAa,CAAC,CAAC,OAAO,QAAQ,IAAI,QAAQ,EAAE;CAClD,MAAM,GAAG,iBAAiB;CAC1B,OAAO,WAAW,GAAG,aAAa,QAAQ,KAAK,mBAAmB;AACpE"}