@storyshelf/core 0.1.2 → 0.3.1

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 (275) 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 +6 -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/helpers.d.mts +1 -0
  14. package/dist/adapter/git-host/helpers.mjs +1 -0
  15. package/dist/adapter/git-host/helpers.mjs.map +1 -1
  16. package/dist/adapter/git-host.d.mts +3 -7
  17. package/dist/adapter/init.d.mts +58 -0
  18. package/dist/adapter/init.mjs +93 -0
  19. package/dist/adapter/init.mjs.map +1 -0
  20. package/dist/adapter/metadata.d.mts +83 -0
  21. package/dist/adapter/metadata.mjs +4 -0
  22. package/dist/adapter/storage.d.mts +15 -4
  23. package/dist/adapters/webhook-events.mjs +20 -4
  24. package/dist/adapters/webhook-events.mjs.map +1 -1
  25. package/dist/capture/adapter.d.mts +7 -1
  26. package/dist/capture/adapter.mjs +3 -0
  27. package/dist/capture/adapter.mjs.map +1 -1
  28. package/dist/capture/dispatch.mjs +150 -0
  29. package/dist/capture/dispatch.mjs.map +1 -0
  30. package/dist/capture/orchestrator.d.mts +9 -0
  31. package/dist/capture/orchestrator.mjs +11 -44
  32. package/dist/capture/orchestrator.mjs.map +1 -1
  33. package/dist/capture/pipeline.d.mts +4 -1
  34. package/dist/capture/pipeline.mjs +6 -6
  35. package/dist/capture/pipeline.mjs.map +1 -1
  36. package/dist/capture/queue.d.mts +4 -1
  37. package/dist/capture/queue.mjs +3 -1
  38. package/dist/capture/queue.mjs.map +1 -1
  39. package/dist/capture/skip-checks.mjs +76 -0
  40. package/dist/capture/skip-checks.mjs.map +1 -0
  41. package/dist/capture/statics.mjs +124 -0
  42. package/dist/capture/statics.mjs.map +1 -0
  43. package/dist/capture/status-fanout.mjs +1 -0
  44. package/dist/capture/status-fanout.mjs.map +1 -1
  45. package/dist/capture/storybook.d.mts +1 -0
  46. package/dist/capture/storybook.mjs +1 -0
  47. package/dist/capture/storybook.mjs.map +1 -1
  48. package/dist/capture.d.mts +8 -0
  49. package/dist/capture.mjs +9 -0
  50. package/dist/config.d.mts +37 -22
  51. package/dist/config.mjs +28 -2
  52. package/dist/config.mjs.map +1 -1
  53. package/dist/ddl.d.mts +2 -1
  54. package/dist/ddl.mjs +2 -1
  55. package/dist/ddl.mjs.map +1 -1
  56. package/dist/diff/engine.d.mts +8 -0
  57. package/dist/diff/engine.mjs +8 -0
  58. package/dist/diff/engine.mjs.map +1 -1
  59. package/dist/diff/options.d.mts +3 -1
  60. package/dist/diff.d.mts +5 -0
  61. package/dist/diff.mjs +6 -0
  62. package/dist/index.d.mts +34 -24
  63. package/dist/index.mjs +211 -252
  64. package/dist/index.mjs.map +1 -1
  65. package/dist/logger.d.mts +3 -3
  66. package/dist/logger.mjs.map +1 -1
  67. package/dist/middleware/auth-gate.mjs +36 -0
  68. package/dist/middleware/auth-gate.mjs.map +1 -0
  69. package/dist/middleware/csrf.mjs +1 -0
  70. package/dist/middleware/csrf.mjs.map +1 -1
  71. package/dist/middleware/rate-limit.mjs +1 -0
  72. package/dist/middleware/rate-limit.mjs.map +1 -1
  73. package/dist/middleware/request-log.mjs +28 -0
  74. package/dist/middleware/request-log.mjs.map +1 -0
  75. package/dist/middleware/store-scope.mjs +29 -0
  76. package/dist/middleware/store-scope.mjs.map +1 -0
  77. package/dist/models/baseline.mjs +9 -5
  78. package/dist/models/baseline.mjs.map +1 -1
  79. package/dist/models/build.mjs +5 -1
  80. package/dist/models/build.mjs.map +1 -1
  81. package/dist/models/comment.mjs +4 -1
  82. package/dist/models/comment.mjs.map +1 -1
  83. package/dist/models/label.mjs +3 -1
  84. package/dist/models/label.mjs.map +1 -1
  85. package/dist/models/member.mjs +2 -1
  86. package/dist/models/member.mjs.map +1 -1
  87. package/dist/models/project.mjs +2 -1
  88. package/dist/models/project.mjs.map +1 -1
  89. package/dist/models/snapshot.mjs +2 -1
  90. package/dist/models/snapshot.mjs.map +1 -1
  91. package/dist/models/status-config.mjs +2 -1
  92. package/dist/models/status-config.mjs.map +1 -1
  93. package/dist/models/token.mjs +2 -1
  94. package/dist/models/token.mjs.map +1 -1
  95. package/dist/models/webhook.mjs +12 -3
  96. package/dist/models/webhook.mjs.map +1 -1
  97. package/dist/pages/build-detail.mjs +10 -4
  98. package/dist/pages/build-detail.mjs.map +1 -1
  99. package/dist/pages/build-diff-comments.mjs +113 -0
  100. package/dist/pages/build-diff-comments.mjs.map +1 -0
  101. package/dist/pages/build-diff-header.mjs +163 -0
  102. package/dist/pages/build-diff-header.mjs.map +1 -0
  103. package/dist/pages/build-diff-nav.mjs +66 -0
  104. package/dist/pages/build-diff-nav.mjs.map +1 -0
  105. package/dist/pages/build-diff-viewer.mjs +197 -0
  106. package/dist/pages/build-diff-viewer.mjs.map +1 -0
  107. package/dist/pages/build-diff.mjs +66 -413
  108. package/dist/pages/build-diff.mjs.map +1 -1
  109. package/dist/pages/compute-jobs.mjs +3 -1
  110. package/dist/pages/compute-jobs.mjs.map +1 -1
  111. package/dist/pages/label-detail.mjs +4 -3
  112. package/dist/pages/label-detail.mjs.map +1 -1
  113. package/dist/pages/login.mjs +1 -0
  114. package/dist/pages/login.mjs.map +1 -1
  115. package/dist/pages/project-builds.mjs +6 -3
  116. package/dist/pages/project-builds.mjs.map +1 -1
  117. package/dist/pages/project-create.mjs +1 -0
  118. package/dist/pages/project-create.mjs.map +1 -1
  119. package/dist/pages/project-settings.mjs +1 -0
  120. package/dist/pages/project-settings.mjs.map +1 -1
  121. package/dist/pages/projects.mjs +12 -6
  122. package/dist/pages/projects.mjs.map +1 -1
  123. package/dist/pages/root.mjs +7 -3
  124. package/dist/pages/root.mjs.map +1 -1
  125. package/dist/pages/settings-general.mjs +1 -0
  126. package/dist/pages/settings-general.mjs.map +1 -1
  127. package/dist/pages/settings-labels.mjs +2 -1
  128. package/dist/pages/settings-labels.mjs.map +1 -1
  129. package/dist/pages/settings-members.mjs +2 -1
  130. package/dist/pages/settings-members.mjs.map +1 -1
  131. package/dist/pages/settings-status.mjs +3 -1
  132. package/dist/pages/settings-status.mjs.map +1 -1
  133. package/dist/pages/settings-tests.mjs +4 -3
  134. package/dist/pages/settings-tests.mjs.map +1 -1
  135. package/dist/pages/settings-tokens.mjs +1 -0
  136. package/dist/pages/settings-tokens.mjs.map +1 -1
  137. package/dist/pages/settings-webhooks.mjs +1 -0
  138. package/dist/pages/settings-webhooks.mjs.map +1 -1
  139. package/dist/pages/storybook.mjs +38 -1
  140. package/dist/pages/storybook.mjs.map +1 -1
  141. package/dist/{utils/paths.d.mts → paths.d.mts} +5 -0
  142. package/dist/{utils/paths.mjs → paths.mjs} +5 -0
  143. package/dist/paths.mjs.map +1 -0
  144. package/dist/retention/purge.mjs +2 -1
  145. package/dist/retention/purge.mjs.map +1 -1
  146. package/dist/routers/admin.mjs +2 -1
  147. package/dist/routers/admin.mjs.map +1 -1
  148. package/dist/routers/assets.mjs +1 -0
  149. package/dist/routers/assets.mjs.map +1 -1
  150. package/dist/routers/auth.mjs +1 -0
  151. package/dist/routers/auth.mjs.map +1 -1
  152. package/dist/routers/builds.handlers.mjs +114 -19
  153. package/dist/routers/builds.handlers.mjs.map +1 -1
  154. package/dist/routers/builds.mjs +55 -34
  155. package/dist/routers/builds.mjs.map +1 -1
  156. package/dist/routers/comments.mjs +1 -0
  157. package/dist/routers/comments.mjs.map +1 -1
  158. package/dist/routers/health.mjs +137 -0
  159. package/dist/routers/health.mjs.map +1 -0
  160. package/dist/routers/helpers.mjs +8 -2
  161. package/dist/routers/helpers.mjs.map +1 -1
  162. package/dist/routers/htmx.mjs +2 -0
  163. package/dist/routers/htmx.mjs.map +1 -1
  164. package/dist/routers/labels.mjs +2 -1
  165. package/dist/routers/labels.mjs.map +1 -1
  166. package/dist/routers/media.mjs +3 -2
  167. package/dist/routers/media.mjs.map +1 -1
  168. package/dist/routers/members.mjs +1 -0
  169. package/dist/routers/members.mjs.map +1 -1
  170. package/dist/routers/projects.mjs +2 -1
  171. package/dist/routers/projects.mjs.map +1 -1
  172. package/dist/routers/schemas.mjs +25 -0
  173. package/dist/routers/schemas.mjs.map +1 -1
  174. package/dist/routers/settings-general.mjs +95 -0
  175. package/dist/routers/settings-general.mjs.map +1 -0
  176. package/dist/routers/settings-labels.mjs +49 -0
  177. package/dist/routers/settings-labels.mjs.map +1 -0
  178. package/dist/routers/settings-members.mjs +30 -0
  179. package/dist/routers/settings-members.mjs.map +1 -0
  180. package/dist/routers/settings-status.mjs +73 -0
  181. package/dist/routers/settings-status.mjs.map +1 -0
  182. package/dist/routers/settings-tokens.mjs +40 -0
  183. package/dist/routers/settings-tokens.mjs.map +1 -0
  184. package/dist/routers/settings-webhooks.mjs +63 -0
  185. package/dist/routers/settings-webhooks.mjs.map +1 -0
  186. package/dist/routers/settings.handlers.mjs +93 -0
  187. package/dist/routers/settings.handlers.mjs.map +1 -0
  188. package/dist/routers/settings.mjs +14 -246
  189. package/dist/routers/settings.mjs.map +1 -1
  190. package/dist/routers/snapshots.mjs +2 -1
  191. package/dist/routers/snapshots.mjs.map +1 -1
  192. package/dist/routers/status-configs.mjs +2 -1
  193. package/dist/routers/status-configs.mjs.map +1 -1
  194. package/dist/routers/storybook.mjs +12 -4
  195. package/dist/routers/storybook.mjs.map +1 -1
  196. package/dist/routers/tokens.mjs +2 -1
  197. package/dist/routers/tokens.mjs.map +1 -1
  198. package/dist/routers/ui.mjs +5 -4
  199. package/dist/routers/ui.mjs.map +1 -1
  200. package/dist/routers/webhooks.mjs +7 -6
  201. package/dist/routers/webhooks.mjs.map +1 -1
  202. package/dist/schema/baseline.d.mts +18 -0
  203. package/dist/schema/baseline.mjs +22 -0
  204. package/dist/schema/baseline.mjs.map +1 -0
  205. package/dist/schema/build.d.mts +26 -0
  206. package/dist/schema/build.mjs +29 -0
  207. package/dist/schema/build.mjs.map +1 -0
  208. package/dist/schema/comment.d.mts +19 -0
  209. package/dist/schema/comment.mjs +26 -0
  210. package/dist/schema/comment.mjs.map +1 -0
  211. package/dist/schema/label.d.mts +25 -0
  212. package/dist/schema/label.mjs +30 -0
  213. package/dist/schema/label.mjs.map +1 -0
  214. package/dist/schema/member.d.mts +15 -0
  215. package/dist/schema/member.mjs +19 -0
  216. package/dist/schema/member.mjs.map +1 -0
  217. package/dist/schema/project.d.mts +34 -0
  218. package/dist/schema/project.mjs +25 -0
  219. package/dist/schema/project.mjs.map +1 -0
  220. package/dist/schema/snapshot.d.mts +30 -0
  221. package/dist/schema/snapshot.mjs +34 -0
  222. package/dist/schema/snapshot.mjs.map +1 -0
  223. package/dist/schema/status-config.d.mts +16 -0
  224. package/dist/schema/status-config.mjs +20 -0
  225. package/dist/schema/status-config.mjs.map +1 -0
  226. package/dist/schema/token.d.mts +15 -0
  227. package/dist/schema/token.mjs +19 -0
  228. package/dist/schema/token.mjs.map +1 -0
  229. package/dist/schema/user.d.mts +17 -0
  230. package/dist/schema/user.mjs +19 -0
  231. package/dist/schema/user.mjs.map +1 -0
  232. package/dist/schema/webhook.d.mts +16 -0
  233. package/dist/schema/webhook.mjs +20 -0
  234. package/dist/schema/webhook.mjs.map +1 -0
  235. package/dist/schema.d.mts +52 -4433
  236. package/dist/schema.mjs +37 -139
  237. package/dist/schema.mjs.map +1 -1
  238. package/dist/store.mjs +2 -0
  239. package/dist/store.mjs.map +1 -1
  240. package/dist/types.d.mts +13 -2
  241. package/dist/types.mjs +8 -0
  242. package/dist/types.mjs.map +1 -1
  243. package/dist/ui/document.mjs +1 -0
  244. package/dist/ui/document.mjs.map +1 -1
  245. package/dist/ui/feedback.mjs +24 -0
  246. package/dist/ui/feedback.mjs.map +1 -0
  247. package/dist/ui/{components.mjs → forms.mjs} +6 -16
  248. package/dist/ui/forms.mjs.map +1 -0
  249. package/dist/ui/styles.mjs +1 -0
  250. package/dist/ui/styles.mjs.map +1 -1
  251. package/dist/ui/theme.mjs +2 -0
  252. package/dist/ui/theme.mjs.map +1 -1
  253. package/dist/urls.d.mts +8 -0
  254. package/dist/urls.mjs +7 -0
  255. package/dist/urls.mjs.map +1 -1
  256. package/dist/utils/hash.mjs +3 -0
  257. package/dist/utils/hash.mjs.map +1 -1
  258. package/dist/utils/ulid.mjs +2 -0
  259. package/dist/utils/ulid.mjs.map +1 -1
  260. package/package.json +47 -45
  261. package/dist/adapters/metadata.d.mts +0 -24
  262. package/dist/models/baseline.d.mts +0 -20
  263. package/dist/models/build.d.mts +0 -50
  264. package/dist/models/comment.d.mts +0 -20
  265. package/dist/models/label.d.mts +0 -49
  266. package/dist/models/member.d.mts +0 -27
  267. package/dist/models/project.d.mts +0 -42
  268. package/dist/models/snapshot.d.mts +0 -46
  269. package/dist/models/token.d.mts +0 -33
  270. package/dist/models/webhook.d.mts +0 -37
  271. package/dist/retention/purge.d.mts +0 -28
  272. package/dist/ui/components.mjs.map +0 -1
  273. package/dist/ui/document.d.mts +0 -8
  274. package/dist/utils/paths.mjs.map +0 -1
  275. package/dist/utils/ulid.d.mts +0 -8
package/dist/index.mjs CHANGED
@@ -1,22 +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 { baselines, buildLabels, builds, comments, labelTypes, projectMembers, projectStatusConfigs, projects, schema, snapshots, tokens, users, webhooks } from "./schema.mjs";
9
- import { slugify, ulid } from "./utils/ulid.mjs";
10
- import { BuildModel } from "./models/build.mjs";
11
- import { ProjectModel } from "./models/project.mjs";
12
- import { BUILD_STATUSES, PERSISTENT_LABEL_KEY, PROJECT_ROLES, RESERVED_LABEL_KEYS, SEEDED_LABEL_KEYS, SITE_ROLES, SNAPSHOT_STATUSES, TERMINAL_BUILD_STATUSES } from "./types.mjs";
13
- import { Retention } from "./retention/purge.mjs";
14
- 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";
15
14
  import { registerAdmin } from "./routers/admin.mjs";
16
15
  import { registerAssets } from "./routers/assets.mjs";
17
16
  import { registerAuth } from "./routers/auth.mjs";
18
- import { baselinePath, diffPath, screenshotPath, storybookDir, storybookZipPath } from "./utils/paths.mjs";
19
17
  import { registerBuilds } from "./routers/builds.mjs";
18
+ import { registerHealth } from "./routers/health.mjs";
20
19
  import { registerLabels } from "./routers/labels.mjs";
21
20
  import { registerMedia } from "./routers/media.mjs";
22
21
  import { registerMembers } from "./routers/members.mjs";
@@ -26,239 +25,160 @@ import { registerStorybook } from "./routers/storybook.mjs";
26
25
  import { registerTokens } from "./routers/tokens.mjs";
27
26
  import { registerUiPages } from "./routers/ui.mjs";
28
27
  import { registerWebhooks } from "./routers/webhooks.mjs";
29
- import { diffImages } from "./diff/engine.mjs";
30
- import { persistCapture } from "./capture/pipeline.mjs";
31
- import { StorybookAdapter } from "./capture/storybook.mjs";
32
- import { DEFAULT_VIEWPORTS } from "./capture/adapter.mjs";
33
- import { executeCaptureJob } from "./capture/orchestrator.mjs";
34
- import { InMemoryCaptureQueue } from "./capture/queue.mjs";
35
- import { buildCommentMarkdown, commentMarker, describeStatus } from "./adapter/git-host/helpers.mjs";
36
- import { postStatusesForBuild } from "./capture/status-fanout.mjs";
37
- import { createUrlBuilder } from "./urls.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";
41
31
  //#region src/index.tsx
32
+ /**
33
+ * StoryShelf core: compose database, storage, capture, auth, and git-host
34
+ * adapters into a complete self-hosted visual-testing Hono server.
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
+ }
42
41
  function buildAdapterSnapshot(options) {
43
- const snap = {};
44
- if (options.database.metadata) snap["database"] = options.database.metadata;
45
- if (options.storage.metadata) snap["storage"] = options.storage.metadata;
46
- if (options.captureRunner?.metadata) snap["captureRunner"] = options.captureRunner.metadata;
47
- if (options.captureQueue?.metadata) snap["captureQueue"] = options.captureQueue.metadata;
48
- 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);
49
47
  for (const p of options.gitHosts ?? []) snap[`git:${p.metadata.kind}`] = p.metadata;
50
48
  return snap;
51
49
  }
52
- async function hasApprovedBuildForSha(db, projectId, sha, excludeBuildId) {
53
- return (await new BuildModel(db).list(projectId)).some((b) => b.gitSha === sha && b.id !== excludeBuildId && b.status === "approved");
54
- }
55
- async function isAlreadyMerged(opts) {
56
- if (opts.providers.length === 0) return false;
57
- const { StatusConfigModel } = await import("./models/status-config.mjs");
58
- const model = new StatusConfigModel(opts.db, opts.secret);
59
- const rows = await model.list(opts.projectId);
60
- for (const row of rows) {
61
- const provider = opts.providers.find((p) => p.metadata.kind === row.provider);
62
- if (!provider) continue;
63
- let parsed;
64
- try {
65
- parsed = JSON.parse(row.config);
66
- provider.metadata.schema.parse(parsed);
67
- } catch {
68
- continue;
69
- }
70
- let token;
71
- try {
72
- token = model.decryptToken(row);
73
- } catch {
74
- continue;
75
- }
76
- const adapter = provider.create({
77
- config: parsed,
78
- token,
79
- logger: opts.logger
80
- });
81
- if (!adapter.isMerged) continue;
82
- try {
83
- if (await adapter.isMerged({
84
- sha: opts.sha,
85
- branch: opts.branch
86
- })) return true;
87
- } catch {}
88
- }
89
- return false;
90
- }
91
- async function resolveUser(c, options) {
92
- if (!options.auth) return null;
93
- if (c.req.header("authorization")) return null;
94
- return await options.auth.check(c.req.raw);
95
- }
96
- function createShelfRouter(options) {
97
- const app = new OpenAPIHono();
50
+ /** Validate config/ui and derive runtime singletons from options. */
51
+ function resolveRuntime(options) {
98
52
  const rawConfig = options.config ? validateConfig(options.config) : {};
99
53
  const ui = options.ui ? validateUiConfig(options.ui) : {};
100
54
  const logger = options.logger ?? createShelfLogger();
101
55
  const authEnabled = options.auth !== void 0;
102
56
  const gitHosts = options.gitHosts ?? [];
103
- const config = rawConfig.adapters ? rawConfig : {
104
- ...rawConfig,
105
- 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
106
66
  };
107
- let queue = null;
108
- let enqueueCapture;
109
- if (options.captureRunner) {
110
- if (!config.scratchDir) throw new Error("captureRunner is enabled but ShelfConfig.scratchDir is not set");
111
- const jobOptions = {
112
- db: options.database,
113
- storage: options.storage,
114
- runner: options.captureRunner,
115
- scratchDir: config.scratchDir,
116
- viewports: config.viewports,
117
- logger
118
- };
119
- const captureQueue = options.captureQueue ?? new InMemoryCaptureQueue({
120
- concurrency: config.captureConcurrency ?? 2,
121
- logger,
122
- runJob: async (job) => {
123
- const builds = new BuildModel(options.database);
124
- const build = await builds.get(job.buildId);
125
- if (!build) {
126
- await executeCaptureJob({
127
- buildId: job.buildId,
128
- reqId: job.reqId
129
- }, jobOptions);
130
- return;
131
- }
132
- const project = await new ProjectModel(options.database).get(build.projectId);
133
- if (!project) {
134
- await executeCaptureJob({
135
- buildId: job.buildId,
136
- reqId: job.reqId
137
- }, jobOptions);
138
- return;
139
- }
140
- const pendingUrl = `/projects/${project.slug}/builds/${job.buildId}`;
141
- await postStatusesForBuild({
142
- db: options.database,
143
- project,
144
- sha: build.gitSha,
145
- status: "pending",
146
- url: pendingUrl,
147
- providers: gitHosts,
148
- secret: config.secret,
149
- logger
150
- }).catch((error) => {
151
- logger.error({ err: error }, "failed to post pending status");
152
- });
153
- if (!build.isDefault) {
154
- if (await isAlreadyMerged({
155
- providers: gitHosts,
156
- sha: build.gitSha,
157
- branch: build.gitBranch,
158
- secret: config.secret,
159
- db: options.database,
160
- projectId: project.id,
161
- logger
162
- }).catch(() => false)) {
163
- logger.info({
164
- buildId: build.id,
165
- sha: build.gitSha,
166
- branch: build.gitBranch
167
- }, "skipping capture — already merged");
168
- await builds.setStatus(build.id, "approved").catch(() => {});
169
- await postStatusesForBuild({
170
- db: options.database,
171
- project,
172
- sha: build.gitSha,
173
- status: "success",
174
- url: pendingUrl,
175
- providers: gitHosts,
176
- secret: config.secret,
177
- logger
178
- }).catch(() => {});
179
- return;
180
- }
181
- }
182
- if (await hasApprovedBuildForSha(options.database, project.id, build.gitSha, build.id)) {
183
- logger.info({
184
- buildId: build.id,
185
- sha: build.gitSha
186
- }, "skipping capture — duplicate sha already approved");
187
- await builds.setStatus(build.id, "approved").catch(() => {});
188
- await postStatusesForBuild({
189
- db: options.database,
190
- project,
191
- sha: build.gitSha,
192
- status: "success",
193
- url: pendingUrl,
194
- providers: gitHosts,
195
- secret: config.secret,
196
- logger
197
- }).catch(() => {});
198
- return;
199
- }
200
- try {
201
- await executeCaptureJob({
202
- buildId: job.buildId,
203
- reqId: job.reqId
204
- }, jobOptions);
205
- const updated = await builds.get(job.buildId);
206
- if (updated) {
207
- const terminal = updated.status === "approved" ? "success" : updated.status === "rejected" || updated.status === "failed" ? "failure" : null;
208
- if (terminal) await postStatusesForBuild({
209
- db: options.database,
210
- project,
211
- sha: updated.gitSha,
212
- status: terminal,
213
- url: pendingUrl,
214
- providers: gitHosts,
215
- secret: config.secret,
216
- logger
217
- }).catch((error) => {
218
- logger.error({ err: error }, "failed to post terminal status");
219
- });
220
- }
221
- } catch (error) {
222
- await postStatusesForBuild({
223
- db: options.database,
224
- project,
225
- sha: build.gitSha,
226
- status: "failure",
227
- url: pendingUrl,
228
- providers: gitHosts,
229
- secret: config.secret,
230
- logger
231
- }).catch(() => {});
232
- throw error;
233
- }
234
- }
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
235
100
  });
236
- queue = captureQueue;
237
- enqueueCapture = async (buildId, reqId) => {
238
- await captureQueue.enqueue({
239
- buildId,
240
- reqId
241
- });
242
- };
243
- }
244
- app.use("*", requestId());
245
- app.use("*", async (c, next) => {
246
- const started = performance.now();
247
- const id = c.get("requestId");
248
- logger.info({
249
- reqId: id,
250
- method: c.req.method,
251
- url: c.req.path
252
- }, "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);
253
131
  await next();
254
- logger.info({
255
- reqId: id,
256
- method: c.req.method,
257
- url: c.req.path,
258
- status: c.res.status,
259
- durationMs: Math.round(performance.now() - started)
260
- }, "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
+ };
261
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));
262
182
  app.use("/api/v1/*", rateLimit({
263
183
  windowMs: 6e4,
264
184
  max: 100
@@ -272,33 +192,22 @@ function createShelfRouter(options) {
272
192
  max: 20
273
193
  }));
274
194
  app.use("/projects/:slug/settings/*", csrf());
275
- app.use("*", async (c, next) => {
276
- const user = await resolveUser(c, options);
277
- return runWithStore({
278
- db: options.database,
279
- storage: options.storage,
280
- config,
281
- ui,
282
- logger,
283
- user,
284
- authEnabled,
285
- enqueueCapture,
286
- captureQueue: queue,
287
- gitHosts
288
- }, async () => {
289
- await next();
290
- });
291
- });
292
- app.use("*", async (c, next) => {
293
- const { user, authEnabled: isAuthEnabled } = getStore();
294
- const { path } = c.req;
295
- if (!isAuthEnabled || user || path.startsWith("/api/") || path.startsWith("/auth/") || path.startsWith("/assets/") || path.startsWith("/projects/") && path.includes("/storybook")) return next();
296
- if (c.req.header("HX-Request") === "true") {
297
- c.header("HX-Redirect", "/auth/login");
298
- return c.body(null, 204);
299
- }
300
- return c.redirect("/auth/login", 302);
301
- });
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) {
302
211
  registerProjects(app);
303
212
  registerBuilds(app);
304
213
  registerLabels(app);
@@ -307,11 +216,18 @@ function createShelfRouter(options) {
307
216
  registerTokens(app);
308
217
  registerWebhooks(app);
309
218
  registerStatusConfigs(app);
219
+ registerHealth(app, health);
310
220
  registerAdmin(app);
221
+ }
222
+ /** Register HTML pages, assets, and the optional auth flow. */
223
+ function registerPageRoutes(app, options) {
311
224
  if (options.auth) registerAuth(app, options.auth);
312
225
  registerAssets(app);
313
226
  registerStorybook(app);
314
227
  registerUiPages(app);
228
+ }
229
+ /** Register the OpenAPI document and interactive docs page. */
230
+ function registerDocs(app) {
315
231
  app.doc("/api/v1/openapi.json", {
316
232
  openapi: "3.0.0",
317
233
  info: {
@@ -321,9 +237,52 @@ function createShelfRouter(options) {
321
237
  }
322
238
  });
323
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);
324
283
  return app;
325
284
  }
326
285
  //#endregion
327
- 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 };
328
287
 
329
288
  //# sourceMappingURL=index.mjs.map
@@ -1 +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 { 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\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\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\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