@storyshelf/core 0.1.3 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (239) hide show
  1. package/README.md +50 -46
  2. package/dist/adapter/auth.d.mts +4 -4
  3. package/dist/adapter/auth.mjs.map +1 -1
  4. package/dist/{adapters → adapter}/capture-queue.d.mts +4 -4
  5. package/dist/adapter/capture-queue.mjs +4 -0
  6. package/dist/adapter/capture-runner.d.mts +7 -7
  7. package/dist/adapter/database.d.mts +20 -9
  8. package/dist/adapter/database.mjs +60 -1
  9. package/dist/adapter/database.mjs.map +1 -0
  10. package/dist/adapter/git-host/comments.d.mts +41 -0
  11. package/dist/adapter/git-host/comments.mjs +54 -0
  12. package/dist/adapter/git-host/comments.mjs.map +1 -0
  13. package/dist/adapter/git-host.d.mts +3 -7
  14. package/dist/adapter/init.d.mts +58 -0
  15. package/dist/adapter/init.mjs +93 -0
  16. package/dist/adapter/init.mjs.map +1 -0
  17. package/dist/adapter/metadata.d.mts +83 -0
  18. package/dist/adapter/metadata.mjs +4 -0
  19. package/dist/adapter/storage.d.mts +15 -4
  20. package/dist/adapters/webhook-events.mjs +12 -4
  21. package/dist/adapters/webhook-events.mjs.map +1 -1
  22. package/dist/capture/adapter.d.mts +5 -1
  23. package/dist/capture/dispatch.mjs +150 -0
  24. package/dist/capture/dispatch.mjs.map +1 -0
  25. package/dist/capture/orchestrator.d.mts +2 -0
  26. package/dist/capture/orchestrator.mjs +5 -44
  27. package/dist/capture/orchestrator.mjs.map +1 -1
  28. package/dist/capture/pipeline.d.mts +4 -1
  29. package/dist/capture/pipeline.mjs +6 -6
  30. package/dist/capture/pipeline.mjs.map +1 -1
  31. package/dist/capture/queue.d.mts +2 -1
  32. package/dist/capture/queue.mjs +2 -1
  33. package/dist/capture/queue.mjs.map +1 -1
  34. package/dist/capture/skip-checks.mjs +76 -0
  35. package/dist/capture/skip-checks.mjs.map +1 -0
  36. package/dist/capture/statics.mjs +124 -0
  37. package/dist/capture/statics.mjs.map +1 -0
  38. package/dist/capture/status-fanout.mjs.map +1 -1
  39. package/dist/capture/storybook.mjs.map +1 -1
  40. package/dist/capture.d.mts +8 -0
  41. package/dist/capture.mjs +9 -0
  42. package/dist/config.d.mts +10 -2
  43. package/dist/config.mjs +14 -2
  44. package/dist/config.mjs.map +1 -1
  45. package/dist/ddl.d.mts +1 -1
  46. package/dist/ddl.mjs +1 -1
  47. package/dist/ddl.mjs.map +1 -1
  48. package/dist/diff/engine.mjs.map +1 -1
  49. package/dist/diff/options.d.mts +3 -1
  50. package/dist/diff.d.mts +5 -0
  51. package/dist/diff.mjs +6 -0
  52. package/dist/index.d.mts +32 -25
  53. package/dist/index.mjs +207 -253
  54. package/dist/index.mjs.map +1 -1
  55. package/dist/logger.d.mts +3 -3
  56. package/dist/middleware/auth-gate.mjs +36 -0
  57. package/dist/middleware/auth-gate.mjs.map +1 -0
  58. package/dist/middleware/csrf.mjs.map +1 -1
  59. package/dist/middleware/request-log.mjs +28 -0
  60. package/dist/middleware/request-log.mjs.map +1 -0
  61. package/dist/middleware/store-scope.mjs +29 -0
  62. package/dist/middleware/store-scope.mjs.map +1 -0
  63. package/dist/models/baseline.mjs +7 -5
  64. package/dist/models/baseline.mjs.map +1 -1
  65. package/dist/models/build.mjs +3 -1
  66. package/dist/models/build.mjs.map +1 -1
  67. package/dist/models/comment.mjs +2 -1
  68. package/dist/models/comment.mjs.map +1 -1
  69. package/dist/models/label.mjs +2 -1
  70. package/dist/models/label.mjs.map +1 -1
  71. package/dist/models/member.mjs +1 -1
  72. package/dist/models/member.mjs.map +1 -1
  73. package/dist/models/project.mjs +1 -1
  74. package/dist/models/project.mjs.map +1 -1
  75. package/dist/models/snapshot.mjs +1 -1
  76. package/dist/models/snapshot.mjs.map +1 -1
  77. package/dist/models/status-config.mjs +1 -1
  78. package/dist/models/status-config.mjs.map +1 -1
  79. package/dist/models/token.mjs +1 -1
  80. package/dist/models/token.mjs.map +1 -1
  81. package/dist/models/webhook.mjs +11 -3
  82. package/dist/models/webhook.mjs.map +1 -1
  83. package/dist/pages/build-detail.mjs +9 -4
  84. package/dist/pages/build-detail.mjs.map +1 -1
  85. package/dist/pages/build-diff-comments.mjs +113 -0
  86. package/dist/pages/build-diff-comments.mjs.map +1 -0
  87. package/dist/pages/build-diff-header.mjs +163 -0
  88. package/dist/pages/build-diff-header.mjs.map +1 -0
  89. package/dist/pages/build-diff-nav.mjs +66 -0
  90. package/dist/pages/build-diff-nav.mjs.map +1 -0
  91. package/dist/pages/build-diff-viewer.mjs +197 -0
  92. package/dist/pages/build-diff-viewer.mjs.map +1 -0
  93. package/dist/pages/build-diff.mjs +65 -413
  94. package/dist/pages/build-diff.mjs.map +1 -1
  95. package/dist/pages/compute-jobs.mjs +1 -1
  96. package/dist/pages/compute-jobs.mjs.map +1 -1
  97. package/dist/pages/label-detail.mjs +4 -3
  98. package/dist/pages/label-detail.mjs.map +1 -1
  99. package/dist/pages/login.mjs.map +1 -1
  100. package/dist/pages/project-builds.mjs +5 -3
  101. package/dist/pages/project-builds.mjs.map +1 -1
  102. package/dist/pages/project-create.mjs.map +1 -1
  103. package/dist/pages/project-settings.mjs.map +1 -1
  104. package/dist/pages/projects.mjs +11 -6
  105. package/dist/pages/projects.mjs.map +1 -1
  106. package/dist/pages/root.mjs +6 -3
  107. package/dist/pages/root.mjs.map +1 -1
  108. package/dist/pages/settings-general.mjs.map +1 -1
  109. package/dist/pages/settings-labels.mjs +1 -1
  110. package/dist/pages/settings-labels.mjs.map +1 -1
  111. package/dist/pages/settings-members.mjs +1 -1
  112. package/dist/pages/settings-members.mjs.map +1 -1
  113. package/dist/pages/settings-status.mjs +2 -1
  114. package/dist/pages/settings-status.mjs.map +1 -1
  115. package/dist/pages/settings-tests.mjs +3 -3
  116. package/dist/pages/settings-tests.mjs.map +1 -1
  117. package/dist/pages/settings-tokens.mjs.map +1 -1
  118. package/dist/pages/settings-webhooks.mjs.map +1 -1
  119. package/dist/pages/storybook.mjs +38 -1
  120. package/dist/pages/storybook.mjs.map +1 -1
  121. package/dist/paths.mjs.map +1 -0
  122. package/dist/retention/purge.mjs +6 -3
  123. package/dist/retention/purge.mjs.map +1 -1
  124. package/dist/routers/admin.mjs +1 -1
  125. package/dist/routers/admin.mjs.map +1 -1
  126. package/dist/routers/assets.mjs.map +1 -1
  127. package/dist/routers/auth.mjs.map +1 -1
  128. package/dist/routers/builds.handlers.mjs +106 -21
  129. package/dist/routers/builds.handlers.mjs.map +1 -1
  130. package/dist/routers/builds.mjs +54 -34
  131. package/dist/routers/builds.mjs.map +1 -1
  132. package/dist/routers/comments.mjs.map +1 -1
  133. package/dist/routers/health.mjs +137 -0
  134. package/dist/routers/health.mjs.map +1 -0
  135. package/dist/routers/helpers.mjs +2 -2
  136. package/dist/routers/helpers.mjs.map +1 -1
  137. package/dist/routers/htmx.mjs.map +1 -1
  138. package/dist/routers/labels.mjs +1 -1
  139. package/dist/routers/labels.mjs.map +1 -1
  140. package/dist/routers/media.mjs +2 -2
  141. package/dist/routers/media.mjs.map +1 -1
  142. package/dist/routers/members.mjs.map +1 -1
  143. package/dist/routers/projects.mjs +1 -1
  144. package/dist/routers/projects.mjs.map +1 -1
  145. package/dist/routers/schemas.mjs.map +1 -1
  146. package/dist/routers/settings-general.mjs +95 -0
  147. package/dist/routers/settings-general.mjs.map +1 -0
  148. package/dist/routers/settings-labels.mjs +49 -0
  149. package/dist/routers/settings-labels.mjs.map +1 -0
  150. package/dist/routers/settings-members.mjs +30 -0
  151. package/dist/routers/settings-members.mjs.map +1 -0
  152. package/dist/routers/settings-status.mjs +73 -0
  153. package/dist/routers/settings-status.mjs.map +1 -0
  154. package/dist/routers/settings-tokens.mjs +40 -0
  155. package/dist/routers/settings-tokens.mjs.map +1 -0
  156. package/dist/routers/settings-webhooks.mjs +63 -0
  157. package/dist/routers/settings-webhooks.mjs.map +1 -0
  158. package/dist/routers/settings.handlers.mjs +93 -0
  159. package/dist/routers/settings.handlers.mjs.map +1 -0
  160. package/dist/routers/settings.mjs +13 -247
  161. package/dist/routers/settings.mjs.map +1 -1
  162. package/dist/routers/snapshots.mjs +1 -1
  163. package/dist/routers/snapshots.mjs.map +1 -1
  164. package/dist/routers/status-configs.mjs +1 -1
  165. package/dist/routers/status-configs.mjs.map +1 -1
  166. package/dist/routers/storybook.mjs +11 -4
  167. package/dist/routers/storybook.mjs.map +1 -1
  168. package/dist/routers/tokens.mjs +1 -1
  169. package/dist/routers/tokens.mjs.map +1 -1
  170. package/dist/routers/ui.mjs +4 -4
  171. package/dist/routers/ui.mjs.map +1 -1
  172. package/dist/routers/webhooks.mjs +6 -6
  173. package/dist/routers/webhooks.mjs.map +1 -1
  174. package/dist/schema/baseline.d.mts +18 -0
  175. package/dist/schema/baseline.mjs +22 -0
  176. package/dist/schema/baseline.mjs.map +1 -0
  177. package/dist/schema/build.d.mts +26 -0
  178. package/dist/schema/build.mjs +29 -0
  179. package/dist/schema/build.mjs.map +1 -0
  180. package/dist/schema/comment.d.mts +19 -0
  181. package/dist/schema/comment.mjs +26 -0
  182. package/dist/schema/comment.mjs.map +1 -0
  183. package/dist/schema/label.d.mts +25 -0
  184. package/dist/schema/label.mjs +30 -0
  185. package/dist/schema/label.mjs.map +1 -0
  186. package/dist/schema/member.d.mts +15 -0
  187. package/dist/schema/member.mjs +19 -0
  188. package/dist/schema/member.mjs.map +1 -0
  189. package/dist/schema/project.d.mts +34 -0
  190. package/dist/schema/project.mjs +25 -0
  191. package/dist/schema/project.mjs.map +1 -0
  192. package/dist/schema/snapshot.d.mts +30 -0
  193. package/dist/schema/snapshot.mjs +34 -0
  194. package/dist/schema/snapshot.mjs.map +1 -0
  195. package/dist/schema/status-config.d.mts +16 -0
  196. package/dist/schema/status-config.mjs +20 -0
  197. package/dist/schema/status-config.mjs.map +1 -0
  198. package/dist/schema/token.d.mts +15 -0
  199. package/dist/schema/token.mjs +19 -0
  200. package/dist/schema/token.mjs.map +1 -0
  201. package/dist/schema/user.d.mts +17 -0
  202. package/dist/schema/user.mjs +19 -0
  203. package/dist/schema/user.mjs.map +1 -0
  204. package/dist/schema/webhook.d.mts +16 -0
  205. package/dist/schema/webhook.mjs +20 -0
  206. package/dist/schema/webhook.mjs.map +1 -0
  207. package/dist/schema.d.mts +13 -168
  208. package/dist/schema.mjs +12 -2
  209. package/dist/schema.mjs.map +1 -1
  210. package/dist/store.mjs.map +1 -1
  211. package/dist/types.d.mts +1 -2
  212. package/dist/types.mjs.map +1 -1
  213. package/dist/ui/document.mjs.map +1 -1
  214. package/dist/ui/feedback.mjs +24 -0
  215. package/dist/ui/feedback.mjs.map +1 -0
  216. package/dist/ui/{components.mjs → forms.mjs} +3 -18
  217. package/dist/ui/forms.mjs.map +1 -0
  218. package/dist/ui/styles.mjs.map +1 -1
  219. package/dist/urls.mjs.map +1 -1
  220. package/package.json +47 -45
  221. package/dist/adapters/metadata.d.mts +0 -24
  222. package/dist/models/baseline.d.mts +0 -21
  223. package/dist/models/build.d.mts +0 -53
  224. package/dist/models/comment.d.mts +0 -22
  225. package/dist/models/label.d.mts +0 -49
  226. package/dist/models/member.d.mts +0 -27
  227. package/dist/models/project.d.mts +0 -43
  228. package/dist/models/snapshot.d.mts +0 -47
  229. package/dist/models/token.d.mts +0 -33
  230. package/dist/models/webhook.d.mts +0 -38
  231. package/dist/retention/purge.d.mts +0 -31
  232. package/dist/schema-tables.mjs +0 -166
  233. package/dist/schema-tables.mjs.map +0 -1
  234. package/dist/ui/components.mjs.map +0 -1
  235. package/dist/ui/document.d.mts +0 -9
  236. package/dist/utils/paths.mjs.map +0 -1
  237. package/dist/utils/ulid.d.mts +0 -10
  238. /package/dist/{utils/paths.d.mts → paths.d.mts} +0 -0
  239. /package/dist/{utils/paths.mjs → paths.mjs} +0 -0
@@ -3,13 +3,17 @@ import "node:url";
3
3
  import.meta.url;
4
4
  import { BuildModel } from "../models/build.mjs";
5
5
  import { ProjectModel } from "../models/project.mjs";
6
- import { BUILD_STATUSES } from "../types.mjs";
7
- import { getStore } from "../store.mjs";
8
- import { notFound } from "./helpers.mjs";
9
6
  import { emitWebhookEvent } from "../adapters/webhook-events.mjs";
10
7
  import { BaselineModel } from "../models/baseline.mjs";
11
8
  import { SnapshotModel } from "../models/snapshot.mjs";
9
+ import { extractStorybookToScratch, persistStorybookStatics } from "../capture/statics.mjs";
10
+ import { getStore } from "../store.mjs";
11
+ import { BUILD_STATUSES } from "../types.mjs";
12
+ import { LabelModel } from "../models/label.mjs";
13
+ import { notFound } from "./helpers.mjs";
12
14
  import { z } from "@hono/zod-openapi";
15
+ import { HTTPException } from "hono/http-exception";
16
+ import { Readable, Transform } from "node:stream";
13
17
  //#region src/routers/builds.handlers.ts
14
18
  /** Roles permitted to view builds, snapshots, and comments. */
15
19
  const VIEW_ROLES = [
@@ -26,18 +30,103 @@ const DEVELOPER_ROLES = [
26
30
  ];
27
31
  /** Roles permitted to approve or reject snapshots and delete builds. */
28
32
  const APPROVER_ROLES = ["approver", "admin"];
29
- /** Multipart input schema for uploading a build with its Storybook bundle. */
30
- const buildUploadSchema = z.object({
31
- gitSha: z.string(),
32
- gitBranch: z.string(),
33
+ /** JSON input schema for creating a build before streaming its bundle. */
34
+ const buildCreateJsonSchema = z.object({
35
+ gitSha: z.string().min(1),
36
+ gitBranch: z.string().min(1),
33
37
  authorEmail: z.string().optional(),
34
38
  authorName: z.string().optional(),
35
39
  message: z.string().optional(),
36
- zip: z.instanceof(File).openapi({
37
- type: "string",
38
- format: "binary"
39
- }).optional()
40
- }).openapi("BuildUpload");
40
+ labels: z.array(z.object({
41
+ key: z.string().min(1),
42
+ value: z.string()
43
+ })).optional()
44
+ }).openapi("BuildCreate");
45
+ async function attachLabels(db, projectId, buildId, labels) {
46
+ const models = new LabelModel(db);
47
+ for (const { key, value } of labels) {
48
+ if (!await models.getType(projectId, key)) await models.createType(projectId, {
49
+ key,
50
+ name: key
51
+ });
52
+ await models.attach(projectId, buildId, key, value);
53
+ }
54
+ }
55
+ /**
56
+ * Create a build record with labels and the `build:created` webhook.
57
+ * Shared by the build-creation route.
58
+ */
59
+ async function createBuildRecord(project, meta) {
60
+ const { db, config } = getStore();
61
+ if (!meta.gitSha || !meta.gitBranch) throw new HTTPException(400, { message: "gitSha and gitBranch are required" });
62
+ const build = await new BuildModel(db).create(project.id, {
63
+ gitSha: meta.gitSha,
64
+ gitBranch: meta.gitBranch,
65
+ isDefault: meta.gitBranch === project.gitDefaultBranch,
66
+ authorEmail: meta.authorEmail,
67
+ authorName: meta.authorName,
68
+ message: meta.message
69
+ });
70
+ if (meta.labels && meta.labels.length > 0) await attachLabels(db, project.id, build.id, meta.labels);
71
+ await emitWebhookEvent(db, project.id, "build:created", {
72
+ buildId: build.id,
73
+ gitSha: meta.gitSha,
74
+ gitBranch: meta.gitBranch,
75
+ authorEmail: meta.authorEmail,
76
+ authorName: meta.authorName,
77
+ message: meta.message
78
+ }, config.secret);
79
+ return build;
80
+ }
81
+ /**
82
+ * Stream a raw request body into storage with a byte cap.
83
+ * Throws 400 for a missing body, 413 once `maxBytes` is exceeded.
84
+ */
85
+ async function storeUploadStream(storage, path, body, maxBytes) {
86
+ if (!body) throw new HTTPException(400, { message: "Missing request body" });
87
+ let seen = 0;
88
+ const limiter = new Transform({ transform(chunk, _encoding, callback) {
89
+ seen += chunk.length;
90
+ if (seen > maxBytes) callback(/* @__PURE__ */ new Error(`Upload exceeds ${maxBytes} byte limit`));
91
+ else callback(null, chunk);
92
+ } });
93
+ try {
94
+ const nodeBody = Readable.fromWeb(body);
95
+ await storage.writeStream(path, nodeBody.pipe(limiter));
96
+ } catch (error) {
97
+ if (seen > maxBytes) throw new HTTPException(413, { message: "Upload exceeds size limit" });
98
+ throw error;
99
+ }
100
+ }
101
+ /** Parse a Content-Length header value into bytes, or undefined when unknown. */
102
+ function parseContentLength(value) {
103
+ if (!value) return;
104
+ const parsed = Math.trunc(Number(value));
105
+ return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : void 0;
106
+ }
107
+ /**
108
+ * Best-effort inline statics extraction for small uploads (see
109
+ * `maxInlineUnzipSize`). Never throws: failures are logged and the capture
110
+ * job backfills the same keys idempotently.
111
+ */
112
+ async function persistInlineStatics(storage, scratchDir, maxInlineBytes, contentLength, projectId, buildId, logger) {
113
+ if (!scratchDir || !maxInlineBytes) return;
114
+ const size = parseContentLength(contentLength);
115
+ if (size === void 0 || size > maxInlineBytes) return;
116
+ try {
117
+ const extractedDir = await extractStorybookToScratch(storage, scratchDir, projectId, buildId);
118
+ await persistStorybookStatics(storage, extractedDir, projectId, buildId);
119
+ logger.info({
120
+ buildId,
121
+ size
122
+ }, "upload statics persisted inline");
123
+ } catch (error) {
124
+ logger.error({
125
+ err: error,
126
+ buildId
127
+ }, "inline statics persist failed; capture will backfill");
128
+ }
129
+ }
41
130
  /** Query filters accepted by the build list endpoint. */
42
131
  const buildListQuery = z.object({
43
132
  status: z.enum(BUILD_STATUSES).optional(),
@@ -59,7 +148,7 @@ async function snapshotForBuild(build, snapshotId) {
59
148
  }
60
149
  /** Recompute a build's counts and roll its status up from its snapshots. */
61
150
  async function refreshBuild(buildId) {
62
- const { db } = getStore();
151
+ const { db, config } = getStore();
63
152
  await new BuildModel(db).updateCounts(buildId);
64
153
  const snapshots = await new SnapshotModel(db).listByBuild(buildId);
65
154
  const unresolved = snapshots.some((s) => s.status === "new" || s.status === "changed");
@@ -73,12 +162,12 @@ async function refreshBuild(buildId) {
73
162
  buildId,
74
163
  status,
75
164
  snapshotCount: snapshots.length
76
- });
165
+ }, config.secret);
77
166
  }
78
167
  }
79
168
  /** Approve a snapshot, promote its screenshot to baseline, and refresh the build. */
80
169
  async function approveSnapshot(snapshotId, userId) {
81
- const { db } = getStore();
170
+ const { db, config } = getStore();
82
171
  const snapshots = new SnapshotModel(db);
83
172
  const snapshot = await snapshots.get(snapshotId);
84
173
  if (!snapshot) notFound("Snapshot not found");
@@ -86,14 +175,10 @@ async function approveSnapshot(snapshotId, userId) {
86
175
  const build = await new BuildModel(db).get(snapshot.buildId);
87
176
  if (!project || !build) notFound("Project or build not found");
88
177
  await snapshots.review(snapshotId, "approved", userId);
89
- await new BaselineModel(db, getStore().storage).upsert(project.id, snapshot.storyId, snapshot.viewportName, build.gitBranch, snapshot.id, snapshot.screenshotPath);
178
+ await new BaselineModel(db, getStore().storage, config.secret).upsert(project.id, snapshot.storyId, snapshot.viewportName, build.gitBranch, snapshot.id, snapshot.screenshotPath);
90
179
  await refreshBuild(build.id);
91
180
  }
92
- /** Extract a string from a multipart form field, ignoring file entries. */
93
- function asString(value) {
94
- return typeof value === "string" ? value : void 0;
95
- }
96
181
  //#endregion
97
- export { APPROVER_ROLES, DEVELOPER_ROLES, VIEW_ROLES, approveSnapshot, asString, buildForProject, buildListQuery, buildUploadSchema, refreshBuild, snapshotForBuild };
182
+ export { APPROVER_ROLES, DEVELOPER_ROLES, VIEW_ROLES, approveSnapshot, buildCreateJsonSchema, buildForProject, buildListQuery, createBuildRecord, persistInlineStatics, refreshBuild, snapshotForBuild, storeUploadStream };
98
183
 
99
184
  //# 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\";\n\nimport { BaselineModel } from \"../models/baseline.ts\";\nimport { BuildModel } from \"../models/build.ts\";\nimport { ProjectModel } from \"../models/project.ts\";\nimport { SnapshotModel } from \"../models/snapshot.ts\";\nimport { emitWebhookEvent } from \"../adapters/webhook-events.ts\";\nimport { getStore } from \"../store.ts\";\nimport { type ProjectRole, BUILD_STATUSES } from \"../types.ts\";\nimport { notFound } from \"./helpers.ts\";\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/** Multipart input schema for uploading a build with its Storybook bundle. */\nexport const buildUploadSchema = z.object({\n gitSha: z.string(),\n gitBranch: z.string(),\n authorEmail: z.string().optional(),\n authorName: z.string().optional(),\n message: z.string().optional(),\n zip: z.instanceof(File).openapi({ type: \"string\", format: \"binary\" }).optional(),\n}).openapi(\"BuildUpload\");\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<import(\"../models/build.ts\").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(build: { id: string }, snapshotId: string): Promise<import(\"../models/snapshot.ts\").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/** 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 unresolved = snapshots.some((s) => s.status === \"new\" || s.status === \"changed\");\n let status: \"reviewing\" | \"rejected\" | \"approved\";\n if (unresolved) {\n status = \"reviewing\";\n } else {\n const rejected = snapshots.some((s) => s.status === \"rejected\");\n status = rejected ? \"rejected\" : \"approved\";\n }\n const build = await new BuildModel(db).get(buildId);\n if (build) {\n await new BuildModel(db).setStatus(buildId, status);\n await emitWebhookEvent(db, build.projectId, `build:${status}`, {\n buildId,\n status,\n snapshotCount: snapshots.length,\n });\n }\n}\n\n/** Approve a snapshot, promote its screenshot to baseline, and refresh the build. */\nexport async function approveSnapshot(snapshotId: string, userId: string): Promise<void> {\n const {db} = 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 await snapshots.review(snapshotId, \"approved\", userId);\n const baselines = new BaselineModel(db, getStore().storage);\n await baselines.upsert(project.id, snapshot.storyId, snapshot.viewportName, build.gitBranch, snapshot.id, snapshot.screenshotPath);\n await refreshBuild(build.id);\n}\n\n/** Extract a string from a multipart form field, ignoring file entries. */\nfunction asString(value: FormDataEntryValue | null): string | undefined {\n return typeof value === \"string\" ? value : undefined;\n}\n\nexport { asString };"],"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 { HTTPException } from \"hono/http-exception\";\nimport { Readable, Transform } from \"node:stream\";\nimport type { ReadableStream as NodeWebStream } from \"node:stream/web\";\nimport type { Logger } from \"pino\";\nimport type { StorageAdapter } from \"../adapters/storage.ts\";\nimport { emitWebhookEvent } from \"../adapters/webhook-events.ts\";\nimport { extractStorybookToScratch, persistStorybookStatics } from \"../capture/statics.ts\";\nimport { BaselineModel } from \"../models/baseline.ts\";\nimport { BuildModel } from \"../models/build.ts\";\nimport { LabelModel } from \"../models/label.ts\";\nimport { ProjectModel } from \"../models/project.ts\";\nimport { SnapshotModel } from \"../models/snapshot.ts\";\nimport type { Project } from \"../schema/project.ts\";\nimport { getStore } from \"../store.ts\";\nimport { type ProjectRole, BUILD_STATUSES } from \"../types.ts\";\nimport { notFound } from \"./helpers.ts\";\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 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 labels?: { key: string; value: string }[];\n}\n\n/* oxlint-disable eslint/no-await-in-loop -- label attach is intentionally sequential */\nasync function attachLabels(\n db: import(\"../adapters/database.ts\").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\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<import(\"../schema/build.ts\").Build> {\n const { db, config } = getStore();\n if (!meta.gitSha || !meta.gitBranch) {\n throw new HTTPException(400, { message: \"gitSha and gitBranch are required\" });\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 });\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 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 config.secret,\n );\n return build;\n}\n\n/**\n * Stream a raw request body into storage with a byte cap.\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<void> {\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 } catch (error) {\n if (seen > maxBytes) {\n throw new HTTPException(413, { message: \"Upload exceeds size limit\" });\n }\n throw error;\n }\n}\n\n/** Parse a Content-Length header value into bytes, or undefined when unknown. */\nfunction parseContentLength(value: string | undefined): number | undefined {\n if (!value) {\n return undefined;\n }\n const parsed = Math.trunc(Number(value));\n return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : undefined;\n}\n\n/**\n * Best-effort inline statics extraction for small uploads (see\n * `maxInlineUnzipSize`). Never throws: failures are logged and the capture\n * job backfills the same keys idempotently.\n */\nexport async function persistInlineStatics(\n storage: StorageAdapter,\n scratchDir: string | undefined,\n maxInlineBytes: number | undefined,\n contentLength: string | undefined,\n projectId: string,\n buildId: string,\n logger: Logger,\n): Promise<void> {\n if (!scratchDir || !maxInlineBytes) {\n return;\n }\n const size = parseContentLength(contentLength);\n if (size === undefined || size > maxInlineBytes) {\n return;\n }\n try {\n const extractedDir = await extractStorybookToScratch(storage, scratchDir, projectId, buildId);\n await persistStorybookStatics(storage, extractedDir, projectId, buildId);\n logger.info({ buildId, size }, \"upload statics persisted inline\");\n } catch (error) {\n logger.error({ err: error, buildId }, \"inline statics persist failed; capture will backfill\");\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(\n projectId: string,\n buildId: string,\n): Promise<import(\"../schema/build.ts\").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<import(\"../schema/snapshot.ts\").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/** Recompute a build's counts and roll its status up from its snapshots. */\nexport async function refreshBuild(buildId: string): Promise<void> {\n const { db, config } = getStore();\n await new BuildModel(db).updateCounts(buildId);\n const snapshots = await new SnapshotModel(db).listByBuild(buildId);\n const unresolved = snapshots.some((s) => s.status === \"new\" || s.status === \"changed\");\n let status: \"reviewing\" | \"rejected\" | \"approved\";\n if (unresolved) {\n status = \"reviewing\";\n } else {\n const rejected = snapshots.some((s) => s.status === \"rejected\");\n status = rejected ? \"rejected\" : \"approved\";\n }\n const build = await new BuildModel(db).get(buildId);\n if (build) {\n await new BuildModel(db).setStatus(buildId, status);\n await emitWebhookEvent(\n db,\n build.projectId,\n `build:${status}`,\n {\n buildId,\n status,\n snapshotCount: snapshots.length,\n },\n config.secret,\n );\n }\n}\n\n/** Approve a snapshot, promote its screenshot to baseline, and refresh the build. */\nexport async function approveSnapshot(snapshotId: string, userId: string): 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 await snapshots.review(snapshotId, \"approved\", userId);\n const baselines = new BaselineModel(db, getStore().storage, config.secret);\n await baselines.upsert(\n project.id,\n snapshot.storyId,\n snapshot.viewportName,\n build.gitBranch,\n snapshot.id,\n snapshot.screenshotPath,\n );\n await refreshBuild(build.id);\n}\n"],"mappings":"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"}
@@ -2,14 +2,14 @@ import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
4
  import { BuildModel } from "../models/build.mjs";
5
+ import { storybookZipPath } from "../paths.mjs";
6
+ import "../config.mjs";
5
7
  import { getStore } from "../store.mjs";
6
8
  import { resolveAuthorizedProject } from "./helpers.mjs";
7
9
  import { badRequest, buildSchema, forbidden, notFound, unauthorized } from "./schemas.mjs";
8
- import { emitWebhookEvent } from "../adapters/webhook-events.mjs";
9
- import { storybookZipPath } from "../utils/paths.mjs";
10
- import { APPROVER_ROLES, DEVELOPER_ROLES, VIEW_ROLES, asString, buildForProject, buildListQuery, buildUploadSchema } from "./builds.handlers.mjs";
11
- import { registerSnapshots } from "./snapshots.mjs";
10
+ import { APPROVER_ROLES, DEVELOPER_ROLES, VIEW_ROLES, buildCreateJsonSchema, buildForProject, buildListQuery, createBuildRecord, persistInlineStatics, storeUploadStream } from "./builds.handlers.mjs";
12
11
  import { registerComments } from "./comments.mjs";
12
+ import { registerSnapshots } from "./snapshots.mjs";
13
13
  import { createRoute, z } from "@hono/zod-openapi";
14
14
  import { HTTPException } from "hono/http-exception";
15
15
  //#region src/routers/builds.ts
@@ -29,17 +29,48 @@ const listBuildsRoute = createRoute({
29
29
  ...unauthorized
30
30
  }
31
31
  });
32
+ const buildCreatedSchema = z.object({
33
+ build: buildSchema,
34
+ uploadUrl: z.string()
35
+ }).openapi("BuildCreated");
32
36
  const createBuildRoute = createRoute({
33
37
  method: "post",
34
38
  path: "/api/v1/projects/{slug}/builds",
35
39
  request: {
36
40
  params: z.object({ slug: z.string() }),
37
- body: { content: { "multipart/form-data": { schema: buildUploadSchema } } }
41
+ body: { content: { "application/json": { schema: buildCreateJsonSchema } } }
42
+ },
43
+ responses: {
44
+ 202: {
45
+ content: { "application/json": { schema: buildCreatedSchema } },
46
+ description: "Build created. PUT the Storybook zip to uploadUrl to queue capture."
47
+ },
48
+ ...badRequest,
49
+ ...forbidden,
50
+ ...notFound
51
+ }
52
+ });
53
+ const uploadZipRoute = createRoute({
54
+ method: "put",
55
+ path: "/api/v1/projects/{slug}/builds/{buildId}/zip",
56
+ request: {
57
+ params: z.object({
58
+ slug: z.string(),
59
+ buildId: z.string()
60
+ }),
61
+ body: { content: { "application/zip": { schema: z.string().openapi({
62
+ type: "string",
63
+ format: "binary"
64
+ }) } } }
38
65
  },
39
66
  responses: {
40
67
  202: {
41
68
  content: { "application/json": { schema: buildSchema } },
42
- description: "Build created and capture queued"
69
+ description: "Zip stored and capture queued"
70
+ },
71
+ 413: {
72
+ content: { "application/json": { schema: z.object({ message: z.string() }).openapi("Error") } },
73
+ description: "Upload exceeds the configured size limit"
43
74
  },
44
75
  ...badRequest,
45
76
  ...forbidden,
@@ -104,36 +135,25 @@ function registerBuilds(app) {
104
135
  });
105
136
  app.openapi(createBuildRoute, async (c) => {
106
137
  const project = await resolveAuthorizedProject(c, c.req.valid("param").slug, ...DEVELOPER_ROLES);
107
- const form = await c.req.formData();
108
- const gitSha = asString(form.get("gitSha")) ?? "";
109
- const gitBranch = asString(form.get("gitBranch")) ?? "";
110
- if (!gitSha || !gitBranch) throw new HTTPException(400, { message: "gitSha and gitBranch are required" });
111
- const authorEmail = asString(form.get("authorEmail"));
112
- const authorName = asString(form.get("authorName"));
113
- const message = asString(form.get("message"));
114
- const build = await new BuildModel(getStore().db).create(project.id, {
115
- gitSha,
116
- gitBranch,
117
- isDefault: gitBranch === project.gitDefaultBranch,
118
- authorEmail,
119
- authorName,
120
- message
121
- });
122
- await emitWebhookEvent(getStore().db, project.id, "build:created", {
123
- buildId: build.id,
124
- gitSha,
125
- gitBranch,
126
- authorEmail,
127
- authorName,
128
- message
129
- });
130
- const zip = form.get("zip");
131
- if (zip && typeof zip !== "string") {
132
- const buffer = Buffer.from(await zip.arrayBuffer());
133
- await getStore().storage.write(storybookZipPath(project.id, build.id), buffer);
134
- }
138
+ const parsed = buildCreateJsonSchema.safeParse(await c.req.json());
139
+ if (!parsed.success) throw new HTTPException(400, { message: parsed.error.message });
140
+ const build = await createBuildRecord(project, parsed.data);
141
+ const uploadUrl = `/api/v1/projects/${project.slug}/builds/${build.id}/zip`;
142
+ return c.json({
143
+ build,
144
+ uploadUrl
145
+ }, 202);
146
+ });
147
+ app.openapi(uploadZipRoute, async (c) => {
148
+ const { slug, buildId } = c.req.valid("param");
149
+ const project = await resolveAuthorizedProject(c, slug, ...DEVELOPER_ROLES);
150
+ const build = await buildForProject(project.id, buildId);
151
+ const maxBytes = getStore().config.maxUploadBytes ?? 1073741824;
152
+ await storeUploadStream(getStore().storage, storybookZipPath(project.id, build.id), c.req.raw.body, maxBytes);
135
153
  const reqId = c.get("requestId");
136
154
  await getStore().enqueueCapture?.(build.id, reqId);
155
+ const { storage, config, logger } = getStore();
156
+ await persistInlineStatics(storage, config.scratchDir, config.maxInlineUnzipSize, c.req.header("content-length"), project.id, build.id, logger);
137
157
  return c.json(build, 202);
138
158
  });
139
159
  app.openapi(getBuildRoute, async (c) => {
@@ -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 type { ShelfApp } from \"../index.tsx\";\nimport { HTTPException } from \"hono/http-exception\";\n\nimport { BuildModel } from \"../models/build.ts\";\nimport { emitWebhookEvent } from \"../adapters/webhook-events.ts\";\nimport { getStore } from \"../store.ts\";\nimport { storybookZipPath } from \"../utils/paths.ts\";\nimport { resolveAuthorizedProject } from \"./helpers.ts\";\nimport { badRequest, buildSchema, forbidden as forbiddenResponse, notFound as notFoundResponse, unauthorized } from \"./schemas.ts\";\nimport { VIEW_ROLES, DEVELOPER_ROLES, APPROVER_ROLES, buildUploadSchema, buildListQuery, buildForProject, asString } from \"./builds.handlers.ts\";\nimport { registerSnapshots } from \"./snapshots.ts\";\nimport { registerComments } from \"./comments.ts\";\n\nconst listBuildsRoute = createRoute({\n method: \"get\",\n path: \"/api/v1/projects/{slug}/builds\",\n request: { params: z.object({ slug: z.string() }), query: buildListQuery },\n responses: {\n 200: { content: { \"application/json\": { schema: buildSchema.array() } }, description: \"List builds for a project\" },\n ...notFoundResponse,\n ...unauthorized,\n },\n});\n\nconst createBuildRoute = createRoute({\n method: \"post\",\n path: \"/api/v1/projects/{slug}/builds\",\n request: {\n params: z.object({ slug: z.string() }),\n body: { content: { \"multipart/form-data\": { schema: buildUploadSchema } } },\n },\n responses: {\n 202: { content: { \"application/json\": { schema: buildSchema } }, description: \"Build created and capture queued\" },\n ...badRequest,\n ...forbiddenResponse,\n ...notFoundResponse,\n },\n});\n\nconst getBuildRoute = createRoute({\n method: \"get\",\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 path: \"/api/v1/projects/{slug}/builds/{buildId}/retry\",\n request: { params: z.object({ slug: z.string(), buildId: z.string() }) },\n responses: {\n 202: { content: { \"application/json\": { schema: buildSchema } }, description: \"Build reset to pending\" },\n ...notFoundResponse,\n },\n});\n\nconst deleteBuildRoute = createRoute({\n method: \"delete\",\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\n/** Register the build list, upload, fetch, retry, and delete endpoints. */\nexport function registerBuilds(app: ShelfApp): 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, { status, branch: branch ?? undefined, labelKey: labelKey ?? undefined, labelValue: labelValue ?? undefined });\n return c.json(await builds);\n });\n\n app.openapi(createBuildRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...DEVELOPER_ROLES);\n const form = await c.req.formData();\n\n const gitSha = asString(form.get(\"gitSha\")) ?? \"\";\n const gitBranch = asString(form.get(\"gitBranch\")) ?? \"\";\n if (!gitSha || !gitBranch) {\n throw new HTTPException(400, { message: \"gitSha and gitBranch are required\" });\n }\n const authorEmail = asString(form.get(\"authorEmail\"));\n const authorName = asString(form.get(\"authorName\"));\n const message = asString(form.get(\"message\"));\n\n const build = await new BuildModel(getStore().db).create(project.id, {\n gitSha,\n gitBranch,\n isDefault: gitBranch === project.gitDefaultBranch,\n authorEmail,\n authorName,\n message,\n });\n\n await emitWebhookEvent(getStore().db, project.id, \"build:created\", {\n buildId: build.id,\n gitSha,\n gitBranch,\n authorEmail,\n authorName,\n message,\n });\n\n const zip = form.get(\"zip\");\n if (zip && typeof zip !== \"string\") {\n const buffer = Buffer.from(await zip.arrayBuffer());\n await getStore().storage.write(storybookZipPath(project.id, build.id), buffer);\n }\n\n const reqId = c.get(\"requestId\");\n await getStore().enqueueCapture?.(build.id, reqId);\n return c.json(build, 202);\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).setStatus(build.id, \"pending\");\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}"],"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 { HTTPException } from \"hono/http-exception\";\nimport { DEFAULT_MAX_UPLOAD_BYTES } from \"../config.ts\";\nimport type { ShelfApp } from \"../index.tsx\";\nimport { BuildModel } from \"../models/build.ts\";\nimport { getStore } from \"../store.ts\";\nimport { storybookZipPath } from \"../utils/paths.ts\";\nimport {\n VIEW_ROLES,\n DEVELOPER_ROLES,\n APPROVER_ROLES,\n buildCreateJsonSchema,\n buildListQuery,\n buildForProject,\n createBuildRecord,\n persistInlineStatics,\n storeUploadStream,\n} from \"./builds.handlers.ts\";\nimport { registerComments } from \"./comments.ts\";\nimport { resolveAuthorizedProject } from \"./helpers.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\nconst listBuildsRoute = createRoute({\n method: \"get\",\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({ build: buildSchema, uploadUrl: z.string() })\n .openapi(\"BuildCreated\");\n\nconst createBuildRoute = createRoute({\n method: \"post\",\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: \"Build created. PUT the Storybook zip to uploadUrl to queue capture.\",\n },\n ...badRequest,\n ...forbiddenResponse,\n ...notFoundResponse,\n },\n});\n\nconst uploadZipRoute = createRoute({\n method: \"put\",\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: \"Zip stored and capture queued\",\n },\n 413: {\n content: {\n \"application/json\": {\n schema: z.object({ message: z.string() }).openapi(\"Error\"),\n },\n },\n description: \"Upload exceeds the configured size limit\",\n },\n ...badRequest,\n ...forbiddenResponse,\n ...notFoundResponse,\n },\n});\n\nconst getBuildRoute = createRoute({\n method: \"get\",\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 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\",\n },\n ...notFoundResponse,\n },\n});\n\nconst deleteBuildRoute = createRoute({\n method: \"delete\",\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\n/** Register the build list, upload, fetch, retry, and delete endpoints. */\nexport function registerBuilds(app: ShelfApp): 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 parsed = buildCreateJsonSchema.safeParse(await c.req.json());\n if (!parsed.success) {\n throw new HTTPException(400, { message: parsed.error.message });\n }\n const build = await createBuildRecord(project, parsed.data);\n const uploadUrl = `/api/v1/projects/${project.slug}/builds/${build.id}/zip`;\n return c.json({ build, uploadUrl }, 202);\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 await storeUploadStream(\n getStore().storage,\n storybookZipPath(project.id, build.id),\n c.req.raw.body,\n maxBytes,\n );\n\n const reqId = c.get(\"requestId\");\n await getStore().enqueueCapture?.(build.id, reqId);\n const { storage, config, logger } = getStore();\n await persistInlineStatics(\n storage,\n config.scratchDir,\n config.maxInlineUnzipSize,\n c.req.header(\"content-length\"),\n project.id,\n build.id,\n logger,\n );\n return c.json(build, 202);\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).setStatus(build.id, \"pending\");\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}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"comments.mjs","names":["notFound"],"sources":["../../src/routers/comments.ts"],"sourcesContent":["import { createRoute, z } from \"@hono/zod-openapi\";\nimport type { ShelfApp } from \"../index.tsx\";\n\nimport { CommentModel } from \"../models/comment.ts\";\nimport { getStore } from \"../store.ts\";\nimport { resolveAuthorizedProject, notFound as throwNotFound } from \"./helpers.ts\";\nimport { commentSchema, commentCreateSchema, notFound, unauthorized } from \"./schemas.ts\";\nimport { VIEW_ROLES, DEVELOPER_ROLES, buildForProject } from \"./builds.handlers.ts\";\n\nconst listCommentsRoute = createRoute({\n method: \"get\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/comments\",\n request: { params: z.object({ slug: z.string(), buildId: z.string() }) },\n responses: {\n 200: { content: { \"application/json\": { schema: commentSchema.array() } }, description: \"List comments on a build\" },\n ...notFound,\n ...unauthorized,\n },\n});\n\nconst createCommentRoute = createRoute({\n method: \"post\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/comments\",\n request: {\n params: z.object({ slug: z.string(), buildId: z.string() }),\n body: { content: { \"application/json\": { schema: commentCreateSchema } } },\n },\n responses: {\n 201: { content: { \"application/json\": { schema: commentSchema } }, description: \"Comment created\" },\n ...notFound,\n },\n});\n\nconst resolveCommentRoute = createRoute({\n method: \"post\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/comments/{commentId}/resolve\",\n request: { params: z.object({ slug: z.string(), buildId: z.string(), commentId: z.string() }) },\n responses: {\n 200: { content: { \"application/json\": { schema: commentSchema } }, description: \"Comment resolved\" },\n ...notFound,\n },\n});\n\n/** Register the build comment list, create, and resolve endpoints. */\nexport function registerComments(app: ShelfApp): void {\n app.openapi(listCommentsRoute, 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(await new CommentModel(getStore().db).listByBuild(build.id));\n });\n\n app.openapi(createCommentRoute, 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 body = c.req.valid(\"json\");\n const userId = getStore().user?.id ?? \"anonymous\";\n const comment = await new CommentModel(getStore().db).create(project.id, build.id, userId, body);\n return c.json(comment, 201);\n });\n\n app.openapi(resolveCommentRoute, async (c) => {\n const { slug, buildId, commentId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...DEVELOPER_ROLES);\n const build = await buildForProject(project.id, buildId);\n const comment = await new CommentModel(getStore().db).resolve(commentId);\n if (comment.buildId !== build.id) {\n throwNotFound(\"Comment not found\");\n }\n return c.json(comment);\n });\n}"],"mappings":";;;;;;;;;;AASA,MAAM,oBAAoB,YAAY;CACpC,QAAQ;CACR,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,cAAc,MAAM,EAAE,EAAE;GAAG,aAAa;EAA2B;EACnH,GAAGA;EACH,GAAG;CACL;AACF,CAAC;AAED,MAAM,qBAAqB,YAAY;CACrC,QAAQ;CACR,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,oBAAoB,EAAE,EAAE;CAC3E;CACA,WAAW;EACT,KAAK;GAAE,SAAS,EAAE,oBAAoB,EAAE,QAAQ,cAAc,EAAE;GAAG,aAAa;EAAkB;EAClG,GAAGA;CACL;AACF,CAAC;AAED,MAAM,sBAAsB,YAAY;CACtC,QAAQ;CACR,MAAM;CACN,SAAS,EAAE,QAAQ,EAAE,OAAO;EAAE,MAAM,EAAE,OAAO;EAAG,SAAS,EAAE,OAAO;EAAG,WAAW,EAAE,OAAO;CAAE,CAAC,EAAE;CAC9F,WAAW;EACT,KAAK;GAAE,SAAS,EAAE,oBAAoB,EAAE,QAAQ,cAAc,EAAE;GAAG,aAAa;EAAmB;EACnG,GAAGA;CACL;AACF,CAAC;;AAGD,SAAgB,iBAAiB,KAAqB;CACpD,IAAI,QAAQ,mBAAmB,OAAO,MAAM;EAC1C,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,MAAM,IAAI,aAAa,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,YAAY,MAAM,EAAE,CAAC;CAC3E,CAAC;CAED,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,QAAQ,MAAM,gBAAgB,QAAQ,IAAI,OAAO;EACvD,MAAM,OAAO,EAAE,IAAI,MAAM,MAAM;EAC/B,MAAM,SAAS,SAAS,CAAC,CAAC,MAAM,MAAM;EACtC,MAAM,UAAU,MAAM,IAAI,aAAa,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,OAAO,QAAQ,IAAI,MAAM,IAAI,QAAQ,IAAI;EAC/F,OAAO,EAAE,KAAK,SAAS,GAAG;CAC5B,CAAC;CAED,IAAI,QAAQ,qBAAqB,OAAO,MAAM;EAC5C,MAAM,EAAE,MAAM,SAAS,cAAc,EAAE,IAAI,MAAM,OAAO;EACxD,MAAM,UAAU,MAAM,yBAAyB,GAAG,MAAM,GAAG,eAAe;EAC1E,MAAM,QAAQ,MAAM,gBAAgB,QAAQ,IAAI,OAAO;EACvD,MAAM,UAAU,MAAM,IAAI,aAAa,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,QAAQ,SAAS;EACvE,IAAI,QAAQ,YAAY,MAAM,IAC5B,SAAc,mBAAmB;EAEnC,OAAO,EAAE,KAAK,OAAO;CACvB,CAAC;AACH"}
1
+ {"version":3,"file":"comments.mjs","names":["notFound"],"sources":["../../src/routers/comments.ts"],"sourcesContent":["import { createRoute, z } from \"@hono/zod-openapi\";\nimport type { ShelfApp } from \"../index.tsx\";\nimport { CommentModel } from \"../models/comment.ts\";\nimport { getStore } from \"../store.ts\";\nimport { VIEW_ROLES, DEVELOPER_ROLES, buildForProject } from \"./builds.handlers.ts\";\nimport { resolveAuthorizedProject, notFound as throwNotFound } from \"./helpers.ts\";\nimport { commentSchema, commentCreateSchema, notFound, unauthorized } from \"./schemas.ts\";\n\nconst listCommentsRoute = createRoute({\n method: \"get\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/comments\",\n request: { params: z.object({ slug: z.string(), buildId: z.string() }) },\n responses: {\n 200: {\n content: { \"application/json\": { schema: commentSchema.array() } },\n description: \"List comments on a build\",\n },\n ...notFound,\n ...unauthorized,\n },\n});\n\nconst createCommentRoute = createRoute({\n method: \"post\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/comments\",\n request: {\n params: z.object({ slug: z.string(), buildId: z.string() }),\n body: { content: { \"application/json\": { schema: commentCreateSchema } } },\n },\n responses: {\n 201: {\n content: { \"application/json\": { schema: commentSchema } },\n description: \"Comment created\",\n },\n ...notFound,\n },\n});\n\nconst resolveCommentRoute = createRoute({\n method: \"post\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/comments/{commentId}/resolve\",\n request: { params: z.object({ slug: z.string(), buildId: z.string(), commentId: z.string() }) },\n responses: {\n 200: {\n content: { \"application/json\": { schema: commentSchema } },\n description: \"Comment resolved\",\n },\n ...notFound,\n },\n});\n\n/** Register the build comment list, create, and resolve endpoints. */\nexport function registerComments(app: ShelfApp): void {\n app.openapi(listCommentsRoute, 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(await new CommentModel(getStore().db).listByBuild(build.id));\n });\n\n app.openapi(createCommentRoute, 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 body = c.req.valid(\"json\");\n const userId = getStore().user?.id ?? \"anonymous\";\n const comment = await new CommentModel(getStore().db).create(\n project.id,\n build.id,\n userId,\n body,\n );\n return c.json(comment, 201);\n });\n\n app.openapi(resolveCommentRoute, async (c) => {\n const { slug, buildId, commentId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...DEVELOPER_ROLES);\n const build = await buildForProject(project.id, buildId);\n const comment = await new CommentModel(getStore().db).resolve(commentId);\n if (comment.buildId !== build.id) {\n throwNotFound(\"Comment not found\");\n }\n return c.json(comment);\n });\n}\n"],"mappings":";;;;;;;;;;AAQA,MAAM,oBAAoB,YAAY;CACpC,QAAQ;CACR,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,cAAc,MAAM,EAAE,EAAE;GACjE,aAAa;EACf;EACA,GAAGA;EACH,GAAG;CACL;AACF,CAAC;AAED,MAAM,qBAAqB,YAAY;CACrC,QAAQ;CACR,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,oBAAoB,EAAE,EAAE;CAC3E;CACA,WAAW;EACT,KAAK;GACH,SAAS,EAAE,oBAAoB,EAAE,QAAQ,cAAc,EAAE;GACzD,aAAa;EACf;EACA,GAAGA;CACL;AACF,CAAC;AAED,MAAM,sBAAsB,YAAY;CACtC,QAAQ;CACR,MAAM;CACN,SAAS,EAAE,QAAQ,EAAE,OAAO;EAAE,MAAM,EAAE,OAAO;EAAG,SAAS,EAAE,OAAO;EAAG,WAAW,EAAE,OAAO;CAAE,CAAC,EAAE;CAC9F,WAAW;EACT,KAAK;GACH,SAAS,EAAE,oBAAoB,EAAE,QAAQ,cAAc,EAAE;GACzD,aAAa;EACf;EACA,GAAGA;CACL;AACF,CAAC;;AAGD,SAAgB,iBAAiB,KAAqB;CACpD,IAAI,QAAQ,mBAAmB,OAAO,MAAM;EAC1C,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,MAAM,IAAI,aAAa,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,YAAY,MAAM,EAAE,CAAC;CAC3E,CAAC;CAED,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,QAAQ,MAAM,gBAAgB,QAAQ,IAAI,OAAO;EACvD,MAAM,OAAO,EAAE,IAAI,MAAM,MAAM;EAC/B,MAAM,SAAS,SAAS,CAAC,CAAC,MAAM,MAAM;EACtC,MAAM,UAAU,MAAM,IAAI,aAAa,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,OACpD,QAAQ,IACR,MAAM,IACN,QACA,IACF;EACA,OAAO,EAAE,KAAK,SAAS,GAAG;CAC5B,CAAC;CAED,IAAI,QAAQ,qBAAqB,OAAO,MAAM;EAC5C,MAAM,EAAE,MAAM,SAAS,cAAc,EAAE,IAAI,MAAM,OAAO;EACxD,MAAM,UAAU,MAAM,yBAAyB,GAAG,MAAM,GAAG,eAAe;EAC1E,MAAM,QAAQ,MAAM,gBAAgB,QAAQ,IAAI,OAAO;EACvD,MAAM,UAAU,MAAM,IAAI,aAAa,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,QAAQ,SAAS;EACvE,IAAI,QAAQ,YAAY,MAAM,IAC5B,SAAc,mBAAmB;EAEnC,OAAO,EAAE,KAAK,OAAO;CACvB,CAAC;AACH"}
@@ -0,0 +1,137 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { requireSiteAdmin } from "./helpers.mjs";
5
+ //#region src/routers/health.ts
6
+ const PROBE_TIMEOUT_MS = 2e3;
7
+ const DETAIL_MAX_LENGTH = 500;
8
+ function pushTarget(targets, adapter) {
9
+ if (adapter) targets.push(adapter);
10
+ }
11
+ function targetsOf(sources) {
12
+ const targets = [];
13
+ pushTarget(targets, sources.database);
14
+ pushTarget(targets, sources.storage);
15
+ pushTarget(targets, sources.captureRunner);
16
+ pushTarget(targets, sources.captureQueue);
17
+ pushTarget(targets, sources.auth);
18
+ for (const provider of sources.gitHosts ?? []) pushTarget(targets, provider);
19
+ return targets;
20
+ }
21
+ function sanitize(detail) {
22
+ return detail.length > DETAIL_MAX_LENGTH ? detail.slice(0, DETAIL_MAX_LENGTH) : detail;
23
+ }
24
+ function messageOf(reason) {
25
+ return sanitize(reason instanceof Error ? reason.message : String(reason));
26
+ }
27
+ async function timeoutError() {
28
+ await new Promise((resolve) => {
29
+ setTimeout(resolve, PROBE_TIMEOUT_MS);
30
+ });
31
+ throw new Error("health probe timed out");
32
+ }
33
+ function okReport(target, latencyMs, detail) {
34
+ const { category, kind, name } = target.metadata;
35
+ return {
36
+ category,
37
+ kind,
38
+ name,
39
+ state: "ok",
40
+ latencyMs,
41
+ ...detail === void 0 ? {} : { detail }
42
+ };
43
+ }
44
+ function failedReport(target, latencyMs, detail) {
45
+ const { category, kind, name } = target.metadata;
46
+ return {
47
+ category,
48
+ kind,
49
+ name,
50
+ state: "failed",
51
+ latencyMs,
52
+ detail
53
+ };
54
+ }
55
+ function detailOf(result, fallback) {
56
+ return result.detail === void 0 ? fallback : sanitize(result.detail);
57
+ }
58
+ async function raceProbe(target, probe, started) {
59
+ try {
60
+ const result = await Promise.race([probe(), timeoutError()]);
61
+ const latencyMs = Date.now() - started;
62
+ if (!result.ok) return failedReport(target, latencyMs, detailOf(result, "probe reported failure"));
63
+ return okReport(target, latencyMs, result.detail === void 0 ? void 0 : sanitize(result.detail));
64
+ } catch (error) {
65
+ return failedReport(target, Date.now() - started, messageOf(error));
66
+ }
67
+ }
68
+ async function probeHealth(target) {
69
+ const probe = target.lifecycle?.health;
70
+ if (!probe) {
71
+ const { category, kind, name } = target.metadata;
72
+ return {
73
+ category,
74
+ kind,
75
+ name,
76
+ state: "ok"
77
+ };
78
+ }
79
+ return await raceProbe(target, probe, Date.now());
80
+ }
81
+ async function probeTarget(target, initErrors, starting) {
82
+ const { category, kind, name } = target.metadata;
83
+ const initError = initErrors.get(`${category}/${kind}`);
84
+ if (initError !== void 0) return {
85
+ category,
86
+ kind,
87
+ name,
88
+ state: "failed",
89
+ detail: sanitize(initError)
90
+ };
91
+ if (starting) return {
92
+ category,
93
+ kind,
94
+ name,
95
+ state: "starting"
96
+ };
97
+ return await probeHealth(target);
98
+ }
99
+ function collectInitErrors(settled) {
100
+ const initErrors = /* @__PURE__ */ new Map();
101
+ if (settled) for (const failure of settled.failures) initErrors.set(`${failure.category}/${failure.kind}`, failure.error);
102
+ return initErrors;
103
+ }
104
+ function overallStatus(reports) {
105
+ if (reports.some((report) => report.state === "failed")) return "degraded";
106
+ if (reports.some((report) => report.state === "starting")) return "starting";
107
+ return "ok";
108
+ }
109
+ /** Register `GET /api/v1/health` (open liveness) and `POST` (authed deep readiness). */
110
+ function registerHealth(app, deps) {
111
+ app.get("/api/v1/health", (c) => {
112
+ const body = {
113
+ status: "ok",
114
+ uptimeSecs: Math.floor((Date.now() - deps.bootTimeMs) / 1e3),
115
+ version: deps.version
116
+ };
117
+ c.header("Cache-Control", "no-store");
118
+ return c.json(body);
119
+ });
120
+ app.post("/api/v1/health", async (c) => {
121
+ requireSiteAdmin();
122
+ const settled = deps.getSettled();
123
+ const initErrors = collectInitErrors(settled);
124
+ const targets = targetsOf(deps.sources);
125
+ const reports = await Promise.all(targets.map(async (target) => await probeTarget(target, initErrors, settled === null)));
126
+ const body = {
127
+ status: overallStatus(reports),
128
+ adapters: reports
129
+ };
130
+ c.header("Cache-Control", "no-store");
131
+ return c.json(body, body.status === "ok" ? 200 : 503);
132
+ });
133
+ }
134
+ //#endregion
135
+ export { registerHealth };
136
+
137
+ //# sourceMappingURL=health.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"health.mjs","names":[],"sources":["../../src/routers/health.ts"],"sourcesContent":["/**\n * Two-tier health: lightweight liveness (GET, no auth) and deep readiness\n * (POST, site-admin). Neither is gated by adapter init — liveness must\n * answer mid-migration so orchestrators don't kill the machine, and deep\n * reports the init settlement state instead of being masked by the gate.\n */\nimport type { AdapterInitResult, AdapterInitSources } from \"../adapters/init.ts\";\nimport type { AdapterHealth, AdapterLifecycle, AdapterMetadata } from \"../adapters/metadata.ts\";\nimport type { ShelfApp } from \"../index.tsx\";\nimport { requireSiteAdmin } from \"./helpers.ts\";\n\n/** Dependencies for the health routes (no adapter I/O on the GET path). */\nexport interface HealthDeps {\n sources: AdapterInitSources;\n getSettled: () => AdapterInitResult | null;\n bootTimeMs: number;\n version: string;\n}\n\n/** Liveness body: process-level only, safe for unauthenticated probes. */\nexport interface LivenessBody {\n status: \"ok\";\n uptimeSecs: number;\n version: string;\n}\n\ntype AdapterState = \"ok\" | \"starting\" | \"failed\";\n\n/** Per-adapter readiness entry (error text sanitized — no secrets). */\nexport interface AdapterReport {\n category: string;\n kind: string;\n name: string;\n state: AdapterState;\n latencyMs?: number;\n detail?: string;\n}\n\n/** Deep readiness body. */\nexport interface ReadinessBody {\n status: \"ok\" | \"starting\" | \"degraded\";\n adapters: AdapterReport[];\n}\n\ninterface ProbeTarget {\n readonly metadata: AdapterMetadata;\n readonly lifecycle?: AdapterLifecycle;\n}\n\nconst PROBE_TIMEOUT_MS = 2000;\nconst DETAIL_MAX_LENGTH = 500;\n\nfunction pushTarget(targets: ProbeTarget[], adapter: ProbeTarget | undefined): void {\n if (adapter) {\n targets.push(adapter);\n }\n}\n\nfunction targetsOf(sources: AdapterInitSources): ProbeTarget[] {\n const targets: ProbeTarget[] = [];\n pushTarget(targets, sources.database);\n pushTarget(targets, sources.storage);\n pushTarget(targets, sources.captureRunner);\n pushTarget(targets, sources.captureQueue);\n pushTarget(targets, sources.auth);\n for (const provider of sources.gitHosts ?? []) {\n pushTarget(targets, provider);\n }\n return targets;\n}\n\nfunction sanitize(detail: string): string {\n return detail.length > DETAIL_MAX_LENGTH ? detail.slice(0, DETAIL_MAX_LENGTH) : detail;\n}\n\nfunction messageOf(reason: unknown): string {\n const message = reason instanceof Error ? reason.message : String(reason);\n return sanitize(message);\n}\n\nasync function timeoutError(): Promise<never> {\n await new Promise<void>((resolve) => {\n setTimeout(resolve, PROBE_TIMEOUT_MS);\n });\n throw new Error(\"health probe timed out\");\n}\n\nfunction okReport(\n target: ProbeTarget,\n latencyMs: number,\n detail: string | undefined,\n): AdapterReport {\n const { category, kind, name } = target.metadata;\n return {\n category,\n kind,\n name,\n state: \"ok\",\n latencyMs,\n ...(detail === undefined ? {} : { detail }),\n };\n}\n\nfunction failedReport(target: ProbeTarget, latencyMs: number, detail: string): AdapterReport {\n const { category, kind, name } = target.metadata;\n return { category, kind, name, state: \"failed\", latencyMs, detail };\n}\n\nfunction detailOf(result: AdapterHealth, fallback: string): string {\n return result.detail === undefined ? fallback : sanitize(result.detail);\n}\n\nasync function raceProbe(\n target: ProbeTarget,\n probe: () => Promise<AdapterHealth>,\n started: number,\n): Promise<AdapterReport> {\n try {\n const result = await Promise.race([probe(), timeoutError()]);\n const latencyMs = Date.now() - started;\n if (!result.ok) {\n return failedReport(target, latencyMs, detailOf(result, \"probe reported failure\"));\n }\n return okReport(\n target,\n latencyMs,\n result.detail === undefined ? undefined : sanitize(result.detail),\n );\n } catch (error) {\n return failedReport(target, Date.now() - started, messageOf(error));\n }\n}\n\nasync function probeHealth(target: ProbeTarget): Promise<AdapterReport> {\n const probe = target.lifecycle?.health;\n if (!probe) {\n const { category, kind, name } = target.metadata;\n return { category, kind, name, state: \"ok\" };\n }\n return await raceProbe(target, probe, Date.now());\n}\n\nasync function probeTarget(\n target: ProbeTarget,\n initErrors: ReadonlyMap<string, string>,\n starting: boolean,\n): Promise<AdapterReport> {\n const { category, kind, name } = target.metadata;\n const initError = initErrors.get(`${category}/${kind}`);\n if (initError !== undefined) {\n return { category, kind, name, state: \"failed\", detail: sanitize(initError) };\n }\n if (starting) {\n return { category, kind, name, state: \"starting\" };\n }\n return await probeHealth(target);\n}\n\nfunction collectInitErrors(settled: AdapterInitResult | null): ReadonlyMap<string, string> {\n const initErrors = new Map<string, string>();\n if (settled) {\n for (const failure of settled.failures) {\n initErrors.set(`${failure.category}/${failure.kind}`, failure.error);\n }\n }\n return initErrors;\n}\n\nfunction overallStatus(reports: AdapterReport[]): ReadinessBody[\"status\"] {\n if (reports.some((report) => report.state === \"failed\")) {\n return \"degraded\";\n }\n if (reports.some((report) => report.state === \"starting\")) {\n return \"starting\";\n }\n return \"ok\";\n}\n\n/** Register `GET /api/v1/health` (open liveness) and `POST` (authed deep readiness). */\nexport function registerHealth(app: ShelfApp, deps: HealthDeps): void {\n app.get(\"/api/v1/health\", (c) => {\n const body: LivenessBody = {\n status: \"ok\",\n uptimeSecs: Math.floor((Date.now() - deps.bootTimeMs) / 1000),\n version: deps.version,\n };\n c.header(\"Cache-Control\", \"no-store\");\n return c.json(body);\n });\n\n app.post(\"/api/v1/health\", async (c) => {\n requireSiteAdmin();\n const settled = deps.getSettled();\n const initErrors = collectInitErrors(settled);\n const targets = targetsOf(deps.sources);\n const reports = await Promise.all(\n targets.map(async (target) => await probeTarget(target, initErrors, settled === null)),\n );\n const body: ReadinessBody = { status: overallStatus(reports), adapters: reports };\n c.header(\"Cache-Control\", \"no-store\");\n return c.json(body, body.status === \"ok\" ? 200 : 503);\n });\n}\n"],"mappings":"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"}
@@ -1,12 +1,12 @@
1
1
  import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
- import { projects } from "../schema-tables.mjs";
4
+ import { projects } from "../schema/project.mjs";
5
5
  import { ProjectModel } from "../models/project.mjs";
6
+ import { sha256 } from "../utils/hash.mjs";
6
7
  import { getStore } from "../store.mjs";
7
8
  import { MemberModel } from "../models/member.mjs";
8
9
  import { TokenModel } from "../models/token.mjs";
9
- import { sha256 } from "../utils/hash.mjs";
10
10
  import { eq } from "drizzle-orm";
11
11
  import { HTTPException } from "hono/http-exception";
12
12
  //#region src/routers/helpers.ts