@storyshelf/app 0.5.3

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 (268) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +18 -0
  3. package/dist/app-types.d.mts +54 -0
  4. package/dist/asset-manifest.mjs +41 -0
  5. package/dist/asset-manifest.mjs.map +1 -0
  6. package/dist/asset-paths.mjs +23 -0
  7. package/dist/asset-paths.mjs.map +1 -0
  8. package/dist/assets/htmx.min.js +3304 -0
  9. package/dist/assets/mark.svg +1 -0
  10. package/dist/capture-setup.mjs +139 -0
  11. package/dist/capture-setup.mjs.map +1 -0
  12. package/dist/credential-keys.mjs +20 -0
  13. package/dist/credential-keys.mjs.map +1 -0
  14. package/dist/credentials.mjs +56 -0
  15. package/dist/credentials.mjs.map +1 -0
  16. package/dist/index.d.mts +18 -0
  17. package/dist/index.mjs +310 -0
  18. package/dist/index.mjs.map +1 -0
  19. package/dist/lifecycle.mjs +132 -0
  20. package/dist/lifecycle.mjs.map +1 -0
  21. package/dist/middleware/auth-gate.mjs +36 -0
  22. package/dist/middleware/auth-gate.mjs.map +1 -0
  23. package/dist/middleware/csrf.mjs +75 -0
  24. package/dist/middleware/csrf.mjs.map +1 -0
  25. package/dist/middleware/rate-limit.mjs +68 -0
  26. package/dist/middleware/rate-limit.mjs.map +1 -0
  27. package/dist/middleware/request-log.mjs +30 -0
  28. package/dist/middleware/request-log.mjs.map +1 -0
  29. package/dist/middleware/server-timing.mjs +39 -0
  30. package/dist/middleware/server-timing.mjs.map +1 -0
  31. package/dist/middleware/setup-gate.mjs +37 -0
  32. package/dist/middleware/setup-gate.mjs.map +1 -0
  33. package/dist/middleware/store-scope.mjs +33 -0
  34. package/dist/middleware/store-scope.mjs.map +1 -0
  35. package/dist/notify.d.mts +17 -0
  36. package/dist/notify.mjs +141 -0
  37. package/dist/notify.mjs.map +1 -0
  38. package/dist/pages/admin-system.mjs +243 -0
  39. package/dist/pages/admin-system.mjs.map +1 -0
  40. package/dist/pages/build-detail.mjs +392 -0
  41. package/dist/pages/build-detail.mjs.map +1 -0
  42. package/dist/pages/build-diff-actions.mjs +162 -0
  43. package/dist/pages/build-diff-actions.mjs.map +1 -0
  44. package/dist/pages/build-diff-comments.mjs +104 -0
  45. package/dist/pages/build-diff-comments.mjs.map +1 -0
  46. package/dist/pages/build-diff-header.mjs +117 -0
  47. package/dist/pages/build-diff-header.mjs.map +1 -0
  48. package/dist/pages/build-diff-model.mjs +48 -0
  49. package/dist/pages/build-diff-model.mjs.map +1 -0
  50. package/dist/pages/build-diff-nav.mjs +122 -0
  51. package/dist/pages/build-diff-nav.mjs.map +1 -0
  52. package/dist/pages/build-diff-shortcuts.mjs +78 -0
  53. package/dist/pages/build-diff-shortcuts.mjs.map +1 -0
  54. package/dist/pages/build-diff-stale.mjs +70 -0
  55. package/dist/pages/build-diff-stale.mjs.map +1 -0
  56. package/dist/pages/build-diff-viewer.mjs +70 -0
  57. package/dist/pages/build-diff-viewer.mjs.map +1 -0
  58. package/dist/pages/build-diff.mjs +110 -0
  59. package/dist/pages/build-diff.mjs.map +1 -0
  60. package/dist/pages/compute-jobs.mjs +165 -0
  61. package/dist/pages/compute-jobs.mjs.map +1 -0
  62. package/dist/pages/invite.mjs +91 -0
  63. package/dist/pages/invite.mjs.map +1 -0
  64. package/dist/pages/label-detail.mjs +154 -0
  65. package/dist/pages/label-detail.mjs.map +1 -0
  66. package/dist/pages/library.mjs +343 -0
  67. package/dist/pages/library.mjs.map +1 -0
  68. package/dist/pages/login.mjs +117 -0
  69. package/dist/pages/login.mjs.map +1 -0
  70. package/dist/pages/passkey-ceremony.mjs +97 -0
  71. package/dist/pages/passkey-ceremony.mjs.map +1 -0
  72. package/dist/pages/profile.mjs +364 -0
  73. package/dist/pages/profile.mjs.map +1 -0
  74. package/dist/pages/project-builds-table.mjs +151 -0
  75. package/dist/pages/project-builds-table.mjs.map +1 -0
  76. package/dist/pages/project-builds.mjs +143 -0
  77. package/dist/pages/project-builds.mjs.map +1 -0
  78. package/dist/pages/project-create.mjs +90 -0
  79. package/dist/pages/project-create.mjs.map +1 -0
  80. package/dist/pages/project-settings.mjs +95 -0
  81. package/dist/pages/project-settings.mjs.map +1 -0
  82. package/dist/pages/projects-card.mjs +194 -0
  83. package/dist/pages/projects-card.mjs.map +1 -0
  84. package/dist/pages/projects.mjs +129 -0
  85. package/dist/pages/projects.mjs.map +1 -0
  86. package/dist/pages/root.mjs +98 -0
  87. package/dist/pages/root.mjs.map +1 -0
  88. package/dist/pages/settings-general.mjs +138 -0
  89. package/dist/pages/settings-general.mjs.map +1 -0
  90. package/dist/pages/settings-labels.mjs +107 -0
  91. package/dist/pages/settings-labels.mjs.map +1 -0
  92. package/dist/pages/settings-members.mjs +192 -0
  93. package/dist/pages/settings-members.mjs.map +1 -0
  94. package/dist/pages/settings-notifications.mjs +240 -0
  95. package/dist/pages/settings-notifications.mjs.map +1 -0
  96. package/dist/pages/settings-status.mjs +137 -0
  97. package/dist/pages/settings-status.mjs.map +1 -0
  98. package/dist/pages/settings-tests.mjs +111 -0
  99. package/dist/pages/settings-tests.mjs.map +1 -0
  100. package/dist/pages/settings-tokens.mjs +90 -0
  101. package/dist/pages/settings-tokens.mjs.map +1 -0
  102. package/dist/pages/settings-webhooks.mjs +104 -0
  103. package/dist/pages/settings-webhooks.mjs.map +1 -0
  104. package/dist/pages/storybook.mjs +116 -0
  105. package/dist/pages/storybook.mjs.map +1 -0
  106. package/dist/retention-timer.mjs +65 -0
  107. package/dist/retention-timer.mjs.map +1 -0
  108. package/dist/routers/admin-pages.mjs +48 -0
  109. package/dist/routers/admin-pages.mjs.map +1 -0
  110. package/dist/routers/admin.mjs +113 -0
  111. package/dist/routers/admin.mjs.map +1 -0
  112. package/dist/routers/assets.mjs +64 -0
  113. package/dist/routers/assets.mjs.map +1 -0
  114. package/dist/routers/attempts.mjs +96 -0
  115. package/dist/routers/attempts.mjs.map +1 -0
  116. package/dist/routers/auth-sync.mjs +64 -0
  117. package/dist/routers/auth-sync.mjs.map +1 -0
  118. package/dist/routers/auth.mjs +236 -0
  119. package/dist/routers/auth.mjs.map +1 -0
  120. package/dist/routers/baseline-guard.mjs +83 -0
  121. package/dist/routers/baseline-guard.mjs.map +1 -0
  122. package/dist/routers/builds.handlers.mjs +217 -0
  123. package/dist/routers/builds.handlers.mjs.map +1 -0
  124. package/dist/routers/builds.mjs +381 -0
  125. package/dist/routers/builds.mjs.map +1 -0
  126. package/dist/routers/comments.mjs +109 -0
  127. package/dist/routers/comments.mjs.map +1 -0
  128. package/dist/routers/flash.mjs +35 -0
  129. package/dist/routers/flash.mjs.map +1 -0
  130. package/dist/routers/health-report.mjs +159 -0
  131. package/dist/routers/health-report.mjs.map +1 -0
  132. package/dist/routers/health.mjs +46 -0
  133. package/dist/routers/health.mjs.map +1 -0
  134. package/dist/routers/helpers.mjs +110 -0
  135. package/dist/routers/helpers.mjs.map +1 -0
  136. package/dist/routers/htmx.mjs +24 -0
  137. package/dist/routers/htmx.mjs.map +1 -0
  138. package/dist/routers/labels.mjs +118 -0
  139. package/dist/routers/labels.mjs.map +1 -0
  140. package/dist/routers/media.mjs +53 -0
  141. package/dist/routers/media.mjs.map +1 -0
  142. package/dist/routers/members.mjs +177 -0
  143. package/dist/routers/members.mjs.map +1 -0
  144. package/dist/routers/notification-channels.mjs +190 -0
  145. package/dist/routers/notification-channels.mjs.map +1 -0
  146. package/dist/routers/notification-subscriptions.mjs +120 -0
  147. package/dist/routers/notification-subscriptions.mjs.map +1 -0
  148. package/dist/routers/profile.mjs +276 -0
  149. package/dist/routers/profile.mjs.map +1 -0
  150. package/dist/routers/projects.mjs +133 -0
  151. package/dist/routers/projects.mjs.map +1 -0
  152. package/dist/routers/review-next.mjs +47 -0
  153. package/dist/routers/review-next.mjs.map +1 -0
  154. package/dist/routers/schemas.mjs +337 -0
  155. package/dist/routers/schemas.mjs.map +1 -0
  156. package/dist/routers/settings-general.mjs +104 -0
  157. package/dist/routers/settings-general.mjs.map +1 -0
  158. package/dist/routers/settings-labels.mjs +52 -0
  159. package/dist/routers/settings-labels.mjs.map +1 -0
  160. package/dist/routers/settings-members.mjs +58 -0
  161. package/dist/routers/settings-members.mjs.map +1 -0
  162. package/dist/routers/settings-notifications.mjs +108 -0
  163. package/dist/routers/settings-notifications.mjs.map +1 -0
  164. package/dist/routers/settings-status.mjs +77 -0
  165. package/dist/routers/settings-status.mjs.map +1 -0
  166. package/dist/routers/settings-tokens.mjs +40 -0
  167. package/dist/routers/settings-tokens.mjs.map +1 -0
  168. package/dist/routers/settings-webhooks.mjs +67 -0
  169. package/dist/routers/settings-webhooks.mjs.map +1 -0
  170. package/dist/routers/settings.handlers.mjs +149 -0
  171. package/dist/routers/settings.handlers.mjs.map +1 -0
  172. package/dist/routers/settings.mjs +25 -0
  173. package/dist/routers/settings.mjs.map +1 -0
  174. package/dist/routers/snapshots.mjs +240 -0
  175. package/dist/routers/snapshots.mjs.map +1 -0
  176. package/dist/routers/status-configs.mjs +108 -0
  177. package/dist/routers/status-configs.mjs.map +1 -0
  178. package/dist/routers/storybook.mjs +171 -0
  179. package/dist/routers/storybook.mjs.map +1 -0
  180. package/dist/routers/tokens.mjs +91 -0
  181. package/dist/routers/tokens.mjs.map +1 -0
  182. package/dist/routers/ui.mjs +166 -0
  183. package/dist/routers/ui.mjs.map +1 -0
  184. package/dist/routers/webhooks.mjs +97 -0
  185. package/dist/routers/webhooks.mjs.map +1 -0
  186. package/dist/routers/well-known.mjs +43 -0
  187. package/dist/routers/well-known.mjs.map +1 -0
  188. package/dist/runtime.mjs +52 -0
  189. package/dist/runtime.mjs.map +1 -0
  190. package/dist/store.mjs +20 -0
  191. package/dist/store.mjs.map +1 -0
  192. package/dist/ui/avatar.mjs +76 -0
  193. package/dist/ui/avatar.mjs.map +1 -0
  194. package/dist/ui/buttons.mjs +205 -0
  195. package/dist/ui/buttons.mjs.map +1 -0
  196. package/dist/ui/code-block.mjs +47 -0
  197. package/dist/ui/code-block.mjs.map +1 -0
  198. package/dist/ui/compare-stage.mjs +344 -0
  199. package/dist/ui/compare-stage.mjs.map +1 -0
  200. package/dist/ui/csrf-field.mjs +20 -0
  201. package/dist/ui/csrf-field.mjs.map +1 -0
  202. package/dist/ui/css.mjs +36 -0
  203. package/dist/ui/css.mjs.map +1 -0
  204. package/dist/ui/document.mjs +130 -0
  205. package/dist/ui/document.mjs.map +1 -0
  206. package/dist/ui/dropdown.mjs +181 -0
  207. package/dist/ui/dropdown.mjs.map +1 -0
  208. package/dist/ui/feedback.mjs +341 -0
  209. package/dist/ui/feedback.mjs.map +1 -0
  210. package/dist/ui/filter-input.mjs +58 -0
  211. package/dist/ui/filter-input.mjs.map +1 -0
  212. package/dist/ui/forms.mjs +291 -0
  213. package/dist/ui/forms.mjs.map +1 -0
  214. package/dist/ui/icons/icon.mjs +53 -0
  215. package/dist/ui/icons/icon.mjs.map +1 -0
  216. package/dist/ui/icons/paths.mjs +61 -0
  217. package/dist/ui/icons/paths.mjs.map +1 -0
  218. package/dist/ui/icons/sprite.mjs +26 -0
  219. package/dist/ui/icons/sprite.mjs.map +1 -0
  220. package/dist/ui/kbd.mjs +33 -0
  221. package/dist/ui/kbd.mjs.map +1 -0
  222. package/dist/ui/layout.mjs +173 -0
  223. package/dist/ui/layout.mjs.map +1 -0
  224. package/dist/ui/progress.mjs +44 -0
  225. package/dist/ui/progress.mjs.map +1 -0
  226. package/dist/ui/segmented.mjs +77 -0
  227. package/dist/ui/segmented.mjs.map +1 -0
  228. package/dist/ui/shell/account.mjs +161 -0
  229. package/dist/ui/shell/account.mjs.map +1 -0
  230. package/dist/ui/shell/bare.mjs +69 -0
  231. package/dist/ui/shell/bare.mjs.map +1 -0
  232. package/dist/ui/shell/boot-script.mjs +46 -0
  233. package/dist/ui/shell/boot-script.mjs.map +1 -0
  234. package/dist/ui/shell/client-script.mjs +259 -0
  235. package/dist/ui/shell/client-script.mjs.map +1 -0
  236. package/dist/ui/shell/nav.mjs +111 -0
  237. package/dist/ui/shell/nav.mjs.map +1 -0
  238. package/dist/ui/shell/review-script.mjs +195 -0
  239. package/dist/ui/shell/review-script.mjs.map +1 -0
  240. package/dist/ui/shell/sidebar.mjs +323 -0
  241. package/dist/ui/shell/sidebar.mjs.map +1 -0
  242. package/dist/ui/shell/topbar.mjs +268 -0
  243. package/dist/ui/shell/topbar.mjs.map +1 -0
  244. package/dist/ui/stacks.mjs +82 -0
  245. package/dist/ui/stacks.mjs.map +1 -0
  246. package/dist/ui/styles/base.mjs +41 -0
  247. package/dist/ui/styles/base.mjs.map +1 -0
  248. package/dist/ui/styles/behavior.mjs +43 -0
  249. package/dist/ui/styles/behavior.mjs.map +1 -0
  250. package/dist/ui/styles/review-layout.mjs +347 -0
  251. package/dist/ui/styles/review-layout.mjs.map +1 -0
  252. package/dist/ui/styles/review.mjs +86 -0
  253. package/dist/ui/styles/review.mjs.map +1 -0
  254. package/dist/ui/styles/tokens.mjs +132 -0
  255. package/dist/ui/styles/tokens.mjs.map +1 -0
  256. package/dist/ui/styles.mjs +19 -0
  257. package/dist/ui/styles.mjs.map +1 -0
  258. package/dist/ui/subnav.mjs +91 -0
  259. package/dist/ui/subnav.mjs.map +1 -0
  260. package/dist/ui/table.mjs +73 -0
  261. package/dist/ui/table.mjs.map +1 -0
  262. package/dist/ui/theme.mjs +64 -0
  263. package/dist/ui/theme.mjs.map +1 -0
  264. package/dist/ui/thumbnail.mjs +51 -0
  265. package/dist/ui/thumbnail.mjs.map +1 -0
  266. package/dist/ui/time.mjs +50 -0
  267. package/dist/ui/time.mjs.map +1 -0
  268. package/package.json +92 -0
@@ -0,0 +1 @@
1
+ <svg xmlns='http://www.w3.org/2000/svg' width='64' height='64' viewBox='0 0 64 64' role='img' aria-label='StoryShelf'><rect width='64' height='64' rx='14' fill='#1d5fcf'/><path d='M15 11 H49 A4 4 0 0 1 53 15 V15 A2 2 0 0 1 51 17 H13 A2 2 0 0 1 11 15 V15 A4 4 0 0 1 15 11 Z' fill='#ffffff'/><path d='M13 29 H51 A2 2 0 0 1 53 31 V33 A2 2 0 0 1 51 35 H13 A2 2 0 0 1 11 33 V31 A2 2 0 0 1 13 29 Z' fill='#ffffff'/><path d='M13 47 H51 A2 2 0 0 1 53 49 V49 A4 4 0 0 1 49 53 H15 A4 4 0 0 1 11 49 V49 A2 2 0 0 1 13 47 Z' fill='#ffffff'/><path d='M16 19 H16 A2 2 0 0 1 18 21 V27 A2 2 0 0 1 16 29 H16 A2 2 0 0 1 14 27 V21 A2 2 0 0 1 16 19 Z' fill='#ffffff'/><path d='M22 21 H22 A2 2 0 0 1 24 23 V27 A2 2 0 0 1 22 29 H22 A2 2 0 0 1 20 27 V23 A2 2 0 0 1 22 21 Z' fill='#ffffff'/><path d='M28 23 H28 A2 2 0 0 1 30 25 V27 A2 2 0 0 1 28 29 H28 A2 2 0 0 1 26 27 V25 A2 2 0 0 1 28 23 Z' fill='#ffffff'/><path d='M48 37 H48 A2 2 0 0 1 50 39 V45 A2 2 0 0 1 48 47 H48 A2 2 0 0 1 46 45 V39 A2 2 0 0 1 48 37 Z' fill='#ffffff'/><path d='M42 39 H42 A2 2 0 0 1 44 41 V45 A2 2 0 0 1 42 47 H42 A2 2 0 0 1 40 45 V41 A2 2 0 0 1 42 39 Z' fill='#ffffff'/><path d='M36 41 H36 A2 2 0 0 1 38 43 V45 A2 2 0 0 1 36 47 H36 A2 2 0 0 1 34 45 V43 A2 2 0 0 1 36 41 Z' fill='#ffffff'/></svg>
@@ -0,0 +1,139 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { credentialKeys } from "./credential-keys.mjs";
5
+ import { notifySystemWith } from "./notify.mjs";
6
+ import { captureMetrics, currentTraceparent } from "@storyshelf/observability";
7
+ import { InMemoryCaptureQueue, createDispatchJob, recoverStuckCaptures } from "@storyshelf/core/capture";
8
+ import { BuildModel } from "@storyshelf/core/models";
9
+ //#region src/capture-setup.ts
10
+ /** Enqueue a build, continuing the active request trace when present. */
11
+ async function enqueueJob(queue, buildId, reqId) {
12
+ const traceparent = currentTraceparent();
13
+ await queue.enqueue({
14
+ buildId,
15
+ reqId,
16
+ ...traceparent ? { traceparent } : {}
17
+ });
18
+ }
19
+ /** Wrap the dispatch job with duration + outcome metrics. */
20
+ async function timedRunJob(runJob, job) {
21
+ const instruments = captureMetrics();
22
+ const start = performance.now();
23
+ try {
24
+ await runJob(job);
25
+ } catch (error) {
26
+ instruments.jobsFailed.add(1);
27
+ instruments.jobDuration.record(performance.now() - start);
28
+ throw error;
29
+ }
30
+ instruments.jobsCompleted.add(1);
31
+ instruments.jobDuration.record(performance.now() - start);
32
+ }
33
+ /** Alert admins when an in-process capture leaves its build failed. */
34
+ async function notifyCaptureFailed(options, config, logger, buildId) {
35
+ const build = await new BuildModel(options.database).get(buildId);
36
+ if (build?.status !== "failed") return;
37
+ await notifySystemWith({
38
+ db: options.database,
39
+ config,
40
+ ui: options.ui ?? {},
41
+ logger,
42
+ notifiers: options.notifiers ?? []
43
+ }, "sys:capture-failed", {
44
+ buildId,
45
+ projectId: build.projectId,
46
+ gitBranch: build.gitBranch
47
+ });
48
+ }
49
+ /** Assemble the capture queue and its enqueue hook. */
50
+ function setupCaptureQueue(options, config, gitHosts, logger) {
51
+ if (!options.captureQueue && !options.captureRunner) return {
52
+ queue: null,
53
+ enqueueCapture: void 0
54
+ };
55
+ if (options.captureQueue) {
56
+ const queue = options.captureQueue;
57
+ const enqueueCapture = async (buildId, reqId) => {
58
+ await enqueueJob(queue, buildId, reqId);
59
+ };
60
+ if (!options.captureRunner) return {
61
+ queue: options.captureQueue,
62
+ enqueueCapture
63
+ };
64
+ if (!config.scratchDir) return {
65
+ queue: options.captureQueue,
66
+ enqueueCapture
67
+ };
68
+ return {
69
+ queue: options.captureQueue,
70
+ enqueueCapture
71
+ };
72
+ }
73
+ if (!options.captureRunner) return {
74
+ queue: null,
75
+ enqueueCapture: void 0
76
+ };
77
+ if (!config.scratchDir) throw new Error("captureRunner is enabled but ShelfConfig.scratchDir is not set");
78
+ const jobOptions = {
79
+ db: options.database,
80
+ tables: {
81
+ projects: options.database.tables.projects,
82
+ builds: options.database.tables.builds,
83
+ buildLabels: options.database.tables.buildLabels,
84
+ snapshots: options.database.tables.snapshots,
85
+ baselines: options.database.tables.baselines,
86
+ captureAttempts: options.database.tables.captureAttempts,
87
+ captureLogs: options.database.tables.captureLogs
88
+ },
89
+ storage: options.storage,
90
+ runner: options.captureRunner,
91
+ scratchDir: config.scratchDir,
92
+ viewports: config.viewports,
93
+ logger,
94
+ secret: credentialKeys(config)
95
+ };
96
+ const dispatchDeps = {
97
+ db: options.database,
98
+ tables: {
99
+ projects: options.database.tables.projects,
100
+ builds: options.database.tables.builds,
101
+ buildLabels: options.database.tables.buildLabels,
102
+ snapshots: options.database.tables.snapshots,
103
+ projectStatusConfigs: options.database.tables.projectStatusConfigs,
104
+ captureAttempts: options.database.tables.captureAttempts,
105
+ captureLogs: options.database.tables.captureLogs
106
+ },
107
+ jobOptions,
108
+ gitHosts,
109
+ secret: credentialKeys(config),
110
+ logger
111
+ };
112
+ const runJob = createDispatchJob(dispatchDeps);
113
+ const captureQueue = options.captureQueue ?? new InMemoryCaptureQueue({
114
+ concurrency: config.captureConcurrency ?? 2,
115
+ logger,
116
+ runJob: async (job) => {
117
+ await timedRunJob(runJob, job);
118
+ await notifyCaptureFailed(options, config, logger, job.buildId);
119
+ }
120
+ });
121
+ const enqueueCapture = async (buildId, reqId) => {
122
+ await enqueueJob(captureQueue, buildId, reqId);
123
+ };
124
+ const recoverStuck = async () => await recoverStuckCaptures({
125
+ deps: dispatchDeps,
126
+ enqueue: async (buildId) => {
127
+ await enqueueCapture(buildId);
128
+ }
129
+ });
130
+ return {
131
+ queue: captureQueue,
132
+ enqueueCapture,
133
+ recoverStuck
134
+ };
135
+ }
136
+ //#endregion
137
+ export { setupCaptureQueue };
138
+
139
+ //# sourceMappingURL=capture-setup.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capture-setup.mjs","names":[],"sources":["../src/capture-setup.ts"],"sourcesContent":["import type { CaptureQueue } from \"@storyshelf/core/adapter/capture-queue\";\nimport type { GitHostProvider } from \"@storyshelf/core/adapter/git-host\";\nimport {\n createDispatchJob,\n InMemoryCaptureQueue,\n recoverStuckCaptures,\n} from \"@storyshelf/core/capture\";\nimport type {\n CaptureDispatchJob,\n CaptureJobOptions,\n DispatchDeps,\n RecoveryResult,\n} from \"@storyshelf/core/capture\";\nimport type { ShelfConfig, ShelfOptions } from \"@storyshelf/core/config\";\nimport type { Logger } from \"@storyshelf/core/logger\";\nimport { BuildModel } from \"@storyshelf/core/models\";\nimport { captureMetrics, currentTraceparent } from \"@storyshelf/observability\";\nimport { credentialKeys } from \"./credential-keys.ts\";\nimport { notifySystemWith } from \"./notify.ts\";\n\n/**\n * Wiring for the capture queue: the queue instance (if any) and a helper\n * to enqueue a build for rendering. When no `captureRunner` is configured\n * both are null/undefined and builds remain `pending` until a runner is added.\n */\nexport interface QueueWiring {\n queue: CaptureQueue | null;\n enqueueCapture: ((buildId: string, reqId?: string) => Promise<void>) | undefined;\n /**\n * Boot sweep for builds a restart left in `capturing` (requeue once, else fail).\n * Only set for the in-process queue; remote queues redeliver on their own.\n */\n recoverStuck?: () => Promise<RecoveryResult>;\n}\n\n/** Enqueue a build, continuing the active request trace when present. */\nasync function enqueueJob(queue: CaptureQueue, buildId: string, reqId?: string): Promise<void> {\n const traceparent = currentTraceparent();\n await queue.enqueue({ buildId, reqId, ...(traceparent ? { traceparent } : {}) });\n}\n\n/** Wrap the dispatch job with duration + outcome metrics. */\nasync function timedRunJob(\n runJob: (job: CaptureDispatchJob) => Promise<void>,\n job: CaptureDispatchJob,\n): Promise<void> {\n const instruments = captureMetrics();\n const start = performance.now();\n try {\n await runJob(job);\n } catch (error) {\n instruments.jobsFailed.add(1);\n instruments.jobDuration.record(performance.now() - start);\n throw error;\n }\n instruments.jobsCompleted.add(1);\n instruments.jobDuration.record(performance.now() - start);\n}\n\n/** Alert admins when an in-process capture leaves its build failed. */\nasync function notifyCaptureFailed(\n options: ShelfOptions,\n config: ShelfConfig,\n logger: Logger,\n buildId: string,\n): Promise<void> {\n const build = await new BuildModel(options.database).get(buildId);\n if (build?.status !== \"failed\") {\n return;\n }\n await notifySystemWith(\n {\n db: options.database,\n config,\n ui: options.ui ?? {},\n logger,\n notifiers: options.notifiers ?? [],\n },\n \"sys:capture-failed\",\n { buildId, projectId: build.projectId, gitBranch: build.gitBranch },\n );\n}\n\n/** Assemble the capture queue and its enqueue hook. */\nexport function setupCaptureQueue(\n options: ShelfOptions,\n config: ShelfConfig,\n gitHosts: GitHostProvider[],\n logger: Logger,\n): QueueWiring {\n if (!options.captureQueue && !options.captureRunner) {\n return { queue: null, enqueueCapture: undefined };\n }\n if (options.captureQueue) {\n const queue = options.captureQueue;\n const enqueueCapture = async (buildId: string, reqId?: string): Promise<void> => {\n await enqueueJob(queue, buildId, reqId);\n };\n if (!options.captureRunner) {\n return { queue: options.captureQueue, enqueueCapture };\n }\n // Both queue and runner supplied: runner is ignored server-side for remote queues;\n // enqueue goes to the supplied queue and capture is handled by an external worker.\n if (!config.scratchDir) {\n return { queue: options.captureQueue, enqueueCapture };\n }\n // If scratchDir is set with both, still prefer the supplied queue; no in-memory queue needed.\n return { queue: options.captureQueue, enqueueCapture };\n }\n if (!options.captureRunner) {\n return { queue: null, enqueueCapture: undefined };\n }\n if (!config.scratchDir) {\n throw new Error(\"captureRunner is enabled but ShelfConfig.scratchDir is not set\");\n }\n const jobOptions: CaptureJobOptions = {\n db: options.database,\n tables: {\n projects: options.database.tables.projects,\n builds: options.database.tables.builds,\n buildLabels: options.database.tables.buildLabels,\n snapshots: options.database.tables.snapshots,\n baselines: options.database.tables.baselines,\n captureAttempts: options.database.tables.captureAttempts,\n captureLogs: options.database.tables.captureLogs,\n },\n storage: options.storage,\n runner: options.captureRunner,\n scratchDir: config.scratchDir,\n viewports: config.viewports,\n logger,\n secret: credentialKeys(config),\n };\n const dispatchDeps: DispatchDeps = {\n db: options.database,\n tables: {\n projects: options.database.tables.projects,\n builds: options.database.tables.builds,\n buildLabels: options.database.tables.buildLabels,\n snapshots: options.database.tables.snapshots,\n projectStatusConfigs: options.database.tables.projectStatusConfigs,\n captureAttempts: options.database.tables.captureAttempts,\n captureLogs: options.database.tables.captureLogs,\n },\n jobOptions,\n gitHosts,\n secret: credentialKeys(config),\n logger,\n };\n const runJob = createDispatchJob(dispatchDeps);\n const captureQueue =\n options.captureQueue ??\n new InMemoryCaptureQueue({\n concurrency: config.captureConcurrency ?? 2,\n logger,\n runJob: async (job: CaptureDispatchJob): Promise<void> => {\n await timedRunJob(runJob, job);\n // In-process completions only: remote-queue workers own their own\n // alerting (the server never sees those outcomes).\n await notifyCaptureFailed(options, config, logger, job.buildId);\n },\n });\n const enqueueCapture = async (buildId: string, reqId?: string): Promise<void> => {\n await enqueueJob(captureQueue, buildId, reqId);\n };\n const recoverStuck = async (): Promise<RecoveryResult> =>\n await recoverStuckCaptures({\n deps: dispatchDeps,\n enqueue: async (buildId) => {\n await enqueueCapture(buildId);\n },\n });\n return { queue: captureQueue, enqueueCapture, recoverStuck };\n}\n"],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ //#region src/credential-keys.ts
5
+ /**
6
+ * Key material for encrypted credentials: the current secret, plus
7
+ * `previousSecret` as a decrypt-only fallback during a rotation. Not for
8
+ * signing (CSRF, sessions), which only ever uses `config.secret`.
9
+ */
10
+ function credentialKeys(config) {
11
+ if (config.secret && config.previousSecret) return {
12
+ current: config.secret,
13
+ previous: config.previousSecret
14
+ };
15
+ return config.secret;
16
+ }
17
+ //#endregion
18
+ export { credentialKeys };
19
+
20
+ //# sourceMappingURL=credential-keys.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credential-keys.mjs","names":[],"sources":["../src/credential-keys.ts"],"sourcesContent":["import type { ShelfConfig } from \"@storyshelf/core/config\";\nimport type { SecretInput } from \"@storyshelf/core/utils\";\n\n/**\n * Key material for encrypted credentials: the current secret, plus\n * `previousSecret` as a decrypt-only fallback during a rotation. Not for\n * signing (CSRF, sessions), which only ever uses `config.secret`.\n */\nexport function credentialKeys(config: ShelfConfig): SecretInput {\n if (config.secret && config.previousSecret) {\n return { current: config.secret, previous: config.previousSecret };\n }\n return config.secret;\n}\n"],"mappings":";;;;;;;;;AAQA,SAAgB,eAAe,QAAkC;CAC/D,IAAI,OAAO,UAAU,OAAO,gBAC1B,OAAO;EAAE,SAAS,OAAO;EAAQ,UAAU,OAAO;CAAe;CAEnE,OAAO,OAAO;AAChB"}
@@ -0,0 +1,56 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { probeCredentials, reencryptCredentials } from "@storyshelf/core/models";
5
+ //#region src/credentials.ts
6
+ /** Current/previous key pair for a probe, or null when no secret is configured. */
7
+ function secretKeysOf(config) {
8
+ if (!config.secret) return null;
9
+ return config.previousSecret ? {
10
+ current: config.secret,
11
+ previous: config.previousSecret
12
+ } : { current: config.secret };
13
+ }
14
+ /** Probe stored credentials; null when no secret is configured. */
15
+ async function loadCredentialProbe(db, config) {
16
+ const keys = secretKeysOf(config);
17
+ return keys ? await probeCredentials(db, keys) : null;
18
+ }
19
+ /** Re-encrypt rows under `previousSecret`; throws when no rotation is configured. */
20
+ async function reencryptWithCurrent(db, config) {
21
+ const keys = secretKeysOf(config);
22
+ if (!keys?.previous) throw new Error("No previousSecret is configured; nothing to re-encrypt");
23
+ return await reencryptCredentials(db, keys);
24
+ }
25
+ /** Log what the probe found; returns whether rows still need re-encryption. */
26
+ function logProbe(probe, config, logger) {
27
+ if (config.migrateCredentialsOnBoot && !config.previousSecret) logger.warn("migrateCredentialsOnBoot has no effect without previousSecret");
28
+ if (probe.unreadable.length > 0) logger.error({
29
+ count: probe.unreadable.length,
30
+ rows: probe.unreadable.slice(0, 20)
31
+ }, "stored credentials cannot be decrypted; check SECRET and SECRET_PREVIOUS");
32
+ if (probe.previous > 0 && !config.migrateCredentialsOnBoot) logger.warn({ count: probe.previous }, "credentials still encrypted with previousSecret; re-encrypt from the System page");
33
+ return probe.previous > 0;
34
+ }
35
+ /** Log the credential state at boot and, if opted in, migrate to the current key. */
36
+ async function checkCredentialsAtBoot(db, config, logger) {
37
+ const probe = await loadCredentialProbe(db, config);
38
+ if (!probe || !logProbe(probe, config, logger) || !config.migrateCredentialsOnBoot) return;
39
+ const result = await reencryptWithCurrent(db, config);
40
+ logger.info({
41
+ reencrypted: result.reencrypted,
42
+ failed: result.failed
43
+ }, "re-encrypted credentials with the current secret");
44
+ }
45
+ /** Run {@link checkCredentialsAtBoot} once adapters are ready; never blocks or throws. */
46
+ function checkCredentialsAfterSetup(cell, db, config, logger) {
47
+ cell.ready.then(async (setup) => {
48
+ if (setup.ok) await checkCredentialsAtBoot(db, config, logger);
49
+ }).catch((error) => {
50
+ logger.error({ err: error }, "credential check failed");
51
+ });
52
+ }
53
+ //#endregion
54
+ export { checkCredentialsAfterSetup, checkCredentialsAtBoot, loadCredentialProbe, reencryptWithCurrent, secretKeysOf };
55
+
56
+ //# sourceMappingURL=credentials.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credentials.mjs","names":[],"sources":["../src/credentials.ts"],"sourcesContent":["/**\n * Runtime side of secret rotation: boot check/migration, the health probe, and\n * the re-encrypt action shared by the System page and the admin API.\n */\nimport type { DatabaseAdapter } from \"@storyshelf/core/adapter/database\";\nimport type { ShelfConfig } from \"@storyshelf/core/config\";\nimport type { Logger } from \"@storyshelf/core/logger\";\nimport {\n probeCredentials,\n reencryptCredentials,\n type CredentialProbe,\n type ReencryptResult,\n} from \"@storyshelf/core/models\";\nimport type { SecretKeys } from \"@storyshelf/core/utils\";\nimport type { LifecycleCell } from \"./lifecycle.ts\";\n\n/** Current/previous key pair for a probe, or null when no secret is configured. */\nexport function secretKeysOf(config: ShelfConfig): SecretKeys | null {\n if (!config.secret) {\n return null;\n }\n return config.previousSecret\n ? { current: config.secret, previous: config.previousSecret }\n : { current: config.secret };\n}\n\n/** Probe stored credentials; null when no secret is configured. */\nexport async function loadCredentialProbe(\n db: DatabaseAdapter,\n config: ShelfConfig,\n): Promise<CredentialProbe | null> {\n const keys = secretKeysOf(config);\n return keys ? await probeCredentials(db, keys) : null;\n}\n\n/** Re-encrypt rows under `previousSecret`; throws when no rotation is configured. */\nexport async function reencryptWithCurrent(\n db: DatabaseAdapter,\n config: ShelfConfig,\n): Promise<ReencryptResult> {\n const keys = secretKeysOf(config);\n if (!keys?.previous) {\n throw new Error(\"No previousSecret is configured; nothing to re-encrypt\");\n }\n return await reencryptCredentials(db, keys);\n}\n\n/** Log what the probe found; returns whether rows still need re-encryption. */\nfunction logProbe(probe: CredentialProbe, config: ShelfConfig, logger: Logger): boolean {\n if (config.migrateCredentialsOnBoot && !config.previousSecret) {\n logger.warn(\"migrateCredentialsOnBoot has no effect without previousSecret\");\n }\n if (probe.unreadable.length > 0) {\n logger.error(\n { count: probe.unreadable.length, rows: probe.unreadable.slice(0, 20) },\n \"stored credentials cannot be decrypted; check SECRET and SECRET_PREVIOUS\",\n );\n }\n if (probe.previous > 0 && !config.migrateCredentialsOnBoot) {\n logger.warn(\n { count: probe.previous },\n \"credentials still encrypted with previousSecret; re-encrypt from the System page\",\n );\n }\n return probe.previous > 0;\n}\n\n/** Log the credential state at boot and, if opted in, migrate to the current key. */\nexport async function checkCredentialsAtBoot(\n db: DatabaseAdapter,\n config: ShelfConfig,\n logger: Logger,\n): Promise<void> {\n const probe = await loadCredentialProbe(db, config);\n if (!probe || !logProbe(probe, config, logger) || !config.migrateCredentialsOnBoot) {\n return;\n }\n const result = await reencryptWithCurrent(db, config);\n logger.info(\n { reencrypted: result.reencrypted, failed: result.failed },\n \"re-encrypted credentials with the current secret\",\n );\n}\n\n/** Run {@link checkCredentialsAtBoot} once adapters are ready; never blocks or throws. */\nexport function checkCredentialsAfterSetup(\n cell: LifecycleCell,\n db: DatabaseAdapter,\n config: ShelfConfig,\n logger: Logger,\n): void {\n cell.ready\n .then(async (setup) => {\n if (setup.ok) {\n await checkCredentialsAtBoot(db, config, logger);\n }\n })\n .catch((error: unknown) => {\n logger.error({ err: error }, \"credential check failed\");\n });\n}\n"],"mappings":";;;;;;AAiBA,SAAgB,aAAa,QAAwC;CACnE,IAAI,CAAC,OAAO,QACV,OAAO;CAET,OAAO,OAAO,iBACV;EAAE,SAAS,OAAO;EAAQ,UAAU,OAAO;CAAe,IAC1D,EAAE,SAAS,OAAO,OAAO;AAC/B;;AAGA,eAAsB,oBACpB,IACA,QACiC;CACjC,MAAM,OAAO,aAAa,MAAM;CAChC,OAAO,OAAO,MAAM,iBAAiB,IAAI,IAAI,IAAI;AACnD;;AAGA,eAAsB,qBACpB,IACA,QAC0B;CAC1B,MAAM,OAAO,aAAa,MAAM;CAChC,IAAI,CAAC,MAAM,UACT,MAAM,IAAI,MAAM,wDAAwD;CAE1E,OAAO,MAAM,qBAAqB,IAAI,IAAI;AAC5C;;AAGA,SAAS,SAAS,OAAwB,QAAqB,QAAyB;CACtF,IAAI,OAAO,4BAA4B,CAAC,OAAO,gBAC7C,OAAO,KAAK,+DAA+D;CAE7E,IAAI,MAAM,WAAW,SAAS,GAC5B,OAAO,MACL;EAAE,OAAO,MAAM,WAAW;EAAQ,MAAM,MAAM,WAAW,MAAM,GAAG,EAAE;CAAE,GACtE,0EACF;CAEF,IAAI,MAAM,WAAW,KAAK,CAAC,OAAO,0BAChC,OAAO,KACL,EAAE,OAAO,MAAM,SAAS,GACxB,kFACF;CAEF,OAAO,MAAM,WAAW;AAC1B;;AAGA,eAAsB,uBACpB,IACA,QACA,QACe;CACf,MAAM,QAAQ,MAAM,oBAAoB,IAAI,MAAM;CAClD,IAAI,CAAC,SAAS,CAAC,SAAS,OAAO,QAAQ,MAAM,KAAK,CAAC,OAAO,0BACxD;CAEF,MAAM,SAAS,MAAM,qBAAqB,IAAI,MAAM;CACpD,OAAO,KACL;EAAE,aAAa,OAAO;EAAa,QAAQ,OAAO;CAAO,GACzD,kDACF;AACF;;AAGA,SAAgB,2BACd,MACA,IACA,QACA,QACM;CACN,KAAK,MACF,KAAK,OAAO,UAAU;EACrB,IAAI,MAAM,IACR,MAAM,uBAAuB,IAAI,QAAQ,MAAM;CAEnD,CAAC,CAAC,CACD,OAAO,UAAmB;EACzB,OAAO,MAAM,EAAE,KAAK,MAAM,GAAG,yBAAyB;CACxD,CAAC;AACL"}
@@ -0,0 +1,18 @@
1
+ import __tsdown_shims_path from 'node:path';
2
+ import __tsdown_shims_url from 'node:url';
3
+ import { ShelfApp, ShelfContext, ShelfLifecycle, ShelfRouter } from "./app-types.mjs";
4
+ import { createAuthSystemHook } from "./notify.mjs";
5
+ import { BrandTheme, NotificationsConfig, ShelfConfig, ShelfOptions, ShelfOptions as ShelfOptions$1, UIConfig } from "@storyshelf/core/config";
6
+ import { Logger, LoggerOptions, PinoTransport, createShelfLogger } from "@storyshelf/core/logger";
7
+ //#region src/index.d.ts
8
+ /**
9
+ * Create the StoryShelf Hono app with all API routes and HTML pages.
10
+ *
11
+ * Adapter `init` hooks kick off eagerly in the background (the constructor
12
+ * stays synchronous). Await `app.lifecycle.setup()` before serving for
13
+ * fail-fast startup, or let the first request gate on settlement.
14
+ */
15
+ declare function createShelfApp(options: ShelfOptions$1): ShelfApp;
16
+ //#endregion
17
+ export { type BrandTheme, type Logger, type LoggerOptions, type NotificationsConfig, type PinoTransport, type ShelfApp, type ShelfConfig, type ShelfContext, type ShelfLifecycle, type ShelfOptions, type ShelfRouter, type UIConfig, createAuthSystemHook, createShelfApp, createShelfLogger };
18
+ //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs ADDED
@@ -0,0 +1,310 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { createAuthSystemHook } from "./notify.mjs";
5
+ import { setupCaptureQueue } from "./capture-setup.mjs";
6
+ import { checkCredentialsAfterSetup } from "./credentials.mjs";
7
+ import { attachLifecycle } from "./lifecycle.mjs";
8
+ import { rateLimit, rateLimitKey } from "./middleware/rate-limit.mjs";
9
+ import { csrf } from "./middleware/csrf.mjs";
10
+ import { requestLogging } from "./middleware/request-log.mjs";
11
+ import { storeScope } from "./middleware/store-scope.mjs";
12
+ import { authGate, resolveRequestUser } from "./middleware/auth-gate.mjs";
13
+ import { serverTiming } from "./middleware/server-timing.mjs";
14
+ import { setupGate } from "./middleware/setup-gate.mjs";
15
+ import { registerAdminPages } from "./routers/admin-pages.mjs";
16
+ import { registerAdmin } from "./routers/admin.mjs";
17
+ import { registerAssets } from "./routers/assets.mjs";
18
+ import { registerEngineAuth } from "./routers/auth.mjs";
19
+ import { registerBuilds } from "./routers/builds.mjs";
20
+ import { registerHealth } from "./routers/health.mjs";
21
+ import { registerLabels } from "./routers/labels.mjs";
22
+ import { registerMedia } from "./routers/media.mjs";
23
+ import { registerMembers } from "./routers/members.mjs";
24
+ import { registerNotificationChannels, registerSystemChannels } from "./routers/notification-channels.mjs";
25
+ import { registerNotificationSubscriptions } from "./routers/notification-subscriptions.mjs";
26
+ import { registerProfile } from "./routers/profile.mjs";
27
+ import { registerProjects } from "./routers/projects.mjs";
28
+ import { registerStatusConfigs } from "./routers/status-configs.mjs";
29
+ import { registerStorybook } from "./routers/storybook.mjs";
30
+ import { registerTokens } from "./routers/tokens.mjs";
31
+ import { registerUiPages } from "./routers/ui.mjs";
32
+ import { registerWebhooks } from "./routers/webhooks.mjs";
33
+ import { registerWellKnown } from "./routers/well-known.mjs";
34
+ import { resolveRuntime } from "./runtime.mjs";
35
+ import { swaggerUI } from "@hono/swagger-ui";
36
+ import { OpenAPIHono } from "@hono/zod-openapi";
37
+ import { AdapterLifecycleError, validateBootAssembly } from "@storyshelf/core/adapter/setup";
38
+ import { createHttpMiddleware, createInstrumentedDatabase, createInstrumentedStorage } from "@storyshelf/observability";
39
+ import { requestId } from "hono/request-id";
40
+ import { createShelfLogger } from "@storyshelf/core/logger";
41
+ //#region src/index.tsx
42
+ /**
43
+ * Fail fast on third-party adapters that cannot satisfy their slot.
44
+ * Throws before any route or lifecycle hook runs.
45
+ */
46
+ function throwOnInvalidAdapters(options) {
47
+ const invalid = validateBootAssembly(options);
48
+ if (invalid.length > 0) throw new AdapterLifecycleError("setup", invalid);
49
+ }
50
+ /** Request/capture-path adapters with OTEL wrappers (lifecycle keeps raw ones). */
51
+ function instrumentedOptions(options) {
52
+ return {
53
+ ...options,
54
+ database: createInstrumentedDatabase(options.database),
55
+ storage: createInstrumentedStorage(options.storage)
56
+ };
57
+ }
58
+ /** Once adapters are ready, requeue-once or fail builds a restart left `capturing`. */
59
+ function recoverAfterSetup(cell, recoverStuck, logger) {
60
+ if (!recoverStuck) return;
61
+ cell.ready.then(async (setup) => {
62
+ if (setup.ok) await recoverStuck();
63
+ }).catch((error) => {
64
+ logger.error({ err: error }, "stuck capture recovery failed");
65
+ });
66
+ }
67
+ /** Boot-time checks that run once adapters are ready: stuck-capture recovery and credential state. */
68
+ function runAfterSetup(cell, scoped, runtime, recoverStuck) {
69
+ recoverAfterSetup(cell, recoverStuck, runtime.logger);
70
+ checkCredentialsAfterSetup(cell, scoped.database, runtime.config, runtime.logger);
71
+ }
72
+ /** Attach lifecycle, queue, middleware, routes, and docs to the app. */
73
+ function assembleApp(app, options, scoped) {
74
+ const runtime = resolveRuntime(options);
75
+ const cell = {
76
+ ready: Promise.resolve({
77
+ ok: true,
78
+ failures: []
79
+ }),
80
+ settled: null
81
+ };
82
+ attachLifecycle(app, options, runtime, cell);
83
+ if (runtime.config.serverTiming === true) app.use("*", serverTiming(true));
84
+ const { queue, enqueueCapture, recoverStuck } = setupCaptureQueue(scoped, runtime.config, runtime.gitHosts, runtime.logger);
85
+ runAfterSetup(cell, scoped, runtime, recoverStuck);
86
+ wireMiddleware(app, {
87
+ ...runtime,
88
+ queue,
89
+ enqueueCapture,
90
+ options: scoped,
91
+ getReady: async () => await cell.ready
92
+ });
93
+ registerAllRoutes(app, options, {
94
+ sources: options,
95
+ getSettled: () => cell.settled,
96
+ bootTimeMs: Date.now(),
97
+ version: packageVersion()
98
+ });
99
+ registerDocs(app);
100
+ }
101
+ /**
102
+ * Create the StoryShelf Hono app with all API routes and HTML pages.
103
+ *
104
+ * Adapter `init` hooks kick off eagerly in the background (the constructor
105
+ * stays synchronous). Await `app.lifecycle.setup()` before serving for
106
+ * fail-fast startup, or let the first request gate on settlement.
107
+ */
108
+ function createShelfApp(options) {
109
+ throwOnInvalidAdapters(options);
110
+ const app = new OpenAPIHono();
111
+ assembleApp(app, options, instrumentedOptions(options));
112
+ return app;
113
+ }
114
+ /** Cookie-authenticated write paths guarded by CSRF tokens. */
115
+ const CSRF_PATHS = [
116
+ "/auth/logout",
117
+ "/auth/invites/*",
118
+ "/projects/:slug/settings/*",
119
+ "/profile/*",
120
+ "/profile",
121
+ "/admin/credentials/*"
122
+ ];
123
+ /** Attach global middleware: ids, logging, init gate, limits, store scope, auth gate. */
124
+ function wireMiddleware(app, wiring) {
125
+ const { config, logger, getReady } = wiring;
126
+ app.use("*", requestId());
127
+ app.use("*", createHttpMiddleware({
128
+ serviceName: "storyshelf",
129
+ serviceVersion: packageVersion()
130
+ }));
131
+ app.use("*", requestLogging(logger));
132
+ app.use("*", setupGate(getReady));
133
+ mountGuards(app, config);
134
+ mountNotificationLimits(app);
135
+ mountStoreScope(app, wiring);
136
+ app.use("*", authGate());
137
+ }
138
+ /** Mount rate limits and CSRF guards (abuse floors, not strict protection). */
139
+ function mountGuards(app, config) {
140
+ app.use("/api/v1/*", rateLimit({
141
+ windowMs: 6e4,
142
+ max: 100
143
+ }));
144
+ app.use("/api/v1/tokens/*", rateLimit({
145
+ windowMs: 6e4,
146
+ max: 10
147
+ }));
148
+ app.use("/api/v1/webhooks/*", rateLimit({
149
+ windowMs: 6e4,
150
+ max: 20
151
+ }));
152
+ app.use("/auth/*", rateLimit({
153
+ windowMs: 6e4,
154
+ max: 100,
155
+ keyGenerator: rateLimitKey("auth")
156
+ }));
157
+ app.use("/api/auth/*", rateLimit({
158
+ windowMs: 6e4,
159
+ max: 300,
160
+ keyGenerator: rateLimitKey("engine")
161
+ }));
162
+ for (const path of CSRF_PATHS) app.use(path, csrf(config.secret));
163
+ }
164
+ /** Abuse floors for notification management endpoints (own counters). */
165
+ function mountNotificationLimits(app) {
166
+ const channel = {
167
+ windowMs: 6e4,
168
+ max: 20,
169
+ keyGenerator: rateLimitKey("notify-channels")
170
+ };
171
+ const admin = {
172
+ windowMs: 6e4,
173
+ max: 20,
174
+ keyGenerator: rateLimitKey("notify-admin")
175
+ };
176
+ const subscription = {
177
+ windowMs: 6e4,
178
+ max: 30,
179
+ keyGenerator: rateLimitKey("notify-subs")
180
+ };
181
+ app.use("/api/v1/projects/:slug/notification-channels", rateLimit(channel));
182
+ app.use("/api/v1/projects/:slug/notification-channels/:channelId", rateLimit(channel));
183
+ app.use("/api/v1/projects/:slug/notifications", rateLimit(subscription));
184
+ app.use("/api/v1/projects/:slug/notifications/me", rateLimit(subscription));
185
+ app.use("/api/v1/admin/notification-channels", rateLimit(admin));
186
+ app.use("/api/v1/admin/notification-channels/:channelId", rateLimit(admin));
187
+ }
188
+ /** Mount the request-scoped store (db/storage/config/user/adapters). */
189
+ function mountStoreScope(app, wiring) {
190
+ const { options, config, ui, logger, authEnabled, enqueueCapture, queue, gitHosts, notifiers } = wiring;
191
+ app.use("*", storeScope({
192
+ db: options.database,
193
+ storage: options.storage,
194
+ config,
195
+ ui,
196
+ logger,
197
+ authEnabled,
198
+ enqueueCapture,
199
+ captureQueue: queue,
200
+ gitHosts,
201
+ notifiers,
202
+ resolveUser: async (c) => await resolveRequestUser(c, options.auth)
203
+ }));
204
+ }
205
+ /** Register every JSON API router. */
206
+ function registerApiRoutes(app, health) {
207
+ registerProjectRoutes(app);
208
+ registerNotificationRoutes(app);
209
+ registerStatusConfigs(app);
210
+ registerHealth(app, health);
211
+ registerAdmin(app);
212
+ }
213
+ /** Register the per-project resource routers. */
214
+ function registerProjectRoutes(app) {
215
+ registerProjects(app);
216
+ registerBuilds(app);
217
+ registerLabels(app);
218
+ registerMedia(app);
219
+ registerMembers(app);
220
+ registerTokens(app);
221
+ registerWebhooks(app);
222
+ }
223
+ /** Register the notification channel and subscription routers. */
224
+ function registerNotificationRoutes(app) {
225
+ registerNotificationChannels(app);
226
+ registerNotificationSubscriptions(app);
227
+ registerSystemChannels(app);
228
+ }
229
+ /** Register the auth flow: engine mount, relying-party documents, profile. */
230
+ function registerAuthFlow(app, options) {
231
+ if (!options.auth) return;
232
+ registerEngineAuth(app, options.auth);
233
+ registerWellKnown(app, options.auth);
234
+ registerProfile(app, options.auth);
235
+ }
236
+ /** Register HTML pages, assets, and the optional auth flow. */
237
+ function registerPageRoutes(app, options, health) {
238
+ registerAuthFlow(app, options);
239
+ registerAssets(app);
240
+ registerStorybook(app);
241
+ registerAdminPages(app, health, options.auth);
242
+ registerUiPages(app);
243
+ }
244
+ /** Register every API router, page set, and optional auth flow. */
245
+ function registerAllRoutes(app, options, health) {
246
+ registerApiRoutes(app, health);
247
+ registerPageRoutes(app, options, health);
248
+ }
249
+ /** Register the OpenAPI document and interactive docs page. */
250
+ function registerDocs(app) {
251
+ app.doc("/api/v1/openapi.json", {
252
+ openapi: "3.0.0",
253
+ info: {
254
+ title: "StoryShelf API",
255
+ description: "REST API for the StoryShelf visual testing platform. JSON endpoints live under /api/v1; HTML pages are served at /.",
256
+ version: "1.0.0"
257
+ },
258
+ tags: [
259
+ {
260
+ name: "Projects",
261
+ description: "Create and manage projects (one project is one Storybook)."
262
+ },
263
+ {
264
+ name: "Builds",
265
+ description: "Upload Storybook builds, inspect capture attempts, retry, and delete."
266
+ },
267
+ {
268
+ name: "Review",
269
+ description: "Approve or reject snapshots, bulk actions, re-diff, and review comments."
270
+ },
271
+ {
272
+ name: "Labels",
273
+ description: "Typed build labels and their link templates."
274
+ },
275
+ {
276
+ name: "Members",
277
+ description: "Project members, roles, and identity-provider group mappings."
278
+ },
279
+ {
280
+ name: "Tokens",
281
+ description: "API tokens used by the CLI and CI."
282
+ },
283
+ {
284
+ name: "Webhooks",
285
+ description: "Signed webhook deliveries for build events."
286
+ },
287
+ {
288
+ name: "Notifications",
289
+ description: "Email and chat notification channels and subscriptions."
290
+ },
291
+ {
292
+ name: "Git status",
293
+ description: "Commit-status integrations for GitHub and GitLab."
294
+ },
295
+ {
296
+ name: "Admin",
297
+ description: "Site-wide administration."
298
+ }
299
+ ]
300
+ });
301
+ app.get("/api/v1/docs", swaggerUI({ url: "/api/v1/openapi.json" }));
302
+ }
303
+ /** Package version injected at build via __PKG_VERSION__. */
304
+ function packageVersion() {
305
+ return globalThis.__PKG_VERSION__ ?? "0.0.0";
306
+ }
307
+ //#endregion
308
+ export { createAuthSystemHook, createShelfApp, createShelfLogger };
309
+
310
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/index.tsx"],"sourcesContent":["import { swaggerUI } from \"@hono/swagger-ui\";\nimport { OpenAPIHono } from \"@hono/zod-openapi\";\nimport { AdapterLifecycleError, validateBootAssembly } from \"@storyshelf/core/adapter/setup\";\nimport type { ShelfConfig, ShelfOptions } from \"@storyshelf/core/config\";\n/**\n * StoryShelf app: compose database, storage, capture, auth, and git-host\n * adapters into a complete self-hosted visual-testing Hono app.\n */\nimport type { Logger } from \"@storyshelf/core/logger\";\nimport {\n createHttpMiddleware,\n createInstrumentedDatabase,\n createInstrumentedStorage,\n} from \"@storyshelf/observability\";\nimport { requestId } from \"hono/request-id\";\nimport type { ShelfApp, ShelfContext, ShelfRouter } from \"./app-types.ts\";\nimport { setupCaptureQueue } from \"./capture-setup.ts\";\nimport { checkCredentialsAfterSetup } from \"./credentials.ts\";\nimport { attachLifecycle, type LifecycleCell } from \"./lifecycle.ts\";\nimport {\n authGate,\n csrf,\n rateLimit,\n rateLimitKey,\n requestLogging,\n resolveRequestUser,\n storeScope,\n} from \"./middleware/index.ts\";\nimport { serverTiming } from \"./middleware/server-timing.ts\";\nimport { setupGate, type MiddlewareWiring } from \"./middleware/setup-gate.ts\";\nimport { registerAdminPages } from \"./routers/admin-pages.ts\";\nimport { registerAdmin } from \"./routers/admin.ts\";\nimport { registerAssets } from \"./routers/assets.ts\";\nimport { registerEngineAuth } from \"./routers/auth.ts\";\nimport { registerBuilds } from \"./routers/builds.ts\";\nimport { registerHealth, type HealthDeps } from \"./routers/health.ts\";\nimport { registerLabels } from \"./routers/labels.ts\";\nimport { registerMedia } from \"./routers/media.ts\";\nimport { registerMembers } from \"./routers/members.ts\";\nimport {\n registerNotificationChannels,\n registerSystemChannels,\n} from \"./routers/notification-channels.ts\";\nimport { registerNotificationSubscriptions } from \"./routers/notification-subscriptions.ts\";\nimport { registerProfile } from \"./routers/profile.ts\";\nimport { registerProjects } from \"./routers/projects.ts\";\nimport { registerStatusConfigs } from \"./routers/status-configs.ts\";\nimport { registerStorybook } from \"./routers/storybook.ts\";\nimport { registerTokens } from \"./routers/tokens.ts\";\nimport { registerUiPages } from \"./routers/ui.ts\";\nimport { registerWebhooks } from \"./routers/webhooks.ts\";\nimport { registerWellKnown } from \"./routers/well-known.ts\";\nimport { resolveRuntime } from \"./runtime.ts\";\n\n/**\n * Fail fast on third-party adapters that cannot satisfy their slot.\n * Throws before any route or lifecycle hook runs.\n */\nfunction throwOnInvalidAdapters(options: ShelfOptions): void {\n const invalid = validateBootAssembly(options);\n if (invalid.length > 0) {\n throw new AdapterLifecycleError(\"setup\", invalid);\n }\n}\n\n/** Request/capture-path adapters with OTEL wrappers (lifecycle keeps raw ones). */\nfunction instrumentedOptions(options: ShelfOptions): ShelfOptions {\n return {\n ...options,\n database: createInstrumentedDatabase(options.database),\n storage: createInstrumentedStorage(options.storage),\n };\n}\n\n/** Once adapters are ready, requeue-once or fail builds a restart left `capturing`. */\nfunction recoverAfterSetup(\n cell: LifecycleCell,\n recoverStuck: (() => Promise<unknown>) | undefined,\n logger: Logger,\n): void {\n if (!recoverStuck) {\n return;\n }\n cell.ready\n .then(async (setup) => {\n if (setup.ok) {\n await recoverStuck();\n }\n })\n .catch((error: unknown) => {\n logger.error({ err: error }, \"stuck capture recovery failed\");\n });\n}\n\n/** Boot-time checks that run once adapters are ready: stuck-capture recovery and credential state. */\nfunction runAfterSetup(\n cell: LifecycleCell,\n scoped: ShelfOptions,\n runtime: ReturnType<typeof resolveRuntime>,\n recoverStuck: (() => Promise<unknown>) | undefined,\n): void {\n recoverAfterSetup(cell, recoverStuck, runtime.logger);\n checkCredentialsAfterSetup(cell, scoped.database, runtime.config, runtime.logger);\n}\n\n/** Attach lifecycle, queue, middleware, routes, and docs to the app. */\nfunction assembleApp(app: ShelfApp, options: ShelfOptions, scoped: ShelfOptions): void {\n const runtime = resolveRuntime(options);\n const cell: LifecycleCell = { ready: Promise.resolve({ ok: true, failures: [] }), settled: null };\n attachLifecycle(app, options, runtime, cell);\n if (runtime.config.serverTiming === true) {\n app.use(\"*\", serverTiming(true));\n }\n const { queue, enqueueCapture, recoverStuck } = setupCaptureQueue(\n scoped,\n runtime.config,\n runtime.gitHosts,\n runtime.logger,\n );\n runAfterSetup(cell, scoped, runtime, recoverStuck);\n wireMiddleware(app, {\n ...runtime,\n queue,\n enqueueCapture,\n options: scoped,\n getReady: async () => await cell.ready,\n });\n registerAllRoutes(app, options, {\n sources: options,\n getSettled: () => cell.settled,\n bootTimeMs: Date.now(),\n version: packageVersion(),\n });\n registerDocs(app);\n}\n\n/**\n * Create the StoryShelf Hono app with all API routes and HTML pages.\n *\n * Adapter `init` hooks kick off eagerly in the background (the constructor\n * stays synchronous). Await `app.lifecycle.setup()` before serving for\n * fail-fast startup, or let the first request gate on settlement.\n */\nexport function createShelfApp(options: ShelfOptions): ShelfApp {\n throwOnInvalidAdapters(options);\n const app = new OpenAPIHono<{ Variables: ShelfContext }>() as ShelfApp;\n assembleApp(app, options, instrumentedOptions(options));\n\n return app;\n}\n\n/** Cookie-authenticated write paths guarded by CSRF tokens. */\nconst CSRF_PATHS = [\n \"/auth/logout\",\n \"/auth/invites/*\",\n \"/projects/:slug/settings/*\",\n \"/profile/*\",\n \"/profile\",\n \"/admin/credentials/*\",\n] as const;\n\n/** Attach global middleware: ids, logging, init gate, limits, store scope, auth gate. */\nfunction wireMiddleware(app: ShelfRouter, wiring: MiddlewareWiring): void {\n const { config, logger, getReady } = wiring;\n app.use(\"*\", requestId());\n // OTEL server spans (W3C extraction, route-template naming). Mounted right\n // after requestId so `storyshelf.req_id` resolves; `enduser.id` resolves\n // once storeScope runs downstream. Noop without an SDK.\n app.use(\n \"*\",\n createHttpMiddleware({ serviceName: \"storyshelf\", serviceVersion: packageVersion() }),\n );\n // Structured request logging. Uses a Hono-native middleware rather than\n // Pino-http, which expects a Node server response (`res.on`) incompatible\n // With Hono's Web `Request`/`Response` model.\n app.use(\"*\", requestLogging(logger));\n app.use(\"*\", setupGate(getReady));\n mountGuards(app, config);\n mountNotificationLimits(app);\n mountStoreScope(app, wiring);\n app.use(\"*\", authGate());\n}\n\n/** Mount rate limits and CSRF guards (abuse floors, not strict protection). */\nfunction mountGuards(app: ShelfRouter, config: ShelfConfig): void {\n app.use(\"/api/v1/*\", rateLimit({ windowMs: 60_000, max: 100 }));\n app.use(\"/api/v1/tokens/*\", rateLimit({ windowMs: 60_000, max: 10 }));\n app.use(\"/api/v1/webhooks/*\", rateLimit({ windowMs: 60_000, max: 20 }));\n // Auth endpoints ride their own buckets (the shared store keys by IP, so\n // prefix the key: login floods must not eat the API budget or vice versa).\n app.use(\"/auth/*\", rateLimit({ windowMs: 60_000, max: 100, keyGenerator: rateLimitKey(\"auth\") }));\n app.use(\n \"/api/auth/*\",\n rateLimit({ windowMs: 60_000, max: 300, keyGenerator: rateLimitKey(\"engine\") }),\n );\n for (const path of CSRF_PATHS) {\n app.use(path, csrf(config.secret));\n }\n}\n\n/** Abuse floors for notification management endpoints (own counters). */\nfunction mountNotificationLimits(app: ShelfRouter): void {\n const channel = { windowMs: 60_000, max: 20, keyGenerator: rateLimitKey(\"notify-channels\") };\n const admin = { windowMs: 60_000, max: 20, keyGenerator: rateLimitKey(\"notify-admin\") };\n const subscription = { windowMs: 60_000, max: 30, keyGenerator: rateLimitKey(\"notify-subs\") };\n app.use(\"/api/v1/projects/:slug/notification-channels\", rateLimit(channel));\n app.use(\"/api/v1/projects/:slug/notification-channels/:channelId\", rateLimit(channel));\n app.use(\"/api/v1/projects/:slug/notifications\", rateLimit(subscription));\n app.use(\"/api/v1/projects/:slug/notifications/me\", rateLimit(subscription));\n app.use(\"/api/v1/admin/notification-channels\", rateLimit(admin));\n app.use(\"/api/v1/admin/notification-channels/:channelId\", rateLimit(admin));\n}\n\n/** Mount the request-scoped store (db/storage/config/user/adapters). */\nfunction mountStoreScope(app: ShelfRouter, wiring: MiddlewareWiring): void {\n const { options, config, ui, logger, authEnabled, enqueueCapture, queue, gitHosts, notifiers } =\n wiring;\n app.use(\n \"*\",\n storeScope({\n db: options.database,\n storage: options.storage,\n config,\n ui,\n logger,\n authEnabled,\n enqueueCapture,\n captureQueue: queue,\n gitHosts,\n notifiers,\n resolveUser: async (c) => await resolveRequestUser(c, options.auth),\n }),\n );\n}\n\n/** Register every JSON API router. */\nfunction registerApiRoutes(app: ShelfRouter, health: HealthDeps): void {\n registerProjectRoutes(app);\n registerNotificationRoutes(app);\n registerStatusConfigs(app);\n registerHealth(app, health);\n registerAdmin(app);\n}\n\n/** Register the per-project resource routers. */\nfunction registerProjectRoutes(app: ShelfRouter): void {\n registerProjects(app);\n registerBuilds(app);\n registerLabels(app);\n registerMedia(app);\n registerMembers(app);\n registerTokens(app);\n registerWebhooks(app);\n}\n\n/** Register the notification channel and subscription routers. */\nfunction registerNotificationRoutes(app: ShelfRouter): void {\n registerNotificationChannels(app);\n registerNotificationSubscriptions(app);\n registerSystemChannels(app);\n}\n\n/** Register the auth flow: engine mount, relying-party documents, profile. */\nfunction registerAuthFlow(app: ShelfRouter, options: ShelfOptions): void {\n // Auth-gated pages mount only with auth: authEnabled=false mounts zero\n // auth routes. Static imports stay — true lazy loading would need an async\n // createShelfApp, which the sync Hono assembly cannot do.\n if (!options.auth) {\n return;\n }\n registerEngineAuth(app, options.auth);\n registerWellKnown(app, options.auth);\n registerProfile(app, options.auth);\n}\n\n/** Register HTML pages, assets, and the optional auth flow. */\nfunction registerPageRoutes(app: ShelfRouter, options: ShelfOptions, health: HealthDeps): void {\n registerAuthFlow(app, options);\n registerAssets(app);\n registerStorybook(app);\n registerAdminPages(app, health, options.auth);\n registerUiPages(app);\n}\n\n/** Register every API router, page set, and optional auth flow. */\nfunction registerAllRoutes(app: ShelfRouter, options: ShelfOptions, health: HealthDeps): void {\n registerApiRoutes(app, health);\n registerPageRoutes(app, options, health);\n}\n\n/** Register the OpenAPI document and interactive docs page. */\nfunction registerDocs(app: ShelfRouter): void {\n app.doc(\"/api/v1/openapi.json\", {\n openapi: \"3.0.0\",\n info: {\n title: \"StoryShelf API\",\n description:\n \"REST API for the StoryShelf visual testing platform. JSON endpoints live under /api/v1; HTML pages are served at /.\",\n version: \"1.0.0\",\n },\n tags: [\n {\n name: \"Projects\",\n description: \"Create and manage projects (one project is one Storybook).\",\n },\n {\n name: \"Builds\",\n description: \"Upload Storybook builds, inspect capture attempts, retry, and delete.\",\n },\n {\n name: \"Review\",\n description: \"Approve or reject snapshots, bulk actions, re-diff, and review comments.\",\n },\n { name: \"Labels\", description: \"Typed build labels and their link templates.\" },\n {\n name: \"Members\",\n description: \"Project members, roles, and identity-provider group mappings.\",\n },\n { name: \"Tokens\", description: \"API tokens used by the CLI and CI.\" },\n { name: \"Webhooks\", description: \"Signed webhook deliveries for build events.\" },\n {\n name: \"Notifications\",\n description: \"Email and chat notification channels and subscriptions.\",\n },\n { name: \"Git status\", description: \"Commit-status integrations for GitHub and GitLab.\" },\n { name: \"Admin\", description: \"Site-wide administration.\" },\n ],\n });\n app.get(\"/api/v1/docs\", swaggerUI({ url: \"/api/v1/openapi.json\" }));\n}\n\n/** Package version injected at build via __PKG_VERSION__. */\nfunction packageVersion(): string {\n return (globalThis as unknown as { __PKG_VERSION__?: string }).__PKG_VERSION__ ?? \"0.0.0\";\n}\n\n/**\n * Public surface: the app and its types only.\n *\n * `createShelfApp` plus the option/config types needed to call it. Adapter\n * interfaces live under `core/adapter/*`; runtime helpers under `core/logger`,\n * `core/capture`, `core/paths`, `core/urls`, `core/diff`; tables and row types\n * under `core/schema`. Importing the barrel must never pull Node-only modules\n * into edge bundles beyond what Hono itself needs.\n */\nexport { createAuthSystemHook } from \"./notify.ts\";\nexport type {\n ShelfOptions,\n ShelfConfig,\n UIConfig,\n BrandTheme,\n NotificationsConfig,\n} from \"@storyshelf/core/config\";\nexport {\n createShelfLogger,\n type Logger,\n type LoggerOptions,\n type PinoTransport,\n} from \"@storyshelf/core/logger\";\nexport type { ShelfApp, ShelfContext, ShelfLifecycle, ShelfRouter } from \"./app-types.ts\";\n"],"mappings":"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"}