@storyshelf/core 0.1.3 → 0.3.2

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 (239) hide show
  1. package/README.md +50 -46
  2. package/dist/adapter/auth.d.mts +4 -4
  3. package/dist/adapter/auth.mjs.map +1 -1
  4. package/dist/{adapters → adapter}/capture-queue.d.mts +4 -4
  5. package/dist/adapter/capture-queue.mjs +4 -0
  6. package/dist/adapter/capture-runner.d.mts +7 -7
  7. package/dist/adapter/database.d.mts +20 -9
  8. package/dist/adapter/database.mjs +60 -1
  9. package/dist/adapter/database.mjs.map +1 -0
  10. package/dist/adapter/git-host/comments.d.mts +41 -0
  11. package/dist/adapter/git-host/comments.mjs +54 -0
  12. package/dist/adapter/git-host/comments.mjs.map +1 -0
  13. package/dist/adapter/git-host.d.mts +3 -7
  14. package/dist/adapter/init.d.mts +58 -0
  15. package/dist/adapter/init.mjs +93 -0
  16. package/dist/adapter/init.mjs.map +1 -0
  17. package/dist/adapter/metadata.d.mts +83 -0
  18. package/dist/adapter/metadata.mjs +4 -0
  19. package/dist/adapter/storage.d.mts +15 -4
  20. package/dist/adapters/webhook-events.mjs +12 -4
  21. package/dist/adapters/webhook-events.mjs.map +1 -1
  22. package/dist/capture/adapter.d.mts +5 -1
  23. package/dist/capture/dispatch.mjs +150 -0
  24. package/dist/capture/dispatch.mjs.map +1 -0
  25. package/dist/capture/orchestrator.d.mts +2 -0
  26. package/dist/capture/orchestrator.mjs +5 -44
  27. package/dist/capture/orchestrator.mjs.map +1 -1
  28. package/dist/capture/pipeline.d.mts +4 -1
  29. package/dist/capture/pipeline.mjs +6 -6
  30. package/dist/capture/pipeline.mjs.map +1 -1
  31. package/dist/capture/queue.d.mts +2 -1
  32. package/dist/capture/queue.mjs +2 -1
  33. package/dist/capture/queue.mjs.map +1 -1
  34. package/dist/capture/skip-checks.mjs +76 -0
  35. package/dist/capture/skip-checks.mjs.map +1 -0
  36. package/dist/capture/statics.mjs +124 -0
  37. package/dist/capture/statics.mjs.map +1 -0
  38. package/dist/capture/status-fanout.mjs.map +1 -1
  39. package/dist/capture/storybook.mjs.map +1 -1
  40. package/dist/capture.d.mts +8 -0
  41. package/dist/capture.mjs +9 -0
  42. package/dist/config.d.mts +10 -2
  43. package/dist/config.mjs +14 -2
  44. package/dist/config.mjs.map +1 -1
  45. package/dist/ddl.d.mts +1 -1
  46. package/dist/ddl.mjs +1 -1
  47. package/dist/ddl.mjs.map +1 -1
  48. package/dist/diff/engine.mjs.map +1 -1
  49. package/dist/diff/options.d.mts +3 -1
  50. package/dist/diff.d.mts +5 -0
  51. package/dist/diff.mjs +6 -0
  52. package/dist/index.d.mts +32 -25
  53. package/dist/index.mjs +207 -253
  54. package/dist/index.mjs.map +1 -1
  55. package/dist/logger.d.mts +3 -3
  56. package/dist/middleware/auth-gate.mjs +36 -0
  57. package/dist/middleware/auth-gate.mjs.map +1 -0
  58. package/dist/middleware/csrf.mjs.map +1 -1
  59. package/dist/middleware/request-log.mjs +28 -0
  60. package/dist/middleware/request-log.mjs.map +1 -0
  61. package/dist/middleware/store-scope.mjs +29 -0
  62. package/dist/middleware/store-scope.mjs.map +1 -0
  63. package/dist/models/baseline.mjs +7 -5
  64. package/dist/models/baseline.mjs.map +1 -1
  65. package/dist/models/build.mjs +3 -1
  66. package/dist/models/build.mjs.map +1 -1
  67. package/dist/models/comment.mjs +2 -1
  68. package/dist/models/comment.mjs.map +1 -1
  69. package/dist/models/label.mjs +2 -1
  70. package/dist/models/label.mjs.map +1 -1
  71. package/dist/models/member.mjs +1 -1
  72. package/dist/models/member.mjs.map +1 -1
  73. package/dist/models/project.mjs +1 -1
  74. package/dist/models/project.mjs.map +1 -1
  75. package/dist/models/snapshot.mjs +1 -1
  76. package/dist/models/snapshot.mjs.map +1 -1
  77. package/dist/models/status-config.mjs +1 -1
  78. package/dist/models/status-config.mjs.map +1 -1
  79. package/dist/models/token.mjs +1 -1
  80. package/dist/models/token.mjs.map +1 -1
  81. package/dist/models/webhook.mjs +11 -3
  82. package/dist/models/webhook.mjs.map +1 -1
  83. package/dist/pages/build-detail.mjs +9 -4
  84. package/dist/pages/build-detail.mjs.map +1 -1
  85. package/dist/pages/build-diff-comments.mjs +113 -0
  86. package/dist/pages/build-diff-comments.mjs.map +1 -0
  87. package/dist/pages/build-diff-header.mjs +163 -0
  88. package/dist/pages/build-diff-header.mjs.map +1 -0
  89. package/dist/pages/build-diff-nav.mjs +66 -0
  90. package/dist/pages/build-diff-nav.mjs.map +1 -0
  91. package/dist/pages/build-diff-viewer.mjs +197 -0
  92. package/dist/pages/build-diff-viewer.mjs.map +1 -0
  93. package/dist/pages/build-diff.mjs +65 -413
  94. package/dist/pages/build-diff.mjs.map +1 -1
  95. package/dist/pages/compute-jobs.mjs +1 -1
  96. package/dist/pages/compute-jobs.mjs.map +1 -1
  97. package/dist/pages/label-detail.mjs +4 -3
  98. package/dist/pages/label-detail.mjs.map +1 -1
  99. package/dist/pages/login.mjs.map +1 -1
  100. package/dist/pages/project-builds.mjs +5 -3
  101. package/dist/pages/project-builds.mjs.map +1 -1
  102. package/dist/pages/project-create.mjs.map +1 -1
  103. package/dist/pages/project-settings.mjs.map +1 -1
  104. package/dist/pages/projects.mjs +11 -6
  105. package/dist/pages/projects.mjs.map +1 -1
  106. package/dist/pages/root.mjs +6 -3
  107. package/dist/pages/root.mjs.map +1 -1
  108. package/dist/pages/settings-general.mjs.map +1 -1
  109. package/dist/pages/settings-labels.mjs +1 -1
  110. package/dist/pages/settings-labels.mjs.map +1 -1
  111. package/dist/pages/settings-members.mjs +1 -1
  112. package/dist/pages/settings-members.mjs.map +1 -1
  113. package/dist/pages/settings-status.mjs +2 -1
  114. package/dist/pages/settings-status.mjs.map +1 -1
  115. package/dist/pages/settings-tests.mjs +3 -3
  116. package/dist/pages/settings-tests.mjs.map +1 -1
  117. package/dist/pages/settings-tokens.mjs.map +1 -1
  118. package/dist/pages/settings-webhooks.mjs.map +1 -1
  119. package/dist/pages/storybook.mjs +38 -1
  120. package/dist/pages/storybook.mjs.map +1 -1
  121. package/dist/paths.mjs.map +1 -0
  122. package/dist/retention/purge.mjs +6 -3
  123. package/dist/retention/purge.mjs.map +1 -1
  124. package/dist/routers/admin.mjs +1 -1
  125. package/dist/routers/admin.mjs.map +1 -1
  126. package/dist/routers/assets.mjs.map +1 -1
  127. package/dist/routers/auth.mjs.map +1 -1
  128. package/dist/routers/builds.handlers.mjs +106 -21
  129. package/dist/routers/builds.handlers.mjs.map +1 -1
  130. package/dist/routers/builds.mjs +54 -34
  131. package/dist/routers/builds.mjs.map +1 -1
  132. package/dist/routers/comments.mjs.map +1 -1
  133. package/dist/routers/health.mjs +137 -0
  134. package/dist/routers/health.mjs.map +1 -0
  135. package/dist/routers/helpers.mjs +2 -2
  136. package/dist/routers/helpers.mjs.map +1 -1
  137. package/dist/routers/htmx.mjs.map +1 -1
  138. package/dist/routers/labels.mjs +1 -1
  139. package/dist/routers/labels.mjs.map +1 -1
  140. package/dist/routers/media.mjs +2 -2
  141. package/dist/routers/media.mjs.map +1 -1
  142. package/dist/routers/members.mjs.map +1 -1
  143. package/dist/routers/projects.mjs +1 -1
  144. package/dist/routers/projects.mjs.map +1 -1
  145. package/dist/routers/schemas.mjs.map +1 -1
  146. package/dist/routers/settings-general.mjs +95 -0
  147. package/dist/routers/settings-general.mjs.map +1 -0
  148. package/dist/routers/settings-labels.mjs +49 -0
  149. package/dist/routers/settings-labels.mjs.map +1 -0
  150. package/dist/routers/settings-members.mjs +30 -0
  151. package/dist/routers/settings-members.mjs.map +1 -0
  152. package/dist/routers/settings-status.mjs +73 -0
  153. package/dist/routers/settings-status.mjs.map +1 -0
  154. package/dist/routers/settings-tokens.mjs +40 -0
  155. package/dist/routers/settings-tokens.mjs.map +1 -0
  156. package/dist/routers/settings-webhooks.mjs +63 -0
  157. package/dist/routers/settings-webhooks.mjs.map +1 -0
  158. package/dist/routers/settings.handlers.mjs +93 -0
  159. package/dist/routers/settings.handlers.mjs.map +1 -0
  160. package/dist/routers/settings.mjs +13 -247
  161. package/dist/routers/settings.mjs.map +1 -1
  162. package/dist/routers/snapshots.mjs +1 -1
  163. package/dist/routers/snapshots.mjs.map +1 -1
  164. package/dist/routers/status-configs.mjs +1 -1
  165. package/dist/routers/status-configs.mjs.map +1 -1
  166. package/dist/routers/storybook.mjs +11 -4
  167. package/dist/routers/storybook.mjs.map +1 -1
  168. package/dist/routers/tokens.mjs +1 -1
  169. package/dist/routers/tokens.mjs.map +1 -1
  170. package/dist/routers/ui.mjs +4 -4
  171. package/dist/routers/ui.mjs.map +1 -1
  172. package/dist/routers/webhooks.mjs +6 -6
  173. package/dist/routers/webhooks.mjs.map +1 -1
  174. package/dist/schema/baseline.d.mts +18 -0
  175. package/dist/schema/baseline.mjs +22 -0
  176. package/dist/schema/baseline.mjs.map +1 -0
  177. package/dist/schema/build.d.mts +26 -0
  178. package/dist/schema/build.mjs +29 -0
  179. package/dist/schema/build.mjs.map +1 -0
  180. package/dist/schema/comment.d.mts +19 -0
  181. package/dist/schema/comment.mjs +26 -0
  182. package/dist/schema/comment.mjs.map +1 -0
  183. package/dist/schema/label.d.mts +25 -0
  184. package/dist/schema/label.mjs +30 -0
  185. package/dist/schema/label.mjs.map +1 -0
  186. package/dist/schema/member.d.mts +15 -0
  187. package/dist/schema/member.mjs +19 -0
  188. package/dist/schema/member.mjs.map +1 -0
  189. package/dist/schema/project.d.mts +34 -0
  190. package/dist/schema/project.mjs +25 -0
  191. package/dist/schema/project.mjs.map +1 -0
  192. package/dist/schema/snapshot.d.mts +30 -0
  193. package/dist/schema/snapshot.mjs +34 -0
  194. package/dist/schema/snapshot.mjs.map +1 -0
  195. package/dist/schema/status-config.d.mts +16 -0
  196. package/dist/schema/status-config.mjs +20 -0
  197. package/dist/schema/status-config.mjs.map +1 -0
  198. package/dist/schema/token.d.mts +15 -0
  199. package/dist/schema/token.mjs +19 -0
  200. package/dist/schema/token.mjs.map +1 -0
  201. package/dist/schema/user.d.mts +17 -0
  202. package/dist/schema/user.mjs +19 -0
  203. package/dist/schema/user.mjs.map +1 -0
  204. package/dist/schema/webhook.d.mts +16 -0
  205. package/dist/schema/webhook.mjs +20 -0
  206. package/dist/schema/webhook.mjs.map +1 -0
  207. package/dist/schema.d.mts +13 -168
  208. package/dist/schema.mjs +12 -2
  209. package/dist/schema.mjs.map +1 -1
  210. package/dist/store.mjs.map +1 -1
  211. package/dist/types.d.mts +1 -2
  212. package/dist/types.mjs.map +1 -1
  213. package/dist/ui/document.mjs.map +1 -1
  214. package/dist/ui/feedback.mjs +24 -0
  215. package/dist/ui/feedback.mjs.map +1 -0
  216. package/dist/ui/{components.mjs → forms.mjs} +3 -18
  217. package/dist/ui/forms.mjs.map +1 -0
  218. package/dist/ui/styles.mjs.map +1 -1
  219. package/dist/urls.mjs.map +1 -1
  220. package/package.json +47 -45
  221. package/dist/adapters/metadata.d.mts +0 -24
  222. package/dist/models/baseline.d.mts +0 -21
  223. package/dist/models/build.d.mts +0 -53
  224. package/dist/models/comment.d.mts +0 -22
  225. package/dist/models/label.d.mts +0 -49
  226. package/dist/models/member.d.mts +0 -27
  227. package/dist/models/project.d.mts +0 -43
  228. package/dist/models/snapshot.d.mts +0 -47
  229. package/dist/models/token.d.mts +0 -33
  230. package/dist/models/webhook.d.mts +0 -38
  231. package/dist/retention/purge.d.mts +0 -31
  232. package/dist/schema-tables.mjs +0 -166
  233. package/dist/schema-tables.mjs.map +0 -1
  234. package/dist/ui/components.mjs.map +0 -1
  235. package/dist/ui/document.d.mts +0 -9
  236. package/dist/utils/paths.mjs.map +0 -1
  237. package/dist/utils/ulid.d.mts +0 -10
  238. /package/dist/{utils/paths.d.mts → paths.d.mts} +0 -0
  239. /package/dist/{utils/paths.mjs → paths.mjs} +0 -0
package/dist/index.mjs CHANGED
@@ -1,21 +1,21 @@
1
1
  import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
+ import { AdapterLifecycleError, collectCloses, collectInits, runAdapterCloses, runAdapterInits } from "./adapter/init.mjs";
5
+ import { createDispatchJob } from "./capture/dispatch.mjs";
6
+ import { InMemoryCaptureQueue } from "./capture/queue.mjs";
4
7
  import { validateConfig, validateUiConfig } from "./config.mjs";
5
8
  import { createShelfLogger } from "./logger.mjs";
6
9
  import { rateLimit } from "./middleware/rate-limit.mjs";
7
10
  import { csrf } from "./middleware/csrf.mjs";
8
- import { slugify, ulid } from "./utils/ulid.mjs";
9
- import { BuildModel } from "./models/build.mjs";
10
- import { ProjectModel } from "./models/project.mjs";
11
- import { BUILD_STATUSES, PERSISTENT_LABEL_KEY, PROJECT_ROLES, RESERVED_LABEL_KEYS, SEEDED_LABEL_KEYS, SITE_ROLES, SNAPSHOT_STATUSES, TERMINAL_BUILD_STATUSES } from "./types.mjs";
12
- import { Retention } from "./retention/purge.mjs";
13
- import { getStore, runWithStore } from "./store.mjs";
11
+ import { requestLogging } from "./middleware/request-log.mjs";
12
+ import { storeScope } from "./middleware/store-scope.mjs";
13
+ import { authGate, resolveRequestUser } from "./middleware/auth-gate.mjs";
14
14
  import { registerAdmin } from "./routers/admin.mjs";
15
15
  import { registerAssets } from "./routers/assets.mjs";
16
16
  import { registerAuth } from "./routers/auth.mjs";
17
- import { baselinePath, diffPath, screenshotPath, storybookDir, storybookZipPath } from "./utils/paths.mjs";
18
17
  import { registerBuilds } from "./routers/builds.mjs";
18
+ import { registerHealth } from "./routers/health.mjs";
19
19
  import { registerLabels } from "./routers/labels.mjs";
20
20
  import { registerMedia } from "./routers/media.mjs";
21
21
  import { registerMembers } from "./routers/members.mjs";
@@ -25,16 +25,6 @@ import { registerStorybook } from "./routers/storybook.mjs";
25
25
  import { registerTokens } from "./routers/tokens.mjs";
26
26
  import { registerUiPages } from "./routers/ui.mjs";
27
27
  import { registerWebhooks } from "./routers/webhooks.mjs";
28
- import { diffImages } from "./diff/engine.mjs";
29
- import { persistCapture } from "./capture/pipeline.mjs";
30
- import { StorybookAdapter } from "./capture/storybook.mjs";
31
- import { DEFAULT_VIEWPORTS } from "./capture/adapter.mjs";
32
- import { executeCaptureJob } from "./capture/orchestrator.mjs";
33
- import { InMemoryCaptureQueue } from "./capture/queue.mjs";
34
- import { buildCommentMarkdown, commentMarker, describeStatus } from "./adapter/git-host/helpers.mjs";
35
- import { postStatusesForBuild } from "./capture/status-fanout.mjs";
36
- import { createUrlBuilder } from "./urls.mjs";
37
- import { baselines, buildLabels, builds, comments, labelTypes, projectMembers, projectStatusConfigs, projects, schema, snapshots, tokens, users, webhooks } from "./schema.mjs";
38
28
  import { swaggerUI } from "@hono/swagger-ui";
39
29
  import { OpenAPIHono } from "@hono/zod-openapi";
40
30
  import { requestId } from "hono/request-id";
@@ -43,227 +33,152 @@ import { requestId } from "hono/request-id";
43
33
  * StoryShelf core: compose database, storage, capture, auth, and git-host
44
34
  * adapters into a complete self-hosted visual-testing Hono server.
45
35
  */
36
+ function addOptionalSnapshots(snap, options) {
37
+ if (options.captureRunner) snap["captureRunner"] = options.captureRunner.metadata;
38
+ if (options.captureQueue) snap["captureQueue"] = options.captureQueue.metadata;
39
+ if (options.auth) snap["auth"] = options.auth.metadata;
40
+ }
46
41
  function buildAdapterSnapshot(options) {
47
- const snap = {};
48
- if (options.database.metadata) snap["database"] = options.database.metadata;
49
- if (options.storage.metadata) snap["storage"] = options.storage.metadata;
50
- if (options.captureRunner?.metadata) snap["captureRunner"] = options.captureRunner.metadata;
51
- if (options.captureQueue?.metadata) snap["captureQueue"] = options.captureQueue.metadata;
52
- if (options.auth?.metadata) snap["auth"] = options.auth.metadata;
42
+ const snap = {
43
+ database: options.database.metadata,
44
+ storage: options.storage.metadata
45
+ };
46
+ addOptionalSnapshots(snap, options);
53
47
  for (const p of options.gitHosts ?? []) snap[`git:${p.metadata.kind}`] = p.metadata;
54
48
  return snap;
55
49
  }
56
- async function hasApprovedBuildForSha(db, projectId, sha, excludeBuildId) {
57
- return (await new BuildModel(db).list(projectId)).some((b) => b.gitSha === sha && b.id !== excludeBuildId && b.status === "approved");
58
- }
59
- async function isAlreadyMerged(opts) {
60
- if (opts.providers.length === 0) return false;
61
- const { StatusConfigModel } = await import("./models/status-config.mjs");
62
- const model = new StatusConfigModel(opts.db, opts.secret);
63
- const rows = await model.list(opts.projectId);
64
- for (const row of rows) {
65
- const provider = opts.providers.find((p) => p.metadata.kind === row.provider);
66
- if (!provider) continue;
67
- let parsed;
68
- try {
69
- parsed = JSON.parse(row.config);
70
- provider.metadata.schema.parse(parsed);
71
- } catch {
72
- continue;
73
- }
74
- let token;
75
- try {
76
- token = model.decryptToken(row);
77
- } catch {
78
- continue;
79
- }
80
- const adapter = provider.create({
81
- config: parsed,
82
- token,
83
- logger: opts.logger
84
- });
85
- if (!adapter.isMerged) continue;
86
- try {
87
- if (await adapter.isMerged({
88
- sha: opts.sha,
89
- branch: opts.branch
90
- })) return true;
91
- } catch {}
92
- }
93
- return false;
94
- }
95
- async function resolveUser(c, options) {
96
- if (!options.auth) return null;
97
- if (c.req.header("authorization")) return null;
98
- return await options.auth.check(c.req.raw);
99
- }
100
- /** Create the StoryShelf Hono router with all API routes and HTML pages. */
101
- function createShelfRouter(options) {
102
- const app = new OpenAPIHono();
50
+ /** Validate config/ui and derive runtime singletons from options. */
51
+ function resolveRuntime(options) {
103
52
  const rawConfig = options.config ? validateConfig(options.config) : {};
104
53
  const ui = options.ui ? validateUiConfig(options.ui) : {};
105
54
  const logger = options.logger ?? createShelfLogger();
106
55
  const authEnabled = options.auth !== void 0;
107
56
  const gitHosts = options.gitHosts ?? [];
108
- const config = rawConfig.adapters ? rawConfig : {
109
- ...rawConfig,
110
- adapters: buildAdapterSnapshot(options)
57
+ return {
58
+ config: rawConfig.adapters ? rawConfig : {
59
+ ...rawConfig,
60
+ adapters: buildAdapterSnapshot(options)
61
+ },
62
+ ui,
63
+ logger,
64
+ authEnabled,
65
+ gitHosts
111
66
  };
112
- let queue = null;
113
- let enqueueCapture;
114
- if (options.captureRunner) {
115
- if (!config.scratchDir) throw new Error("captureRunner is enabled but ShelfConfig.scratchDir is not set");
116
- const jobOptions = {
117
- db: options.database,
118
- storage: options.storage,
119
- runner: options.captureRunner,
120
- scratchDir: config.scratchDir,
121
- viewports: config.viewports,
122
- logger
123
- };
124
- const captureQueue = options.captureQueue ?? new InMemoryCaptureQueue({
125
- concurrency: config.captureConcurrency ?? 2,
126
- logger,
127
- runJob: async (job) => {
128
- const builds = new BuildModel(options.database);
129
- const build = await builds.get(job.buildId);
130
- if (!build) {
131
- await executeCaptureJob({
132
- buildId: job.buildId,
133
- reqId: job.reqId
134
- }, jobOptions);
135
- return;
136
- }
137
- const project = await new ProjectModel(options.database).get(build.projectId);
138
- if (!project) {
139
- await executeCaptureJob({
140
- buildId: job.buildId,
141
- reqId: job.reqId
142
- }, jobOptions);
143
- return;
144
- }
145
- const pendingUrl = `/projects/${project.slug}/builds/${job.buildId}`;
146
- await postStatusesForBuild({
147
- db: options.database,
148
- project,
149
- sha: build.gitSha,
150
- status: "pending",
151
- url: pendingUrl,
152
- providers: gitHosts,
153
- secret: config.secret,
154
- logger
155
- }).catch((error) => {
156
- logger.error({ err: error }, "failed to post pending status");
157
- });
158
- if (!build.isDefault) {
159
- if (await isAlreadyMerged({
160
- providers: gitHosts,
161
- sha: build.gitSha,
162
- branch: build.gitBranch,
163
- secret: config.secret,
164
- db: options.database,
165
- projectId: project.id,
166
- logger
167
- }).catch(() => false)) {
168
- logger.info({
169
- buildId: build.id,
170
- sha: build.gitSha,
171
- branch: build.gitBranch
172
- }, "skipping capture — already merged");
173
- await builds.setStatus(build.id, "approved").catch(() => {});
174
- await postStatusesForBuild({
175
- db: options.database,
176
- project,
177
- sha: build.gitSha,
178
- status: "success",
179
- url: pendingUrl,
180
- providers: gitHosts,
181
- secret: config.secret,
182
- logger
183
- }).catch(() => {});
184
- return;
185
- }
186
- }
187
- if (await hasApprovedBuildForSha(options.database, project.id, build.gitSha, build.id)) {
188
- logger.info({
189
- buildId: build.id,
190
- sha: build.gitSha
191
- }, "skipping capture — duplicate sha already approved");
192
- await builds.setStatus(build.id, "approved").catch(() => {});
193
- await postStatusesForBuild({
194
- db: options.database,
195
- project,
196
- sha: build.gitSha,
197
- status: "success",
198
- url: pendingUrl,
199
- providers: gitHosts,
200
- secret: config.secret,
201
- logger
202
- }).catch(() => {});
203
- return;
204
- }
205
- try {
206
- await executeCaptureJob({
207
- buildId: job.buildId,
208
- reqId: job.reqId
209
- }, jobOptions);
210
- const updated = await builds.get(job.buildId);
211
- if (updated) {
212
- const terminal = updated.status === "approved" ? "success" : updated.status === "rejected" || updated.status === "failed" ? "failure" : null;
213
- if (terminal) await postStatusesForBuild({
214
- db: options.database,
215
- project,
216
- sha: updated.gitSha,
217
- status: terminal,
218
- url: pendingUrl,
219
- providers: gitHosts,
220
- secret: config.secret,
221
- logger
222
- }).catch((error) => {
223
- logger.error({ err: error }, "failed to post terminal status");
224
- });
225
- }
226
- } catch (error) {
227
- await postStatusesForBuild({
228
- db: options.database,
229
- project,
230
- sha: build.gitSha,
231
- status: "failure",
232
- url: pendingUrl,
233
- providers: gitHosts,
234
- secret: config.secret,
235
- logger
236
- }).catch(() => {});
237
- throw error;
238
- }
239
- }
67
+ }
68
+ /** Assemble the capture queue and its enqueue hook when a runner is configured. */
69
+ function setupCaptureQueue(options, config, gitHosts, logger) {
70
+ if (!options.captureRunner) return {
71
+ queue: null,
72
+ enqueueCapture: void 0
73
+ };
74
+ if (!config.scratchDir) throw new Error("captureRunner is enabled but ShelfConfig.scratchDir is not set");
75
+ const jobOptions = {
76
+ db: options.database,
77
+ storage: options.storage,
78
+ runner: options.captureRunner,
79
+ scratchDir: config.scratchDir,
80
+ viewports: config.viewports,
81
+ logger,
82
+ secret: config.secret
83
+ };
84
+ const runJob = createDispatchJob({
85
+ db: options.database,
86
+ jobOptions,
87
+ gitHosts,
88
+ secret: config.secret,
89
+ logger
90
+ });
91
+ const captureQueue = options.captureQueue ?? new InMemoryCaptureQueue({
92
+ concurrency: config.captureConcurrency ?? 2,
93
+ logger,
94
+ runJob
95
+ });
96
+ const enqueueCapture = async (buildId, reqId) => {
97
+ await captureQueue.enqueue({
98
+ buildId,
99
+ reqId
240
100
  });
241
- queue = captureQueue;
242
- enqueueCapture = async (buildId, reqId) => {
243
- await captureQueue.enqueue({
244
- buildId,
245
- reqId
246
- });
247
- };
248
- }
249
- app.use("*", requestId());
250
- app.use("*", async (c, next) => {
251
- const started = performance.now();
252
- const id = c.get("requestId");
253
- logger.info({
254
- reqId: id,
255
- method: c.req.method,
256
- url: c.req.path
257
- }, "request start");
101
+ };
102
+ return {
103
+ queue: captureQueue,
104
+ enqueueCapture
105
+ };
106
+ }
107
+ /** Failures safe to expose on the init gate's 503 (no secrets). */
108
+ function publicFailures(result) {
109
+ return result.failures.map((failure) => ({
110
+ category: failure.category,
111
+ kind: failure.kind,
112
+ error: failure.error
113
+ }));
114
+ }
115
+ /**
116
+ * Block requests until adapter init settles; answer 503 with the per-adapter
117
+ * failures when init failed. Health probes are exempt (they report init
118
+ * state themselves instead of being masked by the gate).
119
+ */
120
+ function initGate(getReady) {
121
+ return async (c, next) => {
122
+ if (c.req.path === "/api/v1/health") {
123
+ await next();
124
+ return;
125
+ }
126
+ const result = await getReady();
127
+ if (!result.ok) return c.json({
128
+ error: "Adapters failed to initialize",
129
+ failures: publicFailures(result)
130
+ }, 503);
258
131
  await next();
259
- logger.info({
260
- reqId: id,
261
- method: c.req.method,
262
- url: c.req.path,
263
- status: c.res.status,
264
- durationMs: Math.round(performance.now() - started)
265
- }, "request end");
132
+ };
133
+ }
134
+ function trackSettlement(cell, promise) {
135
+ promise.then((result) => {
136
+ cell.settled = result;
137
+ }, () => {
138
+ cell.settled = {
139
+ ok: false,
140
+ failures: []
141
+ };
266
142
  });
143
+ }
144
+ /** Kick the eager background init run (resolves; never rejects). */
145
+ function kickInit(options, ctx, logger, cell) {
146
+ cell.ready = runAdapterInits(collectInits(options), ctx, logger);
147
+ trackSettlement(cell, cell.ready);
148
+ }
149
+ /** Kick eager init and attach the `app.lifecycle` namespace (single run). */
150
+ function attachLifecycle(app, options, runtime, cell) {
151
+ const initCtx = {
152
+ config: runtime.config,
153
+ logger: runtime.logger
154
+ };
155
+ kickInit(options, initCtx, runtime.logger, cell);
156
+ Object.assign(app, { lifecycle: createLifecycle(options, initCtx, runtime.logger, cell) });
157
+ }
158
+ /** Build the `app.lifecycle` namespace closing over one settlement cell. */
159
+ function createLifecycle(options, ctx, logger, cell) {
160
+ return {
161
+ get ready() {
162
+ return cell.ready;
163
+ },
164
+ init: async () => {
165
+ const result = await runAdapterInits(collectInits(options), ctx, logger);
166
+ cell.ready = Promise.resolve(result);
167
+ cell.settled = result;
168
+ if (!result.ok) throw new AdapterLifecycleError("init", result.failures);
169
+ },
170
+ close: async () => {
171
+ const result = await runAdapterCloses(collectCloses(options), ctx, logger);
172
+ if (!result.ok) throw new AdapterLifecycleError("close", result.failures);
173
+ }
174
+ };
175
+ }
176
+ /** Attach global middleware: ids, logging, init gate, limits, store scope, auth gate. */
177
+ function wireMiddleware(app, wiring) {
178
+ const { options, config, ui, logger, authEnabled, enqueueCapture, queue, gitHosts, getReady } = wiring;
179
+ app.use("*", requestId());
180
+ app.use("*", requestLogging(logger));
181
+ app.use("*", initGate(getReady));
267
182
  app.use("/api/v1/*", rateLimit({
268
183
  windowMs: 6e4,
269
184
  max: 100
@@ -277,33 +192,22 @@ function createShelfRouter(options) {
277
192
  max: 20
278
193
  }));
279
194
  app.use("/projects/:slug/settings/*", csrf());
280
- app.use("*", async (c, next) => {
281
- const user = await resolveUser(c, options);
282
- return runWithStore({
283
- db: options.database,
284
- storage: options.storage,
285
- config,
286
- ui,
287
- logger,
288
- user,
289
- authEnabled,
290
- enqueueCapture,
291
- captureQueue: queue,
292
- gitHosts
293
- }, async () => {
294
- await next();
295
- });
296
- });
297
- app.use("*", async (c, next) => {
298
- const { user, authEnabled: isAuthEnabled } = getStore();
299
- const { path } = c.req;
300
- if (!isAuthEnabled || user || path.startsWith("/api/") || path.startsWith("/auth/") || path.startsWith("/assets/") || path.startsWith("/projects/") && path.includes("/storybook")) return next();
301
- if (c.req.header("HX-Request") === "true") {
302
- c.header("HX-Redirect", "/auth/login");
303
- return c.body(null, 204);
304
- }
305
- return c.redirect("/auth/login", 302);
306
- });
195
+ app.use("*", storeScope({
196
+ db: options.database,
197
+ storage: options.storage,
198
+ config,
199
+ ui,
200
+ logger,
201
+ authEnabled,
202
+ enqueueCapture,
203
+ captureQueue: queue,
204
+ gitHosts,
205
+ resolveUser: async (c) => await resolveRequestUser(c, options.auth)
206
+ }));
207
+ app.use("*", authGate());
208
+ }
209
+ /** Register every JSON API router. */
210
+ function registerApiRoutes(app, health) {
307
211
  registerProjects(app);
308
212
  registerBuilds(app);
309
213
  registerLabels(app);
@@ -312,11 +216,18 @@ function createShelfRouter(options) {
312
216
  registerTokens(app);
313
217
  registerWebhooks(app);
314
218
  registerStatusConfigs(app);
219
+ registerHealth(app, health);
315
220
  registerAdmin(app);
221
+ }
222
+ /** Register HTML pages, assets, and the optional auth flow. */
223
+ function registerPageRoutes(app, options) {
316
224
  if (options.auth) registerAuth(app, options.auth);
317
225
  registerAssets(app);
318
226
  registerStorybook(app);
319
227
  registerUiPages(app);
228
+ }
229
+ /** Register the OpenAPI document and interactive docs page. */
230
+ function registerDocs(app) {
320
231
  app.doc("/api/v1/openapi.json", {
321
232
  openapi: "3.0.0",
322
233
  info: {
@@ -326,9 +237,52 @@ function createShelfRouter(options) {
326
237
  }
327
238
  });
328
239
  app.get("/api/v1/docs", swaggerUI({ url: "/api/v1/openapi.json" }));
240
+ }
241
+ /** Register every API router, page set, and optional auth flow. */
242
+ function registerAllRoutes(app, options, health) {
243
+ registerApiRoutes(app, health);
244
+ registerPageRoutes(app, options);
245
+ }
246
+ /** Package version injected at build via __PKG_VERSION__. */
247
+ function packageVersion() {
248
+ return globalThis.__PKG_VERSION__ ?? "0.0.0";
249
+ }
250
+ /**
251
+ * Create the StoryShelf Hono router with all API routes and HTML pages.
252
+ *
253
+ * Adapter `init` hooks kick off eagerly in the background (the constructor
254
+ * stays synchronous). Await `app.lifecycle.init()` before serving for
255
+ * fail-fast startup, or let the first request gate on settlement.
256
+ */
257
+ function createShelfRouter(options) {
258
+ const app = new OpenAPIHono();
259
+ const runtime = resolveRuntime(options);
260
+ const cell = {
261
+ ready: Promise.resolve({
262
+ ok: true,
263
+ failures: []
264
+ }),
265
+ settled: null
266
+ };
267
+ attachLifecycle(app, options, runtime, cell);
268
+ const { queue, enqueueCapture } = setupCaptureQueue(options, runtime.config, runtime.gitHosts, runtime.logger);
269
+ wireMiddleware(app, {
270
+ ...runtime,
271
+ queue,
272
+ enqueueCapture,
273
+ options,
274
+ getReady: async () => await cell.ready
275
+ });
276
+ registerAllRoutes(app, options, {
277
+ sources: options,
278
+ getSettled: () => cell.settled,
279
+ bootTimeMs: Date.now(),
280
+ version: packageVersion()
281
+ });
282
+ registerDocs(app);
329
283
  return app;
330
284
  }
331
285
  //#endregion
332
- export { BUILD_STATUSES, DEFAULT_VIEWPORTS, InMemoryCaptureQueue, PERSISTENT_LABEL_KEY, PROJECT_ROLES, RESERVED_LABEL_KEYS, Retention, SEEDED_LABEL_KEYS, SITE_ROLES, SNAPSHOT_STATUSES, StorybookAdapter, TERMINAL_BUILD_STATUSES, baselinePath, baselines, buildCommentMarkdown, buildLabels, builds, commentMarker, comments, createShelfLogger, createShelfRouter, createUrlBuilder, describeStatus, diffImages, diffPath, executeCaptureJob, labelTypes, persistCapture, projectMembers, projectStatusConfigs, projects, schema, screenshotPath, slugify, snapshots, storybookDir, storybookZipPath, tokens, ulid, users, webhooks };
286
+ export { createShelfRouter };
333
287
 
334
288
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/index.tsx"],"sourcesContent":["/**\n * StoryShelf core: compose database, storage, capture, auth, and git-host\n * adapters into a complete self-hosted visual-testing Hono server.\n */\nimport { swaggerUI } from \"@hono/swagger-ui\";\nimport { OpenAPIHono } from \"@hono/zod-openapi\";\nimport { requestId } from \"hono/request-id\";\nimport type { AuthUser } from \"./adapters/auth.ts\";\nimport type { CaptureQueue } from \"./adapters/capture-queue.ts\";\nimport type { ShelfOptions } from \"./config.ts\";\nimport type { AdapterMetadata, GitAdapterMetadata } from \"./adapters/metadata.ts\";\nimport { validateConfig, validateUiConfig } from \"./config.ts\";\nimport { createShelfLogger } from \"./logger.ts\";\nimport { csrf, rateLimit } from \"./middleware/index.ts\";\nimport { BuildModel } from \"./models/build.ts\";\nimport { ProjectModel } from \"./models/project.ts\";\nimport { registerAdmin } from \"./routers/admin.ts\";\nimport { registerAssets } from \"./routers/assets.ts\";\nimport { registerAuth } from \"./routers/auth.ts\";\nimport { registerBuilds } from \"./routers/builds.ts\";\nimport { registerLabels } from \"./routers/labels.ts\";\nimport { registerMedia } from \"./routers/media.ts\";\nimport { registerMembers } from \"./routers/members.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 { getStore, runWithStore } from \"./store.ts\";\nimport { executeCaptureJob, type CaptureJobOptions } from \"./capture/orchestrator.ts\";\nimport { InMemoryCaptureQueue } from \"./capture/queue.ts\";\nimport { postStatusesForBuild } from \"./capture/status-fanout.ts\";\n\n/** Per-request shelf context (adapters, config, user, capture queue). */\nexport interface ShelfContext {\n requestId?: string;\n db: import(\"./adapters/database.ts\").DatabaseAdapter;\n storage: import(\"./adapters/storage.ts\").StorageAdapter;\n config: import(\"./config.ts\").ShelfConfig;\n ui: import(\"./config.ts\").UIConfig;\n logger: ReturnType<typeof createShelfLogger>;\n user: import(\"./adapters/auth.ts\").AuthUser | null;\n authEnabled: boolean;\n enqueueCapture?: (buildId: string, reqId?: string) => Promise<void>;\n captureQueue: import(\"./adapters/capture-queue.ts\").CaptureQueue | null;\n gitHosts: import(\"./adapters/git-host/index.ts\").GitHostProvider[];\n}\n\n/** Hono application type carrying the shelf context variables. */\nexport type ShelfApp = OpenAPIHono<{ Variables: ShelfContext }>;\n\nfunction buildAdapterSnapshot(options: ShelfOptions): Record<string, AdapterMetadata | GitAdapterMetadata> {\n const snap: Record<string, AdapterMetadata | GitAdapterMetadata> = {};\n if (options.database.metadata) {snap[\"database\"] = options.database.metadata;}\n if (options.storage.metadata) {snap[\"storage\"] = options.storage.metadata;}\n if (options.captureRunner?.metadata) {snap[\"captureRunner\"] = options.captureRunner.metadata;}\n if (options.captureQueue?.metadata) {snap[\"captureQueue\"] = options.captureQueue.metadata;}\n if (options.auth?.metadata) {snap[\"auth\"] = options.auth.metadata;}\n for (const p of options.gitHosts ?? []) {\n snap[`git:${p.metadata.kind}`] = p.metadata;\n }\n return snap;\n}\n\nasync function hasApprovedBuildForSha(\n db: import(\"./adapters/database.ts\").DatabaseAdapter,\n projectId: string,\n sha: string,\n excludeBuildId: string,\n): Promise<boolean> {\n const builds = await new BuildModel(db).list(projectId);\n return builds.some((b) => b.gitSha === sha && b.id !== excludeBuildId && b.status === \"approved\");\n}\n\n// eslint-disable-next-line max-statements, complexity\nasync function isAlreadyMerged(opts: {\n providers: import(\"./adapters/git-host/index.ts\").GitHostProvider[];\n sha: string;\n branch: string;\n secret: string | undefined;\n db: import(\"./adapters/database.ts\").DatabaseAdapter;\n projectId: string;\n logger?: import(\"./logger.ts\").Logger;\n}): Promise<boolean> {\n if (opts.providers.length === 0) {return false;}\n const { StatusConfigModel } = await import(\"./models/status-config.ts\");\n const model = new StatusConfigModel(opts.db, opts.secret);\n const rows = await model.list(opts.projectId);\n for (const row of rows) {\n const provider = opts.providers.find((p) => p.metadata.kind === row.provider);\n if (!provider) {continue;}\n let parsed: unknown;\n try {\n parsed = JSON.parse(row.config);\n provider.metadata.schema.parse(parsed);\n } catch {\n continue;\n }\n let token: string;\n try {\n token = model.decryptToken(row);\n } catch {\n continue;\n }\n const adapter = provider.create({ config: parsed, token, logger: opts.logger });\n if (!adapter.isMerged) {continue;}\n try {\n // eslint-disable-next-line no-await-in-loop -- short-circuit on first merged status\n const merged = await adapter.isMerged({ sha: opts.sha, branch: opts.branch });\n if (merged) {return true;}\n } catch {\n // Ignore provider errors — do not skip on failure\n }\n }\n return false;\n}\n\nasync function resolveUser(\n c: { req: { raw: Request; header: (name: string) => string | undefined } },\n options: ShelfOptions,\n): Promise<AuthUser | null> {\n if (!options.auth) {\n return null;\n }\n if (c.req.header(\"authorization\")) {\n return null;\n }\n return await options.auth.check(c.req.raw);\n}\n\n\n/** Create the StoryShelf Hono router with all API routes and HTML pages. */\nexport function createShelfRouter(options: ShelfOptions): ShelfApp {\n const app = new OpenAPIHono<{ Variables: ShelfContext }>();\n // eslint-disable-next-line typescript/no-unnecessary-type-assertion -- ShelfConfig lacks index signature\n const rawConfig = options.config ? validateConfig(options.config as unknown as Record<string, unknown>) : {};\n // eslint-disable-next-line typescript/no-unnecessary-type-assertion -- UIConfig lacks index signature\n const ui = options.ui ? validateUiConfig(options.ui as unknown as Record<string, unknown>) : {};\n const logger = options.logger ?? createShelfLogger();\n const authEnabled = options.auth !== undefined;\n\n const gitHosts = options.gitHosts ?? [];\n\n // Adapter introspection — auto-populate config.adapters if not supplied\n const config: import(\"./config.ts\").ShelfConfig = rawConfig.adapters\n ? rawConfig\n : {\n ...rawConfig,\n adapters: buildAdapterSnapshot(options),\n };\n\n let queue: CaptureQueue | null = null;\n let enqueueCapture: ((buildId: string, reqId?: string) => Promise<void>) | undefined;\n if (options.captureRunner) {\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 storage: options.storage,\n runner: options.captureRunner,\n scratchDir: config.scratchDir,\n viewports: config.viewports,\n logger,\n };\n const captureQueue =\n options.captureQueue ??\n new InMemoryCaptureQueue({\n concurrency: config.captureConcurrency ?? 2,\n logger,\n runJob: async (job): Promise<void> => {\n const builds = new BuildModel(options.database);\n const build = await builds.get(job.buildId);\n if (!build) {\n await executeCaptureJob({ buildId: job.buildId, reqId: job.reqId }, jobOptions);\n return;\n }\n const project = await new ProjectModel(options.database).get(build.projectId);\n if (!project) {\n await executeCaptureJob({ buildId: job.buildId, reqId: job.reqId }, jobOptions);\n return;\n }\n const pendingUrl = `/projects/${project.slug}/builds/${job.buildId}`;\n await postStatusesForBuild({\n db: options.database,\n project,\n sha: build.gitSha,\n status: \"pending\",\n url: pendingUrl,\n providers: gitHosts,\n secret: config.secret,\n logger,\n }).catch((error: unknown) => {\n logger.error({ err: error }, \"failed to post pending status\");\n });\n // Skip capture if already merged (PR closed) or locally deduped\n if (!build.isDefault) {\n const merged = await isAlreadyMerged({\n providers: gitHosts,\n sha: build.gitSha,\n branch: build.gitBranch,\n secret: config.secret,\n db: options.database,\n projectId: project.id,\n logger,\n }).catch(() => false);\n if (merged) {\n logger.info({ buildId: build.id, sha: build.gitSha, branch: build.gitBranch }, \"skipping capture — already merged\");\n await builds.setStatus(build.id, \"approved\").catch(() => {}); // Intentionally empty — fire-and-forget\n await postStatusesForBuild({\n db: options.database,\n project,\n sha: build.gitSha,\n status: \"success\",\n url: pendingUrl,\n providers: gitHosts,\n secret: config.secret,\n logger,\n }).catch(() => {}); // Intentionally empty — fire-and-forget\n return;\n }\n }\n // Local dedupe: if another approved build for same sha exists, skip render\n const dup = await hasApprovedBuildForSha(options.database, project.id, build.gitSha, build.id);\n if (dup) {\n logger.info({ buildId: build.id, sha: build.gitSha }, \"skipping capture — duplicate sha already approved\");\n await builds.setStatus(build.id, \"approved\").catch(() => {}); // Intentionally empty — fire-and-forget\n await postStatusesForBuild({\n db: options.database,\n project,\n sha: build.gitSha,\n status: \"success\",\n url: pendingUrl,\n providers: gitHosts,\n secret: config.secret,\n logger,\n }).catch(() => {}); // Intentionally empty — fire-and-forget\n return;\n }\n try {\n await executeCaptureJob({ buildId: job.buildId, reqId: job.reqId }, jobOptions);\n const updated = await builds.get(job.buildId);\n if (updated) {\n const terminal =\n updated.status === \"approved\"\n ? \"success\"\n : (updated.status === \"rejected\" || updated.status === \"failed\"\n ? \"failure\"\n : null);\n if (terminal) {\n await postStatusesForBuild({\n db: options.database,\n project,\n sha: updated.gitSha,\n status: terminal,\n url: pendingUrl,\n providers: gitHosts,\n secret: config.secret,\n logger,\n }).catch((error: unknown) => {\n logger.error({ err: error }, \"failed to post terminal status\");\n });\n }\n }\n } catch (error: unknown) {\n await postStatusesForBuild({\n db: options.database,\n project,\n sha: build.gitSha,\n status: \"failure\",\n url: pendingUrl,\n providers: gitHosts,\n secret: config.secret,\n logger,\n }).catch(() => {\n // Ignore: status post failure already logged\n });\n throw error;\n }\n },\n });\n queue = captureQueue;\n enqueueCapture = async (buildId: string, reqId?: string): Promise<void> => {\n await captureQueue.enqueue({ buildId, reqId });\n };\n }\n\n app.use(\"*\", requestId());\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(\"*\", async (c, next) => {\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 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 },\n \"request end\",\n );\n });\n\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 app.use(\"/projects/:slug/settings/*\", csrf());\n\n app.use(\"*\", async (c, next) => {\n const user = await resolveUser(c, options);\n return runWithStore(\n {\n db: options.database,\n storage: options.storage,\n config,\n ui,\n logger,\n user,\n authEnabled,\n enqueueCapture,\n captureQueue: queue,\n gitHosts,\n },\n async () => {\n await next();\n },\n );\n });\n\n // Gate the server-rendered UI behind auth (ADR 0008): unauthenticated HTML\n // Requests are redirected to the login page. API and auth routes are handled\n // By their own routers (401/403 vs the login flow).\n // eslint-disable-next-line require-await -- async is required to match Hono's middleware signature\n app.use(\"*\", async (c, next) => {\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 // Published Storybook routes enforce their own auth inside the handler\n // (public builds are viewable without a session, ADR 0011).\n path.startsWith(\"/projects/\") && path.includes(\"/storybook\")\n ) {\n return next();\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 registerProjects(app);\n registerBuilds(app);\n registerLabels(app);\n registerMedia(app);\n registerMembers(app);\n registerTokens(app);\n registerWebhooks(app);\n registerStatusConfigs(app);\n registerAdmin(app);\n if (options.auth) {\n registerAuth(app, options.auth);\n }\n registerAssets(app);\n registerStorybook(app);\n registerUiPages(app);\n\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 });\n app.get(\"/api/v1/docs\", swaggerUI({ url: \"/api/v1/openapi.json\" }));\n\n return app;\n}\n\nexport type { ShelfOptions, ShelfConfig, UIConfig, BrandTheme } from \"./config.ts\";\nexport type { DatabaseAdapter, ListOptions } from \"./adapters/database.ts\";\nexport type { StorageAdapter } from \"./adapters/storage.ts\";\nexport type {\n CaptureRunner,\n JobStatus,\n RenderedSnapshot,\n RenderResult,\n RenderFailure,\n} from \"./adapters/capture-runner.ts\";\nexport type { CaptureQueue, CaptureJob, QueueEntry } from \"./adapters/capture-queue.ts\";\nexport type { AuthAdapter, AuthUser, AuthCallback } from \"./adapters/auth.ts\";\nexport type { GitHostProvider, GitHostAdapter, CheckStatus } from \"./adapters/git-host/index.ts\";\nexport type { AdapterMetadata, GitAdapterMetadata } from \"./adapters/metadata.ts\";\nexport { describeStatus, buildCommentMarkdown, commentMarker } from \"./adapters/git-host/helpers.ts\";\nexport type { DiffOptions, DiffResult } from \"./diff/options.ts\";\nexport type { Viewport, StoryEntry, StorySourceAdapter } from \"./capture/adapter.ts\";\nexport { createShelfLogger, type LoggerOptions, type Logger, type PinoTransport } from \"./logger.ts\";\nexport { diffImages } from \"./diff/engine.ts\";\nexport type { RenderedContent } from \"./ui/document.tsx\";\nexport { StorybookAdapter } from \"./capture/storybook.ts\";\nexport { persistCapture, type CaptureContext } from \"./capture/pipeline.ts\";\nexport { executeCaptureJob, type CaptureJobOptions } from \"./capture/orchestrator.ts\";\nexport { DEFAULT_VIEWPORTS } from \"./capture/adapter.ts\";\nexport { InMemoryCaptureQueue, type InMemoryCaptureQueueOptions } from \"./capture/queue.ts\";\nexport { Retention } from \"./retention/purge.ts\";\nexport { createUrlBuilder, type UrlBuilder } from \"./urls.ts\";\nexport { ulid, slugify } from \"./utils/ulid.ts\";\nexport {\n baselinePath,\n diffPath,\n screenshotPath,\n storybookDir,\n storybookZipPath,\n} from \"./utils/paths.ts\";\nexport * from \"./schema.ts\";\nexport * from \"./types.ts\";\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/index.tsx"],"sourcesContent":["/**\n * StoryShelf core: compose database, storage, capture, auth, and git-host\n * adapters into a complete self-hosted visual-testing Hono server.\n */\nimport { swaggerUI } from \"@hono/swagger-ui\";\nimport { OpenAPIHono } from \"@hono/zod-openapi\";\nimport type { Context, Next } from \"hono\";\nimport { requestId } from \"hono/request-id\";\nimport type { CaptureQueue } from \"./adapters/capture-queue.ts\";\nimport {\n AdapterLifecycleError,\n collectCloses,\n collectInits,\n runAdapterCloses,\n runAdapterInits,\n} from \"./adapters/init.ts\";\nimport type { AdapterInitResult } from \"./adapters/init.ts\";\nimport type {\n AdapterInitContext,\n AdapterMetadata,\n GitAdapterMetadata,\n} from \"./adapters/metadata.ts\";\nimport { createDispatchJob } from \"./capture/dispatch.ts\";\nimport type { CaptureJobOptions } from \"./capture/orchestrator.ts\";\nimport { InMemoryCaptureQueue } from \"./capture/queue.ts\";\nimport type { ShelfOptions } from \"./config.ts\";\nimport { validateConfig, validateUiConfig } from \"./config.ts\";\nimport { createShelfLogger } from \"./logger.ts\";\nimport type { Logger } from \"./logger.ts\";\nimport {\n authGate,\n csrf,\n rateLimit,\n requestLogging,\n resolveRequestUser,\n storeScope,\n} from \"./middleware/index.ts\";\nimport { registerAdmin } from \"./routers/admin.ts\";\nimport { registerAssets } from \"./routers/assets.ts\";\nimport { registerAuth } from \"./routers/auth.ts\";\nimport { registerBuilds } from \"./routers/builds.ts\";\nimport { registerHealth } from \"./routers/health.ts\";\nimport 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 { 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\";\n\n/** Per-request shelf context (adapters, config, user, capture queue). */\nexport interface ShelfContext {\n requestId?: string;\n db: import(\"./adapters/database.ts\").DatabaseAdapter;\n storage: import(\"./adapters/storage.ts\").StorageAdapter;\n config: import(\"./config.ts\").ShelfConfig;\n ui: import(\"./config.ts\").UIConfig;\n logger: ReturnType<typeof createShelfLogger>;\n user: import(\"./adapters/auth.ts\").AuthUser | null;\n authEnabled: boolean;\n enqueueCapture?: (buildId: string, reqId?: string) => Promise<void>;\n captureQueue: import(\"./adapters/capture-queue.ts\").CaptureQueue | null;\n gitHosts: import(\"./adapters/git-host/index.ts\").GitHostProvider[];\n}\n\n/** Hono application type carrying the shelf context variables. */\nexport type ShelfApp = OpenAPIHono<{ Variables: ShelfContext }>;\n\n/** App-scoped adapter lifecycle namespace (single `lifecycle` prop avoids Hono collisions). */\nexport interface ShelfLifecycle {\n /** Settled summary of the eager background init run; never rejects. */\n readonly ready: Promise<AdapterInitResult>;\n /**\n * Fail-fast await for server startup (`await app.lifecycle.init()`).\n * Re-runs init so transient failures are retryable; throws AdapterLifecycleError.\n * Optional — when skipped, the first request gates on `ready` instead.\n */\n init(): Promise<void>;\n /** Graceful teardown for SIGTERM/SIGINT (`await app.lifecycle.close()`). Idempotent. */\n close(): Promise<void>;\n}\n\n/** Hono app with the shelf lifecycle namespace attached. */\nexport type ShelfRouter = ShelfApp & { readonly lifecycle: ShelfLifecycle };\n\nfunction addOptionalSnapshots(\n snap: Record<string, AdapterMetadata | GitAdapterMetadata>,\n options: ShelfOptions,\n): void {\n if (options.captureRunner) {\n snap[\"captureRunner\"] = options.captureRunner.metadata;\n }\n if (options.captureQueue) {\n snap[\"captureQueue\"] = options.captureQueue.metadata;\n }\n if (options.auth) {\n snap[\"auth\"] = options.auth.metadata;\n }\n}\n\nfunction buildAdapterSnapshot(\n options: ShelfOptions,\n): Record<string, AdapterMetadata | GitAdapterMetadata> {\n const snap: Record<string, AdapterMetadata | GitAdapterMetadata> = {\n database: options.database.metadata,\n storage: options.storage.metadata,\n };\n addOptionalSnapshots(snap, options);\n for (const p of options.gitHosts ?? []) {\n snap[`git:${p.metadata.kind}`] = p.metadata;\n }\n return snap;\n}\n\ninterface ServerRuntime {\n config: import(\"./config.ts\").ShelfConfig;\n ui: import(\"./config.ts\").UIConfig;\n logger: import(\"./logger.ts\").Logger;\n authEnabled: boolean;\n gitHosts: import(\"./adapters/git-host/index.ts\").GitHostProvider[];\n}\n\n/** Validate config/ui and derive runtime singletons from options. */\nfunction resolveRuntime(options: ShelfOptions): ServerRuntime {\n // eslint-disable-next-line typescript/no-unnecessary-type-assertion -- ShelfConfig lacks index signature\n const rawConfig = options.config\n ? validateConfig(options.config as unknown as Record<string, unknown>)\n : {};\n // eslint-disable-next-line typescript/no-unnecessary-type-assertion -- UIConfig lacks index signature\n const ui = options.ui ? validateUiConfig(options.ui as unknown as Record<string, unknown>) : {};\n const logger = options.logger ?? createShelfLogger();\n const authEnabled = options.auth !== undefined;\n const gitHosts = options.gitHosts ?? [];\n // Adapter introspection — auto-populate config.adapters if not supplied\n const config: import(\"./config.ts\").ShelfConfig = rawConfig.adapters\n ? rawConfig\n : {\n ...rawConfig,\n adapters: buildAdapterSnapshot(options),\n };\n return { config, ui, logger, authEnabled, gitHosts };\n}\n\ninterface QueueWiring {\n queue: CaptureQueue | null;\n enqueueCapture: ((buildId: string, reqId?: string) => Promise<void>) | undefined;\n}\n\n/** Assemble the capture queue and its enqueue hook when a runner is configured. */\nfunction setupCaptureQueue(\n options: ShelfOptions,\n config: import(\"./config.ts\").ShelfConfig,\n gitHosts: import(\"./adapters/git-host/index.ts\").GitHostProvider[],\n logger: import(\"./logger.ts\").Logger,\n): QueueWiring {\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 storage: options.storage,\n runner: options.captureRunner,\n scratchDir: config.scratchDir,\n viewports: config.viewports,\n logger,\n secret: config.secret,\n };\n const runJob = createDispatchJob({\n db: options.database,\n jobOptions,\n gitHosts,\n secret: config.secret,\n logger,\n });\n const captureQueue =\n options.captureQueue ??\n new InMemoryCaptureQueue({\n concurrency: config.captureConcurrency ?? 2,\n logger,\n runJob,\n });\n const enqueueCapture = async (buildId: string, reqId?: string): Promise<void> => {\n await captureQueue.enqueue({ buildId, reqId });\n };\n return { queue: captureQueue, enqueueCapture };\n}\n\ninterface MiddlewareWiring extends ServerRuntime, QueueWiring {\n options: ShelfOptions;\n getReady: () => Promise<AdapterInitResult>;\n}\n\n/** Failures safe to expose on the init gate's 503 (no secrets). */\nfunction publicFailures(\n result: AdapterInitResult,\n): { category: string; kind: string; error: string }[] {\n return result.failures.map((failure) => ({\n category: failure.category,\n kind: failure.kind,\n error: failure.error,\n }));\n}\n\n/**\n * Block requests until adapter init settles; answer 503 with the per-adapter\n * failures when init failed. Health probes are exempt (they report init\n * state themselves instead of being masked by the gate).\n */\nfunction initGate(getReady: () => Promise<AdapterInitResult>) {\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\") {\n await next();\n return;\n }\n const result = await getReady();\n if (!result.ok) {\n return c.json(\n { error: \"Adapters failed to initialize\", failures: publicFailures(result) },\n 503,\n );\n }\n await next();\n };\n}\n\n/** Mutable init-settlement cell shared by the gate, health, and `init()`. */\ninterface LifecycleCell {\n ready: Promise<AdapterInitResult>;\n settled: AdapterInitResult | null;\n}\n\nfunction trackSettlement(cell: LifecycleCell, promise: Promise<AdapterInitResult>): void {\n promise.then(\n (result) => {\n cell.settled = result;\n },\n () => {\n cell.settled = { ok: false, failures: [] };\n },\n );\n}\n\n/** Kick the eager background init run (resolves; never rejects). */\nfunction kickInit(\n options: ShelfOptions,\n ctx: AdapterInitContext,\n logger: Logger,\n cell: LifecycleCell,\n): void {\n cell.ready = runAdapterInits(collectInits(options), ctx, logger);\n trackSettlement(cell, cell.ready);\n}\n\n/** Kick eager init and attach the `app.lifecycle` namespace (single run). */\nfunction attachLifecycle(\n app: ShelfApp,\n options: ShelfOptions,\n runtime: ServerRuntime,\n cell: LifecycleCell,\n): void {\n const initCtx: AdapterInitContext = { config: runtime.config, logger: runtime.logger };\n kickInit(options, initCtx, runtime.logger, cell);\n Object.assign(app, { lifecycle: createLifecycle(options, initCtx, runtime.logger, cell) });\n}\n\n/** Build the `app.lifecycle` namespace closing over one settlement cell. */\nfunction createLifecycle(\n options: ShelfOptions,\n ctx: AdapterInitContext,\n logger: Logger,\n cell: LifecycleCell,\n): ShelfLifecycle {\n return {\n get ready() {\n return cell.ready;\n },\n init: async () => {\n const result = await runAdapterInits(collectInits(options), ctx, logger);\n cell.ready = Promise.resolve(result);\n cell.settled = result;\n if (!result.ok) {\n throw new AdapterLifecycleError(\"init\", result.failures);\n }\n },\n close: async () => {\n const result = await runAdapterCloses(collectCloses(options), ctx, logger);\n if (!result.ok) {\n throw new AdapterLifecycleError(\"close\", result.failures);\n }\n },\n };\n}\n\n/** Attach global middleware: ids, logging, init gate, limits, store scope, auth gate. */\nfunction wireMiddleware(app: ShelfApp, wiring: MiddlewareWiring): void {\n const { options, config, ui, logger, authEnabled, enqueueCapture, queue, gitHosts, getReady } =\n wiring;\n app.use(\"*\", requestId());\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(\"*\", initGate(getReady));\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 app.use(\"/projects/:slug/settings/*\", csrf());\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 resolveUser: async (c) => await resolveRequestUser(c, options.auth),\n }),\n );\n app.use(\"*\", authGate());\n}\n\n/** Register every JSON API router. */\nfunction registerApiRoutes(app: ShelfApp, health: HealthDeps): void {\n registerProjects(app);\n registerBuilds(app);\n registerLabels(app);\n registerMedia(app);\n registerMembers(app);\n registerTokens(app);\n registerWebhooks(app);\n registerStatusConfigs(app);\n registerHealth(app, health);\n registerAdmin(app);\n}\n\n/** Register HTML pages, assets, and the optional auth flow. */\nfunction registerPageRoutes(app: ShelfApp, options: ShelfOptions): void {\n if (options.auth) {\n registerAuth(app, options.auth);\n }\n registerAssets(app);\n registerStorybook(app);\n registerUiPages(app);\n}\n\n/** Register the OpenAPI document and interactive docs page. */\nfunction registerDocs(app: ShelfApp): 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 });\n app.get(\"/api/v1/docs\", swaggerUI({ url: \"/api/v1/openapi.json\" }));\n}\n\n/** Register every API router, page set, and optional auth flow. */\nfunction registerAllRoutes(app: ShelfApp, options: ShelfOptions, health: HealthDeps): void {\n registerApiRoutes(app, health);\n registerPageRoutes(app, options);\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 * Create the StoryShelf Hono router 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.init()` before serving for\n * fail-fast startup, or let the first request gate on settlement.\n */\nexport function createShelfRouter(options: ShelfOptions): ShelfRouter {\n const app = new OpenAPIHono<{ Variables: ShelfContext }>() as ShelfRouter;\n const runtime = resolveRuntime(options);\n const cell: LifecycleCell = { ready: Promise.resolve({ ok: true, failures: [] }), settled: null };\n attachLifecycle(app, options, runtime, cell);\n const { queue, enqueueCapture } = setupCaptureQueue(\n options,\n runtime.config,\n runtime.gitHosts,\n runtime.logger,\n );\n wireMiddleware(app, {\n ...runtime,\n queue,\n enqueueCapture,\n options,\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 return app;\n}\n\n/**\n * Public surface: the router and its types only.\n *\n * `createShelfRouter` 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`. Models and internal tooling (`store`, `middleware`,\n * `retention`, pages) have no public entry. Importing the barrel\n * must never pull the Hono router into bundles that do not serve it.\n */\nexport type { ShelfOptions, ShelfConfig, UIConfig, BrandTheme } from \"./config.ts\";\n"],"mappings":"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"}
package/dist/logger.d.mts CHANGED
@@ -1,6 +1,6 @@
1
1
  import __tsdown_shims_path from 'node:path';
2
2
  import __tsdown_shims_url from 'node:url';
3
- import { Logger as Logger$1 } from "pino";
3
+ import { Logger } from "pino";
4
4
  //#region src/logger.d.ts
5
5
  /** A pino worker transport to attach to the logger output. */
6
6
  interface PinoTransport {
@@ -33,7 +33,7 @@ interface LoggerOptions {
33
33
  * Datadog, GCP, OTEL collector, etc.) are added as pino worker `transports` —
34
34
  * they are sinks for this logger, not standalone loggers.
35
35
  */
36
- declare function createShelfLogger(options?: LoggerOptions): Logger$1;
36
+ declare function createShelfLogger(options?: LoggerOptions): Logger;
37
37
  //#endregion
38
- export { type Logger$1 as Logger, LoggerOptions, PinoTransport, createShelfLogger };
38
+ export { type Logger, LoggerOptions, PinoTransport, createShelfLogger };
39
39
  //# sourceMappingURL=logger.d.mts.map