@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
@@ -2,7 +2,8 @@ import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
4
  import { getStore } from "../store.mjs";
5
- //#region src/middleware/auth-gate.ts
5
+ import { STORYBOOK_TOKEN_PREFIX } from "@storyshelf/core/utils";
6
+ // src/middleware/auth-gate.ts
6
7
  /** Resolve the request user from the auth adapter (null when auth is off or a token is used). */
7
8
  async function resolveRequestUser(c, auth) {
8
9
  if (!auth) return null;
@@ -13,13 +14,14 @@ async function resolveRequestUser(c, auth) {
13
14
  * Gate the server-rendered UI behind auth (ADR 0008): unauthenticated HTML
14
15
  * requests redirect to login. API/auth routes answer 401/403 themselves, and
15
16
  * published-Storybook routes enforce their own auth (public builds are
16
- * viewable without a session, ADR 0011).
17
+ * viewable without a session, ADR 0011), and `/_s/<token>` is answered by its signed token
18
+ * alone (ADR 0007).
17
19
  */
18
20
  function authGate() {
19
21
  return async (c, next) => {
20
22
  const { user, authEnabled: isAuthEnabled } = getStore();
21
23
  const { path } = c.req;
22
- if (!isAuthEnabled || user || path.startsWith("/api/") || path.startsWith("/auth/") || path.startsWith("/assets/") || path.startsWith("/.well-known/") || path.startsWith("/projects/") && path.includes("/storybook") || /^\/projects\/[^/]+\/?$/u.test(path)) {
24
+ if (!isAuthEnabled || user || path.startsWith("/api/") || path.startsWith("/auth/") || path.startsWith("/assets/") || path.startsWith(STORYBOOK_TOKEN_PREFIX) || path.startsWith("/.well-known/") || path.startsWith("/projects/") && path.includes("/storybook") || /^\/projects\/[^/]+\/?$/u.test(path)) {
23
25
  await next();
24
26
  return;
25
27
  }
@@ -30,7 +32,6 @@ function authGate() {
30
32
  return c.redirect("/auth/login", 302);
31
33
  };
32
34
  }
33
- //#endregion
34
35
  export { authGate, resolveRequestUser };
35
36
 
36
37
  //# sourceMappingURL=auth-gate.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"auth-gate.mjs","names":[],"sources":["../../src/middleware/auth-gate.ts"],"sourcesContent":["import type { Auth } from \"@storyshelf/core/auth\";\nimport type { AuthUser } from \"@storyshelf/core/types\";\nimport type { Context, Next } from \"hono\";\nimport { getStore } from \"../store.ts\";\n\n/** Resolve the request user from the auth adapter (null when auth is off or a token is used). */\nexport async function resolveRequestUser(\n c: Pick<Context, \"req\">,\n auth?: Auth,\n): Promise<AuthUser | null> {\n if (!auth) {\n return null;\n }\n if (c.req.header(\"authorization\")) {\n return null;\n }\n return await auth.check(c.req.raw);\n}\n\n/**\n * Gate the server-rendered UI behind auth (ADR 0008): unauthenticated HTML\n * requests redirect to login. API/auth routes answer 401/403 themselves, and\n * published-Storybook routes enforce their own auth (public builds are\n * viewable without a session, ADR 0011).\n */\nexport function authGate() {\n // oxlint-disable-next-line typescript/no-invalid-void-type -- Hono middleware may not return Response\n return async (c: Context, next: Next): Promise<Response | void> => {\n const { user, authEnabled: isAuthEnabled } = getStore();\n const { path } = c.req;\n if (\n !isAuthEnabled ||\n user ||\n path.startsWith(\"/api/\") ||\n path.startsWith(\"/auth/\") ||\n path.startsWith(\"/assets/\") ||\n path.startsWith(\"/.well-known/\") ||\n (path.startsWith(\"/projects/\") && path.includes(\"/storybook\")) ||\n /^\\/projects\\/[^/]+\\/?$/u.test(path)\n ) {\n await next();\n return;\n }\n if (c.req.header(\"HX-Request\") === \"true\") {\n c.header(\"HX-Redirect\", \"/auth/login\");\n return c.body(null, 204);\n }\n return c.redirect(\"/auth/login\", 302);\n };\n}\n"],"mappings":";;;;;;AAMA,eAAsB,mBACpB,GACA,MAC0B;CAC1B,IAAI,CAAC,MACH,OAAO;CAET,IAAI,EAAE,IAAI,OAAO,eAAe,GAC9B,OAAO;CAET,OAAO,MAAM,KAAK,MAAM,EAAE,IAAI,GAAG;AACnC;;;;;;;AAQA,SAAgB,WAAW;CAEzB,OAAO,OAAO,GAAY,SAAyC;EACjE,MAAM,EAAE,MAAM,aAAa,kBAAkB,SAAS;EACtD,MAAM,EAAE,SAAS,EAAE;EACnB,IACE,CAAC,iBACD,QACA,KAAK,WAAW,OAAO,KACvB,KAAK,WAAW,QAAQ,KACxB,KAAK,WAAW,UAAU,KAC1B,KAAK,WAAW,eAAe,KAC9B,KAAK,WAAW,YAAY,KAAK,KAAK,SAAS,YAAY,KAC5D,0BAA0B,KAAK,IAAI,GACnC;GACA,MAAM,KAAK;GACX;EACF;EACA,IAAI,EAAE,IAAI,OAAO,YAAY,MAAM,QAAQ;GACzC,EAAE,OAAO,eAAe,aAAa;GACrC,OAAO,EAAE,KAAK,MAAM,GAAG;EACzB;EACA,OAAO,EAAE,SAAS,eAAe,GAAG;CACtC;AACF"}
1
+ {"version":3,"file":"auth-gate.mjs","names":[],"sources":["../../src/middleware/auth-gate.ts"],"sourcesContent":["import type { Auth } from \"@storyshelf/core/auth\";\nimport type { AuthUser } from \"@storyshelf/core/types\";\nimport { STORYBOOK_TOKEN_PREFIX } from \"@storyshelf/core/utils\";\nimport type { Context, Next } from \"hono\";\nimport { getStore } from \"../store.ts\";\n\n/** Resolve the request user from the auth adapter (null when auth is off or a token is used). */\nexport async function resolveRequestUser(\n c: Pick<Context, \"req\">,\n auth?: Auth,\n): Promise<AuthUser | null> {\n if (!auth) {\n return null;\n }\n if (c.req.header(\"authorization\")) {\n return null;\n }\n return await auth.check(c.req.raw);\n}\n\n/**\n * Gate the server-rendered UI behind auth (ADR 0008): unauthenticated HTML\n * requests redirect to login. API/auth routes answer 401/403 themselves, and\n * published-Storybook routes enforce their own auth (public builds are\n * viewable without a session, ADR 0011), and `/_s/<token>` is answered by its signed token\n * alone (ADR 0007).\n */\nexport function authGate() {\n // oxlint-disable-next-line typescript/no-invalid-void-type -- Hono middleware may not return Response\n return async (c: Context, next: Next): Promise<Response | void> => {\n const { user, authEnabled: isAuthEnabled } = getStore();\n const { path } = c.req;\n if (\n !isAuthEnabled ||\n user ||\n path.startsWith(\"/api/\") ||\n path.startsWith(\"/auth/\") ||\n path.startsWith(\"/assets/\") ||\n path.startsWith(STORYBOOK_TOKEN_PREFIX) ||\n path.startsWith(\"/.well-known/\") ||\n (path.startsWith(\"/projects/\") && path.includes(\"/storybook\")) ||\n /^\\/projects\\/[^/]+\\/?$/u.test(path)\n ) {\n await next();\n return;\n }\n if (c.req.header(\"HX-Request\") === \"true\") {\n c.header(\"HX-Redirect\", \"/auth/login\");\n return c.body(null, 204);\n }\n return c.redirect(\"/auth/login\", 302);\n };\n}\n"],"mappings":";;;;;;;AAOA,eAAsB,mBACpB,GACA,MAC0B;CAC1B,IAAI,CAAC,MACH,OAAO;CAET,IAAI,EAAE,IAAI,OAAO,eAAe,GAC9B,OAAO;CAET,OAAO,MAAM,KAAK,MAAM,EAAE,IAAI,GAAG;AACnC;;;;;;;;AASA,SAAgB,WAAW;CAEzB,OAAO,OAAO,GAAY,SAAyC;EACjE,MAAM,EAAE,MAAM,aAAa,kBAAkB,SAAS;EACtD,MAAM,EAAE,SAAS,EAAE;EACnB,IACE,CAAC,iBACD,QACA,KAAK,WAAW,OAAO,KACvB,KAAK,WAAW,QAAQ,KACxB,KAAK,WAAW,UAAU,KAC1B,KAAK,WAAW,sBAAsB,KACtC,KAAK,WAAW,eAAe,KAC9B,KAAK,WAAW,YAAY,KAAK,KAAK,SAAS,YAAY,KAC5D,0BAA0B,KAAK,IAAI,GACnC;GACA,MAAM,KAAK;GACX;EACF;EACA,IAAI,EAAE,IAAI,OAAO,YAAY,MAAM,QAAQ;GACzC,EAAE,OAAO,eAAe,aAAa;GACrC,OAAO,EAAE,KAAK,MAAM,GAAG;EACzB;EACA,OAAO,EAAE,SAAS,eAAe,GAAG;CACtC;AACF"}
@@ -4,7 +4,7 @@ import.meta.url;
4
4
  import { timingSafeEqualString } from "@storyshelf/core/utils";
5
5
  import { SESSION_COOKIE } from "@storyshelf/core/types";
6
6
  import { createHash, randomBytes } from "node:crypto";
7
- //#region src/middleware/csrf.ts
7
+ // src/middleware/csrf.ts
8
8
  const DEFAULT_SESSION = "default";
9
9
  const FALLBACK_SECRET = randomBytes(32).toString("hex");
10
10
  /** Use the app secret when provided so tokens survive restarts and instances. */
@@ -69,7 +69,6 @@ function csrf(secret) {
69
69
  function getCsrfToken(secret, sessionId = DEFAULT_SESSION) {
70
70
  return generateToken(resolveSecret(secret), sessionId);
71
71
  }
72
- //#endregion
73
72
  export { csrf, getCsrfToken, sessionIdFrom };
74
73
 
75
74
  //# sourceMappingURL=csrf.mjs.map
@@ -1,7 +1,7 @@
1
1
  import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
- //#region src/middleware/rate-limit.ts
4
+ // src/middleware/rate-limit.ts
5
5
  /** Direct peer IP from server runtimes exposing the socket (unspoofable TCP source). */
6
6
  function peerIp(c) {
7
7
  const env = c.env ?? {};
@@ -25,44 +25,51 @@ function rateLimitKey(prefix) {
25
25
  return `${prefix}:anonymous`;
26
26
  };
27
27
  }
28
- const stores = /* @__PURE__ */ new Map();
29
28
  /** Cap tracked buckets (spoofed keys would otherwise grow the map forever). */
30
29
  const MAX_BUCKETS = 1e4;
31
- function cleanup() {
32
- const now = Date.now();
33
- for (const [key, entry] of stores) if (now > entry.resetAt) stores.delete(key);
34
- }
35
- setInterval(cleanup, 6e4).unref();
36
- /** Hono middleware limiting requests per key within a sliding window. */
37
- function rateLimit(options) {
38
- const { windowMs, max, keyGenerator } = options;
39
- return async (c, next) => {
40
- const key = keyGenerator ? keyGenerator(c) : rateLimitKey("api")(c);
41
- const now = Date.now();
42
- let entry = stores.get(key);
43
- if (!entry || now > entry.resetAt) {
44
- entry = {
45
- count: 0,
46
- resetAt: now + windowMs
47
- };
48
- if (stores.size >= MAX_BUCKETS) {
49
- const oldest = stores.keys().next();
50
- if (!oldest.done) stores.delete(oldest.value);
30
+ const SWEEP_INTERVAL_MS = 6e4;
31
+ /**
32
+ * Create a rate limiter with its own bucket store, so each app instance
33
+ * counts independently. Limiters built from the same `rateLimit` factory that
34
+ * resolve to the same key share one counter (e.g. `/api/v1/*` and
35
+ * `/api/v1/tokens/*` both bill the caller's `api:` bucket).
36
+ */
37
+ function createRateLimiter() {
38
+ const stores = /* @__PURE__ */ new Map();
39
+ let lastSweep = Date.now();
40
+ const sweep = (now) => {
41
+ lastSweep = now;
42
+ for (const [key, entry] of stores) if (now > entry.resetAt) stores.delete(key);
43
+ };
44
+ return (options) => {
45
+ const { windowMs, max, keyGenerator } = options;
46
+ return async (c, next) => {
47
+ const key = keyGenerator ? keyGenerator(c) : rateLimitKey("api")(c);
48
+ const now = Date.now();
49
+ if (now - lastSweep > SWEEP_INTERVAL_MS) sweep(now);
50
+ let entry = stores.get(key);
51
+ if (!entry || now > entry.resetAt) {
52
+ entry = {
53
+ count: 0,
54
+ resetAt: now + windowMs
55
+ };
56
+ if (stores.size >= MAX_BUCKETS) {
57
+ const oldest = stores.keys().next();
58
+ if (!oldest.done) stores.delete(oldest.value);
59
+ }
60
+ stores.set(key, entry);
61
+ }
62
+ entry.count += 1;
63
+ if (entry.count > max) {
64
+ c.header("Retry-After", String(Math.ceil((entry.resetAt - now) / 1e3)));
65
+ return c.json({ error: "Too many requests" }, 429);
51
66
  }
52
- stores.set(key, entry);
53
- }
54
- entry.count += 1;
55
- if (entry.count > max) {
56
- const retryAfter = Math.ceil((entry.resetAt - now) / 1e3);
57
- c.header("Retry-After", String(retryAfter));
58
- return c.json({ error: "Too many requests" }, 429);
59
- }
60
- c.header("X-RateLimit-Limit", String(max));
61
- c.header("X-RateLimit-Remaining", String(Math.max(0, max - entry.count)));
62
- await next();
67
+ c.header("X-RateLimit-Limit", String(max));
68
+ c.header("X-RateLimit-Remaining", String(Math.max(0, max - entry.count)));
69
+ return await next();
70
+ };
63
71
  };
64
72
  }
65
- //#endregion
66
- export { rateLimit, rateLimitKey };
73
+ export { createRateLimiter, rateLimitKey };
67
74
 
68
75
  //# sourceMappingURL=rate-limit.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"rate-limit.mjs","names":[],"sources":["../../src/middleware/rate-limit.ts"],"sourcesContent":["import type { Context, Next } from \"hono\";\n\ninterface RateLimitEntry {\n count: number;\n resetAt: number;\n}\n\ninterface RateLimitOptions {\n windowMs: number;\n max: number;\n keyGenerator?: (c: Context) => string;\n}\n\n/** Direct peer IP from server runtimes exposing the socket (unspoofable TCP source). */\nfunction peerIp(c: Pick<Context, \"env\">): string | undefined {\n const env = (c.env ?? {}) as {\n incoming?: { socket?: { remoteAddress?: unknown } };\n server?: { incoming?: { socket?: { remoteAddress?: unknown } } };\n };\n const address =\n env.incoming?.socket?.remoteAddress ?? env.server?.incoming?.socket?.remoteAddress;\n return typeof address === \"string\" && address ? address : undefined;\n}\n\n/**\n * Rate-limit identity for one bucket prefix: direct peer first, then the\n * first `X-Forwarded-For` entry (client-controlled, so only a fallback),\n * then `X-Real-IP`, then a shared bucket. Deployments behind proxies with\n * many users behind one peer IP share that peer's budget.\n */\nexport function rateLimitKey(prefix: string): (c: Context) => string {\n return (c) => {\n const peer = peerIp(c);\n if (peer) {\n return `${prefix}:${peer}`;\n }\n const forwarded = c.req.header(\"x-forwarded-for\")?.split(\",\")[0]?.trim();\n if (forwarded) {\n return `${prefix}:fwd:${forwarded}`;\n }\n const real = c.req.header(\"x-real-ip\")?.trim();\n if (real) {\n return `${prefix}:fwd:${real}`;\n }\n return `${prefix}:anonymous`;\n };\n}\n\nconst stores = new Map<string, RateLimitEntry>();\n\n/** Cap tracked buckets (spoofed keys would otherwise grow the map forever). */\nconst MAX_BUCKETS = 10_000;\n\nfunction cleanup(): void {\n const now = Date.now();\n for (const [key, entry] of stores) {\n if (now > entry.resetAt) {\n stores.delete(key);\n }\n }\n}\n\nsetInterval(cleanup, 60_000).unref();\n\n/** Hono middleware limiting requests per key within a sliding window. */\nexport function rateLimit(options: RateLimitOptions) {\n const { windowMs, max, keyGenerator } = options;\n // oxlint-disable-next-line typescript/no-invalid-void-type -- Hono middleware may not return Response\n return async (c: Context, next: Next): Promise<Response | void> => {\n const key = keyGenerator ? keyGenerator(c) : rateLimitKey(\"api\")(c);\n const now = Date.now();\n let entry = stores.get(key);\n if (!entry || now > entry.resetAt) {\n entry = { count: 0, resetAt: now + windowMs };\n if (stores.size >= MAX_BUCKETS) {\n const oldest = stores.keys().next();\n if (!oldest.done) {\n stores.delete(oldest.value);\n }\n }\n stores.set(key, entry);\n }\n entry.count += 1;\n if (entry.count > max) {\n const retryAfter = Math.ceil((entry.resetAt - now) / 1000);\n c.header(\"Retry-After\", String(retryAfter));\n return c.json({ error: \"Too many requests\" }, 429);\n }\n c.header(\"X-RateLimit-Limit\", String(max));\n c.header(\"X-RateLimit-Remaining\", String(Math.max(0, max - entry.count)));\n await next();\n };\n}\n"],"mappings":";;;;;AAcA,SAAS,OAAO,GAA6C;CAC3D,MAAM,MAAO,EAAE,OAAO,CAAC;CAIvB,MAAM,UACJ,IAAI,UAAU,QAAQ,iBAAiB,IAAI,QAAQ,UAAU,QAAQ;CACvE,OAAO,OAAO,YAAY,YAAY,UAAU,UAAU,KAAA;AAC5D;;;;;;;AAQA,SAAgB,aAAa,QAAwC;CACnE,QAAQ,MAAM;EACZ,MAAM,OAAO,OAAO,CAAC;EACrB,IAAI,MACF,OAAO,GAAG,OAAO,GAAG;EAEtB,MAAM,YAAY,EAAE,IAAI,OAAO,iBAAiB,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,EAAE,EAAE,KAAK;EACvE,IAAI,WACF,OAAO,GAAG,OAAO,OAAO;EAE1B,MAAM,OAAO,EAAE,IAAI,OAAO,WAAW,CAAC,EAAE,KAAK;EAC7C,IAAI,MACF,OAAO,GAAG,OAAO,OAAO;EAE1B,OAAO,GAAG,OAAO;CACnB;AACF;AAEA,MAAM,yBAAS,IAAI,IAA4B;;AAG/C,MAAM,cAAc;AAEpB,SAAS,UAAgB;CACvB,MAAM,MAAM,KAAK,IAAI;CACrB,KAAK,MAAM,CAAC,KAAK,UAAU,QACzB,IAAI,MAAM,MAAM,SACd,OAAO,OAAO,GAAG;AAGvB;AAEA,YAAY,SAAS,GAAM,CAAC,CAAC,MAAM;;AAGnC,SAAgB,UAAU,SAA2B;CACnD,MAAM,EAAE,UAAU,KAAK,iBAAiB;CAExC,OAAO,OAAO,GAAY,SAAyC;EACjE,MAAM,MAAM,eAAe,aAAa,CAAC,IAAI,aAAa,KAAK,CAAC,CAAC,CAAC;EAClE,MAAM,MAAM,KAAK,IAAI;EACrB,IAAI,QAAQ,OAAO,IAAI,GAAG;EAC1B,IAAI,CAAC,SAAS,MAAM,MAAM,SAAS;GACjC,QAAQ;IAAE,OAAO;IAAG,SAAS,MAAM;GAAS;GAC5C,IAAI,OAAO,QAAQ,aAAa;IAC9B,MAAM,SAAS,OAAO,KAAK,CAAC,CAAC,KAAK;IAClC,IAAI,CAAC,OAAO,MACV,OAAO,OAAO,OAAO,KAAK;GAE9B;GACA,OAAO,IAAI,KAAK,KAAK;EACvB;EACA,MAAM,SAAS;EACf,IAAI,MAAM,QAAQ,KAAK;GACrB,MAAM,aAAa,KAAK,MAAM,MAAM,UAAU,OAAO,GAAI;GACzD,EAAE,OAAO,eAAe,OAAO,UAAU,CAAC;GAC1C,OAAO,EAAE,KAAK,EAAE,OAAO,oBAAoB,GAAG,GAAG;EACnD;EACA,EAAE,OAAO,qBAAqB,OAAO,GAAG,CAAC;EACzC,EAAE,OAAO,yBAAyB,OAAO,KAAK,IAAI,GAAG,MAAM,MAAM,KAAK,CAAC,CAAC;EACxE,MAAM,KAAK;CACb;AACF"}
1
+ {"version":3,"file":"rate-limit.mjs","names":[],"sources":["../../src/middleware/rate-limit.ts"],"sourcesContent":["import type { Context, MiddlewareHandler } from \"hono\";\n\ninterface RateLimitEntry {\n count: number;\n resetAt: number;\n}\n\nexport interface RateLimitOptions {\n windowMs: number;\n max: number;\n keyGenerator?: (c: Context) => string;\n}\n\n/** Direct peer IP from server runtimes exposing the socket (unspoofable TCP source). */\nfunction peerIp(c: Pick<Context, \"env\">): string | undefined {\n const env = (c.env ?? {}) as {\n incoming?: { socket?: { remoteAddress?: unknown } };\n server?: { incoming?: { socket?: { remoteAddress?: unknown } } };\n };\n const address =\n env.incoming?.socket?.remoteAddress ?? env.server?.incoming?.socket?.remoteAddress;\n return typeof address === \"string\" && address ? address : undefined;\n}\n\n/**\n * Rate-limit identity for one bucket prefix: direct peer first, then the\n * first `X-Forwarded-For` entry (client-controlled, so only a fallback),\n * then `X-Real-IP`, then a shared bucket. Deployments behind proxies with\n * many users behind one peer IP share that peer's budget.\n */\nexport function rateLimitKey(prefix: string): (c: Context) => string {\n return (c) => {\n const peer = peerIp(c);\n if (peer) {\n return `${prefix}:${peer}`;\n }\n const forwarded = c.req.header(\"x-forwarded-for\")?.split(\",\")[0]?.trim();\n if (forwarded) {\n return `${prefix}:fwd:${forwarded}`;\n }\n const real = c.req.header(\"x-real-ip\")?.trim();\n if (real) {\n return `${prefix}:fwd:${real}`;\n }\n return `${prefix}:anonymous`;\n };\n}\n\n/** Cap tracked buckets (spoofed keys would otherwise grow the map forever). */\nconst MAX_BUCKETS = 10_000;\nconst SWEEP_INTERVAL_MS = 60_000;\n\n/**\n * Create a rate limiter with its own bucket store, so each app instance\n * counts independently. Limiters built from the same `rateLimit` factory that\n * resolve to the same key share one counter (e.g. `/api/v1/*` and\n * `/api/v1/tokens/*` both bill the caller's `api:` bucket).\n */\nexport function createRateLimiter(): (options: RateLimitOptions) => MiddlewareHandler {\n const stores = new Map<string, RateLimitEntry>();\n let lastSweep = Date.now();\n\n const sweep = (now: number): void => {\n lastSweep = now;\n for (const [key, entry] of stores) {\n if (now > entry.resetAt) {\n stores.delete(key);\n }\n }\n };\n\n return (options) => {\n const { windowMs, max, keyGenerator } = options;\n return async (c, next) => {\n const key = keyGenerator ? keyGenerator(c) : rateLimitKey(\"api\")(c);\n const now = Date.now();\n if (now - lastSweep > SWEEP_INTERVAL_MS) {\n sweep(now);\n }\n let entry = stores.get(key);\n if (!entry || now > entry.resetAt) {\n entry = { count: 0, resetAt: now + windowMs };\n if (stores.size >= MAX_BUCKETS) {\n const oldest = stores.keys().next();\n if (!oldest.done) {\n stores.delete(oldest.value);\n }\n }\n stores.set(key, entry);\n }\n entry.count += 1;\n if (entry.count > max) {\n c.header(\"Retry-After\", String(Math.ceil((entry.resetAt - now) / 1000)));\n return c.json({ error: \"Too many requests\" }, 429);\n }\n c.header(\"X-RateLimit-Limit\", String(max));\n c.header(\"X-RateLimit-Remaining\", String(Math.max(0, max - entry.count)));\n return await next();\n };\n };\n}\n"],"mappings":";;;;;AAcA,SAAS,OAAO,GAA6C;CAC3D,MAAM,MAAO,EAAE,OAAO,CAAC;CAIvB,MAAM,UACJ,IAAI,UAAU,QAAQ,iBAAiB,IAAI,QAAQ,UAAU,QAAQ;CACvE,OAAO,OAAO,YAAY,YAAY,UAAU,UAAU,KAAA;AAC5D;;;;;;;AAQA,SAAgB,aAAa,QAAwC;CACnE,QAAQ,MAAM;EACZ,MAAM,OAAO,OAAO,CAAC;EACrB,IAAI,MACF,OAAO,GAAG,OAAO,GAAG;EAEtB,MAAM,YAAY,EAAE,IAAI,OAAO,iBAAiB,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,EAAE,EAAE,KAAK;EACvE,IAAI,WACF,OAAO,GAAG,OAAO,OAAO;EAE1B,MAAM,OAAO,EAAE,IAAI,OAAO,WAAW,CAAC,EAAE,KAAK;EAC7C,IAAI,MACF,OAAO,GAAG,OAAO,OAAO;EAE1B,OAAO,GAAG,OAAO;CACnB;AACF;;AAGA,MAAM,cAAc;AACpB,MAAM,oBAAoB;;;;;;;AAQ1B,SAAgB,oBAAsE;CACpF,MAAM,yBAAS,IAAI,IAA4B;CAC/C,IAAI,YAAY,KAAK,IAAI;CAEzB,MAAM,SAAS,QAAsB;EACnC,YAAY;EACZ,KAAK,MAAM,CAAC,KAAK,UAAU,QACzB,IAAI,MAAM,MAAM,SACd,OAAO,OAAO,GAAG;CAGvB;CAEA,QAAQ,YAAY;EAClB,MAAM,EAAE,UAAU,KAAK,iBAAiB;EACxC,OAAO,OAAO,GAAG,SAAS;GACxB,MAAM,MAAM,eAAe,aAAa,CAAC,IAAI,aAAa,KAAK,CAAC,CAAC,CAAC;GAClE,MAAM,MAAM,KAAK,IAAI;GACrB,IAAI,MAAM,YAAY,mBACpB,MAAM,GAAG;GAEX,IAAI,QAAQ,OAAO,IAAI,GAAG;GAC1B,IAAI,CAAC,SAAS,MAAM,MAAM,SAAS;IACjC,QAAQ;KAAE,OAAO;KAAG,SAAS,MAAM;IAAS;IAC5C,IAAI,OAAO,QAAQ,aAAa;KAC9B,MAAM,SAAS,OAAO,KAAK,CAAC,CAAC,KAAK;KAClC,IAAI,CAAC,OAAO,MACV,OAAO,OAAO,OAAO,KAAK;IAE9B;IACA,OAAO,IAAI,KAAK,KAAK;GACvB;GACA,MAAM,SAAS;GACf,IAAI,MAAM,QAAQ,KAAK;IACrB,EAAE,OAAO,eAAe,OAAO,KAAK,MAAM,MAAM,UAAU,OAAO,GAAI,CAAC,CAAC;IACvE,OAAO,EAAE,KAAK,EAAE,OAAO,oBAAoB,GAAG,GAAG;GACnD;GACA,EAAE,OAAO,qBAAqB,OAAO,GAAG,CAAC;GACzC,EAAE,OAAO,yBAAyB,OAAO,KAAK,IAAI,GAAG,MAAM,MAAM,KAAK,CAAC,CAAC;GACxE,OAAO,MAAM,KAAK;EACpB;CACF;AACF"}
@@ -1,7 +1,8 @@
1
1
  import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
- //#region src/middleware/request-log.ts
4
+ import { redactSecrets } from "@storyshelf/core/utils";
5
+ // src/middleware/request-log.ts
5
6
  /** Hono middleware logging request start/end with duration. */
6
7
  function requestLogging(logger) {
7
8
  return async (c, next) => {
@@ -10,21 +11,20 @@ function requestLogging(logger) {
10
11
  logger.info({
11
12
  reqId: id,
12
13
  method: c.req.method,
13
- url: c.req.path
14
+ url: redactSecrets(c.req.path)
14
15
  }, "request start");
15
16
  await next();
16
17
  const userId = c.get("userId");
17
18
  logger.info({
18
19
  reqId: id,
19
20
  method: c.req.method,
20
- url: c.req.path,
21
+ url: redactSecrets(c.req.path),
21
22
  status: c.res.status,
22
23
  durationMs: Math.round(performance.now() - started),
23
24
  ...userId ? { userId } : {}
24
25
  }, "request end");
25
26
  };
26
27
  }
27
- //#endregion
28
28
  export { requestLogging };
29
29
 
30
30
  //# sourceMappingURL=request-log.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"request-log.mjs","names":[],"sources":["../../src/middleware/request-log.ts"],"sourcesContent":["import type { Logger } from \"@storyshelf/core/logger\";\nimport type { Context, Next } from \"hono\";\n\n/** Hono middleware logging request start/end with duration. */\nexport function requestLogging(logger: Logger) {\n // oxlint-disable-next-line typescript/no-invalid-void-type -- Hono middleware may not return Response\n return async (c: Context, next: Next): Promise<Response | void> => {\n const started = performance.now();\n const id = c.get(\"requestId\");\n logger.info({ reqId: id, method: c.req.method, url: c.req.path }, \"request start\");\n await next();\n const userId = c.get(\"userId\") as string | null | undefined;\n logger.info(\n {\n reqId: id,\n method: c.req.method,\n url: c.req.path,\n status: c.res.status,\n durationMs: Math.round(performance.now() - started),\n ...(userId ? { userId } : {}),\n },\n \"request end\",\n );\n };\n}\n"],"mappings":";;;;;AAIA,SAAgB,eAAe,QAAgB;CAE7C,OAAO,OAAO,GAAY,SAAyC;EACjE,MAAM,UAAU,YAAY,IAAI;EAChC,MAAM,KAAK,EAAE,IAAI,WAAW;EAC5B,OAAO,KAAK;GAAE,OAAO;GAAI,QAAQ,EAAE,IAAI;GAAQ,KAAK,EAAE,IAAI;EAAK,GAAG,eAAe;EACjF,MAAM,KAAK;EACX,MAAM,SAAS,EAAE,IAAI,QAAQ;EAC7B,OAAO,KACL;GACE,OAAO;GACP,QAAQ,EAAE,IAAI;GACd,KAAK,EAAE,IAAI;GACX,QAAQ,EAAE,IAAI;GACd,YAAY,KAAK,MAAM,YAAY,IAAI,IAAI,OAAO;GAClD,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;EAC7B,GACA,aACF;CACF;AACF"}
1
+ {"version":3,"file":"request-log.mjs","names":[],"sources":["../../src/middleware/request-log.ts"],"sourcesContent":["import type { Logger } from \"@storyshelf/core/logger\";\nimport { redactSecrets } from \"@storyshelf/core/utils\";\nimport type { Context, Next } from \"hono\";\n\n/** Hono middleware logging request start/end with duration. */\nexport function requestLogging(logger: Logger) {\n // oxlint-disable-next-line typescript/no-invalid-void-type -- Hono middleware may not return Response\n return async (c: Context, next: Next): Promise<Response | void> => {\n const started = performance.now();\n const id = c.get(\"requestId\");\n logger.info(\n { reqId: id, method: c.req.method, url: redactSecrets(c.req.path) },\n \"request start\",\n );\n await next();\n const userId = c.get(\"userId\") as string | null | undefined;\n logger.info(\n {\n reqId: id,\n method: c.req.method,\n url: redactSecrets(c.req.path),\n status: c.res.status,\n durationMs: Math.round(performance.now() - started),\n ...(userId ? { userId } : {}),\n },\n \"request end\",\n );\n };\n}\n"],"mappings":";;;;;;AAKA,SAAgB,eAAe,QAAgB;CAE7C,OAAO,OAAO,GAAY,SAAyC;EACjE,MAAM,UAAU,YAAY,IAAI;EAChC,MAAM,KAAK,EAAE,IAAI,WAAW;EAC5B,OAAO,KACL;GAAE,OAAO;GAAI,QAAQ,EAAE,IAAI;GAAQ,KAAK,cAAc,EAAE,IAAI,IAAI;EAAE,GAClE,eACF;EACA,MAAM,KAAK;EACX,MAAM,SAAS,EAAE,IAAI,QAAQ;EAC7B,OAAO,KACL;GACE,OAAO;GACP,QAAQ,EAAE,IAAI;GACd,KAAK,cAAc,EAAE,IAAI,IAAI;GAC7B,QAAQ,EAAE,IAAI;GACd,YAAY,KAAK,MAAM,YAAY,IAAI,IAAI,OAAO;GAClD,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;EAC7B,GACA,aACF;CACF;AACF"}
@@ -0,0 +1,50 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { matchedRouteClass } from "./require-testing.mjs";
5
+ import { resolveAuthorizedProject } from "../routers/helpers.mjs";
6
+ // src/middleware/require-review.ts
7
+ /** Body of the 404 a `review` route answers for a project with review off. */
8
+ const REVIEW_DISABLED_MESSAGE = "Review is off for this project.";
9
+ /** Roles that may see a project's routes at all; the gate answers only to them. */
10
+ const VIEWER_ROLES = [
11
+ "viewer",
12
+ "developer",
13
+ "approver",
14
+ "admin"
15
+ ];
16
+ /** The project slug of `/api/v1/projects/:slug/...` and `/projects/:slug/...` paths. */
17
+ function projectSlugOf(path) {
18
+ return /^(?:\/api\/v1)?\/projects\/(?<slug>[^/]+)\//u.exec(path)?.groups?.["slug"];
19
+ }
20
+ /**
21
+ * Gate routes classified `review` (ADR 0013, decision 10): they answer 404 for a project that has
22
+ * review off, as if they did not exist. The switch is read from the database on every request, so
23
+ * turning review off or on takes effect at once. Only a caller who may view the project is told
24
+ * (anyone else gets the route's own refusal), so the gate reveals nothing about a project the
25
+ * caller cannot see. It runs after the store scope and the auth gate, which it needs to resolve
26
+ * the caller; routes of other classes, and unknown projects, pass through.
27
+ */
28
+ function requireReview() {
29
+ return async (c, next) => {
30
+ if (matchedRouteClass(c) !== "review" || !await reviewIsOff(c)) {
31
+ await next();
32
+ return;
33
+ }
34
+ if (c.req.path.startsWith("/api/")) return c.json({ error: REVIEW_DISABLED_MESSAGE }, 404);
35
+ return c.text(REVIEW_DISABLED_MESSAGE, 404);
36
+ };
37
+ }
38
+ /** Whether the request names a project the caller may view and whose review switch is off. */
39
+ async function reviewIsOff(c) {
40
+ const slug = projectSlugOf(c.req.path);
41
+ if (slug === void 0) return false;
42
+ try {
43
+ return (await resolveAuthorizedProject(c, slug, ...VIEWER_ROLES)).reviewEnabled === false;
44
+ } catch {
45
+ return false;
46
+ }
47
+ }
48
+ export { REVIEW_DISABLED_MESSAGE, requireReview };
49
+
50
+ //# sourceMappingURL=require-review.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"require-review.mjs","names":[],"sources":["../../src/middleware/require-review.ts"],"sourcesContent":["import type { Context, Next } from \"hono\";\nimport { resolveAuthorizedProject } from \"../routers/helpers.ts\";\nimport { matchedRouteClass } from \"./require-testing.ts\";\n\n/** Body of the 404 a `review` route answers for a project with review off. */\nexport const REVIEW_DISABLED_MESSAGE = \"Review is off for this project.\";\n\n/** Roles that may see a project's routes at all; the gate answers only to them. */\nconst VIEWER_ROLES = [\"viewer\", \"developer\", \"approver\", \"admin\"] as const;\n\n/** The project slug of `/api/v1/projects/:slug/...` and `/projects/:slug/...` paths. */\nfunction projectSlugOf(path: string): string | undefined {\n return /^(?:\\/api\\/v1)?\\/projects\\/(?<slug>[^/]+)\\//u.exec(path)?.groups?.[\"slug\"];\n}\n\n/**\n * Gate routes classified `review` (ADR 0013, decision 10): they answer 404 for a project that has\n * review off, as if they did not exist. The switch is read from the database on every request, so\n * turning review off or on takes effect at once. Only a caller who may view the project is told\n * (anyone else gets the route's own refusal), so the gate reveals nothing about a project the\n * caller cannot see. It runs after the store scope and the auth gate, which it needs to resolve\n * the caller; routes of other classes, and unknown projects, pass through.\n */\nexport function requireReview() {\n // oxlint-disable-next-line typescript/no-invalid-void-type -- Hono middleware may not return Response\n return async (c: Context, next: Next): Promise<Response | void> => {\n if (matchedRouteClass(c) !== \"review\" || !(await reviewIsOff(c))) {\n await next();\n return;\n }\n if (c.req.path.startsWith(\"/api/\")) {\n return c.json({ error: REVIEW_DISABLED_MESSAGE }, 404);\n }\n return c.text(REVIEW_DISABLED_MESSAGE, 404);\n };\n}\n\n/** Whether the request names a project the caller may view and whose review switch is off. */\nasync function reviewIsOff(c: Context): Promise<boolean> {\n const slug = projectSlugOf(c.req.path);\n if (slug === undefined) {\n return false;\n }\n try {\n const project = await resolveAuthorizedProject(c, slug, ...VIEWER_ROLES);\n return project.reviewEnabled === false;\n } catch {\n return false;\n }\n}\n"],"mappings":";;;;;;;AAKA,MAAa,0BAA0B;;AAGvC,MAAM,eAAe;CAAC;CAAU;CAAa;CAAY;AAAO;;AAGhE,SAAS,cAAc,MAAkC;CACvD,OAAO,+CAA+C,KAAK,IAAI,CAAC,EAAE,SAAS;AAC7E;;;;;;;;;AAUA,SAAgB,gBAAgB;CAE9B,OAAO,OAAO,GAAY,SAAyC;EACjE,IAAI,kBAAkB,CAAC,MAAM,YAAY,CAAE,MAAM,YAAY,CAAC,GAAI;GAChE,MAAM,KAAK;GACX;EACF;EACA,IAAI,EAAE,IAAI,KAAK,WAAW,OAAO,GAC/B,OAAO,EAAE,KAAK,EAAE,OAAO,wBAAwB,GAAG,GAAG;EAEvD,OAAO,EAAE,KAAK,yBAAyB,GAAG;CAC5C;AACF;;AAGA,eAAe,YAAY,GAA8B;CACvD,MAAM,OAAO,cAAc,EAAE,IAAI,IAAI;CACrC,IAAI,SAAS,KAAA,GACX,OAAO;CAET,IAAI;EAEF,QAAO,MADe,yBAAyB,GAAG,MAAM,GAAG,YAAY,EAAA,CACxD,kBAAkB;CACnC,QAAQ;EACN,OAAO;CACT;AACF"}
@@ -0,0 +1,40 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { classOfRoute } from "../routes/classification.mjs";
5
+ import { matchedRoutes } from "hono/route";
6
+ // src/middleware/require-testing.ts
7
+ /** Body of the 404 a `testing` route answers on a hosting-only instance. */
8
+ const TESTING_DISABLED_MESSAGE = "Testing is not enabled on this instance.";
9
+ /**
10
+ * Gate routes classified `testing` (ADR 0006, decisions 1 and 8): without `options.testing` they
11
+ * answer 404, as if the feature did not exist. The class comes from the route table through the
12
+ * handler Hono matched, so a new route needs no guard of its own, only a table entry. It is mounted
13
+ * before the CSRF and rate-limit guards so a hidden route answers the same 404 for every request. `hosting`,
14
+ * `both` and `review` routes, and requests that match no classified route, pass through.
15
+ */
16
+ function requireTesting(enabled) {
17
+ return async (c, next) => {
18
+ if (enabled || !isTestingRoute(c)) {
19
+ await next();
20
+ return;
21
+ }
22
+ if (c.req.path.startsWith("/api/")) return c.json({ error: TESTING_DISABLED_MESSAGE }, 404);
23
+ return c.text(TESTING_DISABLED_MESSAGE, 404);
24
+ };
25
+ }
26
+ /** The last matched handler that the table classifies (middleware matches come first). */
27
+ function matchedRouteClass(c) {
28
+ const routes = matchedRoutes(c);
29
+ for (let index = routes.length - 1; index >= 0; index -= 1) {
30
+ const route = routes[index];
31
+ const routeClass = route && classOfRoute(route.method, route.path);
32
+ if (routeClass !== void 0) return routeClass;
33
+ }
34
+ }
35
+ function isTestingRoute(c) {
36
+ return matchedRouteClass(c) === "testing";
37
+ }
38
+ export { TESTING_DISABLED_MESSAGE, matchedRouteClass, requireTesting };
39
+
40
+ //# sourceMappingURL=require-testing.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"require-testing.mjs","names":[],"sources":["../../src/middleware/require-testing.ts"],"sourcesContent":["import type { Context, Next } from \"hono\";\nimport { matchedRoutes } from \"hono/route\";\nimport { classOfRoute, type RouteClass } from \"../routes/classification.ts\";\n\n/** Body of the 404 a `testing` route answers on a hosting-only instance. */\nexport const TESTING_DISABLED_MESSAGE = \"Testing is not enabled on this instance.\";\n\n/**\n * Gate routes classified `testing` (ADR 0006, decisions 1 and 8): without `options.testing` they\n * answer 404, as if the feature did not exist. The class comes from the route table through the\n * handler Hono matched, so a new route needs no guard of its own, only a table entry. It is mounted\n * before the CSRF and rate-limit guards so a hidden route answers the same 404 for every request. `hosting`,\n * `both` and `review` routes, and requests that match no classified route, pass through.\n */\nexport function requireTesting(enabled: boolean) {\n // oxlint-disable-next-line typescript/no-invalid-void-type -- Hono middleware may not return Response\n return async (c: Context, next: Next): Promise<Response | void> => {\n if (enabled || !isTestingRoute(c)) {\n await next();\n return;\n }\n if (c.req.path.startsWith(\"/api/\")) {\n return c.json({ error: TESTING_DISABLED_MESSAGE }, 404);\n }\n return c.text(TESTING_DISABLED_MESSAGE, 404);\n };\n}\n\n/** The last matched handler that the table classifies (middleware matches come first). */\nexport function matchedRouteClass(c: Context): RouteClass | undefined {\n const routes = matchedRoutes(c);\n for (let index = routes.length - 1; index >= 0; index -= 1) {\n const route = routes[index];\n const routeClass = route && classOfRoute(route.method, route.path);\n if (routeClass !== undefined) {\n return routeClass;\n }\n }\n return undefined;\n}\n\nfunction isTestingRoute(c: Context): boolean {\n return matchedRouteClass(c) === \"testing\";\n}\n"],"mappings":";;;;;;;AAKA,MAAa,2BAA2B;;;;;;;;AASxC,SAAgB,eAAe,SAAkB;CAE/C,OAAO,OAAO,GAAY,SAAyC;EACjE,IAAI,WAAW,CAAC,eAAe,CAAC,GAAG;GACjC,MAAM,KAAK;GACX;EACF;EACA,IAAI,EAAE,IAAI,KAAK,WAAW,OAAO,GAC/B,OAAO,EAAE,KAAK,EAAE,OAAO,yBAAyB,GAAG,GAAG;EAExD,OAAO,EAAE,KAAK,0BAA0B,GAAG;CAC7C;AACF;;AAGA,SAAgB,kBAAkB,GAAoC;CACpE,MAAM,SAAS,cAAc,CAAC;CAC9B,KAAK,IAAI,QAAQ,OAAO,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG;EAC1D,MAAM,QAAQ,OAAO;EACrB,MAAM,aAAa,SAAS,aAAa,MAAM,QAAQ,MAAM,IAAI;EACjE,IAAI,eAAe,KAAA,GACjB,OAAO;CAEX;AAEF;AAEA,SAAS,eAAe,GAAqB;CAC3C,OAAO,kBAAkB,CAAC,MAAM;AAClC"}
@@ -3,7 +3,7 @@ import "node:url";
3
3
  import.meta.url;
4
4
  import { runWithTimings } from "@storyshelf/core/utils";
5
5
  import { setMetric, timing } from "hono/timing";
6
- //#region src/middleware/server-timing.ts
6
+ // src/middleware/server-timing.ts
7
7
  /**
8
8
  * Opt-in `Server-Timing` response headers over Hono's timing middleware.
9
9
  *
@@ -33,7 +33,6 @@ function serverTiming(enabled) {
33
33
  if (headers.length > 0) c.res.headers.append("Server-Timing", headers.join(","));
34
34
  };
35
35
  }
36
- //#endregion
37
36
  export { serverTiming };
38
37
 
39
38
  //# sourceMappingURL=server-timing.mjs.map
@@ -2,7 +2,7 @@ import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
4
  import { sanitizeErrorText } from "@storyshelf/core/utils";
5
- //#region src/middleware/setup-gate.ts
5
+ // src/middleware/setup-gate.ts
6
6
  /** Failures safe to expose on the setup gate's 503 (redacted, no secrets). */
7
7
  function publicFailures(result) {
8
8
  return result.failures.map((failure) => ({
@@ -31,7 +31,6 @@ function setupGate(getReady) {
31
31
  await next();
32
32
  };
33
33
  }
34
- //#endregion
35
34
  export { setupGate };
36
35
 
37
36
  //# sourceMappingURL=setup-gate.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"setup-gate.mjs","names":[],"sources":["../../src/middleware/setup-gate.ts"],"sourcesContent":["import type { AdapterSetupResult } from \"@storyshelf/core/adapter/setup\";\nimport type { ShelfOptions } from \"@storyshelf/core/config\";\nimport { sanitizeErrorText } from \"@storyshelf/core/utils\";\nimport type { Context, Next } from \"hono\";\nimport type { QueueWiring } from \"../capture-setup.ts\";\nimport type { ServerRuntime } from \"../runtime.ts\";\n\nexport interface MiddlewareWiring extends ServerRuntime, QueueWiring {\n options: ShelfOptions;\n getReady: () => Promise<AdapterSetupResult>;\n}\n\n/** Failures safe to expose on the setup gate's 503 (redacted, no secrets). */\nfunction publicFailures(\n result: AdapterSetupResult,\n): { category: string; kind: string; error: string }[] {\n return result.failures.map((failure) => ({\n category: failure.category,\n kind: failure.kind,\n error: sanitizeErrorText(failure.error, 500),\n }));\n}\n\n/**\n * Block requests until adapter setup settles; answer 503 with the per-adapter\n * failures when setup failed. Health probes and the admin System page are\n * exempt (they report setup state themselves instead of being masked by\n * the gate — both are site-admin-gated where it matters).\n */\nexport function setupGate(getReady: () => Promise<AdapterSetupResult>) {\n // oxlint-disable-next-line typescript/no-invalid-void-type -- Hono middleware may not return Response\n return async (c: Context, next: Next): Promise<Response | void> => {\n if (c.req.path === \"/api/v1/health\" || c.req.path === \"/admin\") {\n await next();\n return;\n }\n const result = await getReady();\n if (!result.ok) {\n return c.json({ error: \"Adapters failed setup\", failures: publicFailures(result) }, 503);\n }\n await next();\n };\n}\n"],"mappings":";;;;;;AAaA,SAAS,eACP,QACqD;CACrD,OAAO,OAAO,SAAS,KAAK,aAAa;EACvC,UAAU,QAAQ;EAClB,MAAM,QAAQ;EACd,OAAO,kBAAkB,QAAQ,OAAO,GAAG;CAC7C,EAAE;AACJ;;;;;;;AAQA,SAAgB,UAAU,UAA6C;CAErE,OAAO,OAAO,GAAY,SAAyC;EACjE,IAAI,EAAE,IAAI,SAAS,oBAAoB,EAAE,IAAI,SAAS,UAAU;GAC9D,MAAM,KAAK;GACX;EACF;EACA,MAAM,SAAS,MAAM,SAAS;EAC9B,IAAI,CAAC,OAAO,IACV,OAAO,EAAE,KAAK;GAAE,OAAO;GAAyB,UAAU,eAAe,MAAM;EAAE,GAAG,GAAG;EAEzF,MAAM,KAAK;CACb;AACF"}
1
+ {"version":3,"file":"setup-gate.mjs","names":[],"sources":["../../src/middleware/setup-gate.ts"],"sourcesContent":["import type { AdapterSetupResult } from \"@storyshelf/core/adapter/setup\";\nimport type { ShelfOptions } from \"@storyshelf/core/config\";\nimport { sanitizeErrorText } from \"@storyshelf/core/utils\";\nimport type { Context, Next } from \"hono\";\nimport type { JobsWiring } from \"../jobs-setup.ts\";\nimport type { ServerRuntime } from \"../runtime.ts\";\n\nexport interface MiddlewareWiring extends ServerRuntime, Omit<JobsWiring, \"consumer\"> {\n options: ShelfOptions;\n getReady: () => Promise<AdapterSetupResult>;\n}\n\n/** Failures safe to expose on the setup gate's 503 (redacted, no secrets). */\nfunction publicFailures(\n result: AdapterSetupResult,\n): { category: string; kind: string; error: string }[] {\n return result.failures.map((failure) => ({\n category: failure.category,\n kind: failure.kind,\n error: sanitizeErrorText(failure.error, 500),\n }));\n}\n\n/**\n * Block requests until adapter setup settles; answer 503 with the per-adapter\n * failures when setup failed. Health probes and the admin System page are\n * exempt (they report setup state themselves instead of being masked by\n * the gate — both are site-admin-gated where it matters).\n */\nexport function setupGate(getReady: () => Promise<AdapterSetupResult>) {\n // oxlint-disable-next-line typescript/no-invalid-void-type -- Hono middleware may not return Response\n return async (c: Context, next: Next): Promise<Response | void> => {\n if (c.req.path === \"/api/v1/health\" || c.req.path === \"/admin\") {\n await next();\n return;\n }\n const result = await getReady();\n if (!result.ok) {\n return c.json({ error: \"Adapters failed setup\", failures: publicFailures(result) }, 503);\n }\n await next();\n };\n}\n"],"mappings":";;;;;;AAaA,SAAS,eACP,QACqD;CACrD,OAAO,OAAO,SAAS,KAAK,aAAa;EACvC,UAAU,QAAQ;EAClB,MAAM,QAAQ;EACd,OAAO,kBAAkB,QAAQ,OAAO,GAAG;CAC7C,EAAE;AACJ;;;;;;;AAQA,SAAgB,UAAU,UAA6C;CAErE,OAAO,OAAO,GAAY,SAAyC;EACjE,IAAI,EAAE,IAAI,SAAS,oBAAoB,EAAE,IAAI,SAAS,UAAU;GAC9D,MAAM,KAAK;GACX;EACF;EACA,MAAM,SAAS,MAAM,SAAS;EAC9B,IAAI,CAAC,OAAO,IACV,OAAO,EAAE,KAAK;GAAE,OAAO;GAAyB,UAAU,eAAe,MAAM;EAAE,GAAG,GAAG;EAEzF,MAAM,KAAK;CACb;AACF"}
@@ -3,7 +3,7 @@ import "node:url";
3
3
  import.meta.url;
4
4
  import { runWithStore } from "../store.mjs";
5
5
  import { sessionIdFrom } from "./csrf.mjs";
6
- //#region src/middleware/store-scope.ts
6
+ // src/middleware/store-scope.ts
7
7
  /** Hono middleware running downstream handlers inside the request store. */
8
8
  function storeScope(deps) {
9
9
  return async (c, next) => {
@@ -17,17 +17,26 @@ function storeScope(deps) {
17
17
  logger: deps.logger,
18
18
  user,
19
19
  authEnabled: deps.authEnabled,
20
+ testing: deps.testing,
20
21
  sessionId: sessionIdFrom(c),
21
22
  enqueueCapture: deps.enqueueCapture,
22
23
  captureQueue: deps.captureQueue,
23
- gitHosts: deps.gitHosts,
24
- notifiers: deps.notifiers
24
+ capturesHere: deps.capturesHere,
25
+ publish: deps.publish,
26
+ cancelBuildJobs: deps.cancelBuildJobs,
27
+ buildState: deps.buildState,
28
+ captureConcurrency: deps.captureConcurrency,
29
+ runner: deps.runner,
30
+ publicOrigin: deps.publicOrigin,
31
+ design: deps.design,
32
+ notifiers: deps.notifiers,
33
+ notificationsConfig: deps.notificationsConfig,
34
+ ai: deps.ai
25
35
  }, async () => {
26
36
  await next();
27
37
  });
28
38
  };
29
39
  }
30
- //#endregion
31
40
  export { storeScope };
32
41
 
33
42
  //# sourceMappingURL=store-scope.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"store-scope.mjs","names":[],"sources":["../../src/middleware/store-scope.ts"],"sourcesContent":["import type { CaptureQueue } from \"@storyshelf/core/adapter/capture-queue\";\nimport type { DatabaseAdapter } from \"@storyshelf/core/adapter/database\";\nimport type { GitHostProvider } from \"@storyshelf/core/adapter/git-host\";\nimport type { NotifierProvider } from \"@storyshelf/core/adapter/notifier\";\nimport type { StorageAdapter } from \"@storyshelf/core/adapter/storage\";\nimport type { ShelfConfig, UIConfig } from \"@storyshelf/core/config\";\nimport type { Logger } from \"@storyshelf/core/logger\";\nimport type { AuthUser } from \"@storyshelf/core/types\";\nimport type { Context, Next } from \"hono\";\nimport { runWithStore } from \"../store.ts\";\nimport { sessionIdFrom } from \"./csrf.ts\";\n\n/** Dependencies scoped into the request store for router handlers. */\nexport interface StoreScopeDeps {\n db: DatabaseAdapter;\n storage: StorageAdapter;\n config: ShelfConfig;\n ui: UIConfig;\n logger: Logger;\n authEnabled: boolean;\n enqueueCapture?: (buildId: string, reqId?: string) => Promise<void>;\n captureQueue: CaptureQueue | null;\n gitHosts: GitHostProvider[];\n notifiers: NotifierProvider[];\n resolveUser: (c: Context) => Promise<AuthUser | null>;\n}\n\n/** Hono middleware running downstream handlers inside the request store. */\nexport function storeScope(deps: StoreScopeDeps) {\n // oxlint-disable-next-line typescript/no-invalid-void-type -- Hono middleware may not return Response\n return async (c: Context, next: Next): Promise<Response | void> => {\n const user = await deps.resolveUser(c);\n c.set(\"userId\", user?.id ?? null);\n await runWithStore(\n {\n db: deps.db,\n storage: deps.storage,\n config: deps.config,\n ui: deps.ui,\n logger: deps.logger,\n user,\n authEnabled: deps.authEnabled,\n sessionId: sessionIdFrom(c),\n enqueueCapture: deps.enqueueCapture,\n captureQueue: deps.captureQueue,\n gitHosts: deps.gitHosts,\n notifiers: deps.notifiers,\n },\n async () => {\n await next();\n },\n );\n };\n}\n"],"mappings":";;;;;;;AA4BA,SAAgB,WAAW,MAAsB;CAE/C,OAAO,OAAO,GAAY,SAAyC;EACjE,MAAM,OAAO,MAAM,KAAK,YAAY,CAAC;EACrC,EAAE,IAAI,UAAU,MAAM,MAAM,IAAI;EAChC,MAAM,aACJ;GACE,IAAI,KAAK;GACT,SAAS,KAAK;GACd,QAAQ,KAAK;GACb,IAAI,KAAK;GACT,QAAQ,KAAK;GACb;GACA,aAAa,KAAK;GAClB,WAAW,cAAc,CAAC;GAC1B,gBAAgB,KAAK;GACrB,cAAc,KAAK;GACnB,UAAU,KAAK;GACf,WAAW,KAAK;EAClB,GACA,YAAY;GACV,MAAM,KAAK;EACb,CACF;CACF;AACF"}
1
+ {"version":3,"file":"store-scope.mjs","names":[],"sources":["../../src/middleware/store-scope.ts"],"sourcesContent":["import type { CaptureQueue } from \"@storyshelf/core/adapter/capture-queue\";\nimport type { CaptureRunner } from \"@storyshelf/core/adapter/capture-runner\";\nimport type { DatabaseAdapter } from \"@storyshelf/core/adapter/database\";\nimport type { DesignProvider } from \"@storyshelf/core/adapter/design\";\nimport type { NotifierProvider } from \"@storyshelf/core/adapter/notifier\";\nimport type { StorageAdapter } from \"@storyshelf/core/adapter/storage\";\nimport type { Ai } from \"@storyshelf/core/ai\";\nimport type { BuildStatePublisher } from \"@storyshelf/core/capture\";\nimport type { NotificationsConfig, ShelfConfig, UIConfig } from \"@storyshelf/core/config\";\nimport type { Logger } from \"@storyshelf/core/logger\";\nimport type { PublicOriginResolver } from \"@storyshelf/core/models\";\nimport type { AuthUser } from \"@storyshelf/core/types\";\nimport type { Context, Next } from \"hono\";\nimport type { PublishService } from \"../publish-setup.ts\";\nimport { runWithStore } from \"../store.ts\";\nimport { sessionIdFrom } from \"./csrf.ts\";\n\n/** Dependencies scoped into the request store for router handlers. */\nexport interface StoreScopeDeps {\n db: DatabaseAdapter;\n storage: StorageAdapter;\n config: ShelfConfig;\n ui: UIConfig;\n logger: Logger;\n authEnabled: boolean;\n testing: boolean;\n enqueueCapture?: (buildId: string, reqId?: string) => Promise<void>;\n captureQueue: CaptureQueue | null;\n /** This instance runs the capture jobs itself (the retry and compute-jobs UI applies). */\n capturesHere?: boolean;\n publish?: PublishService;\n cancelBuildJobs?: (buildId: string) => Promise<boolean>;\n buildState?: BuildStatePublisher;\n captureConcurrency?: number;\n runner?: Pick<CaptureRunner, \"metadata\"> | undefined;\n publicOrigin?: PublicOriginResolver | undefined;\n design: DesignProvider[];\n notifiers: NotifierProvider[];\n notificationsConfig?: NotificationsConfig;\n ai?: Ai;\n resolveUser: (c: Context) => Promise<AuthUser | null>;\n}\n\n/** Hono middleware running downstream handlers inside the request store. */\nexport function storeScope(deps: StoreScopeDeps) {\n // oxlint-disable-next-line typescript/no-invalid-void-type -- Hono middleware may not return Response\n return async (c: Context, next: Next): Promise<Response | void> => {\n const user = await deps.resolveUser(c);\n c.set(\"userId\", user?.id ?? null);\n await runWithStore(\n {\n db: deps.db,\n storage: deps.storage,\n config: deps.config,\n ui: deps.ui,\n logger: deps.logger,\n user,\n authEnabled: deps.authEnabled,\n testing: deps.testing,\n sessionId: sessionIdFrom(c),\n enqueueCapture: deps.enqueueCapture,\n captureQueue: deps.captureQueue,\n capturesHere: deps.capturesHere,\n publish: deps.publish,\n cancelBuildJobs: deps.cancelBuildJobs,\n buildState: deps.buildState,\n captureConcurrency: deps.captureConcurrency,\n runner: deps.runner,\n publicOrigin: deps.publicOrigin,\n design: deps.design,\n notifiers: deps.notifiers,\n notificationsConfig: deps.notificationsConfig,\n ai: deps.ai,\n },\n async () => {\n await next();\n },\n );\n };\n}\n"],"mappings":";;;;;;;AA4CA,SAAgB,WAAW,MAAsB;CAE/C,OAAO,OAAO,GAAY,SAAyC;EACjE,MAAM,OAAO,MAAM,KAAK,YAAY,CAAC;EACrC,EAAE,IAAI,UAAU,MAAM,MAAM,IAAI;EAChC,MAAM,aACJ;GACE,IAAI,KAAK;GACT,SAAS,KAAK;GACd,QAAQ,KAAK;GACb,IAAI,KAAK;GACT,QAAQ,KAAK;GACb;GACA,aAAa,KAAK;GAClB,SAAS,KAAK;GACd,WAAW,cAAc,CAAC;GAC1B,gBAAgB,KAAK;GACrB,cAAc,KAAK;GACnB,cAAc,KAAK;GACnB,SAAS,KAAK;GACd,iBAAiB,KAAK;GACtB,YAAY,KAAK;GACjB,oBAAoB,KAAK;GACzB,QAAQ,KAAK;GACb,cAAc,KAAK;GACnB,QAAQ,KAAK;GACb,WAAW,KAAK;GAChB,qBAAqB,KAAK;GAC1B,IAAI,KAAK;EACX,GACA,YAAY;GACV,MAAM,KAAK;EACb,CACF;CACF;AACF"}
@@ -0,0 +1,32 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { getStore } from "./store.mjs";
5
+ // src/notifications-pause.ts
6
+ /** What the System page and the notification settings tell the viewer. */
7
+ const NOTIFICATIONS_PAUSED_TEXT = "Notifications are paused until a public URL is set; set PUBLIC_BASE_URL or confirm the detected origin.";
8
+ /** The origin notifications link to, or `undefined` while none is resolved. */
9
+ async function notificationOrigin(deps) {
10
+ const trimmed = (deps.publicOrigin ? await deps.publicOrigin.url() : deps.config.publicBaseUrl)?.trim().replace(/\/+$/u, "");
11
+ return trimmed === void 0 || trimmed === "" ? void 0 : trimmed;
12
+ }
13
+ /** Whether the current instance sends no notifications for want of a public origin. */
14
+ async function notificationsPaused(store = getStore()) {
15
+ return await notificationOrigin(store) === void 0;
16
+ }
17
+ /** Loggers that already said so; cleared when an origin resolves, so a later pause logs again. */
18
+ const warned = /* @__PURE__ */ new WeakSet();
19
+ /** Log (once per pause, per logger) that a notification was skipped. */
20
+ function warnPaused(logger, event) {
21
+ if (!warned.has(logger)) {
22
+ warned.add(logger);
23
+ logger.warn({ event }, `${NOTIFICATIONS_PAUSED_TEXT} Skipping notifications.`);
24
+ }
25
+ }
26
+ /** Forget a logged pause: an origin resolved. */
27
+ function clearPaused(logger) {
28
+ warned.delete(logger);
29
+ }
30
+ export { NOTIFICATIONS_PAUSED_TEXT, clearPaused, notificationOrigin, notificationsPaused, warnPaused };
31
+
32
+ //# sourceMappingURL=notifications-pause.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notifications-pause.mjs","names":[],"sources":["../src/notifications-pause.ts"],"sourcesContent":["import type { ShelfConfig } from \"@storyshelf/core/config\";\n/**\n * Notifications need a public origin (ADR 0014, decision 11): every message carries a link and\n * every git status targets one, so while the resolver (ADR 0007, decision 6) finds no confirmed\n * origin (a detected one counts only once an admin confirmed it) nothing is sent. Pausing is not a failure: no channel is marked as errored, and the System page\n * and the notification settings say what to set.\n */\nimport type { Logger } from \"@storyshelf/core/logger\";\nimport type { PublicOriginResolver } from \"@storyshelf/core/models\";\nimport { getStore, type Store } from \"./store.ts\";\n\n/** What the System page and the notification settings tell the viewer. */\nexport const NOTIFICATIONS_PAUSED_TEXT =\n \"Notifications are paused until a public URL is set; set PUBLIC_BASE_URL or confirm the detected origin.\";\n\n/** What a pause check reads. */\ninterface OriginDeps {\n config: Pick<ShelfConfig, \"publicBaseUrl\">;\n /** The resolver; absent (hand-built stores) means `config.publicBaseUrl` only. */\n publicOrigin?: Pick<PublicOriginResolver, \"url\"> | undefined;\n}\n\n/** The origin notifications link to, or `undefined` while none is resolved. */\nexport async function notificationOrigin(deps: OriginDeps): Promise<string | undefined> {\n const origin = deps.publicOrigin ? await deps.publicOrigin.url() : deps.config.publicBaseUrl;\n const trimmed = origin?.trim().replace(/\\/+$/u, \"\");\n return trimmed === undefined || trimmed === \"\" ? undefined : trimmed;\n}\n\n/** Whether the current instance sends no notifications for want of a public origin. */\nexport async function notificationsPaused(store: Store = getStore()): Promise<boolean> {\n return (await notificationOrigin(store)) === undefined;\n}\n\n/** Loggers that already said so; cleared when an origin resolves, so a later pause logs again. */\nconst warned = new WeakSet<object>();\n\n/** Log (once per pause, per logger) that a notification was skipped. */\nexport function warnPaused(logger: Logger, event: string): void {\n if (!warned.has(logger)) {\n warned.add(logger);\n logger.warn({ event }, `${NOTIFICATIONS_PAUSED_TEXT} Skipping notifications.`);\n }\n}\n\n/** Forget a logged pause: an origin resolved. */\nexport function clearPaused(logger: Logger): void {\n warned.delete(logger);\n}\n"],"mappings":";;;;;;AAYA,MAAa,4BACX;;AAUF,eAAsB,mBAAmB,MAA+C;CAEtF,MAAM,WADS,KAAK,eAAe,MAAM,KAAK,aAAa,IAAI,IAAI,KAAK,OAAO,cAAA,EACvD,KAAK,CAAC,CAAC,QAAQ,SAAS,EAAE;CAClD,OAAO,YAAY,KAAA,KAAa,YAAY,KAAK,KAAA,IAAY;AAC/D;;AAGA,eAAsB,oBAAoB,QAAe,SAAS,GAAqB;CACrF,OAAQ,MAAM,mBAAmB,KAAK,MAAO,KAAA;AAC/C;;AAGA,MAAM,yBAAS,IAAI,QAAgB;;AAGnC,SAAgB,WAAW,QAAgB,OAAqB;CAC9D,IAAI,CAAC,OAAO,IAAI,MAAM,GAAG;EACvB,OAAO,IAAI,MAAM;EACjB,OAAO,KAAK,EAAE,MAAM,GAAG,GAAG,0BAA0B,yBAAyB;CAC/E;AACF;;AAGA,SAAgB,YAAY,QAAsB;CAChD,OAAO,OAAO,MAAM;AACtB"}
package/dist/notify.d.mts CHANGED
@@ -1,17 +1,16 @@
1
1
  import __tsdown_shims_path from 'node:path';
2
2
  import __tsdown_shims_url from 'node:url';
3
- import { NotifierProvider } from "@storyshelf/core/adapter/notifier";
4
3
  import { ShelfConfig, UIConfig } from "@storyshelf/core/config";
5
- import { Logger } from "@storyshelf/core/logger";
4
+ import "@storyshelf/core/models";
5
+ import { NotifierProvider } from "@storyshelf/core/adapter/notifier";
6
6
  import { DatabaseAdapter } from "@storyshelf/core/adapter/database";
7
+ import { Logger } from "@storyshelf/core/logger";
7
8
  import "@storyshelf/core/schema";
8
- //#region src/notify.d.ts
9
9
  /**
10
10
  * Host hook for the auth engine (`onAuthSystemEvent`): fans `sys:*` auth
11
11
  * events out to admin channels. Safe to call anywhere — outside a request
12
12
  * scope (e.g. boot) it resolves to a no-op instead of throwing.
13
13
  */
14
14
  declare function createAuthSystemHook(): (event: "sys:user-created" | "sys:invite-issued", data: Record<string, unknown>) => Promise<void>;
15
- //#endregion
16
15
  export { createAuthSystemHook };
17
16
  //# sourceMappingURL=notify.d.mts.map