@storyshelf/app 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (264) hide show
  1. package/dist/app-types.d.mts +1 -3
  2. package/dist/asset-manifest.mjs +1 -2
  3. package/dist/assets/htmx.mjs +1 -2
  4. package/dist/assets/mark.mjs +1 -2
  5. package/dist/boot-tasks.mjs +1 -2
  6. package/dist/build-state-flags.mjs +1 -2
  7. package/dist/build-state-setup.mjs +1 -2
  8. package/dist/capture-alerts.mjs +1 -2
  9. package/dist/capture-process.mjs +1 -2
  10. package/dist/capture-setup.mjs +1 -2
  11. package/dist/connections.mjs +1 -2
  12. package/dist/consumer-setup.mjs +1 -2
  13. package/dist/credential-keys.mjs +1 -2
  14. package/dist/credentials.mjs +1 -2
  15. package/dist/git-status.mjs +1 -2
  16. package/dist/hidden-data.mjs +1 -2
  17. package/dist/index.d.mts +0 -2
  18. package/dist/index.mjs +2 -3
  19. package/dist/insights/budget.mjs +1 -2
  20. package/dist/insights/deps.mjs +1 -2
  21. package/dist/insights/errors.mjs +1 -2
  22. package/dist/insights/health-panel.mjs +0 -1
  23. package/dist/insights/job.mjs +1 -2
  24. package/dist/insights/load-health.mjs +1 -2
  25. package/dist/insights/load-triage.mjs +1 -2
  26. package/dist/insights/panel.mjs +1 -2
  27. package/dist/insights/request.mjs +1 -2
  28. package/dist/insights/stale.mjs +1 -2
  29. package/dist/insights/usage-report.mjs +1 -2
  30. package/dist/insights/view.mjs +1 -2
  31. package/dist/job-canceller.mjs +1 -2
  32. package/dist/job-queue.mjs +1 -2
  33. package/dist/jobs-setup.mjs +1 -2
  34. package/dist/lifecycle.mjs +2 -3
  35. package/dist/lifecycle.mjs.map +1 -1
  36. package/dist/middleware/auth-gate.mjs +1 -2
  37. package/dist/middleware/csrf.mjs +1 -2
  38. package/dist/middleware/rate-limit.mjs +1 -2
  39. package/dist/middleware/request-log.mjs +1 -2
  40. package/dist/middleware/require-review.mjs +1 -2
  41. package/dist/middleware/require-testing.mjs +1 -2
  42. package/dist/middleware/server-timing.mjs +1 -2
  43. package/dist/middleware/setup-gate.mjs +1 -2
  44. package/dist/middleware/store-scope.mjs +1 -2
  45. package/dist/notifications-pause.mjs +1 -2
  46. package/dist/notify.d.mts +1 -3
  47. package/dist/notify.mjs +1 -2
  48. package/dist/pages/a11y-violations.mjs +1 -2
  49. package/dist/pages/admin-ai.mjs +1 -2
  50. package/dist/pages/admin-system-origin.mjs +19 -10
  51. package/dist/pages/admin-system-origin.mjs.map +1 -1
  52. package/dist/pages/admin-system-targets.mjs +1 -2
  53. package/dist/pages/admin-system.mjs +1 -2
  54. package/dist/pages/build-detail.mjs +1 -2
  55. package/dist/pages/build-diff-actions.mjs +1 -2
  56. package/dist/pages/build-diff-comments.mjs +1 -2
  57. package/dist/pages/build-diff-default.mjs +53 -0
  58. package/dist/pages/build-diff-default.mjs.map +1 -0
  59. package/dist/pages/build-diff-environment.mjs +1 -2
  60. package/dist/pages/build-diff-header.mjs +1 -2
  61. package/dist/pages/build-diff-insight.mjs +1 -2
  62. package/dist/pages/build-diff-model.mjs +20 -3
  63. package/dist/pages/build-diff-model.mjs.map +1 -1
  64. package/dist/pages/build-diff-nav.mjs +1 -2
  65. package/dist/pages/build-diff-reference.mjs +84 -0
  66. package/dist/pages/build-diff-reference.mjs.map +1 -0
  67. package/dist/pages/build-diff-shortcuts.mjs +1 -2
  68. package/dist/pages/build-diff-stale.mjs +1 -2
  69. package/dist/pages/build-diff-usage.mjs +1 -2
  70. package/dist/pages/build-diff-viewer.mjs +38 -12
  71. package/dist/pages/build-diff-viewer.mjs.map +1 -1
  72. package/dist/pages/build-diff.mjs +6 -12
  73. package/dist/pages/build-diff.mjs.map +1 -1
  74. package/dist/pages/build-tests.mjs +1 -2
  75. package/dist/pages/component-detail.mjs +1 -2
  76. package/dist/pages/component-stories.mjs +1 -2
  77. package/dist/pages/compute-jobs.mjs +1 -2
  78. package/dist/pages/design-links.mjs +1 -2
  79. package/dist/pages/environment-change.mjs +1 -2
  80. package/dist/pages/health-panel.mjs +1 -2
  81. package/dist/pages/invite.mjs +1 -2
  82. package/dist/pages/label-detail.mjs +1 -2
  83. package/dist/pages/library-grid.mjs +1 -2
  84. package/dist/pages/library-model.mjs +1 -2
  85. package/dist/pages/library-snapshots.mjs +1 -2
  86. package/dist/pages/library.mjs +1 -2
  87. package/dist/pages/login.mjs +1 -2
  88. package/dist/pages/notifications-paused.mjs +1 -2
  89. package/dist/pages/passkey-ceremony.mjs +1 -2
  90. package/dist/pages/profile.mjs +1 -2
  91. package/dist/pages/project-builds-table.mjs +1 -2
  92. package/dist/pages/project-builds.mjs +1 -2
  93. package/dist/pages/project-create.mjs +1 -2
  94. package/dist/pages/project-settings.mjs +1 -2
  95. package/dist/pages/projects-card.mjs +1 -2
  96. package/dist/pages/projects.mjs +1 -2
  97. package/dist/pages/review-changeset.mjs +1 -2
  98. package/dist/pages/review-comments.mjs +1 -2
  99. package/dist/pages/review-verdict.mjs +1 -2
  100. package/dist/pages/root.mjs +1 -2
  101. package/dist/pages/settings-ai.mjs +1 -2
  102. package/dist/pages/settings-capture.mjs +1 -2
  103. package/dist/pages/settings-design-credential.mjs +1 -2
  104. package/dist/pages/settings-design-links.mjs +1 -2
  105. package/dist/pages/settings-design.mjs +1 -2
  106. package/dist/pages/settings-general.mjs +1 -2
  107. package/dist/pages/settings-labels.mjs +1 -2
  108. package/dist/pages/settings-links.mjs +1 -2
  109. package/dist/pages/settings-members.mjs +1 -2
  110. package/dist/pages/settings-notifications-git.mjs +1 -2
  111. package/dist/pages/settings-notifications.mjs +1 -2
  112. package/dist/pages/settings-tests.mjs +1 -2
  113. package/dist/pages/settings-tokens.mjs +1 -2
  114. package/dist/pages/settings-webhooks.mjs +1 -2
  115. package/dist/pages/storybook.mjs +1 -2
  116. package/dist/pages/target-choices.mjs +1 -2
  117. package/dist/publish-setup.mjs +1 -2
  118. package/dist/retention-timer.mjs +10 -11
  119. package/dist/retention-timer.mjs.map +1 -1
  120. package/dist/review-events.mjs +1 -2
  121. package/dist/routers/admin-pages.mjs +1 -2
  122. package/dist/routers/admin-public-origin.mjs +10 -3
  123. package/dist/routers/admin-public-origin.mjs.map +1 -1
  124. package/dist/routers/admin.mjs +8 -4
  125. package/dist/routers/admin.mjs.map +1 -1
  126. package/dist/routers/assets.mjs +1 -2
  127. package/dist/routers/attempts.mjs +1 -2
  128. package/dist/routers/auth-sync.mjs +1 -2
  129. package/dist/routers/auth.mjs +1 -2
  130. package/dist/routers/baseline-guard.mjs +2 -3
  131. package/dist/routers/baseline-guard.mjs.map +1 -1
  132. package/dist/routers/baseline-reference.mjs +72 -0
  133. package/dist/routers/baseline-reference.mjs.map +1 -0
  134. package/dist/routers/baselines.handlers.mjs +1 -2
  135. package/dist/routers/baselines.mjs +1 -2
  136. package/dist/routers/build-cancel.mjs +1 -2
  137. package/dist/routers/build-changeset.mjs +1 -2
  138. package/dist/routers/build-reviews.handlers.mjs +1 -2
  139. package/dist/routers/build-reviews.mjs +1 -2
  140. package/dist/routers/builds.handlers.mjs +23 -9
  141. package/dist/routers/builds.handlers.mjs.map +1 -1
  142. package/dist/routers/builds.mjs +7 -3
  143. package/dist/routers/builds.mjs.map +1 -1
  144. package/dist/routers/capture-cache.mjs +29 -0
  145. package/dist/routers/capture-cache.mjs.map +1 -0
  146. package/dist/routers/capture-config.mjs +1 -2
  147. package/dist/routers/comments.mjs +1 -2
  148. package/dist/routers/comparison-image.mjs +29 -0
  149. package/dist/routers/comparison-image.mjs.map +1 -0
  150. package/dist/routers/component-catalog.handlers.mjs +1 -2
  151. package/dist/routers/component.handlers.mjs +1 -2
  152. package/dist/routers/connections.mjs +1 -2
  153. package/dist/routers/design-connect.handlers.mjs +1 -2
  154. package/dist/routers/design.handlers.mjs +1 -2
  155. package/dist/routers/flash.mjs +1 -2
  156. package/dist/routers/health-report.mjs +1 -2
  157. package/dist/routers/health.mjs +1 -2
  158. package/dist/routers/helpers.mjs +1 -2
  159. package/dist/routers/htmx.mjs +1 -2
  160. package/dist/routers/insights-ui.mjs +1 -2
  161. package/dist/routers/insights.handlers.mjs +1 -2
  162. package/dist/routers/insights.mjs +1 -2
  163. package/dist/routers/labels.mjs +1 -2
  164. package/dist/routers/links.handlers.mjs +1 -2
  165. package/dist/routers/links.mjs +1 -2
  166. package/dist/routers/media.mjs +14 -5
  167. package/dist/routers/media.mjs.map +1 -1
  168. package/dist/routers/members.mjs +1 -2
  169. package/dist/routers/notification-channels.mjs +1 -2
  170. package/dist/routers/notification-subscriptions.mjs +1 -2
  171. package/dist/routers/oauth-state.mjs +1 -2
  172. package/dist/routers/preview-base.mjs +1 -2
  173. package/dist/routers/preview-token.mjs +1 -2
  174. package/dist/routers/profile.mjs +1 -2
  175. package/dist/routers/projects.mjs +1 -2
  176. package/dist/routers/publish-upload.mjs +19 -12
  177. package/dist/routers/publish-upload.mjs.map +1 -1
  178. package/dist/routers/published-access.mjs +1 -2
  179. package/dist/routers/published-compression.mjs +109 -0
  180. package/dist/routers/published-compression.mjs.map +1 -0
  181. package/dist/routers/published-files.mjs +26 -9
  182. package/dist/routers/published-files.mjs.map +1 -1
  183. package/dist/routers/review-forms.mjs +1 -2
  184. package/dist/routers/review-next.mjs +1 -2
  185. package/dist/routers/review-section.handlers.mjs +1 -2
  186. package/dist/routers/review-switch-off.mjs +1 -2
  187. package/dist/routers/schemas.mjs +1 -2
  188. package/dist/routers/scoped-models.mjs +1 -2
  189. package/dist/routers/settings-ai.mjs +1 -2
  190. package/dist/routers/settings-capture.mjs +1 -2
  191. package/dist/routers/settings-design.mjs +1 -2
  192. package/dist/routers/settings-general.mjs +1 -2
  193. package/dist/routers/settings-git-channel.mjs +1 -2
  194. package/dist/routers/settings-labels.mjs +1 -2
  195. package/dist/routers/settings-links.mjs +1 -2
  196. package/dist/routers/settings-members.mjs +1 -2
  197. package/dist/routers/settings-notifications.mjs +1 -2
  198. package/dist/routers/settings-tokens.mjs +1 -2
  199. package/dist/routers/settings-webhooks.mjs +1 -2
  200. package/dist/routers/settings.handlers.mjs +1 -2
  201. package/dist/routers/settings.mjs +1 -2
  202. package/dist/routers/signed-storybook.mjs +5 -3
  203. package/dist/routers/signed-storybook.mjs.map +1 -1
  204. package/dist/routers/snapshot-groups.mjs +1 -2
  205. package/dist/routers/snapshots.mjs +1 -2
  206. package/dist/routers/storybook.mjs +21 -17
  207. package/dist/routers/storybook.mjs.map +1 -1
  208. package/dist/routers/tokens.mjs +1 -2
  209. package/dist/routers/ui.mjs +4 -2
  210. package/dist/routers/ui.mjs.map +1 -1
  211. package/dist/routers/webhooks.mjs +1 -2
  212. package/dist/routers/well-known.mjs +1 -2
  213. package/dist/routes/api-routes.mjs +1 -2
  214. package/dist/routes/classification.mjs +1 -2
  215. package/dist/routes/openapi-classes.mjs +1 -2
  216. package/dist/routes/page-routes.mjs +2 -3
  217. package/dist/routes/page-routes.mjs.map +1 -1
  218. package/dist/runtime.mjs +1 -2
  219. package/dist/serverless.mjs +1 -2
  220. package/dist/store.mjs +1 -2
  221. package/dist/targets.mjs +1 -2
  222. package/dist/ui/avatar.mjs +1 -2
  223. package/dist/ui/buttons.mjs +1 -2
  224. package/dist/ui/code-block.mjs +1 -2
  225. package/dist/ui/compare-stage.mjs +86 -54
  226. package/dist/ui/compare-stage.mjs.map +1 -1
  227. package/dist/ui/csrf-field.mjs +1 -2
  228. package/dist/ui/css.mjs +1 -2
  229. package/dist/ui/design-reference.mjs +1 -2
  230. package/dist/ui/document.mjs +1 -2
  231. package/dist/ui/dropdown.mjs +1 -2
  232. package/dist/ui/feedback.mjs +1 -2
  233. package/dist/ui/filter-input.mjs +1 -2
  234. package/dist/ui/forms.mjs +1 -2
  235. package/dist/ui/icons/icon.mjs +1 -2
  236. package/dist/ui/icons/paths.mjs +1 -2
  237. package/dist/ui/icons/sprite.mjs +1 -2
  238. package/dist/ui/kbd.mjs +1 -2
  239. package/dist/ui/layout.mjs +1 -2
  240. package/dist/ui/preview-embed.mjs +1 -2
  241. package/dist/ui/progress.mjs +1 -2
  242. package/dist/ui/segmented.mjs +1 -2
  243. package/dist/ui/shell/account.mjs +1 -2
  244. package/dist/ui/shell/bare.mjs +1 -2
  245. package/dist/ui/shell/boot-script.mjs +1 -2
  246. package/dist/ui/shell/client-script.mjs +1 -2
  247. package/dist/ui/shell/nav.mjs +1 -2
  248. package/dist/ui/shell/review-script.mjs +17 -3
  249. package/dist/ui/shell/review-script.mjs.map +1 -1
  250. package/dist/ui/shell/sidebar.mjs +1 -2
  251. package/dist/ui/shell/topbar.mjs +1 -2
  252. package/dist/ui/stacks.mjs +1 -2
  253. package/dist/ui/styles/base.mjs +1 -2
  254. package/dist/ui/styles/behavior.mjs +1 -2
  255. package/dist/ui/styles/review-layout.mjs +1 -2
  256. package/dist/ui/styles/review.mjs +1 -2
  257. package/dist/ui/styles/tokens.mjs +1 -2
  258. package/dist/ui/styles.mjs +1 -2
  259. package/dist/ui/subnav.mjs +1 -2
  260. package/dist/ui/table.mjs +1 -2
  261. package/dist/ui/theme.mjs +1 -2
  262. package/dist/ui/thumbnail.mjs +1 -2
  263. package/dist/ui/time.mjs +1 -2
  264. package/package.json +9 -9
@@ -8,7 +8,7 @@ import { buildReviewCreateSchema, buildReviewSchema, buildReviewStateSchema, for
8
8
  import { APPROVER_ROLES, VIEW_ROLES, buildForProject } from "./builds.handlers.mjs";
9
9
  import { giveVerdict, reviewStateOf } from "./build-reviews.handlers.mjs";
10
10
  import { createRoute, z } from "@hono/zod-openapi";
11
- //#region src/routers/build-reviews.ts
11
+ // src/routers/build-reviews.ts
12
12
  const params = z.object({
13
13
  slug: z.string(),
14
14
  buildId: z.string()
@@ -68,7 +68,6 @@ function registerBuildReviews(app) {
68
68
  return c.json(review, 201);
69
69
  });
70
70
  }
71
- //#endregion
72
71
  export { registerBuildReviews };
73
72
 
74
73
  //# sourceMappingURL=build-reviews.mjs.map
@@ -6,14 +6,15 @@ import { getStore } from "../store.mjs";
6
6
  import { notifyProject } from "../notify.mjs";
7
7
  import { notFound } from "./helpers.mjs";
8
8
  import { requestBuildState } from "../git-status.mjs";
9
- import { assertBaselineCurrent } from "./baseline-guard.mjs";
9
+ import { assertBaselineCurrent, baselineChanged } from "./baseline-guard.mjs";
10
10
  import { z } from "@hono/zod-openapi";
11
- import { BaselineModel, BuildModel, LabelModel, ProjectModel, SnapshotModel } from "@storyshelf/core/models";
11
+ import { BaselineApprovalConflict, BaselineModel, BaselineWriteConflict, BuildModel, LabelModel, ProjectModel, SnapshotModel } from "@storyshelf/core/models";
12
12
  import { HTTPException } from "hono/http-exception";
13
13
  import { emitWebhookEvent } from "@storyshelf/core/adapter/webhook-events";
14
14
  import { BUILD_STATUSES } from "@storyshelf/core/types";
15
+ import { ReferenceGenerationConflict } from "@storyshelf/core/adapter/database";
15
16
  import { Readable, Transform } from "node:stream";
16
- //#region src/routers/builds.handlers.ts
17
+ // src/routers/builds.handlers.ts
17
18
  /**
18
19
  * Create a build record with labels and the `build:created` webhook.
19
20
  * Shared by the build-creation route.
@@ -143,13 +144,27 @@ async function approveSnapshot(snapshotId, userId, options = {}) {
143
144
  if (!project || !build) notFound("Project or build not found");
144
145
  if (build.status === "cancelled") throw new HTTPException(409, { message: "The build was cancelled; retry it to review it" });
145
146
  await assertBaselineCurrent(project, build, snapshot, options.force ?? false);
146
- await snapshots.review(snapshotId, "approved", userId);
147
+ if (!db.buildReferenceLifecycle) await snapshots.review(snapshotId, "approved", userId);
147
148
  const baselines = new BaselineModel(db, void 0, getStore().storage, credentialKeys(config));
148
149
  const prior = await baselines.getFor(project.id, snapshot.storyId, snapshot.viewportName, build.gitBranch, snapshot.browser);
149
- await baselines.upsert(project.id, snapshot.storyId, snapshot.viewportName, build.gitBranch, snapshot.id, snapshot.screenshotPath, void 0, {
150
- browser: snapshot.browser,
151
- mode: snapshot.mode
152
- });
150
+ try {
151
+ await baselines.upsert(project.id, snapshot.storyId, snapshot.viewportName, build.gitBranch, snapshot.id, snapshot.screenshotPath, void 0, {
152
+ browser: snapshot.browser,
153
+ mode: snapshot.mode,
154
+ approval: db.buildReferenceLifecycle ? {
155
+ snapshot,
156
+ reviewedBy: userId,
157
+ force: options.force ?? false,
158
+ branch: build.gitBranch
159
+ } : void 0
160
+ });
161
+ } catch (error) {
162
+ if (error instanceof BaselineWriteConflict || error instanceof ReferenceGenerationConflict) {
163
+ if (error instanceof BaselineApprovalConflict && error.driftStatus) throw baselineChanged(snapshot.id, error.driftStatus);
164
+ throw new HTTPException(409, { message: error.message });
165
+ }
166
+ throw error;
167
+ }
153
168
  await notifyProject(project, prior ? "baseline:updated" : "baseline:created", {
154
169
  storyId: snapshot.storyId,
155
170
  viewport: snapshot.viewportName,
@@ -202,7 +217,6 @@ async function attachLabels(db, projectId, buildId, labels) {
202
217
  await models.attach(projectId, buildId, key, value);
203
218
  }
204
219
  }
205
- //#endregion
206
220
  export { APPROVER_ROLES, DEVELOPER_ROLES, VIEW_ROLES, announceBuildStatus, approveSnapshot, buildCreateJsonSchema, buildForProject, buildListQuery, createBuildRecord, refreshBuild, snapshotForBuild, storeUploadStream };
207
221
 
208
222
  //# sourceMappingURL=builds.handlers.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"builds.handlers.mjs","names":[],"sources":["../../src/routers/builds.handlers.ts"],"sourcesContent":["import { z } from \"@hono/zod-openapi\";\nimport type { DatabaseAdapter } from \"@storyshelf/core/adapter/database\";\nimport type { StorageAdapter } from \"@storyshelf/core/adapter/storage\";\nimport { emitWebhookEvent } from \"@storyshelf/core/adapter/webhook-events\";\nimport { BaselineModel } from \"@storyshelf/core/models\";\nimport { BuildModel } from \"@storyshelf/core/models\";\nimport { LabelModel } from \"@storyshelf/core/models\";\nimport { ProjectModel } from \"@storyshelf/core/models\";\nimport { SnapshotModel } from \"@storyshelf/core/models\";\nimport type { Project } from \"@storyshelf/core/schema\";\nimport type { Build, Snapshot } from \"@storyshelf/core/schema\";\nimport { type BuildStatus, type ProjectRole, BUILD_STATUSES } from \"@storyshelf/core/types\";\nimport { HTTPException } from \"hono/http-exception\";\nimport { Readable, Transform } from \"node:stream\";\nimport type { ReadableStream as NodeWebStream } from \"node:stream/web\";\nimport { credentialKeys } from \"../credential-keys.ts\";\nimport { requestBuildState } from \"../git-status.ts\";\nimport { notifyProject } from \"../notify.ts\";\nimport { getStore } from \"../store.ts\";\nimport { assertBaselineCurrent } from \"./baseline-guard.ts\";\nimport { notFound } from \"./helpers.ts\";\n/**\n * Create a build record with labels and the `build:created` webhook.\n * Shared by the build-creation route.\n */\nexport async function createBuildRecord(\n project: Project,\n meta: BuildCreateMetadata,\n): Promise<Build> {\n const { db, config } = getStore();\n if (!meta.gitSha || !meta.gitBranch) {\n throw new HTTPException(400, {\n message: \"gitSha and gitBranch are required (revision SHA and baseline namespace)\",\n });\n }\n const build = await new BuildModel(db).create(project.id, {\n gitSha: meta.gitSha,\n gitBranch: meta.gitBranch,\n isDefault: meta.gitBranch === project.gitDefaultBranch,\n authorEmail: meta.authorEmail,\n authorName: meta.authorName,\n message: meta.message,\n affectedOnly: meta.affectedOnly,\n baselineSha: await resolveBaselineSha(db, project.id, meta.gitBranch, meta.gitSha),\n });\n if (meta.labels && meta.labels.length > 0) {\n await attachLabels(db, project.id, build.id, meta.labels);\n }\n await emitWebhookEvent(\n db,\n { webhooks: getStore().db.tables.webhooks },\n project.id,\n \"build:created\",\n {\n buildId: build.id,\n gitSha: meta.gitSha,\n gitBranch: meta.gitBranch,\n authorEmail: meta.authorEmail,\n authorName: meta.authorName,\n message: meta.message,\n },\n credentialKeys(config),\n );\n await notifyProject(\n project,\n \"build:created\",\n {\n buildId: build.id,\n gitSha: meta.gitSha,\n gitBranch: meta.gitBranch,\n authorEmail: meta.authorEmail,\n authorName: meta.authorName,\n message: meta.message,\n },\n `/builds/${build.id}`,\n );\n await requestBuildState(build.id);\n return build;\n}\n\n/**\n * Stream a raw request body into storage with a byte cap, counting the bytes as they pass (a\n * chunked upload has no `Content-Length`). Resolves to the bytes stored.\n * Throws 400 for a missing body, 413 once `maxBytes` is exceeded.\n */\nexport async function storeUploadStream(\n storage: StorageAdapter,\n path: string,\n body: ReadableStream<Uint8Array> | null,\n maxBytes: number,\n): Promise<number> {\n if (!body) {\n throw new HTTPException(400, { message: \"Missing request body\" });\n }\n let seen = 0;\n const limiter = new Transform({\n transform(\n chunk: Buffer,\n _encoding: string,\n callback: (error?: Error | null, data?: Buffer) => void,\n ): void {\n seen += chunk.length;\n if (seen > maxBytes) {\n callback(new Error(`Upload exceeds ${maxBytes} byte limit`));\n } else {\n callback(null, chunk);\n }\n },\n });\n try {\n const nodeBody = Readable.fromWeb(body as unknown as NodeWebStream);\n await storage.writeStream(path, nodeBody.pipe(limiter));\n return seen;\n } catch (error) {\n if (seen > maxBytes) {\n throw new HTTPException(413, { message: \"Upload exceeds size limit\" });\n }\n throw error;\n }\n}\n\n/** Query filters accepted by the build list endpoint. */\nexport const buildListQuery = z.object({\n status: z.enum(BUILD_STATUSES).optional(),\n branch: z.string().optional(),\n labelKey: z.string().optional(),\n labelValue: z.string().optional(),\n});\n\n/** Fetch a build scoped to its project, throwing 404 when it does not belong. */\nexport async function buildForProject(projectId: string, buildId: string): Promise<Build> {\n const build = await new BuildModel(getStore().db).get(buildId);\n if (!build || build.projectId !== projectId) {\n notFound(\"Build not found\");\n }\n return build;\n}\n\n/** Fetch a snapshot scoped to its build, throwing 404 when it does not belong. */\nexport async function snapshotForBuild(\n build: { id: string },\n snapshotId: string,\n): Promise<Snapshot> {\n const snapshot = await new SnapshotModel(getStore().db).get(snapshotId);\n if (!snapshot || snapshot.buildId !== build.id) {\n notFound(\"Snapshot not found\");\n }\n return snapshot;\n}\n\n/** Roll snapshot statuses up into the build's review status. */\nfunction rollupStatus(snapshots: Snapshot[]): \"reviewing\" | \"rejected\" | \"approved\" {\n if (snapshots.some((s) => s.status === \"new\" || s.status === \"changed\")) {\n return \"reviewing\";\n }\n return snapshots.some((s) => s.status === \"rejected\") ? \"rejected\" : \"approved\";\n}\n\n/** Recompute a build's counts and roll its status up from its snapshots. */\nexport async function refreshBuild(buildId: string): Promise<void> {\n const { db } = getStore();\n await new BuildModel(db).updateCounts(buildId);\n const snapshots = await new SnapshotModel(db).listByBuild(buildId);\n const status = rollupStatus(snapshots);\n const build = await new BuildModel(db).get(buildId);\n // A cancelled build is terminal: its snapshots may still be touched, but no verdict is announced.\n if (!build || build.status === \"cancelled\") return;\n await new BuildModel(db).setStatus(buildId, status);\n await announceBuildStatus(buildId, status, { snapshotCount: snapshots.length });\n}\n\n/**\n * Tell the world a build reached `status`: its git channels, the `build:<status>` webhook and the\n * project's notification channels. Every route that moves a build to a decided status ends here.\n */\nexport async function announceBuildStatus(\n buildId: string,\n status: BuildStatus,\n extra: { snapshotCount?: number } = {},\n): Promise<void> {\n const { db, config } = getStore();\n const build = await new BuildModel(db).get(buildId);\n if (!build) return;\n await requestBuildState(buildId);\n const payload = { buildId, status, ...extra };\n await emitWebhookEvent(\n db,\n { webhooks: getStore().db.tables.webhooks },\n build.projectId,\n `build:${status}`,\n payload,\n credentialKeys(config),\n );\n const project = await new ProjectModel(db).get(build.projectId);\n if (!project) return;\n await notifyProject(project, `build:${status}`, payload, `/builds/${buildId}`);\n}\n\n/**\n * Approve a snapshot, promote its screenshot to baseline, and refresh the build.\n * Refuses with 409 when the baseline changed since the diff, unless `force` is set.\n * Bulk callers pass `deferRefresh` and call {@link refreshBuild} once, so the build's\n * outcome (and its git status) is announced a single time.\n */\nexport async function approveSnapshot(\n snapshotId: string,\n userId: string | null,\n options: { force?: boolean; deferRefresh?: boolean } = {},\n): Promise<void> {\n const { db, config } = getStore();\n const snapshots = new SnapshotModel(db);\n const snapshot = await snapshots.get(snapshotId);\n if (!snapshot) {\n notFound(\"Snapshot not found\");\n }\n const project = await new ProjectModel(db).get(snapshot.projectId);\n const build = await new BuildModel(db).get(snapshot.buildId);\n if (!project || !build) {\n notFound(\"Project or build not found\");\n }\n if (build.status === \"cancelled\") {\n throw new HTTPException(409, { message: \"The build was cancelled; retry it to review it\" });\n }\n await assertBaselineCurrent(project, build, snapshot, options.force ?? false);\n await snapshots.review(snapshotId, \"approved\", userId);\n const baselines = new BaselineModel(db, undefined, getStore().storage, credentialKeys(config));\n const prior = await baselines.getFor(\n project.id,\n snapshot.storyId,\n snapshot.viewportName,\n build.gitBranch,\n snapshot.browser,\n );\n await baselines.upsert(\n project.id,\n snapshot.storyId,\n snapshot.viewportName,\n build.gitBranch,\n snapshot.id,\n snapshot.screenshotPath,\n undefined,\n { browser: snapshot.browser, mode: snapshot.mode },\n );\n await notifyProject(\n project,\n prior ? \"baseline:updated\" : \"baseline:created\",\n {\n storyId: snapshot.storyId,\n viewport: snapshot.viewportName,\n browser: snapshot.browser ?? \"chromium\",\n branch: build.gitBranch,\n snapshotId: snapshot.id,\n buildId: build.id,\n },\n `/builds/${build.id}`,\n );\n if (!options.deferRefresh) {\n await refreshBuild(build.id);\n }\n}\n\n/** Roles permitted to view builds, snapshots, and comments. */\nexport const VIEW_ROLES: readonly ProjectRole[] = [\"viewer\", \"developer\", \"approver\", \"admin\"];\n/** Roles permitted to upload builds and write comments. */\nexport const DEVELOPER_ROLES: readonly ProjectRole[] = [\"developer\", \"approver\", \"admin\"];\n/** Roles permitted to approve or reject snapshots and delete builds. */\nexport const APPROVER_ROLES: readonly ProjectRole[] = [\"approver\", \"admin\"];\n\n/** JSON input schema for creating a build before streaming its bundle. */\nexport const buildCreateJsonSchema = z\n .object({\n gitSha: z.string().min(1),\n gitBranch: z.string().min(1),\n authorEmail: z.string().optional(),\n authorName: z.string().optional(),\n message: z.string().optional(),\n affectedOnly: z.boolean().optional(),\n labels: z.array(z.object({ key: z.string().min(1), value: z.string() })).optional(),\n })\n .openapi(\"BuildCreate\");\n\n/** Metadata accepted by either build-creation route. */\nexport interface BuildCreateMetadata {\n gitSha: string;\n gitBranch: string;\n authorEmail?: string;\n authorName?: string;\n message?: string;\n affectedOnly?: boolean;\n labels?: { key: string; value: string }[];\n}\n\n/** Latest prior revision on a branch with a build, or null for first builds. */\nasync function resolveBaselineSha(\n db: DatabaseAdapter,\n projectId: string,\n branch: string,\n sha: string,\n): Promise<string | null> {\n const builds = await new BuildModel(db).list(projectId, { branch });\n return builds.find((build) => build.gitSha !== sha)?.gitSha ?? null;\n}\n\n/* oxlint-disable eslint/no-await-in-loop -- label attach is intentionally sequential */\nasync function attachLabels(\n db: DatabaseAdapter,\n projectId: string,\n buildId: string,\n labels: { key: string; value: string }[],\n): Promise<void> {\n const models = new LabelModel(db);\n for (const { key, value } of labels) {\n const existing = await models.getType(projectId, key);\n if (!existing) {\n await models.createType(projectId, { key, name: key });\n }\n await models.attach(projectId, buildId, key, value);\n }\n}\n/* oxlint-enable eslint/no-await-in-loop */\n"],"mappings":"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"}
1
+ {"version":3,"file":"builds.handlers.mjs","names":[],"sources":["../../src/routers/builds.handlers.ts"],"sourcesContent":["import { z } from \"@hono/zod-openapi\";\nimport {\n ReferenceGenerationConflict,\n type DatabaseAdapter,\n} from \"@storyshelf/core/adapter/database\";\nimport type { StorageAdapter } from \"@storyshelf/core/adapter/storage\";\nimport { emitWebhookEvent } from \"@storyshelf/core/adapter/webhook-events\";\nimport {\n BaselineApprovalConflict,\n BaselineModel,\n BaselineWriteConflict,\n} from \"@storyshelf/core/models\";\nimport { BuildModel } from \"@storyshelf/core/models\";\nimport { LabelModel } from \"@storyshelf/core/models\";\nimport { ProjectModel } from \"@storyshelf/core/models\";\nimport { SnapshotModel } from \"@storyshelf/core/models\";\nimport type { Project } from \"@storyshelf/core/schema\";\nimport type { Build, Snapshot } from \"@storyshelf/core/schema\";\nimport { type BuildStatus, type ProjectRole, BUILD_STATUSES } from \"@storyshelf/core/types\";\nimport { HTTPException } from \"hono/http-exception\";\nimport { Readable, Transform } from \"node:stream\";\nimport type { ReadableStream as NodeWebStream } from \"node:stream/web\";\nimport { credentialKeys } from \"../credential-keys.ts\";\nimport { requestBuildState } from \"../git-status.ts\";\nimport { notifyProject } from \"../notify.ts\";\nimport { getStore } from \"../store.ts\";\nimport { assertBaselineCurrent, baselineChanged } from \"./baseline-guard.ts\";\nimport { notFound } from \"./helpers.ts\";\n/**\n * Create a build record with labels and the `build:created` webhook.\n * Shared by the build-creation route.\n */\nexport async function createBuildRecord(\n project: Project,\n meta: BuildCreateMetadata,\n): Promise<Build> {\n const { db, config } = getStore();\n if (!meta.gitSha || !meta.gitBranch) {\n throw new HTTPException(400, {\n message: \"gitSha and gitBranch are required (revision SHA and baseline namespace)\",\n });\n }\n const build = await new BuildModel(db).create(project.id, {\n gitSha: meta.gitSha,\n gitBranch: meta.gitBranch,\n isDefault: meta.gitBranch === project.gitDefaultBranch,\n authorEmail: meta.authorEmail,\n authorName: meta.authorName,\n message: meta.message,\n affectedOnly: meta.affectedOnly,\n baselineSha: await resolveBaselineSha(db, project.id, meta.gitBranch, meta.gitSha),\n });\n if (meta.labels && meta.labels.length > 0) {\n await attachLabels(db, project.id, build.id, meta.labels);\n }\n await emitWebhookEvent(\n db,\n { webhooks: getStore().db.tables.webhooks },\n project.id,\n \"build:created\",\n {\n buildId: build.id,\n gitSha: meta.gitSha,\n gitBranch: meta.gitBranch,\n authorEmail: meta.authorEmail,\n authorName: meta.authorName,\n message: meta.message,\n },\n credentialKeys(config),\n );\n await notifyProject(\n project,\n \"build:created\",\n {\n buildId: build.id,\n gitSha: meta.gitSha,\n gitBranch: meta.gitBranch,\n authorEmail: meta.authorEmail,\n authorName: meta.authorName,\n message: meta.message,\n },\n `/builds/${build.id}`,\n );\n await requestBuildState(build.id);\n return build;\n}\n\n/**\n * Stream a raw request body into storage with a byte cap, counting the bytes as they pass (a\n * chunked upload has no `Content-Length`). Resolves to the bytes stored.\n * Throws 400 for a missing body, 413 once `maxBytes` is exceeded.\n */\nexport async function storeUploadStream(\n storage: StorageAdapter,\n path: string,\n body: ReadableStream<Uint8Array> | null,\n maxBytes: number,\n): Promise<number> {\n if (!body) {\n throw new HTTPException(400, { message: \"Missing request body\" });\n }\n let seen = 0;\n const limiter = new Transform({\n transform(\n chunk: Buffer,\n _encoding: string,\n callback: (error?: Error | null, data?: Buffer) => void,\n ): void {\n seen += chunk.length;\n if (seen > maxBytes) {\n callback(new Error(`Upload exceeds ${maxBytes} byte limit`));\n } else {\n callback(null, chunk);\n }\n },\n });\n try {\n const nodeBody = Readable.fromWeb(body as unknown as NodeWebStream);\n await storage.writeStream(path, nodeBody.pipe(limiter));\n return seen;\n } catch (error) {\n if (seen > maxBytes) {\n throw new HTTPException(413, { message: \"Upload exceeds size limit\" });\n }\n throw error;\n }\n}\n\n/** Query filters accepted by the build list endpoint. */\nexport const buildListQuery = z.object({\n status: z.enum(BUILD_STATUSES).optional(),\n branch: z.string().optional(),\n labelKey: z.string().optional(),\n labelValue: z.string().optional(),\n});\n\n/** Fetch a build scoped to its project, throwing 404 when it does not belong. */\nexport async function buildForProject(projectId: string, buildId: string): Promise<Build> {\n const build = await new BuildModel(getStore().db).get(buildId);\n if (!build || build.projectId !== projectId) {\n notFound(\"Build not found\");\n }\n return build;\n}\n\n/** Fetch a snapshot scoped to its build, throwing 404 when it does not belong. */\nexport async function snapshotForBuild(\n build: { id: string },\n snapshotId: string,\n): Promise<Snapshot> {\n const snapshot = await new SnapshotModel(getStore().db).get(snapshotId);\n if (!snapshot || snapshot.buildId !== build.id) {\n notFound(\"Snapshot not found\");\n }\n return snapshot;\n}\n\n/** Roll snapshot statuses up into the build's review status. */\nfunction rollupStatus(snapshots: Snapshot[]): \"reviewing\" | \"rejected\" | \"approved\" {\n if (snapshots.some((s) => s.status === \"new\" || s.status === \"changed\")) {\n return \"reviewing\";\n }\n return snapshots.some((s) => s.status === \"rejected\") ? \"rejected\" : \"approved\";\n}\n\n/** Recompute a build's counts and roll its status up from its snapshots. */\nexport async function refreshBuild(buildId: string): Promise<void> {\n const { db } = getStore();\n await new BuildModel(db).updateCounts(buildId);\n const snapshots = await new SnapshotModel(db).listByBuild(buildId);\n const status = rollupStatus(snapshots);\n const build = await new BuildModel(db).get(buildId);\n // A cancelled build is terminal: its snapshots may still be touched, but no verdict is announced.\n if (!build || build.status === \"cancelled\") return;\n await new BuildModel(db).setStatus(buildId, status);\n await announceBuildStatus(buildId, status, { snapshotCount: snapshots.length });\n}\n\n/**\n * Tell the world a build reached `status`: its git channels, the `build:<status>` webhook and the\n * project's notification channels. Every route that moves a build to a decided status ends here.\n */\nexport async function announceBuildStatus(\n buildId: string,\n status: BuildStatus,\n extra: { snapshotCount?: number } = {},\n): Promise<void> {\n const { db, config } = getStore();\n const build = await new BuildModel(db).get(buildId);\n if (!build) return;\n await requestBuildState(buildId);\n const payload = { buildId, status, ...extra };\n await emitWebhookEvent(\n db,\n { webhooks: getStore().db.tables.webhooks },\n build.projectId,\n `build:${status}`,\n payload,\n credentialKeys(config),\n );\n const project = await new ProjectModel(db).get(build.projectId);\n if (!project) return;\n await notifyProject(project, `build:${status}`, payload, `/builds/${buildId}`);\n}\n\n/**\n * Approve a snapshot, promote its screenshot to baseline, and refresh the build.\n * Refuses with 409 when the baseline changed since the diff, unless `force` is set.\n * Bulk callers pass `deferRefresh` and call {@link refreshBuild} once, so the build's\n * outcome (and its git status) is announced a single time.\n */\nexport async function approveSnapshot(\n snapshotId: string,\n userId: string | null,\n options: { force?: boolean; deferRefresh?: boolean } = {},\n): Promise<void> {\n const { db, config } = getStore();\n const snapshots = new SnapshotModel(db);\n const snapshot = await snapshots.get(snapshotId);\n if (!snapshot) {\n notFound(\"Snapshot not found\");\n }\n const project = await new ProjectModel(db).get(snapshot.projectId);\n const build = await new BuildModel(db).get(snapshot.buildId);\n if (!project || !build) {\n notFound(\"Project or build not found\");\n }\n if (build.status === \"cancelled\") {\n throw new HTTPException(409, { message: \"The build was cancelled; retry it to review it\" });\n }\n await assertBaselineCurrent(project, build, snapshot, options.force ?? false);\n if (!db.buildReferenceLifecycle) await snapshots.review(snapshotId, \"approved\", userId);\n const baselines = new BaselineModel(db, undefined, getStore().storage, credentialKeys(config));\n const prior = await baselines.getFor(\n project.id,\n snapshot.storyId,\n snapshot.viewportName,\n build.gitBranch,\n snapshot.browser,\n );\n try {\n await baselines.upsert(\n project.id,\n snapshot.storyId,\n snapshot.viewportName,\n build.gitBranch,\n snapshot.id,\n snapshot.screenshotPath,\n undefined,\n {\n browser: snapshot.browser,\n mode: snapshot.mode,\n approval: db.buildReferenceLifecycle\n ? { snapshot, reviewedBy: userId, force: options.force ?? false, branch: build.gitBranch }\n : undefined,\n },\n );\n } catch (error) {\n if (error instanceof BaselineWriteConflict || error instanceof ReferenceGenerationConflict) {\n if (error instanceof BaselineApprovalConflict && error.driftStatus)\n throw baselineChanged(snapshot.id, error.driftStatus);\n throw new HTTPException(409, { message: error.message });\n }\n throw error;\n }\n await notifyProject(\n project,\n prior ? \"baseline:updated\" : \"baseline:created\",\n {\n storyId: snapshot.storyId,\n viewport: snapshot.viewportName,\n browser: snapshot.browser ?? \"chromium\",\n branch: build.gitBranch,\n snapshotId: snapshot.id,\n buildId: build.id,\n },\n `/builds/${build.id}`,\n );\n if (!options.deferRefresh) {\n await refreshBuild(build.id);\n }\n}\n\n/** Roles permitted to view builds, snapshots, and comments. */\nexport const VIEW_ROLES: readonly ProjectRole[] = [\"viewer\", \"developer\", \"approver\", \"admin\"];\n/** Roles permitted to upload builds and write comments. */\nexport const DEVELOPER_ROLES: readonly ProjectRole[] = [\"developer\", \"approver\", \"admin\"];\n/** Roles permitted to approve or reject snapshots and delete builds. */\nexport const APPROVER_ROLES: readonly ProjectRole[] = [\"approver\", \"admin\"];\n\n/** JSON input schema for creating a build before streaming its bundle. */\nexport const buildCreateJsonSchema = z\n .object({\n gitSha: z.string().min(1),\n gitBranch: z.string().min(1),\n authorEmail: z.string().optional(),\n authorName: z.string().optional(),\n message: z.string().optional(),\n affectedOnly: z.boolean().optional(),\n labels: z.array(z.object({ key: z.string().min(1), value: z.string() })).optional(),\n })\n .openapi(\"BuildCreate\");\n\n/** Metadata accepted by either build-creation route. */\nexport interface BuildCreateMetadata {\n gitSha: string;\n gitBranch: string;\n authorEmail?: string;\n authorName?: string;\n message?: string;\n affectedOnly?: boolean;\n labels?: { key: string; value: string }[];\n}\n\n/** Latest prior revision on a branch with a build, or null for first builds. */\nasync function resolveBaselineSha(\n db: DatabaseAdapter,\n projectId: string,\n branch: string,\n sha: string,\n): Promise<string | null> {\n const builds = await new BuildModel(db).list(projectId, { branch });\n return builds.find((build) => build.gitSha !== sha)?.gitSha ?? null;\n}\n\n/* oxlint-disable eslint/no-await-in-loop -- label attach is intentionally sequential */\nasync function attachLabels(\n db: DatabaseAdapter,\n projectId: string,\n buildId: string,\n labels: { key: string; value: string }[],\n): Promise<void> {\n const models = new LabelModel(db);\n for (const { key, value } of labels) {\n const existing = await models.getType(projectId, key);\n if (!existing) {\n await models.createType(projectId, { key, name: key });\n }\n await models.attach(projectId, buildId, key, value);\n }\n}\n/* oxlint-enable eslint/no-await-in-loop */\n"],"mappings":"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"}
@@ -19,7 +19,7 @@ import { DEFAULT_MAX_UPLOAD_BYTES } from "@storyshelf/core/config";
19
19
  import { storybookZipPath } from "@storyshelf/core/utils";
20
20
  import { BuildModel, PackageUsageModel } from "@storyshelf/core/models";
21
21
  import { HTTPException } from "hono/http-exception";
22
- //#region src/routers/builds.ts
22
+ // src/routers/builds.ts
23
23
  /** Decide how the stored zip is published; a zip this host cannot publish is removed and refused. */
24
24
  async function planOrDiscard(zipBytes, projectId, buildId) {
25
25
  try {
@@ -97,8 +97,9 @@ function registerBuilds(app) {
97
97
  const maxBytes = getStore().config.maxUploadBytes ?? DEFAULT_MAX_UPLOAD_BYTES;
98
98
  const route = await planOrDiscard(await storeUploadStream(getStore().storage, storybookZipPath(project.id, build.id), c.req.raw.body, maxBytes), project.id, build.id);
99
99
  const reqId = c.get("requestId");
100
+ const published = await publishUpload(route, build, reqId);
100
101
  await getStore().enqueueCapture?.(build.id, reqId);
101
- return c.json(await publishUpload(route, build, reqId), 202);
102
+ return c.json(published, 202);
102
103
  });
103
104
  app.openapi(dedupRoute, async (c) => {
104
105
  const { slug, buildId } = c.req.valid("param");
@@ -246,6 +247,10 @@ const uploadZipRoute = createRoute({
246
247
  content: { "application/json": { schema: buildSchema } },
247
248
  description: "Zip stored. A zip up to maxInlineUnzipSize (default 50 MB) is extracted inline in this request; a larger one is published by a background job. With testing off, the build becomes approved when publishing finishes on the project's default branch (failed if it cannot be published) and stays pending on other branches; with testing on, capture sets the status."
248
249
  },
250
+ 422: {
251
+ content: { "text/plain": { schema: z.string() } },
252
+ description: "The inline zip is invalid, unsafe, or exceeds the extraction limits"
253
+ },
249
254
  413: {
250
255
  content: { "application/json": { schema: z.object({ message: z.string() }).openapi("Error") } },
251
256
  description: "Upload exceeds the configured size limit, or the zip is over the inline limit and this host (serverless) runs no publish consumer: upload file by file instead (POST /dedup, PUT /content, POST /manifest)"
@@ -429,7 +434,6 @@ const deleteBuildRoute = createRoute({
429
434
  ...notFound
430
435
  }
431
436
  });
432
- //#endregion
433
437
  export { registerBuilds };
434
438
 
435
439
  //# sourceMappingURL=builds.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"builds.mjs","names":["notFoundResponse","forbiddenResponse"],"sources":["../../src/routers/builds.ts"],"sourcesContent":["import { createRoute, z } from \"@hono/zod-openapi\";\nimport { DEFAULT_MAX_UPLOAD_BYTES } from \"@storyshelf/core/config\";\nimport { BuildModel, PackageUsageModel } from \"@storyshelf/core/models\";\nimport { storybookZipPath } from \"@storyshelf/core/utils\";\nimport { HTTPException } from \"hono/http-exception\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { getStore } from \"../store.ts\";\nimport { registerAttempts } from \"./attempts.ts\";\nimport {\n VIEW_ROLES,\n DEVELOPER_ROLES,\n APPROVER_ROLES,\n buildCreateJsonSchema,\n buildListQuery,\n buildForProject,\n createBuildRecord,\n storeUploadStream,\n} from \"./builds.handlers.ts\";\nimport { discardZip, planUpload, publishUpload } from \"./publish-upload.ts\";\n\n/** Decide how the stored zip is published; a zip this host cannot publish is removed and refused. */\nasync function planOrDiscard(\n zipBytes: number,\n projectId: string,\n buildId: string,\n): Promise<\"inline\" | \"job\"> {\n try {\n return planUpload(zipBytes);\n } catch (error) {\n await discardZip(projectId, buildId);\n throw error;\n }\n}\n\nasync function upsertContentRef(hash: string): Promise<void> {\n try {\n const contentRefs = getStore().db.tables.contentRefs;\n const now = new Date().toISOString();\n const existing = await getStore().db.get(contentRefs, hash);\n if (existing) {\n await getStore().db.update(contentRefs, hash, {\n refCount: (existing as { refCount: number }).refCount + 1,\n lastSeenAt: now,\n });\n } else {\n await getStore().db.insert(contentRefs, {\n hash,\n refCount: 1,\n lastSeenAt: now,\n createdAt: now,\n });\n }\n } catch {\n // ignore — content_refs may not exist on old DB\n }\n}\nimport { registerBuildCancel } from \"./build-cancel.ts\";\nimport { registerBuildChangeset } from \"./build-changeset.ts\";\nimport { registerBuildReviews } from \"./build-reviews.ts\";\nimport { registerComments } from \"./comments.ts\";\nimport { flashHx } from \"./flash.ts\";\nimport { resolveAuthorizedProject } from \"./helpers.ts\";\nimport { hxRefresh } from \"./htmx.ts\";\nimport {\n badRequest,\n buildSchema,\n forbidden as forbiddenResponse,\n notFound as notFoundResponse,\n unauthorized,\n} from \"./schemas.ts\";\nimport { registerSnapshots } from \"./snapshots.ts\";\n/** Register the build list, upload, fetch, retry, and delete endpoints. */\nexport function registerBuilds(app: ShelfRouter): void {\n app.openapi(listBuildsRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...VIEW_ROLES);\n const { status, branch, labelKey, labelValue } = c.req.valid(\"query\");\n const builds = new BuildModel(getStore().db).list(project.id, {\n status,\n branch: branch ?? undefined,\n labelKey: labelKey ?? undefined,\n labelValue: labelValue ?? undefined,\n });\n return c.json(await builds);\n });\n\n app.openapi(createBuildRoute, async (c) => {\n const project = await resolveAuthorizedProject(\n c,\n c.req.valid(\"param\").slug,\n ...DEVELOPER_ROLES,\n );\n const build = await createBuildRecord(project, c.req.valid(\"json\"));\n const uploadUrl = `/api/v1/projects/${project.slug}/builds/${build.id}/zip`;\n return c.json(\n { build, uploadUrl, baselineSha: build.baselineSha, testing: getStore().testing },\n 202,\n );\n });\n\n app.openapi(affectedRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...DEVELOPER_ROLES);\n const build = await buildForProject(project.id, buildId);\n const { baselineSha, changedFiles, affectedImportPaths } = c.req.valid(\"json\");\n const updated = await new BuildModel(getStore().db).setAffected(build.id, {\n baselineSha,\n changedFiles,\n affectedImportPaths,\n });\n return c.json(updated);\n });\n\n app.openapi(usageRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...DEVELOPER_ROLES);\n const build = await buildForProject(project.id, buildId);\n const { usage } = c.req.valid(\"json\");\n const stored = await new PackageUsageModel(getStore().db).replaceForBuild(\n project.id,\n build.id,\n usage,\n );\n return c.json({ stored });\n });\n\n app.openapi(uploadZipRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...DEVELOPER_ROLES);\n const build = await buildForProject(project.id, buildId);\n const maxBytes = getStore().config.maxUploadBytes ?? DEFAULT_MAX_UPLOAD_BYTES;\n const zipBytes = await storeUploadStream(\n getStore().storage,\n storybookZipPath(project.id, build.id),\n c.req.raw.body,\n maxBytes,\n );\n const route = await planOrDiscard(zipBytes, project.id, build.id);\n const reqId = c.get(\"requestId\");\n await getStore().enqueueCapture?.(build.id, reqId);\n return c.json(await publishUpload(route, build, reqId), 202);\n });\n\n app.openapi(dedupRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...DEVELOPER_ROLES);\n await buildForProject(project.id, buildId);\n const { hashes } = c.req.valid(\"json\");\n const needed: string[] = [];\n for (const hash of hashes) {\n if (!(await getStore().storage.exists(`content/${hash}`))) {\n needed.push(hash);\n }\n }\n return c.json({ needed });\n });\n\n // oxlint-disable eslint(max-depth) -- dedup upload handler is intentionally nested for batch handling\n app.openapi(contentUploadRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...DEVELOPER_ROLES);\n await buildForProject(project.id, buildId);\n const contentType = c.req.header(\"content-type\") ?? \"\";\n if (contentType.includes(\"multipart/\")) {\n const form = await c.req.formData();\n for (const [key, value] of form.entries()) {\n if (value instanceof File) {\n const hash = key;\n const buffer = Buffer.from(await value.arrayBuffer());\n await getStore().storage.write(`content/${hash}`, buffer);\n await upsertContentRef(hash);\n }\n }\n } else {\n // Single content upload via PUT with hash header\n const hash = c.req.header(\"x-content-hash\") ?? \"\";\n if (!hash) throw new HTTPException(400, { message: \"Missing X-Content-Hash\" });\n const buffer = Buffer.from(await c.req.arrayBuffer());\n await getStore().storage.write(`content/${hash}`, buffer);\n }\n return c.json({ ok: true });\n });\n\n app.openapi(manifestRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...DEVELOPER_ROLES);\n const build = await buildForProject(project.id, buildId);\n const { files } = c.req.valid(\"json\");\n const manifest: Record<string, string> = {};\n for (const f of files) {\n manifest[f.rel] = f.hash;\n }\n await getStore().storage.write(\n `${project.id}/builds/${build.id}/storybook/manifest.json`,\n Buffer.from(JSON.stringify(manifest)),\n );\n // Upsert content_refs for each hash\n for (const f of files) {\n try {\n const contentRefs = getStore().db.tables.contentRefs;\n const now = new Date().toISOString();\n const existing = await getStore().db.get(contentRefs, f.hash);\n // oxlint-disable-next-line unicorn/no-if-else -- upsert is clearer as if/else\n if (!existing) {\n await getStore().db.insert(contentRefs, {\n hash: f.hash,\n refCount: 1,\n lastSeenAt: now,\n createdAt: now,\n });\n } else {\n await getStore().db.update(contentRefs, f.hash, {\n lastSeenAt: now,\n });\n }\n } catch {\n // ignore — content_refs may not exist\n }\n }\n // The files are stored content-addressed: nothing to extract, so publishing is complete.\n await getStore().publish?.completer.complete(build.id, { ok: true });\n const reqId = c.get(\"requestId\");\n await getStore().enqueueCapture?.(build.id, reqId);\n return c.json({ ok: true });\n });\n\n app.openapi(getBuildRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...VIEW_ROLES);\n const build = await buildForProject(project.id, buildId);\n return c.json(build);\n });\n\n app.openapi(retryBuildRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...DEVELOPER_ROLES);\n const build = await buildForProject(project.id, buildId);\n const updated = await new BuildModel(getStore().db).resetStatus(build.id, \"pending\");\n await getStore().enqueueCapture?.(build.id, c.get(\"requestId\"));\n hxRefresh(c);\n flashHx(c, \"Capture queued\");\n return c.json(updated, 202);\n });\n\n app.openapi(deleteBuildRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...APPROVER_ROLES);\n const build = await buildForProject(project.id, buildId);\n await new BuildModel(getStore().db).remove(build.id);\n return c.body(null, 204);\n });\n\n registerSnapshots(app);\n registerComments(app);\n registerBuildReviews(app);\n registerBuildChangeset(app);\n registerBuildCancel(app);\n registerAttempts(app);\n}\n\nconst listBuildsRoute = createRoute({\n method: \"get\",\n tags: [\"Builds\"],\n summary: \"List builds\",\n path: \"/api/v1/projects/{slug}/builds\",\n request: { params: z.object({ slug: z.string() }), query: buildListQuery },\n responses: {\n 200: {\n content: { \"application/json\": { schema: buildSchema.array() } },\n description: \"List builds for a project\",\n },\n ...notFoundResponse,\n ...unauthorized,\n },\n});\n\nconst buildCreatedSchema = z\n .object({\n build: buildSchema,\n uploadUrl: z.string(),\n baselineSha: z.string().nullable(),\n testing: z.boolean().openapi({\n description:\n \"False when the server has no `testing` option: the build is published but never captured or compared.\",\n }),\n })\n .openapi(\"BuildCreated\");\n\nconst createBuildRoute = createRoute({\n method: \"post\",\n tags: [\"Builds\"],\n summary: \"Create a build\",\n path: \"/api/v1/projects/{slug}/builds\",\n request: {\n params: z.object({ slug: z.string() }),\n body: {\n content: {\n \"application/json\": { schema: buildCreateJsonSchema },\n },\n },\n },\n responses: {\n 202: {\n content: {\n \"application/json\": {\n schema: buildCreatedSchema,\n },\n },\n description:\n \"Build created. PUT the Storybook zip to uploadUrl to publish it (and queue capture when testing is on).\",\n },\n ...badRequest,\n ...forbiddenResponse,\n ...notFoundResponse,\n },\n});\n\nconst uploadZipRoute = createRoute({\n method: \"put\",\n tags: [\"Builds\"],\n summary: \"Upload the Storybook zip\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/zip\",\n request: {\n params: z.object({ slug: z.string(), buildId: z.string() }),\n body: {\n content: {\n \"application/zip\": {\n schema: z.string().openapi({ type: \"string\", format: \"binary\" }),\n },\n },\n },\n },\n responses: {\n 202: {\n content: { \"application/json\": { schema: buildSchema } },\n description:\n \"Zip stored. A zip up to maxInlineUnzipSize (default 50 MB) is extracted inline in this request; a larger one is published by a background job. With testing off, the build becomes approved when publishing finishes on the project's default branch (failed if it cannot be published) and stays pending on other branches; with testing on, capture sets the status.\",\n },\n 413: {\n content: {\n \"application/json\": {\n schema: z.object({ message: z.string() }).openapi(\"Error\"),\n },\n },\n description:\n \"Upload exceeds the configured size limit, or the zip is over the inline limit and this host (serverless) runs no publish consumer: upload file by file instead (POST /dedup, PUT /content, POST /manifest)\",\n },\n ...badRequest,\n ...forbiddenResponse,\n ...notFoundResponse,\n },\n});\n\nconst dedupHashesSchema = z.object({ hashes: z.array(z.string()) });\nconst dedupNeededSchema = z.object({ needed: z.array(z.string()) });\nconst dedupRoute = createRoute({\n method: \"post\",\n tags: [\"Builds\"],\n summary: \"Check which files need uploading\",\n description:\n \"File-by-file upload, step 1 (dedup, content, manifest): needs no zip extraction and works on any host, serverless included. Send the content hashes; the response lists those the server does not hold yet.\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/dedup\",\n request: {\n params: z.object({ slug: z.string(), buildId: z.string() }),\n body: { content: { \"application/json\": { schema: dedupHashesSchema } } },\n },\n responses: {\n 200: {\n content: { \"application/json\": { schema: dedupNeededSchema } },\n description: \"Hashes needed for upload\",\n },\n ...notFoundResponse,\n },\n});\n\nconst contentUploadRoute = createRoute({\n method: \"post\",\n tags: [\"Builds\"],\n summary: \"Upload build content\",\n description:\n \"File-by-file upload, step 2: store the missing files content-addressed (multipart, or one file with an X-Content-Hash header).\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/content\",\n request: {\n params: z.object({ slug: z.string(), buildId: z.string() }),\n body: { content: { \"multipart/form-data\": { schema: z.any() } } },\n },\n responses: {\n 200: {\n content: { \"application/json\": { schema: z.object({ ok: z.boolean() }) } },\n description: \"Content uploaded\",\n },\n ...notFoundResponse,\n },\n});\n\nconst manifestFileSchema = z.object({ rel: z.string(), hash: z.string(), size: z.number() });\nconst manifestRoute = createRoute({\n method: \"post\",\n tags: [\"Builds\"],\n summary: \"Store the build manifest\",\n description:\n \"File-by-file upload, step 3: store the path-to-hash manifest. This completes publishing. With testing off the build becomes approved when it is on the project's default branch and stays pending on other branches; with testing on, a capture is queued and sets the status.\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/manifest\",\n request: {\n params: z.object({ slug: z.string(), buildId: z.string() }),\n body: {\n content: {\n \"application/json\": {\n schema: z.object({ files: z.array(manifestFileSchema) }),\n },\n },\n },\n },\n responses: {\n 200: {\n content: { \"application/json\": { schema: z.object({ ok: z.boolean() }) } },\n description: \"Manifest stored\",\n },\n ...notFoundResponse,\n },\n});\n\nconst affectedPayloadSchema = z.object({\n baselineSha: z.string().nullable(),\n changedFiles: z.array(z.string()).max(20_000),\n affectedImportPaths: z.array(z.string()).max(20_000).nullable(),\n});\nconst affectedRoute = createRoute({\n method: \"post\",\n tags: [\"Builds\"],\n summary: \"Record the affected stories\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/affected\",\n request: {\n params: z.object({ slug: z.string(), buildId: z.string() }),\n body: { content: { \"application/json\": { schema: affectedPayloadSchema } } },\n },\n responses: {\n 200: {\n content: { \"application/json\": { schema: buildSchema } },\n description: \"Affected set recorded\",\n },\n ...badRequest,\n ...forbiddenResponse,\n ...notFoundResponse,\n },\n});\n\nconst usagePayloadSchema = z.object({\n usage: z\n .array(\n z.object({\n storyImportPath: z.string().min(1).max(1024),\n packageName: z.string().min(1).max(214),\n modulePath: z.string().min(1).max(2048),\n version: z.string().max(128).nullable().optional(),\n }),\n )\n .max(20_000),\n});\nconst usageRoute = createRoute({\n method: \"post\",\n tags: [\"Builds\"],\n summary: \"Record the dependency packages each story uses\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/usage\",\n request: {\n params: z.object({ slug: z.string(), buildId: z.string() }),\n body: { content: { \"application/json\": { schema: usagePayloadSchema } } },\n },\n responses: {\n 200: {\n content: { \"application/json\": { schema: z.object({ stored: z.number() }) } },\n description: \"Usage recorded (replaces any earlier report for the build)\",\n },\n ...badRequest,\n ...forbiddenResponse,\n ...notFoundResponse,\n },\n});\n\nconst getBuildRoute = createRoute({\n method: \"get\",\n tags: [\"Builds\"],\n summary: \"Get a build\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}\",\n request: { params: z.object({ slug: z.string(), buildId: z.string() }) },\n responses: {\n 200: { content: { \"application/json\": { schema: buildSchema } }, description: \"Fetch a build\" },\n ...notFoundResponse,\n ...unauthorized,\n },\n});\n\nconst retryBuildRoute = createRoute({\n method: \"post\",\n tags: [\"Builds\"],\n summary: \"Retry capture\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/retry\",\n request: { params: z.object({ slug: z.string(), buildId: z.string() }) },\n responses: {\n 202: {\n content: { \"application/json\": { schema: buildSchema } },\n description: \"Build reset to pending and capture re-queued (new attempt)\",\n },\n ...notFoundResponse,\n },\n});\n\nconst deleteBuildRoute = createRoute({\n method: \"delete\",\n tags: [\"Builds\"],\n summary: \"Delete a build\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}\",\n request: { params: z.object({ slug: z.string(), buildId: z.string() }) },\n responses: {\n 204: { description: \"Build deleted\" },\n ...notFoundResponse,\n },\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"builds.mjs","names":["notFoundResponse","forbiddenResponse"],"sources":["../../src/routers/builds.ts"],"sourcesContent":["import { createRoute, z } from \"@hono/zod-openapi\";\nimport { DEFAULT_MAX_UPLOAD_BYTES } from \"@storyshelf/core/config\";\nimport { BuildModel, PackageUsageModel } from \"@storyshelf/core/models\";\nimport { storybookZipPath } from \"@storyshelf/core/utils\";\nimport { HTTPException } from \"hono/http-exception\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { getStore } from \"../store.ts\";\nimport { registerAttempts } from \"./attempts.ts\";\nimport {\n VIEW_ROLES,\n DEVELOPER_ROLES,\n APPROVER_ROLES,\n buildCreateJsonSchema,\n buildListQuery,\n buildForProject,\n createBuildRecord,\n storeUploadStream,\n} from \"./builds.handlers.ts\";\nimport { discardZip, planUpload, publishUpload } from \"./publish-upload.ts\";\n\n/** Decide how the stored zip is published; a zip this host cannot publish is removed and refused. */\nasync function planOrDiscard(\n zipBytes: number,\n projectId: string,\n buildId: string,\n): Promise<\"inline\" | \"job\"> {\n try {\n return planUpload(zipBytes);\n } catch (error) {\n await discardZip(projectId, buildId);\n throw error;\n }\n}\n\nasync function upsertContentRef(hash: string): Promise<void> {\n try {\n const contentRefs = getStore().db.tables.contentRefs;\n const now = new Date().toISOString();\n const existing = await getStore().db.get(contentRefs, hash);\n if (existing) {\n await getStore().db.update(contentRefs, hash, {\n refCount: (existing as { refCount: number }).refCount + 1,\n lastSeenAt: now,\n });\n } else {\n await getStore().db.insert(contentRefs, {\n hash,\n refCount: 1,\n lastSeenAt: now,\n createdAt: now,\n });\n }\n } catch {\n // ignore — content_refs may not exist on old DB\n }\n}\nimport { registerBuildCancel } from \"./build-cancel.ts\";\nimport { registerBuildChangeset } from \"./build-changeset.ts\";\nimport { registerBuildReviews } from \"./build-reviews.ts\";\nimport { registerComments } from \"./comments.ts\";\nimport { flashHx } from \"./flash.ts\";\nimport { resolveAuthorizedProject } from \"./helpers.ts\";\nimport { hxRefresh } from \"./htmx.ts\";\nimport {\n badRequest,\n buildSchema,\n forbidden as forbiddenResponse,\n notFound as notFoundResponse,\n unauthorized,\n} from \"./schemas.ts\";\nimport { registerSnapshots } from \"./snapshots.ts\";\n/** Register the build list, upload, fetch, retry, and delete endpoints. */\nexport function registerBuilds(app: ShelfRouter): void {\n app.openapi(listBuildsRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...VIEW_ROLES);\n const { status, branch, labelKey, labelValue } = c.req.valid(\"query\");\n const builds = new BuildModel(getStore().db).list(project.id, {\n status,\n branch: branch ?? undefined,\n labelKey: labelKey ?? undefined,\n labelValue: labelValue ?? undefined,\n });\n return c.json(await builds);\n });\n\n app.openapi(createBuildRoute, async (c) => {\n const project = await resolveAuthorizedProject(\n c,\n c.req.valid(\"param\").slug,\n ...DEVELOPER_ROLES,\n );\n const build = await createBuildRecord(project, c.req.valid(\"json\"));\n const uploadUrl = `/api/v1/projects/${project.slug}/builds/${build.id}/zip`;\n return c.json(\n { build, uploadUrl, baselineSha: build.baselineSha, testing: getStore().testing },\n 202,\n );\n });\n\n app.openapi(affectedRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...DEVELOPER_ROLES);\n const build = await buildForProject(project.id, buildId);\n const { baselineSha, changedFiles, affectedImportPaths } = c.req.valid(\"json\");\n const updated = await new BuildModel(getStore().db).setAffected(build.id, {\n baselineSha,\n changedFiles,\n affectedImportPaths,\n });\n return c.json(updated);\n });\n\n app.openapi(usageRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...DEVELOPER_ROLES);\n const build = await buildForProject(project.id, buildId);\n const { usage } = c.req.valid(\"json\");\n const stored = await new PackageUsageModel(getStore().db).replaceForBuild(\n project.id,\n build.id,\n usage,\n );\n return c.json({ stored });\n });\n\n app.openapi(uploadZipRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...DEVELOPER_ROLES);\n const build = await buildForProject(project.id, buildId);\n const maxBytes = getStore().config.maxUploadBytes ?? DEFAULT_MAX_UPLOAD_BYTES;\n const zipBytes = await storeUploadStream(\n getStore().storage,\n storybookZipPath(project.id, build.id),\n c.req.raw.body,\n maxBytes,\n );\n const route = await planOrDiscard(zipBytes, project.id, build.id);\n const reqId = c.get(\"requestId\");\n const published = await publishUpload(route, build, reqId);\n await getStore().enqueueCapture?.(build.id, reqId);\n return c.json(published, 202);\n });\n\n app.openapi(dedupRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...DEVELOPER_ROLES);\n await buildForProject(project.id, buildId);\n const { hashes } = c.req.valid(\"json\");\n const needed: string[] = [];\n for (const hash of hashes) {\n if (!(await getStore().storage.exists(`content/${hash}`))) {\n needed.push(hash);\n }\n }\n return c.json({ needed });\n });\n\n // oxlint-disable eslint(max-depth) -- dedup upload handler is intentionally nested for batch handling\n app.openapi(contentUploadRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...DEVELOPER_ROLES);\n await buildForProject(project.id, buildId);\n const contentType = c.req.header(\"content-type\") ?? \"\";\n if (contentType.includes(\"multipart/\")) {\n const form = await c.req.formData();\n for (const [key, value] of form.entries()) {\n if (value instanceof File) {\n const hash = key;\n const buffer = Buffer.from(await value.arrayBuffer());\n await getStore().storage.write(`content/${hash}`, buffer);\n await upsertContentRef(hash);\n }\n }\n } else {\n // Single content upload via PUT with hash header\n const hash = c.req.header(\"x-content-hash\") ?? \"\";\n if (!hash) throw new HTTPException(400, { message: \"Missing X-Content-Hash\" });\n const buffer = Buffer.from(await c.req.arrayBuffer());\n await getStore().storage.write(`content/${hash}`, buffer);\n }\n return c.json({ ok: true });\n });\n\n app.openapi(manifestRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...DEVELOPER_ROLES);\n const build = await buildForProject(project.id, buildId);\n const { files } = c.req.valid(\"json\");\n const manifest: Record<string, string> = {};\n for (const f of files) {\n manifest[f.rel] = f.hash;\n }\n await getStore().storage.write(\n `${project.id}/builds/${build.id}/storybook/manifest.json`,\n Buffer.from(JSON.stringify(manifest)),\n );\n // Upsert content_refs for each hash\n for (const f of files) {\n try {\n const contentRefs = getStore().db.tables.contentRefs;\n const now = new Date().toISOString();\n const existing = await getStore().db.get(contentRefs, f.hash);\n // oxlint-disable-next-line unicorn/no-if-else -- upsert is clearer as if/else\n if (!existing) {\n await getStore().db.insert(contentRefs, {\n hash: f.hash,\n refCount: 1,\n lastSeenAt: now,\n createdAt: now,\n });\n } else {\n await getStore().db.update(contentRefs, f.hash, {\n lastSeenAt: now,\n });\n }\n } catch {\n // ignore — content_refs may not exist\n }\n }\n // The files are stored content-addressed: nothing to extract, so publishing is complete.\n await getStore().publish?.completer.complete(build.id, { ok: true });\n const reqId = c.get(\"requestId\");\n await getStore().enqueueCapture?.(build.id, reqId);\n return c.json({ ok: true });\n });\n\n app.openapi(getBuildRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...VIEW_ROLES);\n const build = await buildForProject(project.id, buildId);\n return c.json(build);\n });\n\n app.openapi(retryBuildRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...DEVELOPER_ROLES);\n const build = await buildForProject(project.id, buildId);\n const updated = await new BuildModel(getStore().db).resetStatus(build.id, \"pending\");\n await getStore().enqueueCapture?.(build.id, c.get(\"requestId\"));\n hxRefresh(c);\n flashHx(c, \"Capture queued\");\n return c.json(updated, 202);\n });\n\n app.openapi(deleteBuildRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...APPROVER_ROLES);\n const build = await buildForProject(project.id, buildId);\n await new BuildModel(getStore().db).remove(build.id);\n return c.body(null, 204);\n });\n\n registerSnapshots(app);\n registerComments(app);\n registerBuildReviews(app);\n registerBuildChangeset(app);\n registerBuildCancel(app);\n registerAttempts(app);\n}\n\nconst listBuildsRoute = createRoute({\n method: \"get\",\n tags: [\"Builds\"],\n summary: \"List builds\",\n path: \"/api/v1/projects/{slug}/builds\",\n request: { params: z.object({ slug: z.string() }), query: buildListQuery },\n responses: {\n 200: {\n content: { \"application/json\": { schema: buildSchema.array() } },\n description: \"List builds for a project\",\n },\n ...notFoundResponse,\n ...unauthorized,\n },\n});\n\nconst buildCreatedSchema = z\n .object({\n build: buildSchema,\n uploadUrl: z.string(),\n baselineSha: z.string().nullable(),\n testing: z.boolean().openapi({\n description:\n \"False when the server has no `testing` option: the build is published but never captured or compared.\",\n }),\n })\n .openapi(\"BuildCreated\");\n\nconst createBuildRoute = createRoute({\n method: \"post\",\n tags: [\"Builds\"],\n summary: \"Create a build\",\n path: \"/api/v1/projects/{slug}/builds\",\n request: {\n params: z.object({ slug: z.string() }),\n body: {\n content: {\n \"application/json\": { schema: buildCreateJsonSchema },\n },\n },\n },\n responses: {\n 202: {\n content: {\n \"application/json\": {\n schema: buildCreatedSchema,\n },\n },\n description:\n \"Build created. PUT the Storybook zip to uploadUrl to publish it (and queue capture when testing is on).\",\n },\n ...badRequest,\n ...forbiddenResponse,\n ...notFoundResponse,\n },\n});\n\nconst uploadZipRoute = createRoute({\n method: \"put\",\n tags: [\"Builds\"],\n summary: \"Upload the Storybook zip\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/zip\",\n request: {\n params: z.object({ slug: z.string(), buildId: z.string() }),\n body: {\n content: {\n \"application/zip\": {\n schema: z.string().openapi({ type: \"string\", format: \"binary\" }),\n },\n },\n },\n },\n responses: {\n 202: {\n content: { \"application/json\": { schema: buildSchema } },\n description:\n \"Zip stored. A zip up to maxInlineUnzipSize (default 50 MB) is extracted inline in this request; a larger one is published by a background job. With testing off, the build becomes approved when publishing finishes on the project's default branch (failed if it cannot be published) and stays pending on other branches; with testing on, capture sets the status.\",\n },\n 422: {\n content: { \"text/plain\": { schema: z.string() } },\n description: \"The inline zip is invalid, unsafe, or exceeds the extraction limits\",\n },\n 413: {\n content: {\n \"application/json\": {\n schema: z.object({ message: z.string() }).openapi(\"Error\"),\n },\n },\n description:\n \"Upload exceeds the configured size limit, or the zip is over the inline limit and this host (serverless) runs no publish consumer: upload file by file instead (POST /dedup, PUT /content, POST /manifest)\",\n },\n ...badRequest,\n ...forbiddenResponse,\n ...notFoundResponse,\n },\n});\n\nconst dedupHashesSchema = z.object({ hashes: z.array(z.string()) });\nconst dedupNeededSchema = z.object({ needed: z.array(z.string()) });\nconst dedupRoute = createRoute({\n method: \"post\",\n tags: [\"Builds\"],\n summary: \"Check which files need uploading\",\n description:\n \"File-by-file upload, step 1 (dedup, content, manifest): needs no zip extraction and works on any host, serverless included. Send the content hashes; the response lists those the server does not hold yet.\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/dedup\",\n request: {\n params: z.object({ slug: z.string(), buildId: z.string() }),\n body: { content: { \"application/json\": { schema: dedupHashesSchema } } },\n },\n responses: {\n 200: {\n content: { \"application/json\": { schema: dedupNeededSchema } },\n description: \"Hashes needed for upload\",\n },\n ...notFoundResponse,\n },\n});\n\nconst contentUploadRoute = createRoute({\n method: \"post\",\n tags: [\"Builds\"],\n summary: \"Upload build content\",\n description:\n \"File-by-file upload, step 2: store the missing files content-addressed (multipart, or one file with an X-Content-Hash header).\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/content\",\n request: {\n params: z.object({ slug: z.string(), buildId: z.string() }),\n body: { content: { \"multipart/form-data\": { schema: z.any() } } },\n },\n responses: {\n 200: {\n content: { \"application/json\": { schema: z.object({ ok: z.boolean() }) } },\n description: \"Content uploaded\",\n },\n ...notFoundResponse,\n },\n});\n\nconst manifestFileSchema = z.object({ rel: z.string(), hash: z.string(), size: z.number() });\nconst manifestRoute = createRoute({\n method: \"post\",\n tags: [\"Builds\"],\n summary: \"Store the build manifest\",\n description:\n \"File-by-file upload, step 3: store the path-to-hash manifest. This completes publishing. With testing off the build becomes approved when it is on the project's default branch and stays pending on other branches; with testing on, a capture is queued and sets the status.\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/manifest\",\n request: {\n params: z.object({ slug: z.string(), buildId: z.string() }),\n body: {\n content: {\n \"application/json\": {\n schema: z.object({ files: z.array(manifestFileSchema) }),\n },\n },\n },\n },\n responses: {\n 200: {\n content: { \"application/json\": { schema: z.object({ ok: z.boolean() }) } },\n description: \"Manifest stored\",\n },\n ...notFoundResponse,\n },\n});\n\nconst affectedPayloadSchema = z.object({\n baselineSha: z.string().nullable(),\n changedFiles: z.array(z.string()).max(20_000),\n affectedImportPaths: z.array(z.string()).max(20_000).nullable(),\n});\nconst affectedRoute = createRoute({\n method: \"post\",\n tags: [\"Builds\"],\n summary: \"Record the affected stories\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/affected\",\n request: {\n params: z.object({ slug: z.string(), buildId: z.string() }),\n body: { content: { \"application/json\": { schema: affectedPayloadSchema } } },\n },\n responses: {\n 200: {\n content: { \"application/json\": { schema: buildSchema } },\n description: \"Affected set recorded\",\n },\n ...badRequest,\n ...forbiddenResponse,\n ...notFoundResponse,\n },\n});\n\nconst usagePayloadSchema = z.object({\n usage: z\n .array(\n z.object({\n storyImportPath: z.string().min(1).max(1024),\n packageName: z.string().min(1).max(214),\n modulePath: z.string().min(1).max(2048),\n version: z.string().max(128).nullable().optional(),\n }),\n )\n .max(20_000),\n});\nconst usageRoute = createRoute({\n method: \"post\",\n tags: [\"Builds\"],\n summary: \"Record the dependency packages each story uses\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/usage\",\n request: {\n params: z.object({ slug: z.string(), buildId: z.string() }),\n body: { content: { \"application/json\": { schema: usagePayloadSchema } } },\n },\n responses: {\n 200: {\n content: { \"application/json\": { schema: z.object({ stored: z.number() }) } },\n description: \"Usage recorded (replaces any earlier report for the build)\",\n },\n ...badRequest,\n ...forbiddenResponse,\n ...notFoundResponse,\n },\n});\n\nconst getBuildRoute = createRoute({\n method: \"get\",\n tags: [\"Builds\"],\n summary: \"Get a build\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}\",\n request: { params: z.object({ slug: z.string(), buildId: z.string() }) },\n responses: {\n 200: { content: { \"application/json\": { schema: buildSchema } }, description: \"Fetch a build\" },\n ...notFoundResponse,\n ...unauthorized,\n },\n});\n\nconst retryBuildRoute = createRoute({\n method: \"post\",\n tags: [\"Builds\"],\n summary: \"Retry capture\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/retry\",\n request: { params: z.object({ slug: z.string(), buildId: z.string() }) },\n responses: {\n 202: {\n content: { \"application/json\": { schema: buildSchema } },\n description: \"Build reset to pending and capture re-queued (new attempt)\",\n },\n ...notFoundResponse,\n },\n});\n\nconst deleteBuildRoute = createRoute({\n method: \"delete\",\n tags: [\"Builds\"],\n summary: \"Delete a build\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}\",\n request: { params: z.object({ slug: z.string(), buildId: z.string() }) },\n responses: {\n 204: { description: \"Build deleted\" },\n ...notFoundResponse,\n },\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAqBA,eAAe,cACb,UACA,WACA,SAC2B;CAC3B,IAAI;EACF,OAAO,WAAW,QAAQ;CAC5B,SAAS,OAAO;EACd,MAAM,WAAW,WAAW,OAAO;EACnC,MAAM;CACR;AACF;AAEA,eAAe,iBAAiB,MAA6B;CAC3D,IAAI;EACF,MAAM,cAAc,SAAS,CAAC,CAAC,GAAG,OAAO;EACzC,MAAM,uBAAM,IAAI,KAAK,EAAA,CAAE,YAAY;EACnC,MAAM,WAAW,MAAM,SAAS,CAAC,CAAC,GAAG,IAAI,aAAa,IAAI;EAC1D,IAAI,UACF,MAAM,SAAS,CAAC,CAAC,GAAG,OAAO,aAAa,MAAM;GAC5C,UAAW,SAAkC,WAAW;GACxD,YAAY;EACd,CAAC;OAED,MAAM,SAAS,CAAC,CAAC,GAAG,OAAO,aAAa;GACtC;GACA,UAAU;GACV,YAAY;GACZ,WAAW;EACb,CAAC;CAEL,QAAQ,CAER;AACF;;AAiBA,SAAgB,eAAe,KAAwB;CACrD,IAAI,QAAQ,iBAAiB,OAAO,MAAM;EACxC,MAAM,UAAU,MAAM,yBAAyB,GAAG,EAAE,IAAI,MAAM,OAAO,CAAC,CAAC,MAAM,GAAG,UAAU;EAC1F,MAAM,EAAE,QAAQ,QAAQ,UAAU,eAAe,EAAE,IAAI,MAAM,OAAO;EACpE,MAAM,SAAS,IAAI,WAAW,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,QAAQ,IAAI;GAC5D;GACA,QAAQ,UAAU,KAAA;GAClB,UAAU,YAAY,KAAA;GACtB,YAAY,cAAc,KAAA;EAC5B,CAAC;EACD,OAAO,EAAE,KAAK,MAAM,MAAM;CAC5B,CAAC;CAED,IAAI,QAAQ,kBAAkB,OAAO,MAAM;EACzC,MAAM,UAAU,MAAM,yBACpB,GACA,EAAE,IAAI,MAAM,OAAO,CAAC,CAAC,MACrB,GAAG,eACL;EACA,MAAM,QAAQ,MAAM,kBAAkB,SAAS,EAAE,IAAI,MAAM,MAAM,CAAC;EAClE,MAAM,YAAY,oBAAoB,QAAQ,KAAK,UAAU,MAAM,GAAG;EACtE,OAAO,EAAE,KACP;GAAE;GAAO;GAAW,aAAa,MAAM;GAAa,SAAS,SAAS,CAAC,CAAC;EAAQ,GAChF,GACF;CACF,CAAC;CAED,IAAI,QAAQ,eAAe,OAAO,MAAM;EACtC,MAAM,EAAE,MAAM,YAAY,EAAE,IAAI,MAAM,OAAO;EAC7C,MAAM,UAAU,MAAM,yBAAyB,GAAG,MAAM,GAAG,eAAe;EAC1E,MAAM,QAAQ,MAAM,gBAAgB,QAAQ,IAAI,OAAO;EACvD,MAAM,EAAE,aAAa,cAAc,wBAAwB,EAAE,IAAI,MAAM,MAAM;EAC7E,MAAM,UAAU,MAAM,IAAI,WAAW,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,YAAY,MAAM,IAAI;GACxE;GACA;GACA;EACF,CAAC;EACD,OAAO,EAAE,KAAK,OAAO;CACvB,CAAC;CAED,IAAI,QAAQ,YAAY,OAAO,MAAM;EACnC,MAAM,EAAE,MAAM,YAAY,EAAE,IAAI,MAAM,OAAO;EAC7C,MAAM,UAAU,MAAM,yBAAyB,GAAG,MAAM,GAAG,eAAe;EAC1E,MAAM,QAAQ,MAAM,gBAAgB,QAAQ,IAAI,OAAO;EACvD,MAAM,EAAE,UAAU,EAAE,IAAI,MAAM,MAAM;EACpC,MAAM,SAAS,MAAM,IAAI,kBAAkB,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,gBACxD,QAAQ,IACR,MAAM,IACN,KACF;EACA,OAAO,EAAE,KAAK,EAAE,OAAO,CAAC;CAC1B,CAAC;CAED,IAAI,QAAQ,gBAAgB,OAAO,MAAM;EACvC,MAAM,EAAE,MAAM,YAAY,EAAE,IAAI,MAAM,OAAO;EAC7C,MAAM,UAAU,MAAM,yBAAyB,GAAG,MAAM,GAAG,eAAe;EAC1E,MAAM,QAAQ,MAAM,gBAAgB,QAAQ,IAAI,OAAO;EACvD,MAAM,WAAW,SAAS,CAAC,CAAC,OAAO,kBAAkB;EAOrD,MAAM,QAAQ,MAAM,cAAc,MANX,kBACrB,SAAS,CAAC,CAAC,SACX,iBAAiB,QAAQ,IAAI,MAAM,EAAE,GACrC,EAAE,IAAI,IAAI,MACV,QACF,GAC4C,QAAQ,IAAI,MAAM,EAAE;EAChE,MAAM,QAAQ,EAAE,IAAI,WAAW;EAC/B,MAAM,YAAY,MAAM,cAAc,OAAO,OAAO,KAAK;EACzD,MAAM,SAAS,CAAC,CAAC,iBAAiB,MAAM,IAAI,KAAK;EACjD,OAAO,EAAE,KAAK,WAAW,GAAG;CAC9B,CAAC;CAED,IAAI,QAAQ,YAAY,OAAO,MAAM;EACnC,MAAM,EAAE,MAAM,YAAY,EAAE,IAAI,MAAM,OAAO;EAC7C,MAAM,UAAU,MAAM,yBAAyB,GAAG,MAAM,GAAG,eAAe;EAC1E,MAAM,gBAAgB,QAAQ,IAAI,OAAO;EACzC,MAAM,EAAE,WAAW,EAAE,IAAI,MAAM,MAAM;EACrC,MAAM,SAAmB,CAAC;EAC1B,KAAK,MAAM,QAAQ,QACjB,IAAI,CAAE,MAAM,SAAS,CAAC,CAAC,QAAQ,OAAO,WAAW,MAAM,GACrD,OAAO,KAAK,IAAI;EAGpB,OAAO,EAAE,KAAK,EAAE,OAAO,CAAC;CAC1B,CAAC;CAGD,IAAI,QAAQ,oBAAoB,OAAO,MAAM;EAC3C,MAAM,EAAE,MAAM,YAAY,EAAE,IAAI,MAAM,OAAO;EAC7C,MAAM,UAAU,MAAM,yBAAyB,GAAG,MAAM,GAAG,eAAe;EAC1E,MAAM,gBAAgB,QAAQ,IAAI,OAAO;EAEzC,KADoB,EAAE,IAAI,OAAO,cAAc,KAAK,GAAA,CACpC,SAAS,YAAY,GAAG;GACtC,MAAM,OAAO,MAAM,EAAE,IAAI,SAAS;GAClC,KAAK,MAAM,CAAC,KAAK,UAAU,KAAK,QAAQ,GACtC,IAAI,iBAAiB,MAAM;IACzB,MAAM,OAAO;IACb,MAAM,SAAS,OAAO,KAAK,MAAM,MAAM,YAAY,CAAC;IACpD,MAAM,SAAS,CAAC,CAAC,QAAQ,MAAM,WAAW,QAAQ,MAAM;IACxD,MAAM,iBAAiB,IAAI;GAC7B;EAEJ,OAAO;GAEL,MAAM,OAAO,EAAE,IAAI,OAAO,gBAAgB,KAAK;GAC/C,IAAI,CAAC,MAAM,MAAM,IAAI,cAAc,KAAK,EAAE,SAAS,yBAAyB,CAAC;GAC7E,MAAM,SAAS,OAAO,KAAK,MAAM,EAAE,IAAI,YAAY,CAAC;GACpD,MAAM,SAAS,CAAC,CAAC,QAAQ,MAAM,WAAW,QAAQ,MAAM;EAC1D;EACA,OAAO,EAAE,KAAK,EAAE,IAAI,KAAK,CAAC;CAC5B,CAAC;CAED,IAAI,QAAQ,eAAe,OAAO,MAAM;EACtC,MAAM,EAAE,MAAM,YAAY,EAAE,IAAI,MAAM,OAAO;EAC7C,MAAM,UAAU,MAAM,yBAAyB,GAAG,MAAM,GAAG,eAAe;EAC1E,MAAM,QAAQ,MAAM,gBAAgB,QAAQ,IAAI,OAAO;EACvD,MAAM,EAAE,UAAU,EAAE,IAAI,MAAM,MAAM;EACpC,MAAM,WAAmC,CAAC;EAC1C,KAAK,MAAM,KAAK,OACd,SAAS,EAAE,OAAO,EAAE;EAEtB,MAAM,SAAS,CAAC,CAAC,QAAQ,MACvB,GAAG,QAAQ,GAAG,UAAU,MAAM,GAAG,2BACjC,OAAO,KAAK,KAAK,UAAU,QAAQ,CAAC,CACtC;EAEA,KAAK,MAAM,KAAK,OACd,IAAI;GACF,MAAM,cAAc,SAAS,CAAC,CAAC,GAAG,OAAO;GACzC,MAAM,uBAAM,IAAI,KAAK,EAAA,CAAE,YAAY;GAGnC,IAAI,CAAC,MAFkB,SAAS,CAAC,CAAC,GAAG,IAAI,aAAa,EAAE,IAAI,GAG1D,MAAM,SAAS,CAAC,CAAC,GAAG,OAAO,aAAa;IACtC,MAAM,EAAE;IACR,UAAU;IACV,YAAY;IACZ,WAAW;GACb,CAAC;QAED,MAAM,SAAS,CAAC,CAAC,GAAG,OAAO,aAAa,EAAE,MAAM,EAC9C,YAAY,IACd,CAAC;EAEL,QAAQ,CAER;EAGF,MAAM,SAAS,CAAC,CAAC,SAAS,UAAU,SAAS,MAAM,IAAI,EAAE,IAAI,KAAK,CAAC;EACnE,MAAM,QAAQ,EAAE,IAAI,WAAW;EAC/B,MAAM,SAAS,CAAC,CAAC,iBAAiB,MAAM,IAAI,KAAK;EACjD,OAAO,EAAE,KAAK,EAAE,IAAI,KAAK,CAAC;CAC5B,CAAC;CAED,IAAI,QAAQ,eAAe,OAAO,MAAM;EACtC,MAAM,EAAE,MAAM,YAAY,EAAE,IAAI,MAAM,OAAO;EAC7C,MAAM,UAAU,MAAM,yBAAyB,GAAG,MAAM,GAAG,UAAU;EACrE,MAAM,QAAQ,MAAM,gBAAgB,QAAQ,IAAI,OAAO;EACvD,OAAO,EAAE,KAAK,KAAK;CACrB,CAAC;CAED,IAAI,QAAQ,iBAAiB,OAAO,MAAM;EACxC,MAAM,EAAE,MAAM,YAAY,EAAE,IAAI,MAAM,OAAO;EAC7C,MAAM,UAAU,MAAM,yBAAyB,GAAG,MAAM,GAAG,eAAe;EAC1E,MAAM,QAAQ,MAAM,gBAAgB,QAAQ,IAAI,OAAO;EACvD,MAAM,UAAU,MAAM,IAAI,WAAW,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,YAAY,MAAM,IAAI,SAAS;EACnF,MAAM,SAAS,CAAC,CAAC,iBAAiB,MAAM,IAAI,EAAE,IAAI,WAAW,CAAC;EAC9D,UAAU,CAAC;EACX,QAAQ,GAAG,gBAAgB;EAC3B,OAAO,EAAE,KAAK,SAAS,GAAG;CAC5B,CAAC;CAED,IAAI,QAAQ,kBAAkB,OAAO,MAAM;EACzC,MAAM,EAAE,MAAM,YAAY,EAAE,IAAI,MAAM,OAAO;EAC7C,MAAM,UAAU,MAAM,yBAAyB,GAAG,MAAM,GAAG,cAAc;EACzE,MAAM,QAAQ,MAAM,gBAAgB,QAAQ,IAAI,OAAO;EACvD,MAAM,IAAI,WAAW,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,OAAO,MAAM,EAAE;EACnD,OAAO,EAAE,KAAK,MAAM,GAAG;CACzB,CAAC;CAED,kBAAkB,GAAG;CACrB,iBAAiB,GAAG;CACpB,qBAAqB,GAAG;CACxB,uBAAuB,GAAG;CAC1B,oBAAoB,GAAG;CACvB,iBAAiB,GAAG;AACtB;AAEA,MAAM,kBAAkB,YAAY;CAClC,QAAQ;CACR,MAAM,CAAC,QAAQ;CACf,SAAS;CACT,MAAM;CACN,SAAS;EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;EAAG,OAAO;CAAe;CACzE,WAAW;EACT,KAAK;GACH,SAAS,EAAE,oBAAoB,EAAE,QAAQ,YAAY,MAAM,EAAE,EAAE;GAC/D,aAAa;EACf;EACA,GAAGA;EACH,GAAG;CACL;AACF,CAAC;AAED,MAAM,qBAAqB,EACxB,OAAO;CACN,OAAO;CACP,WAAW,EAAE,OAAO;CACpB,aAAa,EAAE,OAAO,CAAC,CAAC,SAAS;CACjC,SAAS,EAAE,QAAQ,CAAC,CAAC,QAAQ,EAC3B,aACE,wGACJ,CAAC;AACH,CAAC,CAAC,CACD,QAAQ,cAAc;AAEzB,MAAM,mBAAmB,YAAY;CACnC,QAAQ;CACR,MAAM,CAAC,QAAQ;CACf,SAAS;CACT,MAAM;CACN,SAAS;EACP,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;EACrC,MAAM,EACJ,SAAS,EACP,oBAAoB,EAAE,QAAQ,sBAAsB,EACtD,EACF;CACF;CACA,WAAW;EACT,KAAK;GACH,SAAS,EACP,oBAAoB,EAClB,QAAQ,mBACV,EACF;GACA,aACE;EACJ;EACA,GAAG;EACH,GAAGC;EACH,GAAGD;CACL;AACF,CAAC;AAED,MAAM,iBAAiB,YAAY;CACjC,QAAQ;CACR,MAAM,CAAC,QAAQ;CACf,SAAS;CACT,MAAM;CACN,SAAS;EACP,QAAQ,EAAE,OAAO;GAAE,MAAM,EAAE,OAAO;GAAG,SAAS,EAAE,OAAO;EAAE,CAAC;EAC1D,MAAM,EACJ,SAAS,EACP,mBAAmB,EACjB,QAAQ,EAAE,OAAO,CAAC,CAAC,QAAQ;GAAE,MAAM;GAAU,QAAQ;EAAS,CAAC,EACjE,EACF,EACF;CACF;CACA,WAAW;EACT,KAAK;GACH,SAAS,EAAE,oBAAoB,EAAE,QAAQ,YAAY,EAAE;GACvD,aACE;EACJ;EACA,KAAK;GACH,SAAS,EAAE,cAAc,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE;GAChD,aAAa;EACf;EACA,KAAK;GACH,SAAS,EACP,oBAAoB,EAClB,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,QAAQ,OAAO,EAC3D,EACF;GACA,aACE;EACJ;EACA,GAAG;EACH,GAAGC;EACH,GAAGD;CACL;AACF,CAAC;AAED,MAAM,oBAAoB,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;AAClE,MAAM,oBAAoB,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;AAClE,MAAM,aAAa,YAAY;CAC7B,QAAQ;CACR,MAAM,CAAC,QAAQ;CACf,SAAS;CACT,aACE;CACF,MAAM;CACN,SAAS;EACP,QAAQ,EAAE,OAAO;GAAE,MAAM,EAAE,OAAO;GAAG,SAAS,EAAE,OAAO;EAAE,CAAC;EAC1D,MAAM,EAAE,SAAS,EAAE,oBAAoB,EAAE,QAAQ,kBAAkB,EAAE,EAAE;CACzE;CACA,WAAW;EACT,KAAK;GACH,SAAS,EAAE,oBAAoB,EAAE,QAAQ,kBAAkB,EAAE;GAC7D,aAAa;EACf;EACA,GAAGA;CACL;AACF,CAAC;AAED,MAAM,qBAAqB,YAAY;CACrC,QAAQ;CACR,MAAM,CAAC,QAAQ;CACf,SAAS;CACT,aACE;CACF,MAAM;CACN,SAAS;EACP,QAAQ,EAAE,OAAO;GAAE,MAAM,EAAE,OAAO;GAAG,SAAS,EAAE,OAAO;EAAE,CAAC;EAC1D,MAAM,EAAE,SAAS,EAAE,uBAAuB,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,EAAE;CAClE;CACA,WAAW;EACT,KAAK;GACH,SAAS,EAAE,oBAAoB,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,EAAE,EAAE;GACzE,aAAa;EACf;EACA,GAAGA;CACL;AACF,CAAC;AAED,MAAM,qBAAqB,EAAE,OAAO;CAAE,KAAK,EAAE,OAAO;CAAG,MAAM,EAAE,OAAO;CAAG,MAAM,EAAE,OAAO;AAAE,CAAC;AAC3F,MAAM,gBAAgB,YAAY;CAChC,QAAQ;CACR,MAAM,CAAC,QAAQ;CACf,SAAS;CACT,aACE;CACF,MAAM;CACN,SAAS;EACP,QAAQ,EAAE,OAAO;GAAE,MAAM,EAAE,OAAO;GAAG,SAAS,EAAE,OAAO;EAAE,CAAC;EAC1D,MAAM,EACJ,SAAS,EACP,oBAAoB,EAClB,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,EAAE,CAAC,EACzD,EACF,EACF;CACF;CACA,WAAW;EACT,KAAK;GACH,SAAS,EAAE,oBAAoB,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,EAAE,EAAE;GACzE,aAAa;EACf;EACA,GAAGA;CACL;AACF,CAAC;AAED,MAAM,wBAAwB,EAAE,OAAO;CACrC,aAAa,EAAE,OAAO,CAAC,CAAC,SAAS;CACjC,cAAc,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,IAAI,GAAM;CAC5C,qBAAqB,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,IAAI,GAAM,CAAC,CAAC,SAAS;AAChE,CAAC;AACD,MAAM,gBAAgB,YAAY;CAChC,QAAQ;CACR,MAAM,CAAC,QAAQ;CACf,SAAS;CACT,MAAM;CACN,SAAS;EACP,QAAQ,EAAE,OAAO;GAAE,MAAM,EAAE,OAAO;GAAG,SAAS,EAAE,OAAO;EAAE,CAAC;EAC1D,MAAM,EAAE,SAAS,EAAE,oBAAoB,EAAE,QAAQ,sBAAsB,EAAE,EAAE;CAC7E;CACA,WAAW;EACT,KAAK;GACH,SAAS,EAAE,oBAAoB,EAAE,QAAQ,YAAY,EAAE;GACvD,aAAa;EACf;EACA,GAAG;EACH,GAAGC;EACH,GAAGD;CACL;AACF,CAAC;AAED,MAAM,qBAAqB,EAAE,OAAO,EAClC,OAAO,EACJ,MACC,EAAE,OAAO;CACP,iBAAiB,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI;CAC3C,aAAa,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,GAAG;CACtC,YAAY,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI;CACtC,SAAS,EAAE,OAAO,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,SAAS,CAAC,CAAC,SAAS;AACnD,CAAC,CACH,CAAC,CACA,IAAI,GAAM,EACf,CAAC;AACD,MAAM,aAAa,YAAY;CAC7B,QAAQ;CACR,MAAM,CAAC,QAAQ;CACf,SAAS;CACT,MAAM;CACN,SAAS;EACP,QAAQ,EAAE,OAAO;GAAE,MAAM,EAAE,OAAO;GAAG,SAAS,EAAE,OAAO;EAAE,CAAC;EAC1D,MAAM,EAAE,SAAS,EAAE,oBAAoB,EAAE,QAAQ,mBAAmB,EAAE,EAAE;CAC1E;CACA,WAAW;EACT,KAAK;GACH,SAAS,EAAE,oBAAoB,EAAE,QAAQ,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,EAAE,EAAE;GAC5E,aAAa;EACf;EACA,GAAG;EACH,GAAGC;EACH,GAAGD;CACL;AACF,CAAC;AAED,MAAM,gBAAgB,YAAY;CAChC,QAAQ;CACR,MAAM,CAAC,QAAQ;CACf,SAAS;CACT,MAAM;CACN,SAAS,EAAE,QAAQ,EAAE,OAAO;EAAE,MAAM,EAAE,OAAO;EAAG,SAAS,EAAE,OAAO;CAAE,CAAC,EAAE;CACvE,WAAW;EACT,KAAK;GAAE,SAAS,EAAE,oBAAoB,EAAE,QAAQ,YAAY,EAAE;GAAG,aAAa;EAAgB;EAC9F,GAAGA;EACH,GAAG;CACL;AACF,CAAC;AAED,MAAM,kBAAkB,YAAY;CAClC,QAAQ;CACR,MAAM,CAAC,QAAQ;CACf,SAAS;CACT,MAAM;CACN,SAAS,EAAE,QAAQ,EAAE,OAAO;EAAE,MAAM,EAAE,OAAO;EAAG,SAAS,EAAE,OAAO;CAAE,CAAC,EAAE;CACvE,WAAW;EACT,KAAK;GACH,SAAS,EAAE,oBAAoB,EAAE,QAAQ,YAAY,EAAE;GACvD,aAAa;EACf;EACA,GAAGA;CACL;AACF,CAAC;AAED,MAAM,mBAAmB,YAAY;CACnC,QAAQ;CACR,MAAM,CAAC,QAAQ;CACf,SAAS;CACT,MAAM;CACN,SAAS,EAAE,QAAQ,EAAE,OAAO;EAAE,MAAM,EAAE,OAAO;EAAG,SAAS,EAAE,OAAO;CAAE,CAAC,EAAE;CACvE,WAAW;EACT,KAAK,EAAE,aAAa,gBAAgB;EACpC,GAAGA;CACL;AACF,CAAC"}
@@ -0,0 +1,29 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { MAX_CAPTURE_ASSET_CACHE_SECONDS } from "@storyshelf/core/config";
5
+ // src/routers/capture-cache.ts
6
+ /**
7
+ * `Cache-Control` of a signed Storybook answer (ADR 0007, decision 5): `private, no-store` unless
8
+ * the server has opted a capture-scoped grant into browser-private freshness. The single owner of
9
+ * that rule; the runner never sets headers, it only receives a URL.
10
+ */
11
+ /** Every signed answer's default, and the only value for an error, a preview or a legacy token. */
12
+ const SIGNED_NO_STORE = "private, no-store";
13
+ /**
14
+ * `private, max-age=N` with N the whole seconds of the smallest of the configured cap, one hour
15
+ * and the grant's remaining verified lifetime at this response; `no-store` when freshness is not
16
+ * opted in, the grant is not a capture grant, the file is not content-addressed, or none is left.
17
+ * It never grants freshness past the token's expiry. It does not affect authorization, which the
18
+ * route re-checks on every network request.
19
+ */
20
+ function signedCacheControl(input) {
21
+ const { cacheMaxAgeSeconds, grant, immutable } = input;
22
+ if (!cacheMaxAgeSeconds || cacheMaxAgeSeconds <= 0 || !immutable || grant?.purpose !== "capture") return SIGNED_NO_STORE;
23
+ const remaining = grant.expiresAt - (input.now ?? Date.now()) / 1e3;
24
+ const seconds = Math.floor(Math.min(cacheMaxAgeSeconds, MAX_CAPTURE_ASSET_CACHE_SECONDS, remaining));
25
+ return seconds > 0 ? `private, max-age=${String(seconds)}` : SIGNED_NO_STORE;
26
+ }
27
+ export { SIGNED_NO_STORE, signedCacheControl };
28
+
29
+ //# sourceMappingURL=capture-cache.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capture-cache.mjs","names":[],"sources":["../../src/routers/capture-cache.ts"],"sourcesContent":["/**\n * `Cache-Control` of a signed Storybook answer (ADR 0007, decision 5): `private, no-store` unless\n * the server has opted a capture-scoped grant into browser-private freshness. The single owner of\n * that rule; the runner never sets headers, it only receives a URL.\n */\nimport { MAX_CAPTURE_ASSET_CACHE_SECONDS } from \"@storyshelf/core/config\";\nimport type { StorybookTokenClaims } from \"@storyshelf/core/utils\";\n\n/** Every signed answer's default, and the only value for an error, a preview or a legacy token. */\nexport const SIGNED_NO_STORE = \"private, no-store\";\n\n/** What decides the freshness of one successful signed answer. */\nexport interface SignedCacheInput {\n /** `config.captureAssets.cacheMaxAgeSeconds`; unset or 0 keeps `no-store`. */\n cacheMaxAgeSeconds: number | undefined;\n /** The verified claims of the token that authorized the request. */\n grant: Pick<StorybookTokenClaims, \"purpose\" | \"expiresAt\"> | undefined;\n /** Whether the file came through the build's finalized, content-addressed manifest. */\n immutable: boolean;\n /** Clock override for tests (ms). */\n now?: number;\n}\n\n/**\n * `private, max-age=N` with N the whole seconds of the smallest of the configured cap, one hour\n * and the grant's remaining verified lifetime at this response; `no-store` when freshness is not\n * opted in, the grant is not a capture grant, the file is not content-addressed, or none is left.\n * It never grants freshness past the token's expiry. It does not affect authorization, which the\n * route re-checks on every network request.\n */\nexport function signedCacheControl(input: SignedCacheInput): string {\n const { cacheMaxAgeSeconds, grant, immutable } = input;\n if (\n !cacheMaxAgeSeconds ||\n cacheMaxAgeSeconds <= 0 ||\n !immutable ||\n grant?.purpose !== \"capture\"\n ) {\n return SIGNED_NO_STORE;\n }\n const remaining = grant.expiresAt - (input.now ?? Date.now()) / 1000;\n const seconds = Math.floor(\n Math.min(cacheMaxAgeSeconds, MAX_CAPTURE_ASSET_CACHE_SECONDS, remaining),\n );\n return seconds > 0 ? `private, max-age=${String(seconds)}` : SIGNED_NO_STORE;\n}\n"],"mappings":";;;;;;;;;;;AASA,MAAa,kBAAkB;;;;;;;;AAqB/B,SAAgB,mBAAmB,OAAiC;CAClE,MAAM,EAAE,oBAAoB,OAAO,cAAc;CACjD,IACE,CAAC,sBACD,sBAAsB,KACtB,CAAC,aACD,OAAO,YAAY,WAEnB,OAAO;CAET,MAAM,YAAY,MAAM,aAAa,MAAM,OAAO,KAAK,IAAI,KAAK;CAChE,MAAM,UAAU,KAAK,MACnB,KAAK,IAAI,oBAAoB,iCAAiC,SAAS,CACzE;CACA,OAAO,UAAU,IAAI,oBAAoB,OAAO,OAAO,MAAM;AAC/D"}
@@ -3,7 +3,7 @@ import "node:url";
3
3
  import.meta.url;
4
4
  import { modeSlug } from "@storyshelf/core/capture";
5
5
  import { HTTPException } from "hono/http-exception";
6
- //#region src/routers/capture-config.ts
6
+ // src/routers/capture-config.ts
7
7
  /** Translating the capture settings (browsers, mode allowlist) into stored project columns. */
8
8
  /**
9
9
  * Translate the array fields into the stored JSON columns and keep the legacy
@@ -29,7 +29,6 @@ function assertModeNames(names) {
29
29
  }
30
30
  return [...new Set(names)];
31
31
  }
32
- //#endregion
33
32
  export { assertModeNames, toProjectPatch };
34
33
 
35
34
  //# sourceMappingURL=capture-config.mjs.map
@@ -12,7 +12,7 @@ import { DEVELOPER_ROLES, VIEW_ROLES, buildForProject } from "./builds.handlers.
12
12
  import { createRoute, z } from "@hono/zod-openapi";
13
13
  import { CommentModel } from "@storyshelf/core/models";
14
14
  import { HTTPException } from "hono/http-exception";
15
- //#region src/routers/comments.ts
15
+ // src/routers/comments.ts
16
16
  const listCommentsRoute = createRoute({
17
17
  method: "get",
18
18
  tags: ["Review"],
@@ -129,7 +129,6 @@ async function createComment(project, buildId, body) {
129
129
  await announceComment(project, buildId, comment);
130
130
  return comment;
131
131
  }
132
- //#endregion
133
132
  export { createComment, registerComments, resolveBuildComment };
134
133
 
135
134
  //# sourceMappingURL=comments.mjs.map
@@ -0,0 +1,29 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { getStore } from "../store.mjs";
5
+ import { isComparisonImagePath, parseComparisonReference } from "@storyshelf/core/capture";
6
+ import { createHash } from "node:crypto";
7
+ // src/routers/comparison-image.ts
8
+ /** Validate provenance and ownership before reading a frozen reference. */
9
+ function validFrozenReference(snapshot, projectId, buildId) {
10
+ const reference = parseComparisonReference(snapshot.comparisonMetadata);
11
+ const path = snapshot.comparisonImagePath;
12
+ if (!reference || !path || !isComparisonImagePath(path, projectId, buildId) || reference.baselineId !== snapshot.baselineId || reference.baselineVersion !== snapshot.baselineVersion) return null;
13
+ return {
14
+ reference,
15
+ path
16
+ };
17
+ }
18
+ /** A comparison is usable only when its bytes match the capture-time record. */
19
+ async function readComparisonImage(path, sha256) {
20
+ try {
21
+ const image = await getStore().storage.read(path);
22
+ return createHash("sha256").update(image).digest("hex") === sha256 ? image : null;
23
+ } catch {
24
+ return null;
25
+ }
26
+ }
27
+ export { readComparisonImage, validFrozenReference };
28
+
29
+ //# sourceMappingURL=comparison-image.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"comparison-image.mjs","names":[],"sources":["../../src/routers/comparison-image.ts"],"sourcesContent":["import {\n isComparisonImagePath,\n parseComparisonReference,\n type ComparisonReference,\n} from \"@storyshelf/core/capture\";\nimport type { Snapshot } from \"@storyshelf/core/schema\";\nimport { createHash } from \"node:crypto\";\nimport { getStore } from \"../store.ts\";\n\n/** Validate provenance and ownership before reading a frozen reference. */\nexport function validFrozenReference(\n snapshot: Snapshot,\n projectId: string,\n buildId: string,\n): { reference: ComparisonReference; path: string } | null {\n const reference = parseComparisonReference(snapshot.comparisonMetadata);\n const path = snapshot.comparisonImagePath;\n if (\n !reference ||\n !path ||\n !isComparisonImagePath(path, projectId, buildId) ||\n reference.baselineId !== snapshot.baselineId ||\n reference.baselineVersion !== snapshot.baselineVersion\n )\n return null;\n return { reference, path };\n}\n\n/** A comparison is usable only when its bytes match the capture-time record. */\nexport async function readComparisonImage(path: string, sha256: string): Promise<Buffer | null> {\n try {\n const image = await getStore().storage.read(path);\n return createHash(\"sha256\").update(image).digest(\"hex\") === sha256 ? image : null;\n } catch {\n return null;\n }\n}\n"],"mappings":";;;;;;;;AAUA,SAAgB,qBACd,UACA,WACA,SACyD;CACzD,MAAM,YAAY,yBAAyB,SAAS,kBAAkB;CACtE,MAAM,OAAO,SAAS;CACtB,IACE,CAAC,aACD,CAAC,QACD,CAAC,sBAAsB,MAAM,WAAW,OAAO,KAC/C,UAAU,eAAe,SAAS,cAClC,UAAU,oBAAoB,SAAS,iBAEvC,OAAO;CACT,OAAO;EAAE;EAAW;CAAK;AAC3B;;AAGA,eAAsB,oBAAoB,MAAc,QAAwC;CAC9F,IAAI;EACF,MAAM,QAAQ,MAAM,SAAS,CAAC,CAAC,QAAQ,KAAK,IAAI;EAChD,OAAO,WAAW,QAAQ,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,OAAO,KAAK,MAAM,SAAS,QAAQ;CAC/E,QAAQ;EACN,OAAO;CACT;AACF"}
@@ -4,7 +4,7 @@ import.meta.url;
4
4
  import { catalogOf, getStore } from "../store.mjs";
5
5
  import { catalogPresence, componentBuildHistory, diffEntries } from "@storyshelf/core/capture";
6
6
  import { BuildComponentModel, BuildModel } from "@storyshelf/core/models";
7
- //#region src/routers/component-catalog.handlers.ts
7
+ // src/routers/component-catalog.handlers.ts
8
8
  /** Newest builds a component's history lists. */
9
9
  const HISTORY_LIMIT = 50;
10
10
  /** Changed rows whose added/removed/renamed counts are worked out (each loads two catalogs). */
@@ -65,7 +65,6 @@ async function withChanges(projectId, importPath, history) {
65
65
  };
66
66
  }));
67
67
  }
68
- //#endregion
69
68
  export { latestProjectBuild, loadCatalogView };
70
69
 
71
70
  //# sourceMappingURL=component-catalog.handlers.mjs.map
@@ -11,7 +11,7 @@ import { componentTabs } from "../pages/component-detail.mjs";
11
11
  import { entriesFromSnapshots } from "../pages/library-model.mjs";
12
12
  import { latestProjectBuild, loadCatalogView } from "./component-catalog.handlers.mjs";
13
13
  import { BuildModel, ProjectModel, SnapshotModel, componentHistory, componentPresence, normalizeImportPath } from "@storyshelf/core/models";
14
- //#region src/routers/component.handlers.ts
14
+ // src/routers/component.handlers.ts
15
15
  async function buildsOf(snapshots) {
16
16
  const model = new BuildModel(getStore().db);
17
17
  return (await Promise.all([...new Set(snapshots.map((snap) => snap.buildId))].map(async (id) => await model.get(id)))).filter((build) => build !== null);
@@ -115,7 +115,6 @@ function viewOfCaptured(loaded) {
115
115
  function resolveTab(requested, testing, captured) {
116
116
  return componentTabs(testing).find((candidate) => candidate === requested) ?? (testing && captured ? "states" : "stories");
117
117
  }
118
- //#endregion
119
118
  export { loadComponentPage };
120
119
 
121
120
  //# sourceMappingURL=component.handlers.mjs.map
@@ -7,7 +7,7 @@ import { flash } from "./flash.mjs";
7
7
  import { connectionModel, findConnectable } from "../connections.mjs";
8
8
  import { safeReturnPath, signState, verifyState } from "./oauth-state.mjs";
9
9
  import { deleteCookie, getCookie, setCookie } from "hono/cookie";
10
- //#region src/routers/connections.ts
10
+ // src/routers/connections.ts
11
11
  const COOKIE = "storyshelf_oauth_state";
12
12
  const COOKIE_PATH = "/connections";
13
13
  /**
@@ -112,7 +112,6 @@ async function handleDisconnect(c) {
112
112
  function formString(value) {
113
113
  return typeof value === "string" ? value : "";
114
114
  }
115
- //#endregion
116
115
  export { registerConnections };
117
116
 
118
117
  //# sourceMappingURL=connections.mjs.map
@@ -5,7 +5,7 @@ import { credentialKeys } from "../credential-keys.mjs";
5
5
  import { getStore } from "../store.mjs";
6
6
  import { canConnect, connectionModel } from "../connections.mjs";
7
7
  import { DesignCredentialModel } from "@storyshelf/core/models";
8
- //#region src/routers/design-connect.handlers.ts
8
+ // src/routers/design-connect.handlers.ts
9
9
  /** The signed-in user whose own connection may serve previews (none when auth is off). */
10
10
  function viewerIdOf() {
11
11
  const { user, authEnabled } = getStore();
@@ -38,7 +38,6 @@ function connectOffer(kind, row, returnTo) {
38
38
  returnTo
39
39
  };
40
40
  }
41
- //#endregion
42
41
  export { loadConnectStates, viewerIdOf };
43
42
 
44
43
  //# sourceMappingURL=design-connect.handlers.mjs.map
@@ -7,7 +7,7 @@ import { connectionModel } from "../connections.mjs";
7
7
  import { loadConnectStates, viewerIdOf } from "./design-connect.handlers.mjs";
8
8
  import { DesignCredentialModel, DesignModel, DesignNodeModel, isDesignStale, normalizeImportPath, resolveDesigns } from "@storyshelf/core/models";
9
9
  import { parseDesignUrl } from "@storyshelf/core/adapter/design";
10
- //#region src/routers/design.handlers.ts
10
+ // src/routers/design.handlers.ts
11
11
  /** Most a page render waits for fresh design metadata; slower fetches finish into the cache for next time. */
12
12
  const REFRESH_BUDGET_MS = 1500;
13
13
  /** Path + query of the current request, to return to after a connect round trip. */
@@ -108,7 +108,6 @@ async function refreshWithinBudget(nodes, projectId, urls, viewerId) {
108
108
  ...viewerId ? { viewerId } : {}
109
109
  }).catch(() => /* @__PURE__ */ new Map());
110
110
  }
111
- //#endregion
112
111
  export { designUrlsFor, loadDesignPreviews, loadDesignResolutions, loadProjectDesignLinks, loadReviewDesign, providerNameFor, returnPathOf };
113
112
 
114
113
  //# sourceMappingURL=design.handlers.mjs.map
@@ -3,7 +3,7 @@ import "node:url";
3
3
  import.meta.url;
4
4
  import { isHxRequest } from "./htmx.mjs";
5
5
  import { setCookie } from "hono/cookie";
6
- //#region src/routers/flash.ts
6
+ // src/routers/flash.ts
7
7
  /** Cookie the client script reads, shows as a toast, and clears. */
8
8
  const FLASH_COOKIE = "storyshelf_flash";
9
9
  const MAX_MESSAGE_LENGTH = 140;
@@ -29,7 +29,6 @@ function flash(c, message, tone = "success") {
29
29
  function flashHx(c, message, tone = "success") {
30
30
  if (isHxRequest(c)) flash(c, message, tone);
31
31
  }
32
- //#endregion
33
32
  export { FLASH_COOKIE, flash, flashHx };
34
33
 
35
34
  //# sourceMappingURL=flash.mjs.map
@@ -2,7 +2,7 @@ import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
4
  import { sanitizeErrorText } from "@storyshelf/core/utils";
5
- //#region src/routers/health-report.ts
5
+ // src/routers/health-report.ts
6
6
  const PROBE_TIMEOUT_MS = 2e3;
7
7
  const DETAIL_MAX_LENGTH = 500;
8
8
  function pushTarget(targets, adapter) {
@@ -152,7 +152,6 @@ async function collectHealthReport(sources, settled, bootTimeMs, version, creden
152
152
  adapters
153
153
  };
154
154
  }
155
- //#endregion
156
155
  export { collectHealthReport };
157
156
 
158
157
  //# sourceMappingURL=health-report.mjs.map
@@ -5,7 +5,7 @@ import { getStore, testingEnabled } from "../store.mjs";
5
5
  import { loadCredentialProbe } from "../credentials.mjs";
6
6
  import { requireSiteAdmin } from "./helpers.mjs";
7
7
  import { collectHealthReport } from "./health-report.mjs";
8
- //#region src/routers/health.ts
8
+ // src/routers/health.ts
9
9
  /** Register `GET /api/v1/health` (open liveness) and `POST` (authed deep readiness). */
10
10
  function registerHealth(app, deps) {
11
11
  app.get("/api/v1/health", (c) => {
@@ -41,7 +41,6 @@ function registerHealth(app, deps) {
41
41
  return c.json(body, body.status === "ok" ? 200 : 503);
42
42
  });
43
43
  }
44
- //#endregion
45
44
  export { registerHealth };
46
45
 
47
46
  //# sourceMappingURL=health.mjs.map