@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
@@ -1 +1 @@
1
- {"version":3,"file":"snapshots.mjs","names":[],"sources":["../../src/routers/snapshots.ts"],"sourcesContent":["import { createRoute, z } from \"@hono/zod-openapi\";\nimport type { ShelfApp } from \"../index.tsx\";\n\nimport { SnapshotModel } from \"../models/snapshot.ts\";\nimport { getStore } from \"../store.ts\";\nimport { resolveAuthorizedProject } from \"./helpers.ts\";\nimport { snapshotSchema, okSchema, notFound, unauthorized } from \"./schemas.ts\";\nimport { VIEW_ROLES, APPROVER_ROLES, snapshotForBuild, refreshBuild, approveSnapshot, buildForProject } from \"./builds.handlers.ts\";\n\nconst listSnapshotsRoute = createRoute({\n method: \"get\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/snapshots\",\n request: { params: z.object({ slug: z.string(), buildId: z.string() }) },\n responses: {\n 200: { content: { \"application/json\": { schema: snapshotSchema.array() } }, description: \"List snapshots for a build\" },\n ...notFound,\n ...unauthorized,\n },\n});\n\nconst approveSnapshotRoute = createRoute({\n method: \"post\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/snapshots/{snapshotId}/approve\",\n request: { params: z.object({ slug: z.string(), buildId: z.string(), snapshotId: z.string() }) },\n responses: {\n 200: { content: { \"application/json\": { schema: okSchema } }, description: \"Snapshot approved\" },\n ...notFound,\n },\n});\n\nconst rejectSnapshotRoute = createRoute({\n method: \"post\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/snapshots/{snapshotId}/reject\",\n request: { params: z.object({ slug: z.string(), buildId: z.string(), snapshotId: z.string() }) },\n responses: {\n 200: { content: { \"application/json\": { schema: okSchema } }, description: \"Snapshot rejected\" },\n ...notFound,\n },\n});\n\nconst approveAllRoute = createRoute({\n method: \"post\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/approve-all\",\n request: { params: z.object({ slug: z.string(), buildId: z.string() }) },\n responses: {\n 200: { content: { \"application/json\": { schema: okSchema } }, description: \"All snapshots approved\" },\n ...notFound,\n },\n});\n\nconst rejectAllRoute = createRoute({\n method: \"post\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/reject-all\",\n request: { params: z.object({ slug: z.string(), buildId: z.string() }) },\n responses: {\n 200: { content: { \"application/json\": { schema: okSchema } }, description: \"All snapshots rejected\" },\n ...notFound,\n },\n});\n\n/** Register the snapshot list, approve, reject, and bulk-review endpoints. */\nexport function registerSnapshots(app: ShelfApp): void {\n app.openapi(listSnapshotsRoute, 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 const snapshots = new SnapshotModel(getStore().db).listByBuild(build.id);\n return c.json(await snapshots);\n });\n\n app.openapi(approveSnapshotRoute, async (c) => {\n const { slug, buildId, snapshotId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...APPROVER_ROLES);\n const build = await buildForProject(project.id, buildId);\n await snapshotForBuild(build, snapshotId);\n const userId = getStore().user?.id ?? \"anonymous\";\n await approveSnapshot(snapshotId, userId);\n return c.json({ ok: true });\n });\n\n app.openapi(rejectSnapshotRoute, async (c) => {\n const { slug, buildId, snapshotId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...APPROVER_ROLES);\n const build = await buildForProject(project.id, buildId);\n const snapshot = await snapshotForBuild(build, snapshotId);\n const userId = getStore().user?.id ?? \"anonymous\";\n await new SnapshotModel(getStore().db).review(snapshot.id, \"rejected\", userId);\n await refreshBuild(build.id);\n return c.json({ ok: true });\n });\n\n app.openapi(approveAllRoute, 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 const snapshots = await new SnapshotModel(getStore().db).listByBuild(build.id);\n const userId = getStore().user?.id ?? \"anonymous\";\n await Promise.all(\n snapshots\n .filter((snapshot) => snapshot.status === \"new\" || snapshot.status === \"changed\")\n .map(async (snapshot) => {\n await approveSnapshot(snapshot.id, userId);\n }),\n );\n return c.json({ ok: true });\n });\n\n app.openapi(rejectAllRoute, 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 const snapshots = await new SnapshotModel(getStore().db).listByBuild(build.id);\n const userId = getStore().user?.id ?? \"anonymous\";\n await Promise.all(\n snapshots\n .filter((snapshot) => snapshot.status === \"new\" || snapshot.status === \"changed\")\n .map(async (snapshot) => {\n await new SnapshotModel(getStore().db).review(snapshot.id, \"rejected\", userId);\n }),\n );\n await refreshBuild(build.id);\n return c.json({ ok: true });\n });\n}"],"mappings":"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"}
1
+ {"version":3,"file":"snapshots.mjs","names":[],"sources":["../../src/routers/snapshots.ts"],"sourcesContent":["import { createRoute, z } from \"@hono/zod-openapi\";\nimport type { ShelfApp } from \"../index.tsx\";\nimport { SnapshotModel } from \"../models/snapshot.ts\";\nimport { getStore } from \"../store.ts\";\nimport {\n VIEW_ROLES,\n APPROVER_ROLES,\n snapshotForBuild,\n refreshBuild,\n approveSnapshot,\n buildForProject,\n} from \"./builds.handlers.ts\";\nimport { resolveAuthorizedProject } from \"./helpers.ts\";\nimport { snapshotSchema, okSchema, notFound, unauthorized } from \"./schemas.ts\";\n\nconst listSnapshotsRoute = createRoute({\n method: \"get\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/snapshots\",\n request: { params: z.object({ slug: z.string(), buildId: z.string() }) },\n responses: {\n 200: {\n content: { \"application/json\": { schema: snapshotSchema.array() } },\n description: \"List snapshots for a build\",\n },\n ...notFound,\n ...unauthorized,\n },\n});\n\nconst approveSnapshotRoute = createRoute({\n method: \"post\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/snapshots/{snapshotId}/approve\",\n request: { params: z.object({ slug: z.string(), buildId: z.string(), snapshotId: z.string() }) },\n responses: {\n 200: {\n content: { \"application/json\": { schema: okSchema } },\n description: \"Snapshot approved\",\n },\n ...notFound,\n },\n});\n\nconst rejectSnapshotRoute = createRoute({\n method: \"post\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/snapshots/{snapshotId}/reject\",\n request: { params: z.object({ slug: z.string(), buildId: z.string(), snapshotId: z.string() }) },\n responses: {\n 200: {\n content: { \"application/json\": { schema: okSchema } },\n description: \"Snapshot rejected\",\n },\n ...notFound,\n },\n});\n\nconst approveAllRoute = createRoute({\n method: \"post\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/approve-all\",\n request: { params: z.object({ slug: z.string(), buildId: z.string() }) },\n responses: {\n 200: {\n content: { \"application/json\": { schema: okSchema } },\n description: \"All snapshots approved\",\n },\n ...notFound,\n },\n});\n\nconst rejectAllRoute = createRoute({\n method: \"post\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/reject-all\",\n request: { params: z.object({ slug: z.string(), buildId: z.string() }) },\n responses: {\n 200: {\n content: { \"application/json\": { schema: okSchema } },\n description: \"All snapshots rejected\",\n },\n ...notFound,\n },\n});\n\n/** Register the snapshot list, approve, reject, and bulk-review endpoints. */\nexport function registerSnapshots(app: ShelfApp): void {\n app.openapi(listSnapshotsRoute, 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 const snapshots = new SnapshotModel(getStore().db).listByBuild(build.id);\n return c.json(await snapshots);\n });\n\n app.openapi(approveSnapshotRoute, async (c) => {\n const { slug, buildId, snapshotId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...APPROVER_ROLES);\n const build = await buildForProject(project.id, buildId);\n await snapshotForBuild(build, snapshotId);\n const userId = getStore().user?.id ?? \"anonymous\";\n await approveSnapshot(snapshotId, userId);\n return c.json({ ok: true });\n });\n\n app.openapi(rejectSnapshotRoute, async (c) => {\n const { slug, buildId, snapshotId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...APPROVER_ROLES);\n const build = await buildForProject(project.id, buildId);\n const snapshot = await snapshotForBuild(build, snapshotId);\n const userId = getStore().user?.id ?? \"anonymous\";\n await new SnapshotModel(getStore().db).review(snapshot.id, \"rejected\", userId);\n await refreshBuild(build.id);\n return c.json({ ok: true });\n });\n\n app.openapi(approveAllRoute, 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 const snapshots = await new SnapshotModel(getStore().db).listByBuild(build.id);\n const userId = getStore().user?.id ?? \"anonymous\";\n await Promise.all(\n snapshots\n .filter((snapshot) => snapshot.status === \"new\" || snapshot.status === \"changed\")\n .map(async (snapshot) => {\n await approveSnapshot(snapshot.id, userId);\n }),\n );\n return c.json({ ok: true });\n });\n\n app.openapi(rejectAllRoute, 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 const snapshots = await new SnapshotModel(getStore().db).listByBuild(build.id);\n const userId = getStore().user?.id ?? \"anonymous\";\n await Promise.all(\n snapshots\n .filter((snapshot) => snapshot.status === \"new\" || snapshot.status === \"changed\")\n .map(async (snapshot) => {\n await new SnapshotModel(getStore().db).review(snapshot.id, \"rejected\", userId);\n }),\n );\n await refreshBuild(build.id);\n return c.json({ ok: true });\n });\n}\n"],"mappings":"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"}
@@ -1,10 +1,10 @@
1
1
  import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
+ import { StatusConfigModel } from "../models/status-config.mjs";
4
5
  import { getStore } from "../store.mjs";
5
6
  import { resolveAuthorizedProject } from "./helpers.mjs";
6
7
  import { notFound, statusConfigCreateSchema, statusConfigSchema, unauthorized } from "./schemas.mjs";
7
- import { StatusConfigModel } from "../models/status-config.mjs";
8
8
  import { createRoute, z } from "@hono/zod-openapi";
9
9
  //#region src/routers/status-configs.ts
10
10
  const ADMIN_ROLES = ["admin"];
@@ -1 +1 @@
1
- {"version":3,"file":"status-configs.mjs","names":[],"sources":["../../src/routers/status-configs.ts"],"sourcesContent":["import { createRoute, z } from \"@hono/zod-openapi\";\n\nimport type { ShelfApp } from \"../index.tsx\";\nimport { StatusConfigModel } from \"../models/status-config.ts\";\nimport type { ProjectStatusConfig } from \"../schema.ts\";\nimport { getStore } from \"../store.ts\";\nimport { resolveAuthorizedProject } from \"./helpers.ts\";\nimport { notFound, statusConfigCreateSchema, statusConfigSchema, unauthorized } from \"./schemas.ts\";\n\nconst ADMIN_ROLES = [\"admin\"] as const;\n\nconst listRoute = createRoute({\n method: \"get\",\n path: \"/api/v1/projects/{slug}/status-configs\",\n request: { params: z.object({ slug: z.string() }) },\n responses: {\n 200: { content: { \"application/json\": { schema: statusConfigSchema.array() } }, description: \"List status configs\" },\n ...notFound,\n ...unauthorized,\n },\n});\n\nconst createRouteDef = createRoute({\n method: \"post\",\n path: \"/api/v1/projects/{slug}/status-configs\",\n request: {\n params: z.object({ slug: z.string() }),\n body: { content: { \"application/json\": { schema: statusConfigCreateSchema } } },\n },\n responses: {\n 201: { content: { \"application/json\": { schema: statusConfigSchema } }, description: \"Created status config\" },\n 400: { content: { \"application/json\": { schema: z.object({ message: z.string() }) } }, description: \"Bad request\" },\n ...notFound,\n },\n});\n\nconst deleteRoute = createRoute({\n method: \"delete\",\n path: \"/api/v1/projects/{slug}/status-configs/{id}\",\n request: { params: z.object({ slug: z.string(), id: z.string() }) },\n responses: {\n 204: { description: \"Deleted status config\" },\n ...notFound,\n },\n});\n\nfunction toPublic(row: ProjectStatusConfig): {\n id: string;\n projectId: string;\n provider: string;\n config: Record<string, unknown>;\n hasToken: boolean;\n createdAt: string;\n updatedAt: string;\n} {\n return {\n id: row.id,\n projectId: row.projectId,\n provider: row.provider,\n config: JSON.parse(row.config) as Record<string, unknown>,\n hasToken: true,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n };\n}\n\n/** Register the git status-config list, create, and delete endpoints. */\nexport function registerStatusConfigs(app: ShelfApp): void {\n app.openapi(listRoute, async (c) => {\n const { slug } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, \"admin\");\n const model = new StatusConfigModel(getStore().db, getStore().config.secret);\n const rows = await model.list(project.id);\n return c.json(rows.map((row) => toPublic(row)));\n });\n\n app.openapi(createRouteDef, async (c) => {\n const { slug } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...ADMIN_ROLES);\n const body = c.req.valid(\"json\");\n const providers = getStore().gitHosts;\n const provider = providers.find((p) => p.metadata.kind === body.provider);\n if (!provider) {\n return c.json({ message: `Unknown provider: ${body.provider}` }, 400);\n }\n const parsed = provider.metadata.schema.safeParse(body.config);\n if (!parsed.success) {\n return c.json({ message: parsed.error.message }, 400);\n }\n const model = new StatusConfigModel(getStore().db, getStore().config.secret);\n const row = await model.create(project.id, {\n provider: body.provider,\n config: parsed.data,\n token: body.token,\n });\n return c.json(toPublic(row), 201);\n });\n\n app.openapi(deleteRoute, async (c) => {\n const { slug, id } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...ADMIN_ROLES);\n const model = new StatusConfigModel(getStore().db, getStore().config.secret);\n const existing = await model.get(project.id, id);\n if (!existing) {\n return c.json({ message: \"Status config not found\" }, 404);\n }\n await model.remove(project.id, id);\n return c.body(null, 204);\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"status-configs.mjs","names":[],"sources":["../../src/routers/status-configs.ts"],"sourcesContent":["import { createRoute, z } from \"@hono/zod-openapi\";\nimport type { ShelfApp } from \"../index.tsx\";\nimport { StatusConfigModel } from \"../models/status-config.ts\";\nimport type { ProjectStatusConfig } from \"../schema/status-config.ts\";\nimport { getStore } from \"../store.ts\";\nimport { resolveAuthorizedProject } from \"./helpers.ts\";\nimport { notFound, statusConfigCreateSchema, statusConfigSchema, unauthorized } from \"./schemas.ts\";\n\nconst ADMIN_ROLES = [\"admin\"] as const;\n\nconst listRoute = createRoute({\n method: \"get\",\n path: \"/api/v1/projects/{slug}/status-configs\",\n request: { params: z.object({ slug: z.string() }) },\n responses: {\n 200: {\n content: { \"application/json\": { schema: statusConfigSchema.array() } },\n description: \"List status configs\",\n },\n ...notFound,\n ...unauthorized,\n },\n});\n\nconst createRouteDef = createRoute({\n method: \"post\",\n path: \"/api/v1/projects/{slug}/status-configs\",\n request: {\n params: z.object({ slug: z.string() }),\n body: { content: { \"application/json\": { schema: statusConfigCreateSchema } } },\n },\n responses: {\n 201: {\n content: { \"application/json\": { schema: statusConfigSchema } },\n description: \"Created status config\",\n },\n 400: {\n content: { \"application/json\": { schema: z.object({ message: z.string() }) } },\n description: \"Bad request\",\n },\n ...notFound,\n },\n});\n\nconst deleteRoute = createRoute({\n method: \"delete\",\n path: \"/api/v1/projects/{slug}/status-configs/{id}\",\n request: { params: z.object({ slug: z.string(), id: z.string() }) },\n responses: {\n 204: { description: \"Deleted status config\" },\n ...notFound,\n },\n});\n\nfunction toPublic(row: ProjectStatusConfig): {\n id: string;\n projectId: string;\n provider: string;\n config: Record<string, unknown>;\n hasToken: boolean;\n createdAt: string;\n updatedAt: string;\n} {\n return {\n id: row.id,\n projectId: row.projectId,\n provider: row.provider,\n config: JSON.parse(row.config) as Record<string, unknown>,\n hasToken: true,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n };\n}\n\n/** Register the git status-config list, create, and delete endpoints. */\nexport function registerStatusConfigs(app: ShelfApp): void {\n app.openapi(listRoute, async (c) => {\n const { slug } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, \"admin\");\n const model = new StatusConfigModel(getStore().db, getStore().config.secret);\n const rows = await model.list(project.id);\n return c.json(rows.map((row) => toPublic(row)));\n });\n\n app.openapi(createRouteDef, async (c) => {\n const { slug } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...ADMIN_ROLES);\n const body = c.req.valid(\"json\");\n const providers = getStore().gitHosts;\n const provider = providers.find((p) => p.metadata.kind === body.provider);\n if (!provider) {\n return c.json({ message: `Unknown provider: ${body.provider}` }, 400);\n }\n const parsed = provider.metadata.schema.safeParse(body.config);\n if (!parsed.success) {\n return c.json({ message: parsed.error.message }, 400);\n }\n const model = new StatusConfigModel(getStore().db, getStore().config.secret);\n const row = await model.create(project.id, {\n provider: body.provider,\n config: parsed.data,\n token: body.token,\n });\n return c.json(toPublic(row), 201);\n });\n\n app.openapi(deleteRoute, async (c) => {\n const { slug, id } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...ADMIN_ROLES);\n const model = new StatusConfigModel(getStore().db, getStore().config.secret);\n const existing = await model.get(project.id, id);\n if (!existing) {\n return c.json({ message: \"Status config not found\" }, 404);\n }\n await model.remove(project.id, id);\n return c.body(null, 204);\n });\n}\n"],"mappings":"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"}
@@ -3,11 +3,11 @@ import "node:url";
3
3
  import.meta.url;
4
4
  import { BuildModel, isPublicBuild } from "../models/build.mjs";
5
5
  import { ProjectModel } from "../models/project.mjs";
6
- import { LabelModel } from "../models/label.mjs";
6
+ import { storybookDir } from "../paths.mjs";
7
7
  import { getStore } from "../store.mjs";
8
+ import { LabelModel } from "../models/label.mjs";
8
9
  import { currentProjectRole, notFound } from "./helpers.mjs";
9
- import { storybookDir } from "../utils/paths.mjs";
10
- import { renderStorybookPage } from "../pages/storybook.mjs";
10
+ import { renderStorybookPage, renderStorybookPreparingPage } from "../pages/storybook.mjs";
11
11
  import { posix } from "node:path";
12
12
  //#region src/routers/storybook.ts
13
13
  const VIEW_ROLES = /* @__PURE__ */ new Set([
@@ -56,6 +56,10 @@ async function canViewBuild(build, project) {
56
56
  const role = await currentProjectRole(project.id);
57
57
  return Boolean(role && VIEW_ROLES.has(role));
58
58
  }
59
+ /** Check whether a build's extracted statics are available for serving. */
60
+ async function staticsReady(projectId, buildId) {
61
+ return await getStore().storage.exists(posix.join(storybookDir(projectId, buildId), "iframe.html"));
62
+ }
59
63
  /** Register the published Storybook resolver and static-asset routes. */
60
64
  function registerStorybook(app) {
61
65
  app.get("/projects/:slug/storybook", async (c) => {
@@ -96,7 +100,10 @@ function registerStorybook(app) {
96
100
  const build = await new BuildModel(getStore().db).get(buildId);
97
101
  if (!build || build.projectId !== project.id) notFound("Build not found");
98
102
  if (!await canViewBuild(build, project)) return c.redirect("/auth/login", 302);
99
- if (rest === "") return c.html(renderStorybookPage(project, build, slug), 200);
103
+ if (rest === "") {
104
+ if (!await staticsReady(project.id, build.id)) return c.html(renderStorybookPreparingPage(project, build, slug), 200);
105
+ return c.html(renderStorybookPage(project, build, slug), 200);
106
+ }
100
107
  if (rest.split("/").some((segment) => !isSafeSegment(segment))) return c.notFound();
101
108
  const path = posix.join(storybookDir(project.id, build.id), rest);
102
109
  if (!await getStore().storage.exists(path)) return c.notFound();
@@ -1 +1 @@
1
- {"version":3,"file":"storybook.mjs","names":[],"sources":["../../src/routers/storybook.ts"],"sourcesContent":["import { posix } from \"node:path\";\n\nimport type { ShelfApp } from \"../index.tsx\";\n\nimport { isPublicBuild, BuildModel } from \"../models/build.ts\";\nimport { LabelModel } from \"../models/label.ts\";\nimport { ProjectModel } from \"../models/project.ts\";\nimport { renderStorybookPage } from \"../pages/storybook.tsx\";\nimport { getStore } from \"../store.ts\";\nimport type { Build, Project } from \"../schema.ts\";\nimport { storybookDir } from \"../utils/paths.ts\";\nimport { currentProjectRole, notFound } from \"./helpers.ts\";\n\nconst VIEW_ROLES: ReadonlySet<string> = new Set([\"viewer\", \"developer\", \"approver\", \"admin\"]);\n\nconst MIME: Record<string, string> = {\n \".html\": \"text/html; charset=utf-8\",\n \".htm\": \"text/html; charset=utf-8\",\n \".js\": \"application/javascript\",\n \".mjs\": \"application/javascript\",\n \".json\": \"application/json\",\n \".css\": \"text/css; charset=utf-8\",\n \".png\": \"image/png\",\n \".jpg\": \"image/jpeg\",\n \".jpeg\": \"image/jpeg\",\n \".gif\": \"image/gif\",\n \".svg\": \"image/svg+xml\",\n \".webp\": \"image/webp\",\n \".ico\": \"image/x-icon\",\n \".woff\": \"font/woff\",\n \".woff2\": \"font/woff2\",\n \".ttf\": \"font/ttf\",\n \".map\": \"application/json\",\n \".wasm\": \"application/wasm\",\n};\n\nfunction contentTypeFor(path: string): string {\n const ext = posix.extname(path).toLowerCase();\n return MIME[ext] ?? \"application/octet-stream\";\n}\n\nfunction isSafeSegment(segment: string): boolean {\n return segment !== \"..\" && !segment.startsWith(\"/\") && !segment.includes(\"\\\\\") && !segment.includes(\"..\");\n}\n\n/**\n * Decide whether the current request may view `build`'s published Storybook.\n *\n * Public builds (ADR 0011) are viewable without auth; every other build requires\n * a logged-in session with at least `viewer` membership on the project.\n */\nasync function canViewBuild(build: Pick<Build, \"public\" | \"gitBranch\">, project: Project): Promise<boolean> {\n if (isPublicBuild(project, build)) {\n return true;\n }\n const { authEnabled } = getStore();\n if (!authEnabled) {\n return true;\n }\n const role = await currentProjectRole(project.id);\n return Boolean(role && VIEW_ROLES.has(role));\n}\n\n/** Register the published Storybook resolver and static-asset routes. */\nexport function registerStorybook(app: ShelfApp): void {\n // Resolver: latest published build on the default branch.\n app.get(\"/projects/:slug/storybook\", async (c) => {\n const slug = c.req.param(\"slug\");\n const project = await new ProjectModel(getStore().db).getBySlug(slug);\n if (!project) {\n notFound(\"Project not found\");\n }\n const build = await new BuildModel(getStore().db).latestPublished(project);\n if (!build) {\n notFound(\"No published Storybook for this project\");\n }\n if (!(await canViewBuild(build, project))) {\n return c.redirect(\"/auth/login\", 302);\n }\n return c.redirect(`/projects/${slug}/storybook/build/${build.id}/`, 302);\n });\n\n // Resolver: latest build bearing a label (`build` is a reserved label key).\n app.get(\"/projects/:slug/storybook/:key/:value\", async (c) => {\n const slug = c.req.param(\"slug\");\n const key = c.req.param(\"key\");\n if (key === \"build\") {\n return c.notFound();\n }\n const value = c.req.param(\"value\");\n const project = await new ProjectModel(getStore().db).getBySlug(slug);\n if (!project) {\n notFound(\"Project not found\");\n }\n const buildId = await new LabelModel(getStore().db).latestBuildId(project.id, key, value);\n if (!buildId) {\n notFound(\"No build carries that label\");\n }\n const build = await new BuildModel(getStore().db).get(buildId);\n if (!build || !(await canViewBuild(build, project))) {\n return c.redirect(\"/auth/login\", 302);\n }\n return c.redirect(`/projects/${slug}/storybook/build/${buildId}/`, 302);\n });\n\n // Canonical build route: serves the static Storybook and the landing page.\n app.get(\"/projects/:slug/storybook/build/:buildId/*\", async (c) => {\n const slug = c.req.param(\"slug\");\n const buildId = c.req.param(\"buildId\");\n // Hono's bare `/*` wildcard is not exposed through `param()`, so derive the\n // remainder from the raw request path (un-encoded) and decode it ourselves.\n const base = `/projects/${slug}/storybook/build/${buildId}/`;\n if (!c.req.path.startsWith(base)) {\n return c.notFound();\n }\n let rest: string;\n try {\n rest = decodeURIComponent(c.req.path.slice(base.length));\n } catch {\n return c.notFound();\n }\n const project = await new ProjectModel(getStore().db).getBySlug(slug);\n if (!project) {\n notFound(\"Project not found\");\n }\n const build = await new BuildModel(getStore().db).get(buildId);\n if (!build || build.projectId !== project.id) {\n notFound(\"Build not found\");\n }\n if (!(await canViewBuild(build, project))) {\n return c.redirect(\"/auth/login\", 302);\n }\n\n if (rest === \"\") {\n return c.html(renderStorybookPage(project, build, slug), 200);\n }\n\n const segments = rest.split(\"/\");\n if (segments.some((segment) => !isSafeSegment(segment))) {\n return c.notFound();\n }\n const path = posix.join(storybookDir(project.id, build.id), rest);\n if (!(await getStore().storage.exists(path))) {\n return c.notFound();\n }\n const buffer = await getStore().storage.read(path);\n return c.body(new Uint8Array(buffer), 200, {\n \"content-type\": contentTypeFor(rest),\n \"cache-control\": \"public, max-age=3600\",\n });\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"storybook.mjs","names":[],"sources":["../../src/routers/storybook.ts"],"sourcesContent":["import { posix } from \"node:path\";\nimport type { ShelfApp } from \"../index.tsx\";\nimport { isPublicBuild, BuildModel } from \"../models/build.ts\";\nimport { LabelModel } from \"../models/label.ts\";\nimport { ProjectModel } from \"../models/project.ts\";\nimport { renderStorybookPage, renderStorybookPreparingPage } from \"../pages/storybook.tsx\";\nimport type { Build } from \"../schema/build.ts\";\nimport type { Project } from \"../schema/project.ts\";\nimport { getStore } from \"../store.ts\";\nimport { storybookDir } from \"../utils/paths.ts\";\nimport { currentProjectRole, notFound } from \"./helpers.ts\";\n\nconst VIEW_ROLES: ReadonlySet<string> = new Set([\"viewer\", \"developer\", \"approver\", \"admin\"]);\n\nconst MIME: Record<string, string> = {\n \".html\": \"text/html; charset=utf-8\",\n \".htm\": \"text/html; charset=utf-8\",\n \".js\": \"application/javascript\",\n \".mjs\": \"application/javascript\",\n \".json\": \"application/json\",\n \".css\": \"text/css; charset=utf-8\",\n \".png\": \"image/png\",\n \".jpg\": \"image/jpeg\",\n \".jpeg\": \"image/jpeg\",\n \".gif\": \"image/gif\",\n \".svg\": \"image/svg+xml\",\n \".webp\": \"image/webp\",\n \".ico\": \"image/x-icon\",\n \".woff\": \"font/woff\",\n \".woff2\": \"font/woff2\",\n \".ttf\": \"font/ttf\",\n \".map\": \"application/json\",\n \".wasm\": \"application/wasm\",\n};\n\nfunction contentTypeFor(path: string): string {\n const ext = posix.extname(path).toLowerCase();\n return MIME[ext] ?? \"application/octet-stream\";\n}\n\nfunction isSafeSegment(segment: string): boolean {\n return (\n segment !== \"..\" &&\n !segment.startsWith(\"/\") &&\n !segment.includes(\"\\\\\") &&\n !segment.includes(\"..\")\n );\n}\n\n/**\n * Decide whether the current request may view `build`'s published Storybook.\n *\n * Public builds (ADR 0011) are viewable without auth; every other build requires\n * a logged-in session with at least `viewer` membership on the project.\n */\nasync function canViewBuild(\n build: Pick<Build, \"public\" | \"gitBranch\">,\n project: Project,\n): Promise<boolean> {\n if (isPublicBuild(project, build)) {\n return true;\n }\n const { authEnabled } = getStore();\n if (!authEnabled) {\n return true;\n }\n const role = await currentProjectRole(project.id);\n return Boolean(role && VIEW_ROLES.has(role));\n}\n\n/** Check whether a build's extracted statics are available for serving. */\nasync function staticsReady(projectId: string, buildId: string): Promise<boolean> {\n return await getStore().storage.exists(\n posix.join(storybookDir(projectId, buildId), \"iframe.html\"),\n );\n}\n\n/** Register the published Storybook resolver and static-asset routes. */\nexport function registerStorybook(app: ShelfApp): void {\n // Resolver: latest published build on the default branch.\n app.get(\"/projects/:slug/storybook\", async (c) => {\n const slug = c.req.param(\"slug\");\n const project = await new ProjectModel(getStore().db).getBySlug(slug);\n if (!project) {\n notFound(\"Project not found\");\n }\n const build = await new BuildModel(getStore().db).latestPublished(project);\n if (!build) {\n notFound(\"No published Storybook for this project\");\n }\n if (!(await canViewBuild(build, project))) {\n return c.redirect(\"/auth/login\", 302);\n }\n return c.redirect(`/projects/${slug}/storybook/build/${build.id}/`, 302);\n });\n\n // Resolver: latest build bearing a label (`build` is a reserved label key).\n app.get(\"/projects/:slug/storybook/:key/:value\", async (c) => {\n const slug = c.req.param(\"slug\");\n const key = c.req.param(\"key\");\n if (key === \"build\") {\n return c.notFound();\n }\n const value = c.req.param(\"value\");\n const project = await new ProjectModel(getStore().db).getBySlug(slug);\n if (!project) {\n notFound(\"Project not found\");\n }\n const buildId = await new LabelModel(getStore().db).latestBuildId(project.id, key, value);\n if (!buildId) {\n notFound(\"No build carries that label\");\n }\n const build = await new BuildModel(getStore().db).get(buildId);\n if (!build || !(await canViewBuild(build, project))) {\n return c.redirect(\"/auth/login\", 302);\n }\n return c.redirect(`/projects/${slug}/storybook/build/${buildId}/`, 302);\n });\n\n // Canonical build route: serves the static Storybook and the landing page.\n app.get(\"/projects/:slug/storybook/build/:buildId/*\", async (c) => {\n const slug = c.req.param(\"slug\");\n const buildId = c.req.param(\"buildId\");\n // Hono's bare `/*` wildcard is not exposed through `param()`, so derive the\n // remainder from the raw request path (un-encoded) and decode it ourselves.\n const base = `/projects/${slug}/storybook/build/${buildId}/`;\n if (!c.req.path.startsWith(base)) {\n return c.notFound();\n }\n let rest: string;\n try {\n rest = decodeURIComponent(c.req.path.slice(base.length));\n } catch {\n return c.notFound();\n }\n const project = await new ProjectModel(getStore().db).getBySlug(slug);\n if (!project) {\n notFound(\"Project not found\");\n }\n const build = await new BuildModel(getStore().db).get(buildId);\n if (!build || build.projectId !== project.id) {\n notFound(\"Build not found\");\n }\n if (!(await canViewBuild(build, project))) {\n return c.redirect(\"/auth/login\", 302);\n }\n\n if (rest === \"\") {\n if (!(await staticsReady(project.id, build.id))) {\n return c.html(renderStorybookPreparingPage(project, build, slug), 200);\n }\n return c.html(renderStorybookPage(project, build, slug), 200);\n }\n\n const segments = rest.split(\"/\");\n if (segments.some((segment) => !isSafeSegment(segment))) {\n return c.notFound();\n }\n const path = posix.join(storybookDir(project.id, build.id), rest);\n if (!(await getStore().storage.exists(path))) {\n return c.notFound();\n }\n const buffer = await getStore().storage.read(path);\n return c.body(new Uint8Array(buffer), 200, {\n \"content-type\": contentTypeFor(rest),\n \"cache-control\": \"public, max-age=3600\",\n });\n });\n}\n"],"mappings":"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"}
@@ -1,9 +1,9 @@
1
1
  import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
+ import { randomToken } from "../utils/hash.mjs";
4
5
  import { getStore } from "../store.mjs";
5
6
  import { TokenModel } from "../models/token.mjs";
6
- import { randomToken } from "../utils/hash.mjs";
7
7
  import { notFound, resolveAuthorizedProject } from "./helpers.mjs";
8
8
  import { notFound as notFound$1, tokenCreateSchema, tokenCreatedSchema, tokenPublicSchema, unauthorized } from "./schemas.mjs";
9
9
  import { createRoute, z } from "@hono/zod-openapi";
@@ -1 +1 @@
1
- {"version":3,"file":"tokens.mjs","names":["notFoundResponse"],"sources":["../../src/routers/tokens.ts"],"sourcesContent":["import { createRoute, z } from \"@hono/zod-openapi\";\nimport type { ShelfApp } from \"../index.tsx\";\n\nimport { TokenModel } from \"../models/token.ts\";\nimport { getStore } from \"../store.ts\";\nimport type { ProjectRole } from \"../types.ts\";\nimport { randomToken } from \"../utils/hash.ts\";\nimport { resolveAuthorizedProject, notFound } from \"./helpers.ts\";\nimport {\n notFound as notFoundResponse,\n tokenCreatedSchema,\n tokenCreateSchema,\n tokenPublicSchema,\n unauthorized,\n} from \"./schemas.ts\";\n\nconst ADMIN_ROLES: readonly ProjectRole[] = [\"admin\"];\n\nconst listTokensRoute = createRoute({\n method: \"get\",\n path: \"/api/v1/projects/{slug}/tokens\",\n request: { params: z.object({ slug: z.string() }) },\n responses: {\n 200: { content: { \"application/json\": { schema: tokenPublicSchema.array() } }, description: \"List API tokens\" },\n ...notFoundResponse,\n ...unauthorized,\n },\n});\n\nconst createTokenRoute = createRoute({\n method: \"post\",\n path: \"/api/v1/projects/{slug}/tokens\",\n request: {\n params: z.object({ slug: z.string() }),\n body: { content: { \"application/json\": { schema: tokenCreateSchema } } },\n },\n responses: {\n 201: { content: { \"application/json\": { schema: tokenCreatedSchema } }, description: \"Created token\" },\n ...notFoundResponse,\n },\n});\n\nconst deleteTokenRoute = createRoute({\n method: \"delete\",\n path: \"/api/v1/projects/{slug}/tokens/{tokenId}\",\n request: { params: z.object({ slug: z.string(), tokenId: z.string() }) },\n responses: {\n 204: { description: \"Deleted token\" },\n ...notFoundResponse,\n },\n});\n\n/** Register the project API token list, create, and revoke endpoints. */\nexport function registerTokens(app: ShelfApp): void {\n app.openapi(listTokensRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...ADMIN_ROLES);\n const tokens = await new TokenModel(getStore().db).list(project.id);\n return c.json(tokens.map(({ hash: _hash, ...rest }) => rest));\n });\n\n app.openapi(createTokenRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...ADMIN_ROLES);\n const body = c.req.valid(\"json\");\n const token = randomToken(\"shelf_\");\n await new TokenModel(getStore().db).create(project.id, body.name, token.hash);\n return c.json({ ...body, token: token.value }, 201);\n });\n\n app.openapi(deleteTokenRoute, async (c) => {\n const { slug, tokenId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...ADMIN_ROLES);\n const found = await new TokenModel(getStore().db).get(project.id, tokenId);\n if (!found) {\n notFound(\"Token not found\");\n }\n await new TokenModel(getStore().db).remove(found.id);\n return c.body(null, 204);\n });\n}"],"mappings":";;;;;;;;;;AAgBA,MAAM,cAAsC,CAAC,OAAO;AAEpD,MAAM,kBAAkB,YAAY;CAClC,QAAQ;CACR,MAAM;CACN,SAAS,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,EAAE;CAClD,WAAW;EACT,KAAK;GAAE,SAAS,EAAE,oBAAoB,EAAE,QAAQ,kBAAkB,MAAM,EAAE,EAAE;GAAG,aAAa;EAAkB;EAC9G,GAAGA;EACH,GAAG;CACL;AACF,CAAC;AAED,MAAM,mBAAmB,YAAY;CACnC,QAAQ;CACR,MAAM;CACN,SAAS;EACP,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;EACrC,MAAM,EAAE,SAAS,EAAE,oBAAoB,EAAE,QAAQ,kBAAkB,EAAE,EAAE;CACzE;CACA,WAAW;EACT,KAAK;GAAE,SAAS,EAAE,oBAAoB,EAAE,QAAQ,mBAAmB,EAAE;GAAG,aAAa;EAAgB;EACrG,GAAGA;CACL;AACF,CAAC;AAED,MAAM,mBAAmB,YAAY;CACnC,QAAQ;CACR,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;;AAGD,SAAgB,eAAe,KAAqB;CAClD,IAAI,QAAQ,iBAAiB,OAAO,MAAM;EACxC,MAAM,UAAU,MAAM,yBAAyB,GAAG,EAAE,IAAI,MAAM,OAAO,CAAC,CAAC,MAAM,GAAG,WAAW;EAC3F,MAAM,SAAS,MAAM,IAAI,WAAW,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,QAAQ,EAAE;EAClE,OAAO,EAAE,KAAK,OAAO,KAAK,EAAE,MAAM,OAAO,GAAG,WAAW,IAAI,CAAC;CAC9D,CAAC;CAED,IAAI,QAAQ,kBAAkB,OAAO,MAAM;EACzC,MAAM,UAAU,MAAM,yBAAyB,GAAG,EAAE,IAAI,MAAM,OAAO,CAAC,CAAC,MAAM,GAAG,WAAW;EAC3F,MAAM,OAAO,EAAE,IAAI,MAAM,MAAM;EAC/B,MAAM,QAAQ,YAAY,QAAQ;EAClC,MAAM,IAAI,WAAW,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,OAAO,QAAQ,IAAI,KAAK,MAAM,MAAM,IAAI;EAC5E,OAAO,EAAE,KAAK;GAAE,GAAG;GAAM,OAAO,MAAM;EAAM,GAAG,GAAG;CACpD,CAAC;CAED,IAAI,QAAQ,kBAAkB,OAAO,MAAM;EACzC,MAAM,EAAE,MAAM,YAAY,EAAE,IAAI,MAAM,OAAO;EAC7C,MAAM,UAAU,MAAM,yBAAyB,GAAG,MAAM,GAAG,WAAW;EACtE,MAAM,QAAQ,MAAM,IAAI,WAAW,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,QAAQ,IAAI,OAAO;EACzE,IAAI,CAAC,OACH,SAAS,iBAAiB;EAE5B,MAAM,IAAI,WAAW,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,OAAO,MAAM,EAAE;EACnD,OAAO,EAAE,KAAK,MAAM,GAAG;CACzB,CAAC;AACH"}
1
+ {"version":3,"file":"tokens.mjs","names":["notFoundResponse"],"sources":["../../src/routers/tokens.ts"],"sourcesContent":["import { createRoute, z } from \"@hono/zod-openapi\";\nimport type { ShelfApp } from \"../index.tsx\";\nimport { TokenModel } from \"../models/token.ts\";\nimport { getStore } from \"../store.ts\";\nimport type { ProjectRole } from \"../types.ts\";\nimport { randomToken } from \"../utils/hash.ts\";\nimport { resolveAuthorizedProject, notFound } from \"./helpers.ts\";\nimport {\n notFound as notFoundResponse,\n tokenCreatedSchema,\n tokenCreateSchema,\n tokenPublicSchema,\n unauthorized,\n} from \"./schemas.ts\";\n\nconst ADMIN_ROLES: readonly ProjectRole[] = [\"admin\"];\n\nconst listTokensRoute = createRoute({\n method: \"get\",\n path: \"/api/v1/projects/{slug}/tokens\",\n request: { params: z.object({ slug: z.string() }) },\n responses: {\n 200: {\n content: { \"application/json\": { schema: tokenPublicSchema.array() } },\n description: \"List API tokens\",\n },\n ...notFoundResponse,\n ...unauthorized,\n },\n});\n\nconst createTokenRoute = createRoute({\n method: \"post\",\n path: \"/api/v1/projects/{slug}/tokens\",\n request: {\n params: z.object({ slug: z.string() }),\n body: { content: { \"application/json\": { schema: tokenCreateSchema } } },\n },\n responses: {\n 201: {\n content: { \"application/json\": { schema: tokenCreatedSchema } },\n description: \"Created token\",\n },\n ...notFoundResponse,\n },\n});\n\nconst deleteTokenRoute = createRoute({\n method: \"delete\",\n path: \"/api/v1/projects/{slug}/tokens/{tokenId}\",\n request: { params: z.object({ slug: z.string(), tokenId: z.string() }) },\n responses: {\n 204: { description: \"Deleted token\" },\n ...notFoundResponse,\n },\n});\n\n/** Register the project API token list, create, and revoke endpoints. */\nexport function registerTokens(app: ShelfApp): void {\n app.openapi(listTokensRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...ADMIN_ROLES);\n const tokens = await new TokenModel(getStore().db).list(project.id);\n return c.json(tokens.map(({ hash: _hash, ...rest }) => rest));\n });\n\n app.openapi(createTokenRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...ADMIN_ROLES);\n const body = c.req.valid(\"json\");\n const token = randomToken(\"shelf_\");\n await new TokenModel(getStore().db).create(project.id, body.name, token.hash);\n return c.json({ ...body, token: token.value }, 201);\n });\n\n app.openapi(deleteTokenRoute, async (c) => {\n const { slug, tokenId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...ADMIN_ROLES);\n const found = await new TokenModel(getStore().db).get(project.id, tokenId);\n if (!found) {\n notFound(\"Token not found\");\n }\n await new TokenModel(getStore().db).remove(found.id);\n return c.body(null, 204);\n });\n}\n"],"mappings":";;;;;;;;;;AAeA,MAAM,cAAsC,CAAC,OAAO;AAEpD,MAAM,kBAAkB,YAAY;CAClC,QAAQ;CACR,MAAM;CACN,SAAS,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,EAAE;CAClD,WAAW;EACT,KAAK;GACH,SAAS,EAAE,oBAAoB,EAAE,QAAQ,kBAAkB,MAAM,EAAE,EAAE;GACrE,aAAa;EACf;EACA,GAAGA;EACH,GAAG;CACL;AACF,CAAC;AAED,MAAM,mBAAmB,YAAY;CACnC,QAAQ;CACR,MAAM;CACN,SAAS;EACP,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;EACrC,MAAM,EAAE,SAAS,EAAE,oBAAoB,EAAE,QAAQ,kBAAkB,EAAE,EAAE;CACzE;CACA,WAAW;EACT,KAAK;GACH,SAAS,EAAE,oBAAoB,EAAE,QAAQ,mBAAmB,EAAE;GAC9D,aAAa;EACf;EACA,GAAGA;CACL;AACF,CAAC;AAED,MAAM,mBAAmB,YAAY;CACnC,QAAQ;CACR,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;;AAGD,SAAgB,eAAe,KAAqB;CAClD,IAAI,QAAQ,iBAAiB,OAAO,MAAM;EACxC,MAAM,UAAU,MAAM,yBAAyB,GAAG,EAAE,IAAI,MAAM,OAAO,CAAC,CAAC,MAAM,GAAG,WAAW;EAC3F,MAAM,SAAS,MAAM,IAAI,WAAW,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,QAAQ,EAAE;EAClE,OAAO,EAAE,KAAK,OAAO,KAAK,EAAE,MAAM,OAAO,GAAG,WAAW,IAAI,CAAC;CAC9D,CAAC;CAED,IAAI,QAAQ,kBAAkB,OAAO,MAAM;EACzC,MAAM,UAAU,MAAM,yBAAyB,GAAG,EAAE,IAAI,MAAM,OAAO,CAAC,CAAC,MAAM,GAAG,WAAW;EAC3F,MAAM,OAAO,EAAE,IAAI,MAAM,MAAM;EAC/B,MAAM,QAAQ,YAAY,QAAQ;EAClC,MAAM,IAAI,WAAW,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,OAAO,QAAQ,IAAI,KAAK,MAAM,MAAM,IAAI;EAC5E,OAAO,EAAE,KAAK;GAAE,GAAG;GAAM,OAAO,MAAM;EAAM,GAAG,GAAG;CACpD,CAAC;CAED,IAAI,QAAQ,kBAAkB,OAAO,MAAM;EACzC,MAAM,EAAE,MAAM,YAAY,EAAE,IAAI,MAAM,OAAO;EAC7C,MAAM,UAAU,MAAM,yBAAyB,GAAG,MAAM,GAAG,WAAW;EACtE,MAAM,QAAQ,MAAM,IAAI,WAAW,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,QAAQ,IAAI,OAAO;EACzE,IAAI,CAAC,OACH,SAAS,iBAAiB;EAE5B,MAAM,IAAI,WAAW,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,OAAO,MAAM,EAAE;EACnD,OAAO,EAAE,KAAK,MAAM,GAAG;CACzB,CAAC;AACH"}
@@ -3,19 +3,19 @@ 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 { LabelModel } from "../models/label.mjs";
6
+ import { BaselineModel } from "../models/baseline.mjs";
7
+ import { SnapshotModel } from "../models/snapshot.mjs";
7
8
  import { getStore } from "../store.mjs";
9
+ import { LabelModel } from "../models/label.mjs";
8
10
  import { currentProjectRole } from "./helpers.mjs";
9
11
  import { hxRedirect } from "./htmx.mjs";
10
- import { BaselineModel } from "../models/baseline.mjs";
11
- import { SnapshotModel } from "../models/snapshot.mjs";
12
12
  import { CommentModel } from "../models/comment.mjs";
13
13
  import { renderBuildDetailPage } from "../pages/build-detail.mjs";
14
14
  import { renderBuildDiffPage } from "../pages/build-diff.mjs";
15
15
  import { renderActiveQueue, renderComputeJobsPage } from "../pages/compute-jobs.mjs";
16
16
  import { renderLabelDetailPage, renderLabelsPage } from "../pages/label-detail.mjs";
17
- import { renderProjectCreatePage } from "../pages/project-create.mjs";
18
17
  import { renderProjectBuildsPage } from "../pages/project-builds.mjs";
18
+ import { renderProjectCreatePage } from "../pages/project-create.mjs";
19
19
  import { renderProjectsPage } from "../pages/projects.mjs";
20
20
  import { renderRootPage } from "../pages/root.mjs";
21
21
  import { registerSettingsPages } from "./settings.mjs";
@@ -1 +1 @@
1
- {"version":3,"file":"ui.mjs","names":[],"sources":["../../src/routers/ui.ts"],"sourcesContent":["import type { ShelfApp } from \"../index.tsx\";\n\nimport { BaselineModel } from \"../models/baseline.ts\";\nimport { BuildModel } from \"../models/build.ts\";\nimport { CommentModel } from \"../models/comment.ts\";\nimport { LabelModel } from \"../models/label.ts\";\nimport { ProjectModel } from \"../models/project.ts\";\nimport { SnapshotModel } from \"../models/snapshot.ts\";\nimport { renderBuildDetailPage } from \"../pages/build-detail.tsx\";\nimport { renderBuildDiffPage } from \"../pages/build-diff.tsx\";\nimport { renderComputeJobsPage, renderActiveQueue } from \"../pages/compute-jobs.tsx\";\nimport { renderLabelDetailPage, renderLabelsPage } from \"../pages/label-detail.tsx\";\nimport { renderProjectCreatePage } from \"../pages/project-create.tsx\";\nimport { renderProjectBuildsPage } from \"../pages/project-builds.tsx\";\nimport { renderProjectsPage } from \"../pages/projects.tsx\";\nimport { renderRootPage } from \"../pages/root.tsx\";\nimport { getStore } from \"../store.ts\";\nimport type { ProjectRole } from \"../types.ts\";\nimport { currentProjectRole } from \"./helpers.ts\";\nimport { hxRedirect } from \"./htmx.ts\";\nimport { registerSettingsPages } from \"./settings.ts\";\n\nfunction asString(value: FormDataEntryValue | null): string | undefined {\n return typeof value === \"string\" ? value.trim() : undefined;\n}\n\nfunction decodeRest(value: string): string {\n try {\n return decodeURIComponent(value);\n } catch {\n return \"\";\n }\n}\n\ninterface QueueView {\n buildId: string;\n status: string;\n queuedAt: string;\n startedAt?: string;\n finishedAt?: string;\n error?: string;\n}\n\nasync function getQueueView(): Promise<QueueView[]> {\n const queue = getStore().captureQueue;\n if (!queue) {\n return [];\n }\n const active = await queue.active();\n return active.map((entry) => ({\n buildId: entry.buildId,\n status: entry.status,\n queuedAt: entry.queuedAt,\n startedAt: entry.startedAt,\n finishedAt: entry.finishedAt,\n error: entry.error,\n }));\n}\n\nasync function canManageJobs(projectId: string): Promise<boolean> {\n const { authEnabled } = getStore();\n if (!authEnabled) {\n return true;\n }\n const roles: readonly ProjectRole[] = [\"developer\", \"approver\", \"admin\"];\n const role = await currentProjectRole(projectId);\n return Boolean(role && roles.includes(role));\n}\n\n/** Register the server-rendered HTML pages (projects, builds, diffs, labels, settings). */\nexport function registerUiPages(app: ShelfApp): void {\n // eslint-disable-next-line promise-function-async -- renderRootPage returns RenderedContent (string | Promise<string>)\n app.get(\"/\", async (c) => c.html(await renderRootPage()));\n app.get(\"/projects\", async (c) => c.html(await renderProjectsPage()));\n // eslint-disable-next-line promise-function-async -- renderProjectCreatePage returns RenderedContent (string | Promise<string>)\n app.get(\"/projects/new\", async (c) => c.html(await renderProjectCreatePage()));\n\n app.post(\"/projects/new\", async (c) => {\n const form = await c.req.formData();\n const name = asString(form.get(\"name\"));\n const gitRepository = asString(form.get(\"gitRepository\"));\n const gitDefaultBranch = asString(form.get(\"gitDefaultBranch\"));\n if (!name) {\n return c.html(renderProjectCreatePage({ values: { name, gitRepository, gitDefaultBranch }, errors: { name: \"Name is required\" } }), 400);\n }\n try {\n const project = await new ProjectModel(getStore().db).create({ name, gitRepository, gitDefaultBranch });\n await new LabelModel(getStore().db).seedFor(project.id);\n return hxRedirect(c, `/projects/${project.slug}/builds`);\n } catch (error) {\n const message = error instanceof Error ? error.message : \"Failed to create project\";\n return c.html(renderProjectCreatePage({ values: { name, gitRepository, gitDefaultBranch }, globalError: message }), 400);\n }\n });\n\n app.get(\"/projects/:slug/builds\", async (c) => {\n const html = await renderProjectBuildsPage(c.req.param(\"slug\"), {\n status: c.req.query(\"status\"),\n branch: c.req.query(\"branch\"),\n });\n if (!html) {\n return c.notFound();\n }\n return c.html(html);\n });\n\n app.get(\"/projects/:slug/builds/:buildId\", async (c) => {\n const html = await renderBuildDetailPage(c.req.param(\"buildId\"));\n if (!html) {\n return c.notFound();\n }\n return c.html(html);\n });\n\n app.get(\"/projects/:slug/jobs\", async (c) => {\n const slug = c.req.param(\"slug\");\n const project = await new ProjectModel(getStore().db).getBySlug(slug);\n if (!project) {\n return c.notFound();\n }\n const queueView = await getQueueView();\n const canRetry = await canManageJobs(project.id);\n if (c.req.query(\"partial\") === \"queue\") {\n return c.html(renderActiveQueue(slug, queueView));\n }\n const html = await renderComputeJobsPage(slug, queueView, canRetry);\n if (!html) {\n return c.notFound();\n }\n return c.html(html);\n });\n\n app.get(\"/projects/:slug/builds/:buildId/diff\", async (c) => {\n const slug = c.req.param(\"slug\");\n const buildId = c.req.param(\"buildId\");\n const snapshotId = c.req.query(\"snapshot\");\n const project = await new ProjectModel(getStore().db).getBySlug(slug);\n if (!project) {\n return c.notFound();\n }\n const build = await new BuildModel(getStore().db).get(buildId);\n if (!build || build.projectId !== project.id) {\n return c.notFound();\n }\n const snapshots = await new SnapshotModel(getStore().db).listByBuild(build.id);\n const comments = await new CommentModel(getStore().db).listByBuild(build.id);\n const baselines = new BaselineModel(getStore().db, getStore().storage);\n const hasBaselineEntries = await Promise.all(\n snapshots.map(async (snapshot) => {\n const baseline = await baselines.resolve(project.id, snapshot.storyId, snapshot.viewportName, build.gitBranch, project.gitDefaultBranch);\n return [snapshot.id, Boolean(baseline)] as const;\n }),\n );\n const hasBaseline = Object.fromEntries(hasBaselineEntries);\n const { user, authEnabled } = getStore();\n const canReview = !authEnabled || Boolean(user);\n return c.html(\n renderBuildDiffPage({\n project,\n build,\n snapshots,\n comments,\n selectedId: snapshotId,\n canReview,\n hasBaseline,\n }),\n );\n });\n\n app.get(\"/projects/:slug/labels\", async (c) => {\n const html = await renderLabelsPage(c.req.param(\"slug\"));\n if (!html) {\n return c.notFound();\n }\n return c.html(html);\n });\n\n app.get(\"/projects/:slug/labels/:key/*\", async (c) => {\n const slug = c.req.param(\"slug\");\n const key = c.req.param(\"key\");\n // Hono's bare `/*` wildcard is not exposed through `param()`, so derive the\n // label value from the raw request path and decode it ourselves.\n const base = `/projects/${slug}/labels/${key}/`;\n const value = c.req.path.startsWith(base) ? decodeRest(c.req.path.slice(base.length)) : \"\";\n const html = await renderLabelDetailPage(slug, key, value);\n if (!html) {\n return c.notFound();\n }\n return c.html(html);\n });\n\n registerSettingsPages(app);\n}"],"mappings":"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"}
1
+ {"version":3,"file":"ui.mjs","names":[],"sources":["../../src/routers/ui.ts"],"sourcesContent":["import type { ShelfApp } from \"../index.tsx\";\nimport { BaselineModel } from \"../models/baseline.ts\";\nimport { BuildModel } from \"../models/build.ts\";\nimport { CommentModel } from \"../models/comment.ts\";\nimport { LabelModel } from \"../models/label.ts\";\nimport { ProjectModel } from \"../models/project.ts\";\nimport { SnapshotModel } from \"../models/snapshot.ts\";\nimport { renderBuildDetailPage } from \"../pages/build-detail.tsx\";\nimport { renderBuildDiffPage } from \"../pages/build-diff.tsx\";\nimport { renderComputeJobsPage, renderActiveQueue } from \"../pages/compute-jobs.tsx\";\nimport { renderLabelDetailPage, renderLabelsPage } from \"../pages/label-detail.tsx\";\nimport { renderProjectBuildsPage } from \"../pages/project-builds.tsx\";\nimport { renderProjectCreatePage } from \"../pages/project-create.tsx\";\nimport { renderProjectsPage } from \"../pages/projects.tsx\";\nimport { renderRootPage } from \"../pages/root.tsx\";\nimport { getStore } from \"../store.ts\";\nimport type { ProjectRole } from \"../types.ts\";\nimport { currentProjectRole } from \"./helpers.ts\";\nimport { hxRedirect } from \"./htmx.ts\";\nimport { registerSettingsPages } from \"./settings.ts\";\n\nfunction asString(value: FormDataEntryValue | null): string | undefined {\n return typeof value === \"string\" ? value.trim() : undefined;\n}\n\nfunction decodeRest(value: string): string {\n try {\n return decodeURIComponent(value);\n } catch {\n return \"\";\n }\n}\n\ninterface QueueView {\n buildId: string;\n status: string;\n queuedAt: string;\n startedAt?: string;\n finishedAt?: string;\n error?: string;\n}\n\nasync function getQueueView(): Promise<QueueView[]> {\n const queue = getStore().captureQueue;\n if (!queue) {\n return [];\n }\n const active = await queue.active();\n return active.map((entry) => ({\n buildId: entry.buildId,\n status: entry.status,\n queuedAt: entry.queuedAt,\n startedAt: entry.startedAt,\n finishedAt: entry.finishedAt,\n error: entry.error,\n }));\n}\n\nasync function canManageJobs(projectId: string): Promise<boolean> {\n const { authEnabled } = getStore();\n if (!authEnabled) {\n return true;\n }\n const roles: readonly ProjectRole[] = [\"developer\", \"approver\", \"admin\"];\n const role = await currentProjectRole(projectId);\n return Boolean(role && roles.includes(role));\n}\n\n/** Register the server-rendered HTML pages (projects, builds, diffs, labels, settings). */\nexport function registerUiPages(app: ShelfApp): void {\n // eslint-disable-next-line promise-function-async -- renderRootPage returns RenderedContent (string | Promise<string>)\n app.get(\"/\", async (c) => c.html(await renderRootPage()));\n app.get(\"/projects\", async (c) => c.html(await renderProjectsPage()));\n // eslint-disable-next-line promise-function-async -- renderProjectCreatePage returns RenderedContent (string | Promise<string>)\n app.get(\"/projects/new\", async (c) => c.html(await renderProjectCreatePage()));\n\n app.post(\"/projects/new\", async (c) => {\n const form = await c.req.formData();\n const name = asString(form.get(\"name\"));\n const gitRepository = asString(form.get(\"gitRepository\"));\n const gitDefaultBranch = asString(form.get(\"gitDefaultBranch\"));\n if (!name) {\n return c.html(\n renderProjectCreatePage({\n values: { name, gitRepository, gitDefaultBranch },\n errors: { name: \"Name is required\" },\n }),\n 400,\n );\n }\n try {\n const project = await new ProjectModel(getStore().db).create({\n name,\n gitRepository,\n gitDefaultBranch,\n });\n await new LabelModel(getStore().db).seedFor(project.id);\n return hxRedirect(c, `/projects/${project.slug}/builds`);\n } catch (error) {\n const message = error instanceof Error ? error.message : \"Failed to create project\";\n return c.html(\n renderProjectCreatePage({\n values: { name, gitRepository, gitDefaultBranch },\n globalError: message,\n }),\n 400,\n );\n }\n });\n\n app.get(\"/projects/:slug/builds\", async (c) => {\n const html = await renderProjectBuildsPage(c.req.param(\"slug\"), {\n status: c.req.query(\"status\"),\n branch: c.req.query(\"branch\"),\n });\n if (!html) {\n return c.notFound();\n }\n return c.html(html);\n });\n\n app.get(\"/projects/:slug/builds/:buildId\", async (c) => {\n const html = await renderBuildDetailPage(c.req.param(\"buildId\"));\n if (!html) {\n return c.notFound();\n }\n return c.html(html);\n });\n\n app.get(\"/projects/:slug/jobs\", async (c) => {\n const slug = c.req.param(\"slug\");\n const project = await new ProjectModel(getStore().db).getBySlug(slug);\n if (!project) {\n return c.notFound();\n }\n const queueView = await getQueueView();\n const canRetry = await canManageJobs(project.id);\n if (c.req.query(\"partial\") === \"queue\") {\n return c.html(renderActiveQueue(slug, queueView));\n }\n const html = await renderComputeJobsPage(slug, queueView, canRetry);\n if (!html) {\n return c.notFound();\n }\n return c.html(html);\n });\n\n app.get(\"/projects/:slug/builds/:buildId/diff\", async (c) => {\n const slug = c.req.param(\"slug\");\n const buildId = c.req.param(\"buildId\");\n const snapshotId = c.req.query(\"snapshot\");\n const project = await new ProjectModel(getStore().db).getBySlug(slug);\n if (!project) {\n return c.notFound();\n }\n const build = await new BuildModel(getStore().db).get(buildId);\n if (!build || build.projectId !== project.id) {\n return c.notFound();\n }\n const snapshots = await new SnapshotModel(getStore().db).listByBuild(build.id);\n const comments = await new CommentModel(getStore().db).listByBuild(build.id);\n const baselines = new BaselineModel(getStore().db, getStore().storage);\n const hasBaselineEntries = await Promise.all(\n snapshots.map(async (snapshot) => {\n const baseline = await baselines.resolve(\n project.id,\n snapshot.storyId,\n snapshot.viewportName,\n build.gitBranch,\n project.gitDefaultBranch,\n );\n return [snapshot.id, Boolean(baseline)] as const;\n }),\n );\n const hasBaseline = Object.fromEntries(hasBaselineEntries);\n const { user, authEnabled } = getStore();\n const canReview = !authEnabled || Boolean(user);\n return c.html(\n renderBuildDiffPage({\n project,\n build,\n snapshots,\n comments,\n selectedId: snapshotId,\n canReview,\n hasBaseline,\n }),\n );\n });\n\n app.get(\"/projects/:slug/labels\", async (c) => {\n const html = await renderLabelsPage(c.req.param(\"slug\"));\n if (!html) {\n return c.notFound();\n }\n return c.html(html);\n });\n\n app.get(\"/projects/:slug/labels/:key/*\", async (c) => {\n const slug = c.req.param(\"slug\");\n const key = c.req.param(\"key\");\n // Hono's bare `/*` wildcard is not exposed through `param()`, so derive the\n // label value from the raw request path and decode it ourselves.\n const base = `/projects/${slug}/labels/${key}/`;\n const value = c.req.path.startsWith(base) ? decodeRest(c.req.path.slice(base.length)) : \"\";\n const html = await renderLabelDetailPage(slug, key, value);\n if (!html) {\n return c.notFound();\n }\n return c.html(html);\n });\n\n registerSettingsPages(app);\n}\n"],"mappings":"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"}
@@ -1,11 +1,11 @@
1
1
  import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
- import { getStore } from "../store.mjs";
4
+ import { WebhookModel } from "../models/webhook.mjs";
5
5
  import { randomToken } from "../utils/hash.mjs";
6
+ import { getStore } from "../store.mjs";
6
7
  import { notFound, resolveAuthorizedProject } from "./helpers.mjs";
7
8
  import { notFound as notFound$1, unauthorized, webhookCreateSchema, webhookCreatedSchema, webhookPublicSchema } from "./schemas.mjs";
8
- import { WebhookModel } from "../models/webhook.mjs";
9
9
  import { createRoute, z } from "@hono/zod-openapi";
10
10
  //#region src/routers/webhooks.ts
11
11
  const ADMIN_ROLES = ["admin"];
@@ -53,7 +53,7 @@ const deleteWebhookRoute = createRoute({
53
53
  function registerWebhooks(app) {
54
54
  app.openapi(listWebhooksRoute, async (c) => {
55
55
  const project = await resolveAuthorizedProject(c, c.req.valid("param").slug, ...ADMIN_ROLES);
56
- const webhooks = await new WebhookModel(getStore().db).list(project.id);
56
+ const webhooks = await new WebhookModel(getStore().db, getStore().config.secret).list(project.id);
57
57
  return c.json(webhooks.map((webhook) => ({
58
58
  id: webhook.id,
59
59
  url: webhook.url,
@@ -64,7 +64,7 @@ function registerWebhooks(app) {
64
64
  const project = await resolveAuthorizedProject(c, c.req.valid("param").slug, ...ADMIN_ROLES);
65
65
  const body = c.req.valid("json");
66
66
  const secret = randomToken("whsec_").value;
67
- const webhook = await new WebhookModel(getStore().db).create(project.id, {
67
+ const webhook = await new WebhookModel(getStore().db, getStore().config.secret).create(project.id, {
68
68
  ...body,
69
69
  secret
70
70
  });
@@ -78,9 +78,9 @@ function registerWebhooks(app) {
78
78
  app.openapi(deleteWebhookRoute, async (c) => {
79
79
  const { slug, webhookId } = c.req.valid("param");
80
80
  const project = await resolveAuthorizedProject(c, slug, ...ADMIN_ROLES);
81
- const webhook = await new WebhookModel(getStore().db).get(project.id, webhookId);
81
+ const webhook = await new WebhookModel(getStore().db, getStore().config.secret).get(project.id, webhookId);
82
82
  if (!webhook) notFound("Webhook not found");
83
- await new WebhookModel(getStore().db).remove(project.id, webhook.id);
83
+ await new WebhookModel(getStore().db, getStore().config.secret).remove(project.id, webhook.id);
84
84
  return c.body(null, 204);
85
85
  });
86
86
  }
@@ -1 +1 @@
1
- {"version":3,"file":"webhooks.mjs","names":["notFoundResponse"],"sources":["../../src/routers/webhooks.ts"],"sourcesContent":["import { createRoute, z } from \"@hono/zod-openapi\";\nimport type { ShelfApp } from \"../index.tsx\";\n\nimport { WebhookModel } from \"../models/webhook.ts\";\nimport { getStore } from \"../store.ts\";\nimport type { ProjectRole } from \"../types.ts\";\nimport { randomToken } from \"../utils/hash.ts\";\nimport { resolveAuthorizedProject, notFound } from \"./helpers.ts\";\nimport {\n notFound as notFoundResponse,\n unauthorized,\n webhookCreatedSchema,\n webhookCreateSchema,\n webhookPublicSchema,\n} from \"./schemas.ts\";\n\nconst ADMIN_ROLES: readonly ProjectRole[] = [\"admin\"];\n\nconst listWebhooksRoute = createRoute({\n method: \"get\",\n path: \"/api/v1/projects/{slug}/webhooks\",\n request: { params: z.object({ slug: z.string() }) },\n responses: {\n 200: { content: { \"application/json\": { schema: webhookPublicSchema.array() } }, description: \"List webhooks\" },\n ...notFoundResponse,\n ...unauthorized,\n },\n});\n\nconst createWebhookRoute = createRoute({\n method: \"post\",\n path: \"/api/v1/projects/{slug}/webhooks\",\n request: {\n params: z.object({ slug: z.string() }),\n body: { content: { \"application/json\": { schema: webhookCreateSchema } } },\n },\n responses: {\n 201: { content: { \"application/json\": { schema: webhookCreatedSchema } }, description: \"Created webhook\" },\n ...notFoundResponse,\n },\n});\n\nconst deleteWebhookRoute = createRoute({\n method: \"delete\",\n path: \"/api/v1/projects/{slug}/webhooks/{webhookId}\",\n request: { params: z.object({ slug: z.string(), webhookId: z.string() }) },\n responses: {\n 204: { description: \"Deleted webhook\" },\n ...notFoundResponse,\n },\n});\n\n/** Register the project webhook list, create, and delete endpoints. */\nexport function registerWebhooks(app: ShelfApp): void {\n app.openapi(listWebhooksRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...ADMIN_ROLES);\n const webhooks = await new WebhookModel(getStore().db).list(project.id);\n return c.json(webhooks.map((webhook) => ({ id: webhook.id, url: webhook.url, events: WebhookModel.eventsOf(webhook) })));\n });\n\n app.openapi(createWebhookRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...ADMIN_ROLES);\n const body = c.req.valid(\"json\");\n const secret = randomToken(\"whsec_\").value;\n const webhook = await new WebhookModel(getStore().db).create(project.id, { ...body, secret });\n return c.json({ id: webhook.id, url: webhook.url, events: body.events ?? [], secret }, 201);\n });\n\n app.openapi(deleteWebhookRoute, async (c) => {\n const { slug, webhookId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...ADMIN_ROLES);\n const webhook = await new WebhookModel(getStore().db).get(project.id, webhookId);\n if (!webhook) {\n notFound(\"Webhook not found\");\n }\n await new WebhookModel(getStore().db).remove(project.id, webhook.id);\n return c.body(null, 204);\n });\n}"],"mappings":";;;;;;;;;;AAgBA,MAAM,cAAsC,CAAC,OAAO;AAEpD,MAAM,oBAAoB,YAAY;CACpC,QAAQ;CACR,MAAM;CACN,SAAS,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,EAAE;CAClD,WAAW;EACT,KAAK;GAAE,SAAS,EAAE,oBAAoB,EAAE,QAAQ,oBAAoB,MAAM,EAAE,EAAE;GAAG,aAAa;EAAgB;EAC9G,GAAGA;EACH,GAAG;CACL;AACF,CAAC;AAED,MAAM,qBAAqB,YAAY;CACrC,QAAQ;CACR,MAAM;CACN,SAAS;EACP,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;EACrC,MAAM,EAAE,SAAS,EAAE,oBAAoB,EAAE,QAAQ,oBAAoB,EAAE,EAAE;CAC3E;CACA,WAAW;EACT,KAAK;GAAE,SAAS,EAAE,oBAAoB,EAAE,QAAQ,qBAAqB,EAAE;GAAG,aAAa;EAAkB;EACzG,GAAGA;CACL;AACF,CAAC;AAED,MAAM,qBAAqB,YAAY;CACrC,QAAQ;CACR,MAAM;CACN,SAAS,EAAE,QAAQ,EAAE,OAAO;EAAE,MAAM,EAAE,OAAO;EAAG,WAAW,EAAE,OAAO;CAAE,CAAC,EAAE;CACzE,WAAW;EACT,KAAK,EAAE,aAAa,kBAAkB;EACtC,GAAGA;CACL;AACF,CAAC;;AAGD,SAAgB,iBAAiB,KAAqB;CACpD,IAAI,QAAQ,mBAAmB,OAAO,MAAM;EAC1C,MAAM,UAAU,MAAM,yBAAyB,GAAG,EAAE,IAAI,MAAM,OAAO,CAAC,CAAC,MAAM,GAAG,WAAW;EAC3F,MAAM,WAAW,MAAM,IAAI,aAAa,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,QAAQ,EAAE;EACtE,OAAO,EAAE,KAAK,SAAS,KAAK,aAAa;GAAE,IAAI,QAAQ;GAAI,KAAK,QAAQ;GAAK,QAAQ,aAAa,SAAS,OAAO;EAAE,EAAE,CAAC;CACzH,CAAC;CAED,IAAI,QAAQ,oBAAoB,OAAO,MAAM;EAC3C,MAAM,UAAU,MAAM,yBAAyB,GAAG,EAAE,IAAI,MAAM,OAAO,CAAC,CAAC,MAAM,GAAG,WAAW;EAC3F,MAAM,OAAO,EAAE,IAAI,MAAM,MAAM;EAC/B,MAAM,SAAS,YAAY,QAAQ,CAAC,CAAC;EACrC,MAAM,UAAU,MAAM,IAAI,aAAa,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,OAAO,QAAQ,IAAI;GAAE,GAAG;GAAM;EAAO,CAAC;EAC5F,OAAO,EAAE,KAAK;GAAE,IAAI,QAAQ;GAAI,KAAK,QAAQ;GAAK,QAAQ,KAAK,UAAU,CAAC;GAAG;EAAO,GAAG,GAAG;CAC5F,CAAC;CAED,IAAI,QAAQ,oBAAoB,OAAO,MAAM;EAC3C,MAAM,EAAE,MAAM,cAAc,EAAE,IAAI,MAAM,OAAO;EAC/C,MAAM,UAAU,MAAM,yBAAyB,GAAG,MAAM,GAAG,WAAW;EACtE,MAAM,UAAU,MAAM,IAAI,aAAa,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,QAAQ,IAAI,SAAS;EAC/E,IAAI,CAAC,SACH,SAAS,mBAAmB;EAE9B,MAAM,IAAI,aAAa,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,OAAO,QAAQ,IAAI,QAAQ,EAAE;EACnE,OAAO,EAAE,KAAK,MAAM,GAAG;CACzB,CAAC;AACH"}
1
+ {"version":3,"file":"webhooks.mjs","names":["notFoundResponse"],"sources":["../../src/routers/webhooks.ts"],"sourcesContent":["import { createRoute, z } from \"@hono/zod-openapi\";\nimport type { ShelfApp } from \"../index.tsx\";\nimport { WebhookModel } from \"../models/webhook.ts\";\nimport { getStore } from \"../store.ts\";\nimport type { ProjectRole } from \"../types.ts\";\nimport { randomToken } from \"../utils/hash.ts\";\nimport { resolveAuthorizedProject, notFound } from \"./helpers.ts\";\nimport {\n notFound as notFoundResponse,\n unauthorized,\n webhookCreatedSchema,\n webhookCreateSchema,\n webhookPublicSchema,\n} from \"./schemas.ts\";\n\nconst ADMIN_ROLES: readonly ProjectRole[] = [\"admin\"];\n\nconst listWebhooksRoute = createRoute({\n method: \"get\",\n path: \"/api/v1/projects/{slug}/webhooks\",\n request: { params: z.object({ slug: z.string() }) },\n responses: {\n 200: {\n content: { \"application/json\": { schema: webhookPublicSchema.array() } },\n description: \"List webhooks\",\n },\n ...notFoundResponse,\n ...unauthorized,\n },\n});\n\nconst createWebhookRoute = createRoute({\n method: \"post\",\n path: \"/api/v1/projects/{slug}/webhooks\",\n request: {\n params: z.object({ slug: z.string() }),\n body: { content: { \"application/json\": { schema: webhookCreateSchema } } },\n },\n responses: {\n 201: {\n content: { \"application/json\": { schema: webhookCreatedSchema } },\n description: \"Created webhook\",\n },\n ...notFoundResponse,\n },\n});\n\nconst deleteWebhookRoute = createRoute({\n method: \"delete\",\n path: \"/api/v1/projects/{slug}/webhooks/{webhookId}\",\n request: { params: z.object({ slug: z.string(), webhookId: z.string() }) },\n responses: {\n 204: { description: \"Deleted webhook\" },\n ...notFoundResponse,\n },\n});\n\n/** Register the project webhook list, create, and delete endpoints. */\nexport function registerWebhooks(app: ShelfApp): void {\n app.openapi(listWebhooksRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...ADMIN_ROLES);\n const webhooks = await new WebhookModel(getStore().db, getStore().config.secret).list(project.id);\n return c.json(\n webhooks.map((webhook) => ({\n id: webhook.id,\n url: webhook.url,\n events: WebhookModel.eventsOf(webhook),\n })),\n );\n });\n\n app.openapi(createWebhookRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...ADMIN_ROLES);\n const body = c.req.valid(\"json\");\n const secret = randomToken(\"whsec_\").value;\n const webhook = await new WebhookModel(getStore().db, getStore().config.secret).create(project.id, { ...body, secret });\n return c.json({ id: webhook.id, url: webhook.url, events: body.events ?? [], secret }, 201);\n });\n\n app.openapi(deleteWebhookRoute, async (c) => {\n const { slug, webhookId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...ADMIN_ROLES);\n const webhook = await new WebhookModel(getStore().db, getStore().config.secret).get(project.id, webhookId);\n if (!webhook) {\n notFound(\"Webhook not found\");\n }\n await new WebhookModel(getStore().db, getStore().config.secret).remove(project.id, webhook.id);\n return c.body(null, 204);\n });\n}\n"],"mappings":";;;;;;;;;;AAeA,MAAM,cAAsC,CAAC,OAAO;AAEpD,MAAM,oBAAoB,YAAY;CACpC,QAAQ;CACR,MAAM;CACN,SAAS,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,EAAE;CAClD,WAAW;EACT,KAAK;GACH,SAAS,EAAE,oBAAoB,EAAE,QAAQ,oBAAoB,MAAM,EAAE,EAAE;GACvE,aAAa;EACf;EACA,GAAGA;EACH,GAAG;CACL;AACF,CAAC;AAED,MAAM,qBAAqB,YAAY;CACrC,QAAQ;CACR,MAAM;CACN,SAAS;EACP,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;EACrC,MAAM,EAAE,SAAS,EAAE,oBAAoB,EAAE,QAAQ,oBAAoB,EAAE,EAAE;CAC3E;CACA,WAAW;EACT,KAAK;GACH,SAAS,EAAE,oBAAoB,EAAE,QAAQ,qBAAqB,EAAE;GAChE,aAAa;EACf;EACA,GAAGA;CACL;AACF,CAAC;AAED,MAAM,qBAAqB,YAAY;CACrC,QAAQ;CACR,MAAM;CACN,SAAS,EAAE,QAAQ,EAAE,OAAO;EAAE,MAAM,EAAE,OAAO;EAAG,WAAW,EAAE,OAAO;CAAE,CAAC,EAAE;CACzE,WAAW;EACT,KAAK,EAAE,aAAa,kBAAkB;EACtC,GAAGA;CACL;AACF,CAAC;;AAGD,SAAgB,iBAAiB,KAAqB;CACpD,IAAI,QAAQ,mBAAmB,OAAO,MAAM;EAC1C,MAAM,UAAU,MAAM,yBAAyB,GAAG,EAAE,IAAI,MAAM,OAAO,CAAC,CAAC,MAAM,GAAG,WAAW;EAC3F,MAAM,WAAW,MAAM,IAAI,aAAa,SAAS,CAAC,CAAC,IAAI,SAAS,CAAC,CAAC,OAAO,MAAM,CAAC,CAAC,KAAK,QAAQ,EAAE;EAChG,OAAO,EAAE,KACP,SAAS,KAAK,aAAa;GACzB,IAAI,QAAQ;GACZ,KAAK,QAAQ;GACb,QAAQ,aAAa,SAAS,OAAO;EACvC,EAAE,CACJ;CACF,CAAC;CAED,IAAI,QAAQ,oBAAoB,OAAO,MAAM;EAC3C,MAAM,UAAU,MAAM,yBAAyB,GAAG,EAAE,IAAI,MAAM,OAAO,CAAC,CAAC,MAAM,GAAG,WAAW;EAC3F,MAAM,OAAO,EAAE,IAAI,MAAM,MAAM;EAC/B,MAAM,SAAS,YAAY,QAAQ,CAAC,CAAC;EACrC,MAAM,UAAU,MAAM,IAAI,aAAa,SAAS,CAAC,CAAC,IAAI,SAAS,CAAC,CAAC,OAAO,MAAM,CAAC,CAAC,OAAO,QAAQ,IAAI;GAAE,GAAG;GAAM;EAAO,CAAC;EACtH,OAAO,EAAE,KAAK;GAAE,IAAI,QAAQ;GAAI,KAAK,QAAQ;GAAK,QAAQ,KAAK,UAAU,CAAC;GAAG;EAAO,GAAG,GAAG;CAC5F,CAAC;CAED,IAAI,QAAQ,oBAAoB,OAAO,MAAM;EAC3C,MAAM,EAAE,MAAM,cAAc,EAAE,IAAI,MAAM,OAAO;EAC/C,MAAM,UAAU,MAAM,yBAAyB,GAAG,MAAM,GAAG,WAAW;EACtE,MAAM,UAAU,MAAM,IAAI,aAAa,SAAS,CAAC,CAAC,IAAI,SAAS,CAAC,CAAC,OAAO,MAAM,CAAC,CAAC,IAAI,QAAQ,IAAI,SAAS;EACzG,IAAI,CAAC,SACH,SAAS,mBAAmB;EAE9B,MAAM,IAAI,aAAa,SAAS,CAAC,CAAC,IAAI,SAAS,CAAC,CAAC,OAAO,MAAM,CAAC,CAAC,OAAO,QAAQ,IAAI,QAAQ,EAAE;EAC7F,OAAO,EAAE,KAAK,MAAM,GAAG;CACzB,CAAC;AACH"}
@@ -0,0 +1,18 @@
1
+ import __tsdown_shims_path from 'node:path';
2
+ import __tsdown_shims_url from 'node:url';
3
+ //#region src/schema/baseline.d.ts
4
+ /** A baseline row. */
5
+ interface Baseline {
6
+ id: string;
7
+ projectId: string;
8
+ storyId: string;
9
+ viewportName: string;
10
+ branch: string;
11
+ snapshotId: string | null;
12
+ screenshotPath: string;
13
+ createdAt: string;
14
+ updatedAt: string;
15
+ }
16
+ //#endregion
17
+ export { Baseline };
18
+ //# sourceMappingURL=baseline.d.mts.map
@@ -0,0 +1,22 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { projects } from "./project.mjs";
5
+ import { index, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core";
6
+ //#region src/schema/baseline.ts
7
+ /** Narrow `baselines` table definition. */
8
+ const baselines = sqliteTable("baselines", {
9
+ id: text("id").primaryKey(),
10
+ projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
11
+ storyId: text("story_id").notNull(),
12
+ viewportName: text("viewport_name").notNull().default("desktop"),
13
+ branch: text("branch").notNull(),
14
+ snapshotId: text("snapshot_id"),
15
+ screenshotPath: text("screenshot_path").notNull(),
16
+ createdAt: text("created_at").notNull(),
17
+ updatedAt: text("updated_at").notNull()
18
+ }, (t) => [uniqueIndex("baselines_project_story_viewport_branch_idx").on(t.projectId, t.storyId, t.viewportName, t.branch), index("baselines_project_story_idx").on(t.projectId, t.storyId)]);
19
+ //#endregion
20
+ export { baselines };
21
+
22
+ //# sourceMappingURL=baseline.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"baseline.mjs","names":[],"sources":["../../src/schema/baseline.ts"],"sourcesContent":["import { index, sqliteTable, text, uniqueIndex } from \"drizzle-orm/sqlite-core\";\nimport { projects } from \"./project.ts\";\n\n/** Narrow `baselines` table definition. */\nexport const baselines = sqliteTable(\n \"baselines\",\n {\n id: text(\"id\").primaryKey(),\n projectId: text(\"project_id\")\n .notNull()\n .references(() => projects.id, { onDelete: \"cascade\" }),\n storyId: text(\"story_id\").notNull(),\n viewportName: text(\"viewport_name\").notNull().default(\"desktop\"),\n branch: text(\"branch\").notNull(),\n snapshotId: text(\"snapshot_id\"),\n screenshotPath: text(\"screenshot_path\").notNull(),\n createdAt: text(\"created_at\").notNull(),\n updatedAt: text(\"updated_at\").notNull(),\n },\n (t) => [\n uniqueIndex(\"baselines_project_story_viewport_branch_idx\").on(\n t.projectId,\n t.storyId,\n t.viewportName,\n t.branch,\n ),\n index(\"baselines_project_story_idx\").on(t.projectId, t.storyId),\n ],\n);\n\n/** A baseline row. */\nexport interface Baseline {\n id: string;\n projectId: string;\n storyId: string;\n viewportName: string;\n branch: string;\n snapshotId: string | null;\n screenshotPath: string;\n createdAt: string;\n updatedAt: string;\n}\n"],"mappings":";;;;;;;AAIA,MAAa,YAAY,YACvB,aACA;CACE,IAAI,KAAK,IAAI,CAAC,CAAC,WAAW;CAC1B,WAAW,KAAK,YAAY,CAAC,CAC1B,QAAQ,CAAC,CACT,iBAAiB,SAAS,IAAI,EAAE,UAAU,UAAU,CAAC;CACxD,SAAS,KAAK,UAAU,CAAC,CAAC,QAAQ;CAClC,cAAc,KAAK,eAAe,CAAC,CAAC,QAAQ,CAAC,CAAC,QAAQ,SAAS;CAC/D,QAAQ,KAAK,QAAQ,CAAC,CAAC,QAAQ;CAC/B,YAAY,KAAK,aAAa;CAC9B,gBAAgB,KAAK,iBAAiB,CAAC,CAAC,QAAQ;CAChD,WAAW,KAAK,YAAY,CAAC,CAAC,QAAQ;CACtC,WAAW,KAAK,YAAY,CAAC,CAAC,QAAQ;AACxC,IACC,MAAM,CACL,YAAY,6CAA6C,CAAC,CAAC,GACzD,EAAE,WACF,EAAE,SACF,EAAE,cACF,EAAE,MACJ,GACA,MAAM,6BAA6B,CAAC,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,CAChE,CACF"}
@@ -0,0 +1,26 @@
1
+ import __tsdown_shims_path from 'node:path';
2
+ import __tsdown_shims_url from 'node:url';
3
+ import { BuildStatus } from "../types.mjs";
4
+ //#region src/schema/build.d.ts
5
+ /** A build row. */
6
+ interface Build {
7
+ id: string;
8
+ projectId: string;
9
+ gitSha: string;
10
+ gitBranch: string;
11
+ isDefault: boolean;
12
+ authorEmail: string | null;
13
+ authorName: string | null;
14
+ message: string | null;
15
+ public: boolean;
16
+ status: BuildStatus;
17
+ snapshotCount: number;
18
+ changedCount: number;
19
+ approvedCount: number;
20
+ rejectedCount: number;
21
+ createdAt: string;
22
+ updatedAt: string;
23
+ }
24
+ //#endregion
25
+ export { Build };
26
+ //# sourceMappingURL=build.d.mts.map
@@ -0,0 +1,29 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { projects } from "./project.mjs";
5
+ import { index, integer, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core";
6
+ //#region src/schema/build.ts
7
+ /** Narrow `builds` table definition. */
8
+ const builds = sqliteTable("builds", {
9
+ id: text("id").primaryKey(),
10
+ projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
11
+ gitSha: text("git_sha").notNull(),
12
+ gitBranch: text("git_branch").notNull(),
13
+ isDefault: integer("is_default", { mode: "boolean" }).notNull().default(false),
14
+ authorEmail: text("author_email"),
15
+ authorName: text("author_name"),
16
+ message: text("message"),
17
+ public: integer("public", { mode: "boolean" }).notNull().default(false),
18
+ status: text("status").$type().notNull().default("pending"),
19
+ snapshotCount: integer("snapshot_count").notNull().default(0),
20
+ changedCount: integer("changed_count").notNull().default(0),
21
+ approvedCount: integer("approved_count").notNull().default(0),
22
+ rejectedCount: integer("rejected_count").notNull().default(0),
23
+ createdAt: text("created_at").notNull(),
24
+ updatedAt: text("updated_at").notNull()
25
+ }, (t) => [uniqueIndex("builds_project_gitsha_idx").on(t.projectId, t.gitSha), index("builds_git_branch_idx").on(t.gitBranch)]);
26
+ //#endregion
27
+ export { builds };
28
+
29
+ //# sourceMappingURL=build.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build.mjs","names":[],"sources":["../../src/schema/build.ts"],"sourcesContent":["import { index, integer, sqliteTable, text, uniqueIndex } from \"drizzle-orm/sqlite-core\";\nimport type { BuildStatus } from \"../types.ts\";\nimport { projects } from \"./project.ts\";\n\n/** Narrow `builds` table definition. */\nexport const builds = sqliteTable(\n \"builds\",\n {\n id: text(\"id\").primaryKey(),\n projectId: text(\"project_id\")\n .notNull()\n .references(() => projects.id, { onDelete: \"cascade\" }),\n gitSha: text(\"git_sha\").notNull(),\n gitBranch: text(\"git_branch\").notNull(),\n isDefault: integer(\"is_default\", { mode: \"boolean\" }).notNull().default(false),\n authorEmail: text(\"author_email\"),\n authorName: text(\"author_name\"),\n message: text(\"message\"),\n public: integer(\"public\", { mode: \"boolean\" }).notNull().default(false),\n status: text(\"status\").$type<BuildStatus>().notNull().default(\"pending\"),\n snapshotCount: integer(\"snapshot_count\").notNull().default(0),\n changedCount: integer(\"changed_count\").notNull().default(0),\n approvedCount: integer(\"approved_count\").notNull().default(0),\n rejectedCount: integer(\"rejected_count\").notNull().default(0),\n createdAt: text(\"created_at\").notNull(),\n updatedAt: text(\"updated_at\").notNull(),\n },\n (t) => [\n uniqueIndex(\"builds_project_gitsha_idx\").on(t.projectId, t.gitSha),\n index(\"builds_git_branch_idx\").on(t.gitBranch),\n ],\n);\n\n/** A build row. */\nexport interface Build {\n id: string;\n projectId: string;\n gitSha: string;\n gitBranch: string;\n isDefault: boolean;\n authorEmail: string | null;\n authorName: string | null;\n message: string | null;\n public: boolean;\n status: BuildStatus;\n snapshotCount: number;\n changedCount: number;\n approvedCount: number;\n rejectedCount: number;\n createdAt: string;\n updatedAt: string;\n}\n"],"mappings":";;;;;;;AAKA,MAAa,SAAS,YACpB,UACA;CACE,IAAI,KAAK,IAAI,CAAC,CAAC,WAAW;CAC1B,WAAW,KAAK,YAAY,CAAC,CAC1B,QAAQ,CAAC,CACT,iBAAiB,SAAS,IAAI,EAAE,UAAU,UAAU,CAAC;CACxD,QAAQ,KAAK,SAAS,CAAC,CAAC,QAAQ;CAChC,WAAW,KAAK,YAAY,CAAC,CAAC,QAAQ;CACtC,WAAW,QAAQ,cAAc,EAAE,MAAM,UAAU,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,QAAQ,KAAK;CAC7E,aAAa,KAAK,cAAc;CAChC,YAAY,KAAK,aAAa;CAC9B,SAAS,KAAK,SAAS;CACvB,QAAQ,QAAQ,UAAU,EAAE,MAAM,UAAU,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,QAAQ,KAAK;CACtE,QAAQ,KAAK,QAAQ,CAAC,CAAC,MAAmB,CAAC,CAAC,QAAQ,CAAC,CAAC,QAAQ,SAAS;CACvE,eAAe,QAAQ,gBAAgB,CAAC,CAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC;CAC5D,cAAc,QAAQ,eAAe,CAAC,CAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC;CAC1D,eAAe,QAAQ,gBAAgB,CAAC,CAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC;CAC5D,eAAe,QAAQ,gBAAgB,CAAC,CAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC;CAC5D,WAAW,KAAK,YAAY,CAAC,CAAC,QAAQ;CACtC,WAAW,KAAK,YAAY,CAAC,CAAC,QAAQ;AACxC,IACC,MAAM,CACL,YAAY,2BAA2B,CAAC,CAAC,GAAG,EAAE,WAAW,EAAE,MAAM,GACjE,MAAM,uBAAuB,CAAC,CAAC,GAAG,EAAE,SAAS,CAC/C,CACF"}
@@ -0,0 +1,19 @@
1
+ import __tsdown_shims_path from 'node:path';
2
+ import __tsdown_shims_url from 'node:url';
3
+ //#region src/schema/comment.d.ts
4
+ /** A comment row. */
5
+ interface Comment {
6
+ id: string;
7
+ projectId: string;
8
+ buildId: string;
9
+ snapshotId: string | null;
10
+ userId: string;
11
+ body: string;
12
+ parentId: string | null;
13
+ resolved: boolean;
14
+ createdAt: string;
15
+ updatedAt: string;
16
+ }
17
+ //#endregion
18
+ export { Comment };
19
+ //# sourceMappingURL=comment.d.mts.map
@@ -0,0 +1,26 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { projects } from "./project.mjs";
5
+ import { builds } from "./build.mjs";
6
+ import { snapshots } from "./snapshot.mjs";
7
+ import { users } from "./user.mjs";
8
+ import { index, integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
9
+ //#region src/schema/comment.ts
10
+ /** Narrow `comments` table definition. */
11
+ const comments = sqliteTable("comments", {
12
+ id: text("id").primaryKey(),
13
+ projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
14
+ buildId: text("build_id").notNull().references(() => builds.id, { onDelete: "cascade" }),
15
+ snapshotId: text("snapshot_id").references(() => snapshots.id, { onDelete: "cascade" }),
16
+ userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
17
+ body: text("body").notNull(),
18
+ parentId: text("parent_id"),
19
+ resolved: integer("resolved", { mode: "boolean" }).notNull().default(false),
20
+ createdAt: text("created_at").notNull(),
21
+ updatedAt: text("updated_at").notNull()
22
+ }, (t) => [index("comments_build_id_idx").on(t.buildId)]);
23
+ //#endregion
24
+ export { comments };
25
+
26
+ //# sourceMappingURL=comment.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"comment.mjs","names":[],"sources":["../../src/schema/comment.ts"],"sourcesContent":["import { index, integer, sqliteTable, text } from \"drizzle-orm/sqlite-core\";\nimport { builds } from \"./build.ts\";\nimport { projects } from \"./project.ts\";\nimport { snapshots } from \"./snapshot.ts\";\nimport { users } from \"./user.ts\";\n\n/** Narrow `comments` table definition. */\nexport const comments = sqliteTable(\n \"comments\",\n {\n id: text(\"id\").primaryKey(),\n projectId: text(\"project_id\")\n .notNull()\n .references(() => projects.id, { onDelete: \"cascade\" }),\n buildId: text(\"build_id\")\n .notNull()\n .references(() => builds.id, { onDelete: \"cascade\" }),\n snapshotId: text(\"snapshot_id\").references(() => snapshots.id, { onDelete: \"cascade\" }),\n userId: text(\"user_id\")\n .notNull()\n .references(() => users.id, { onDelete: \"cascade\" }),\n body: text(\"body\").notNull(),\n parentId: text(\"parent_id\"),\n resolved: integer(\"resolved\", { mode: \"boolean\" }).notNull().default(false),\n createdAt: text(\"created_at\").notNull(),\n updatedAt: text(\"updated_at\").notNull(),\n },\n (t) => [index(\"comments_build_id_idx\").on(t.buildId)],\n);\n\n/** A comment row. */\nexport interface Comment {\n id: string;\n projectId: string;\n buildId: string;\n snapshotId: string | null;\n userId: string;\n body: string;\n parentId: string | null;\n resolved: boolean;\n createdAt: string;\n updatedAt: string;\n}\n"],"mappings":";;;;;;;;;;AAOA,MAAa,WAAW,YACtB,YACA;CACE,IAAI,KAAK,IAAI,CAAC,CAAC,WAAW;CAC1B,WAAW,KAAK,YAAY,CAAC,CAC1B,QAAQ,CAAC,CACT,iBAAiB,SAAS,IAAI,EAAE,UAAU,UAAU,CAAC;CACxD,SAAS,KAAK,UAAU,CAAC,CACtB,QAAQ,CAAC,CACT,iBAAiB,OAAO,IAAI,EAAE,UAAU,UAAU,CAAC;CACtD,YAAY,KAAK,aAAa,CAAC,CAAC,iBAAiB,UAAU,IAAI,EAAE,UAAU,UAAU,CAAC;CACtF,QAAQ,KAAK,SAAS,CAAC,CACpB,QAAQ,CAAC,CACT,iBAAiB,MAAM,IAAI,EAAE,UAAU,UAAU,CAAC;CACrD,MAAM,KAAK,MAAM,CAAC,CAAC,QAAQ;CAC3B,UAAU,KAAK,WAAW;CAC1B,UAAU,QAAQ,YAAY,EAAE,MAAM,UAAU,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,QAAQ,KAAK;CAC1E,WAAW,KAAK,YAAY,CAAC,CAAC,QAAQ;CACtC,WAAW,KAAK,YAAY,CAAC,CAAC,QAAQ;AACxC,IACC,MAAM,CAAC,MAAM,uBAAuB,CAAC,CAAC,GAAG,EAAE,OAAO,CAAC,CACtD"}
@@ -0,0 +1,25 @@
1
+ import __tsdown_shims_path from 'node:path';
2
+ import __tsdown_shims_url from 'node:url';
3
+ //#region src/schema/label.d.ts
4
+ /** A label-type row. */
5
+ interface LabelType {
6
+ id: string;
7
+ projectId: string;
8
+ key: string;
9
+ name: string;
10
+ linkTemplate: string | null;
11
+ color: string | null;
12
+ createdAt: string;
13
+ }
14
+ /** A build-label row. */
15
+ interface BuildLabel {
16
+ id: string;
17
+ projectId: string;
18
+ buildId: string;
19
+ typeKey: string;
20
+ value: string;
21
+ createdAt: string;
22
+ }
23
+ //#endregion
24
+ export { BuildLabel, LabelType };
25
+ //# sourceMappingURL=label.d.mts.map