@storyshelf/core 0.1.2 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (275) hide show
  1. package/README.md +50 -46
  2. package/dist/adapter/auth.d.mts +4 -4
  3. package/dist/adapter/auth.mjs.map +1 -1
  4. package/dist/{adapters → adapter}/capture-queue.d.mts +6 -4
  5. package/dist/adapter/capture-queue.mjs +4 -0
  6. package/dist/adapter/capture-runner.d.mts +7 -7
  7. package/dist/adapter/database.d.mts +20 -9
  8. package/dist/adapter/database.mjs +60 -1
  9. package/dist/adapter/database.mjs.map +1 -0
  10. package/dist/adapter/git-host/comments.d.mts +41 -0
  11. package/dist/adapter/git-host/comments.mjs +54 -0
  12. package/dist/adapter/git-host/comments.mjs.map +1 -0
  13. package/dist/adapter/git-host/helpers.d.mts +1 -0
  14. package/dist/adapter/git-host/helpers.mjs +1 -0
  15. package/dist/adapter/git-host/helpers.mjs.map +1 -1
  16. package/dist/adapter/git-host.d.mts +3 -7
  17. package/dist/adapter/init.d.mts +58 -0
  18. package/dist/adapter/init.mjs +93 -0
  19. package/dist/adapter/init.mjs.map +1 -0
  20. package/dist/adapter/metadata.d.mts +83 -0
  21. package/dist/adapter/metadata.mjs +4 -0
  22. package/dist/adapter/storage.d.mts +15 -4
  23. package/dist/adapters/webhook-events.mjs +20 -4
  24. package/dist/adapters/webhook-events.mjs.map +1 -1
  25. package/dist/capture/adapter.d.mts +7 -1
  26. package/dist/capture/adapter.mjs +3 -0
  27. package/dist/capture/adapter.mjs.map +1 -1
  28. package/dist/capture/dispatch.mjs +150 -0
  29. package/dist/capture/dispatch.mjs.map +1 -0
  30. package/dist/capture/orchestrator.d.mts +9 -0
  31. package/dist/capture/orchestrator.mjs +11 -44
  32. package/dist/capture/orchestrator.mjs.map +1 -1
  33. package/dist/capture/pipeline.d.mts +4 -1
  34. package/dist/capture/pipeline.mjs +6 -6
  35. package/dist/capture/pipeline.mjs.map +1 -1
  36. package/dist/capture/queue.d.mts +4 -1
  37. package/dist/capture/queue.mjs +3 -1
  38. package/dist/capture/queue.mjs.map +1 -1
  39. package/dist/capture/skip-checks.mjs +76 -0
  40. package/dist/capture/skip-checks.mjs.map +1 -0
  41. package/dist/capture/statics.mjs +124 -0
  42. package/dist/capture/statics.mjs.map +1 -0
  43. package/dist/capture/status-fanout.mjs +1 -0
  44. package/dist/capture/status-fanout.mjs.map +1 -1
  45. package/dist/capture/storybook.d.mts +1 -0
  46. package/dist/capture/storybook.mjs +1 -0
  47. package/dist/capture/storybook.mjs.map +1 -1
  48. package/dist/capture.d.mts +8 -0
  49. package/dist/capture.mjs +9 -0
  50. package/dist/config.d.mts +37 -22
  51. package/dist/config.mjs +28 -2
  52. package/dist/config.mjs.map +1 -1
  53. package/dist/ddl.d.mts +2 -1
  54. package/dist/ddl.mjs +2 -1
  55. package/dist/ddl.mjs.map +1 -1
  56. package/dist/diff/engine.d.mts +8 -0
  57. package/dist/diff/engine.mjs +8 -0
  58. package/dist/diff/engine.mjs.map +1 -1
  59. package/dist/diff/options.d.mts +3 -1
  60. package/dist/diff.d.mts +5 -0
  61. package/dist/diff.mjs +6 -0
  62. package/dist/index.d.mts +34 -24
  63. package/dist/index.mjs +211 -252
  64. package/dist/index.mjs.map +1 -1
  65. package/dist/logger.d.mts +3 -3
  66. package/dist/logger.mjs.map +1 -1
  67. package/dist/middleware/auth-gate.mjs +36 -0
  68. package/dist/middleware/auth-gate.mjs.map +1 -0
  69. package/dist/middleware/csrf.mjs +1 -0
  70. package/dist/middleware/csrf.mjs.map +1 -1
  71. package/dist/middleware/rate-limit.mjs +1 -0
  72. package/dist/middleware/rate-limit.mjs.map +1 -1
  73. package/dist/middleware/request-log.mjs +28 -0
  74. package/dist/middleware/request-log.mjs.map +1 -0
  75. package/dist/middleware/store-scope.mjs +29 -0
  76. package/dist/middleware/store-scope.mjs.map +1 -0
  77. package/dist/models/baseline.mjs +9 -5
  78. package/dist/models/baseline.mjs.map +1 -1
  79. package/dist/models/build.mjs +5 -1
  80. package/dist/models/build.mjs.map +1 -1
  81. package/dist/models/comment.mjs +4 -1
  82. package/dist/models/comment.mjs.map +1 -1
  83. package/dist/models/label.mjs +3 -1
  84. package/dist/models/label.mjs.map +1 -1
  85. package/dist/models/member.mjs +2 -1
  86. package/dist/models/member.mjs.map +1 -1
  87. package/dist/models/project.mjs +2 -1
  88. package/dist/models/project.mjs.map +1 -1
  89. package/dist/models/snapshot.mjs +2 -1
  90. package/dist/models/snapshot.mjs.map +1 -1
  91. package/dist/models/status-config.mjs +2 -1
  92. package/dist/models/status-config.mjs.map +1 -1
  93. package/dist/models/token.mjs +2 -1
  94. package/dist/models/token.mjs.map +1 -1
  95. package/dist/models/webhook.mjs +12 -3
  96. package/dist/models/webhook.mjs.map +1 -1
  97. package/dist/pages/build-detail.mjs +10 -4
  98. package/dist/pages/build-detail.mjs.map +1 -1
  99. package/dist/pages/build-diff-comments.mjs +113 -0
  100. package/dist/pages/build-diff-comments.mjs.map +1 -0
  101. package/dist/pages/build-diff-header.mjs +163 -0
  102. package/dist/pages/build-diff-header.mjs.map +1 -0
  103. package/dist/pages/build-diff-nav.mjs +66 -0
  104. package/dist/pages/build-diff-nav.mjs.map +1 -0
  105. package/dist/pages/build-diff-viewer.mjs +197 -0
  106. package/dist/pages/build-diff-viewer.mjs.map +1 -0
  107. package/dist/pages/build-diff.mjs +66 -413
  108. package/dist/pages/build-diff.mjs.map +1 -1
  109. package/dist/pages/compute-jobs.mjs +3 -1
  110. package/dist/pages/compute-jobs.mjs.map +1 -1
  111. package/dist/pages/label-detail.mjs +4 -3
  112. package/dist/pages/label-detail.mjs.map +1 -1
  113. package/dist/pages/login.mjs +1 -0
  114. package/dist/pages/login.mjs.map +1 -1
  115. package/dist/pages/project-builds.mjs +6 -3
  116. package/dist/pages/project-builds.mjs.map +1 -1
  117. package/dist/pages/project-create.mjs +1 -0
  118. package/dist/pages/project-create.mjs.map +1 -1
  119. package/dist/pages/project-settings.mjs +1 -0
  120. package/dist/pages/project-settings.mjs.map +1 -1
  121. package/dist/pages/projects.mjs +12 -6
  122. package/dist/pages/projects.mjs.map +1 -1
  123. package/dist/pages/root.mjs +7 -3
  124. package/dist/pages/root.mjs.map +1 -1
  125. package/dist/pages/settings-general.mjs +1 -0
  126. package/dist/pages/settings-general.mjs.map +1 -1
  127. package/dist/pages/settings-labels.mjs +2 -1
  128. package/dist/pages/settings-labels.mjs.map +1 -1
  129. package/dist/pages/settings-members.mjs +2 -1
  130. package/dist/pages/settings-members.mjs.map +1 -1
  131. package/dist/pages/settings-status.mjs +3 -1
  132. package/dist/pages/settings-status.mjs.map +1 -1
  133. package/dist/pages/settings-tests.mjs +4 -3
  134. package/dist/pages/settings-tests.mjs.map +1 -1
  135. package/dist/pages/settings-tokens.mjs +1 -0
  136. package/dist/pages/settings-tokens.mjs.map +1 -1
  137. package/dist/pages/settings-webhooks.mjs +1 -0
  138. package/dist/pages/settings-webhooks.mjs.map +1 -1
  139. package/dist/pages/storybook.mjs +38 -1
  140. package/dist/pages/storybook.mjs.map +1 -1
  141. package/dist/{utils/paths.d.mts → paths.d.mts} +5 -0
  142. package/dist/{utils/paths.mjs → paths.mjs} +5 -0
  143. package/dist/paths.mjs.map +1 -0
  144. package/dist/retention/purge.mjs +2 -1
  145. package/dist/retention/purge.mjs.map +1 -1
  146. package/dist/routers/admin.mjs +2 -1
  147. package/dist/routers/admin.mjs.map +1 -1
  148. package/dist/routers/assets.mjs +1 -0
  149. package/dist/routers/assets.mjs.map +1 -1
  150. package/dist/routers/auth.mjs +1 -0
  151. package/dist/routers/auth.mjs.map +1 -1
  152. package/dist/routers/builds.handlers.mjs +114 -19
  153. package/dist/routers/builds.handlers.mjs.map +1 -1
  154. package/dist/routers/builds.mjs +55 -34
  155. package/dist/routers/builds.mjs.map +1 -1
  156. package/dist/routers/comments.mjs +1 -0
  157. package/dist/routers/comments.mjs.map +1 -1
  158. package/dist/routers/health.mjs +137 -0
  159. package/dist/routers/health.mjs.map +1 -0
  160. package/dist/routers/helpers.mjs +8 -2
  161. package/dist/routers/helpers.mjs.map +1 -1
  162. package/dist/routers/htmx.mjs +2 -0
  163. package/dist/routers/htmx.mjs.map +1 -1
  164. package/dist/routers/labels.mjs +2 -1
  165. package/dist/routers/labels.mjs.map +1 -1
  166. package/dist/routers/media.mjs +3 -2
  167. package/dist/routers/media.mjs.map +1 -1
  168. package/dist/routers/members.mjs +1 -0
  169. package/dist/routers/members.mjs.map +1 -1
  170. package/dist/routers/projects.mjs +2 -1
  171. package/dist/routers/projects.mjs.map +1 -1
  172. package/dist/routers/schemas.mjs +25 -0
  173. package/dist/routers/schemas.mjs.map +1 -1
  174. package/dist/routers/settings-general.mjs +95 -0
  175. package/dist/routers/settings-general.mjs.map +1 -0
  176. package/dist/routers/settings-labels.mjs +49 -0
  177. package/dist/routers/settings-labels.mjs.map +1 -0
  178. package/dist/routers/settings-members.mjs +30 -0
  179. package/dist/routers/settings-members.mjs.map +1 -0
  180. package/dist/routers/settings-status.mjs +73 -0
  181. package/dist/routers/settings-status.mjs.map +1 -0
  182. package/dist/routers/settings-tokens.mjs +40 -0
  183. package/dist/routers/settings-tokens.mjs.map +1 -0
  184. package/dist/routers/settings-webhooks.mjs +63 -0
  185. package/dist/routers/settings-webhooks.mjs.map +1 -0
  186. package/dist/routers/settings.handlers.mjs +93 -0
  187. package/dist/routers/settings.handlers.mjs.map +1 -0
  188. package/dist/routers/settings.mjs +14 -246
  189. package/dist/routers/settings.mjs.map +1 -1
  190. package/dist/routers/snapshots.mjs +2 -1
  191. package/dist/routers/snapshots.mjs.map +1 -1
  192. package/dist/routers/status-configs.mjs +2 -1
  193. package/dist/routers/status-configs.mjs.map +1 -1
  194. package/dist/routers/storybook.mjs +12 -4
  195. package/dist/routers/storybook.mjs.map +1 -1
  196. package/dist/routers/tokens.mjs +2 -1
  197. package/dist/routers/tokens.mjs.map +1 -1
  198. package/dist/routers/ui.mjs +5 -4
  199. package/dist/routers/ui.mjs.map +1 -1
  200. package/dist/routers/webhooks.mjs +7 -6
  201. package/dist/routers/webhooks.mjs.map +1 -1
  202. package/dist/schema/baseline.d.mts +18 -0
  203. package/dist/schema/baseline.mjs +22 -0
  204. package/dist/schema/baseline.mjs.map +1 -0
  205. package/dist/schema/build.d.mts +26 -0
  206. package/dist/schema/build.mjs +29 -0
  207. package/dist/schema/build.mjs.map +1 -0
  208. package/dist/schema/comment.d.mts +19 -0
  209. package/dist/schema/comment.mjs +26 -0
  210. package/dist/schema/comment.mjs.map +1 -0
  211. package/dist/schema/label.d.mts +25 -0
  212. package/dist/schema/label.mjs +30 -0
  213. package/dist/schema/label.mjs.map +1 -0
  214. package/dist/schema/member.d.mts +15 -0
  215. package/dist/schema/member.mjs +19 -0
  216. package/dist/schema/member.mjs.map +1 -0
  217. package/dist/schema/project.d.mts +34 -0
  218. package/dist/schema/project.mjs +25 -0
  219. package/dist/schema/project.mjs.map +1 -0
  220. package/dist/schema/snapshot.d.mts +30 -0
  221. package/dist/schema/snapshot.mjs +34 -0
  222. package/dist/schema/snapshot.mjs.map +1 -0
  223. package/dist/schema/status-config.d.mts +16 -0
  224. package/dist/schema/status-config.mjs +20 -0
  225. package/dist/schema/status-config.mjs.map +1 -0
  226. package/dist/schema/token.d.mts +15 -0
  227. package/dist/schema/token.mjs +19 -0
  228. package/dist/schema/token.mjs.map +1 -0
  229. package/dist/schema/user.d.mts +17 -0
  230. package/dist/schema/user.mjs +19 -0
  231. package/dist/schema/user.mjs.map +1 -0
  232. package/dist/schema/webhook.d.mts +16 -0
  233. package/dist/schema/webhook.mjs +20 -0
  234. package/dist/schema/webhook.mjs.map +1 -0
  235. package/dist/schema.d.mts +52 -4433
  236. package/dist/schema.mjs +37 -139
  237. package/dist/schema.mjs.map +1 -1
  238. package/dist/store.mjs +2 -0
  239. package/dist/store.mjs.map +1 -1
  240. package/dist/types.d.mts +13 -2
  241. package/dist/types.mjs +8 -0
  242. package/dist/types.mjs.map +1 -1
  243. package/dist/ui/document.mjs +1 -0
  244. package/dist/ui/document.mjs.map +1 -1
  245. package/dist/ui/feedback.mjs +24 -0
  246. package/dist/ui/feedback.mjs.map +1 -0
  247. package/dist/ui/{components.mjs → forms.mjs} +6 -16
  248. package/dist/ui/forms.mjs.map +1 -0
  249. package/dist/ui/styles.mjs +1 -0
  250. package/dist/ui/styles.mjs.map +1 -1
  251. package/dist/ui/theme.mjs +2 -0
  252. package/dist/ui/theme.mjs.map +1 -1
  253. package/dist/urls.d.mts +8 -0
  254. package/dist/urls.mjs +7 -0
  255. package/dist/urls.mjs.map +1 -1
  256. package/dist/utils/hash.mjs +3 -0
  257. package/dist/utils/hash.mjs.map +1 -1
  258. package/dist/utils/ulid.mjs +2 -0
  259. package/dist/utils/ulid.mjs.map +1 -1
  260. package/package.json +47 -45
  261. package/dist/adapters/metadata.d.mts +0 -24
  262. package/dist/models/baseline.d.mts +0 -20
  263. package/dist/models/build.d.mts +0 -50
  264. package/dist/models/comment.d.mts +0 -20
  265. package/dist/models/label.d.mts +0 -49
  266. package/dist/models/member.d.mts +0 -27
  267. package/dist/models/project.d.mts +0 -42
  268. package/dist/models/snapshot.d.mts +0 -46
  269. package/dist/models/token.d.mts +0 -33
  270. package/dist/models/webhook.d.mts +0 -37
  271. package/dist/retention/purge.d.mts +0 -28
  272. package/dist/ui/components.mjs.map +0 -1
  273. package/dist/ui/document.d.mts +0 -8
  274. package/dist/utils/paths.mjs.map +0 -1
  275. package/dist/utils/ulid.d.mts +0 -8
@@ -1,255 +1,23 @@
1
1
  import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
- import { ProjectModel } from "../models/project.mjs";
5
- import { LabelModel } from "../models/label.mjs";
6
- import { getStore } from "../store.mjs";
7
- import { MemberModel } from "../models/member.mjs";
8
- import { TokenModel } from "../models/token.mjs";
9
- import { randomToken } from "../utils/hash.mjs";
10
- import { notFound } from "./helpers.mjs";
11
- import { hxRedirect } from "./htmx.mjs";
12
- import { WebhookModel } from "../models/webhook.mjs";
13
- import { StatusConfigModel } from "../models/status-config.mjs";
14
- import { renderProjectSettingsPage } from "../pages/project-settings.mjs";
4
+ import { registerGeneralSettings } from "./settings-general.mjs";
5
+ import { registerLabelSettings } from "./settings-labels.mjs";
6
+ import { registerMemberSettings } from "./settings-members.mjs";
7
+ import { registerStatusSettings } from "./settings-status.mjs";
8
+ import { registerTokenSettings } from "./settings-tokens.mjs";
9
+ import { registerWebhookSettings } from "./settings-webhooks.mjs";
15
10
  //#region src/routers/settings.ts
16
- async function loadSettingsData(slug) {
17
- const project = await new ProjectModel(getStore().db).getBySlug(slug);
18
- if (!project) return null;
19
- const { db, config, user, authEnabled, gitHosts } = getStore();
20
- const labelTypes = await new LabelModel(db).listTypes(project.id);
21
- const tokens = (await new TokenModel(db).list(project.id)).map(({ hash: _hash, ...rest }) => rest);
22
- const members = await new MemberModel(db).list(project.id);
23
- return {
24
- project,
25
- labelTypes,
26
- tokens,
27
- members,
28
- webhooks: (await new WebhookModel(db).list(project.id)).map((webhook) => ({
29
- id: webhook.id,
30
- url: webhook.url,
31
- events: WebhookModel.eventsOf(webhook)
32
- })),
33
- statusConfigs: (await new StatusConfigModel(db, config.secret).list(project.id)).map((row) => ({
34
- id: row.id,
35
- provider: row.provider,
36
- config: JSON.parse(row.config),
37
- hasToken: row.tokenEncrypted.length > 0,
38
- createdAt: row.createdAt,
39
- updatedAt: row.updatedAt
40
- })),
41
- gitHosts,
42
- isAdmin: !authEnabled || user?.role === "admin" || members.some((member) => member.userId === user?.id && member.role === "admin")
43
- };
44
- }
45
- async function renderSettingsPage(c, tab, formState) {
46
- const data = await loadSettingsData(c.req.param("slug") ?? "");
47
- if (!data) notFound("Project not found");
48
- return await renderProjectSettingsPage({
49
- project: data.project,
50
- activeTab: tab,
51
- labelTypes: data.labelTypes,
52
- tokens: data.tokens,
53
- members: data.members,
54
- webhooks: data.webhooks,
55
- statusConfigs: data.statusConfigs,
56
- gitHosts: data.gitHosts,
57
- isAdmin: data.isAdmin
58
- }, formState);
59
- }
60
- async function settingsPage(c, tab) {
61
- const html = await renderSettingsPage(c, tab);
62
- if (!html) notFound("Project not found");
63
- return html;
64
- }
65
- async function findProject(slug) {
66
- const project = await new ProjectModel(getStore().db).getBySlug(slug);
67
- if (!project) notFound("Project not found");
68
- return project;
69
- }
11
+ /** Register the server-rendered project settings pages and their form handlers. */
70
12
  function registerSettingsPages(app) {
71
- app.get("/projects/:slug/settings", async (c) => c.html(await settingsPage(c, "general")));
72
- app.get("/projects/:slug/settings/tests", async (c) => c.html(await settingsPage(c, "tests")));
73
- app.get("/projects/:slug/settings/labels", async (c) => c.html(await settingsPage(c, "labels")));
74
- app.get("/projects/:slug/settings/tokens", async (c) => c.html(await settingsPage(c, "tokens")));
75
- app.get("/projects/:slug/settings/webhooks", async (c) => c.html(await settingsPage(c, "webhooks")));
76
- app.get("/projects/:slug/settings/members", async (c) => c.html(await settingsPage(c, "members")));
77
- app.get("/projects/:slug/settings/status", async (c) => c.html(await settingsPage(c, "status")));
78
- app.post("/projects/:slug/settings", async (c) => {
79
- const slug = c.req.param("slug");
80
- const project = await findProject(slug);
81
- const form = await c.req.formData();
82
- const name = form.get("name");
83
- if (typeof name !== "string" || name.trim() === "") return c.html(await renderSettingsPage(c, "general", { errors: { name: "Name is required" } }) ?? "", 400);
84
- const gitRepository = asString(form.get("gitRepository"));
85
- const gitDefaultBranch = asString(form.get("gitDefaultBranch"));
86
- const pixelThreshold = asString(form.get("pixelThreshold"));
87
- const maxDiffRatio = asString(form.get("maxDiffRatio"));
88
- const publicBranchRegex = asString(form.get("publicBranchRegex"));
89
- try {
90
- await new ProjectModel(getStore().db).update(project.id, {
91
- name: name.trim(),
92
- gitRepository: gitRepository ?? void 0,
93
- gitDefaultBranch: gitDefaultBranch ?? void 0,
94
- pixelThreshold: pixelThreshold ? Number(pixelThreshold) : void 0,
95
- maxDiffRatio: maxDiffRatio ? Number(maxDiffRatio) : void 0,
96
- publicBranchRegex: publicBranchRegex === "" ? null : publicBranchRegex ?? void 0
97
- });
98
- return hxRedirect(c, `/projects/${slug}/settings`);
99
- } catch (error) {
100
- const message = error instanceof Error ? error.message : "Failed to update";
101
- return c.html(await renderSettingsPage(c, "general", { globalError: message }) ?? "", 400);
102
- }
103
- });
104
- app.post("/projects/:slug/settings/tests", async (c) => {
105
- const slug = c.req.param("slug");
106
- const project = await findProject(slug);
107
- const form = await c.req.formData();
108
- const executePlay = form.get("executePlay") === "true";
109
- const playTimeoutMsRaw = asString(form.get("playTimeoutMs"));
110
- const playTimeoutMs = playTimeoutMsRaw ? Number(playTimeoutMsRaw) : void 0;
111
- if (playTimeoutMs !== void 0 && (Number.isNaN(playTimeoutMs) || playTimeoutMs < 1e3 || playTimeoutMs > 3e4)) return c.html(await renderSettingsPage(c, "tests", { globalError: "Play timeout must be between 1000 and 30000" }) ?? "", 400);
112
- try {
113
- await new ProjectModel(getStore().db).update(project.id, {
114
- executePlay,
115
- playTimeoutMs: playTimeoutMs ?? void 0
116
- });
117
- return hxRedirect(c, `/projects/${slug}/settings/tests`);
118
- } catch (error) {
119
- const message = error instanceof Error ? error.message : "Failed to update";
120
- return c.html(await renderSettingsPage(c, "tests", { globalError: message }) ?? "", 400);
121
- }
122
- });
123
- app.post("/projects/:slug/delete", async (c) => {
124
- const project = await findProject(c.req.param("slug"));
125
- await new ProjectModel(getStore().db).remove(project.id);
126
- return hxRedirect(c, "/projects");
127
- });
128
- app.post("/projects/:slug/settings/labels", async (c) => {
129
- const project = await findProject(c.req.param("slug"));
130
- const form = await c.req.formData();
131
- const key = asString(form.get("key"));
132
- const labelName = asString(form.get("labelName")) ?? asString(form.get("name"));
133
- const linkTemplate = asString(form.get("linkTemplate"));
134
- if (!key || !labelName) return c.html(await renderSettingsPage(c, "labels", { globalError: "Key and name are required" }) ?? "", 400);
135
- try {
136
- await new LabelModel(getStore().db).createType(project.id, {
137
- key,
138
- name: labelName,
139
- linkTemplate
140
- });
141
- return hxRedirect(c, `/projects/${project.slug}/settings/labels`);
142
- } catch (error) {
143
- const message = error instanceof Error ? error.message : "Failed to create label";
144
- return c.html(await renderSettingsPage(c, "labels", { globalError: message }) ?? "", 400);
145
- }
146
- });
147
- app.post("/projects/:slug/settings/labels/:key/delete", async (c) => {
148
- const project = await findProject(c.req.param("slug"));
149
- try {
150
- await new LabelModel(getStore().db).removeType(project.id, c.req.param("key"));
151
- } catch {
152
- return c.html(await renderSettingsPage(c, "labels", { globalError: "Cannot delete built-in label" }) ?? "", 400);
153
- }
154
- return hxRedirect(c, `/projects/${project.slug}/settings/labels`);
155
- });
156
- app.post("/projects/:slug/settings/tokens", async (c) => {
157
- const project = await findProject(c.req.param("slug"));
158
- const form = await c.req.formData();
159
- const tokenName = asString(form.get("tokenName")) ?? asString(form.get("name"));
160
- if (!tokenName) return c.html(await renderSettingsPage(c, "tokens", { globalError: "Name is required" }) ?? "", 400);
161
- const token = randomToken("shelf_");
162
- await new TokenModel(getStore().db).create(project.id, tokenName, token.hash);
163
- const html = await renderSettingsPage(c, "tokens");
164
- const inject = `<div class="alert alert--success" role="alert"><strong class="alert__title">Token created</strong><div class="alert__body">Copy now — shown once: <code>${token.value}</code></div></div>`;
165
- const withToken = html.replace("<nav class=\"tabs\"", `${inject}<nav class="tabs"`);
166
- return c.html(withToken);
167
- });
168
- app.post("/projects/:slug/settings/tokens/:tokenId/delete", async (c) => {
169
- const project = await findProject(c.req.param("slug"));
170
- await new TokenModel(getStore().db).remove(c.req.param("tokenId"));
171
- return hxRedirect(c, `/projects/${project.slug}/settings/tokens`);
172
- });
173
- app.post("/projects/:slug/settings/webhooks", async (c) => {
174
- const project = await findProject(c.req.param("slug"));
175
- const form = await c.req.formData();
176
- const url = asString(form.get("url"));
177
- if (!url) return c.html(await renderSettingsPage(c, "webhooks", { errors: { url: "A valid URL is required" } }) ?? "", 400);
178
- let parsedUrl;
179
- try {
180
- parsedUrl = new URL(url);
181
- } catch {
182
- return c.html(await renderSettingsPage(c, "webhooks", { errors: { url: "Enter a valid URL" } }) ?? "", 400);
183
- }
184
- if (parsedUrl.protocol !== "https:" && parsedUrl.protocol !== "http:") return c.html(await renderSettingsPage(c, "webhooks", { errors: { url: "URL must use http or https" } }) ?? "", 400);
185
- const eventsRaw = asString(form.get("events"));
186
- const events = eventsRaw ? eventsRaw.split(",").map((event) => event.trim()).filter((event) => event.length > 0) : void 0;
187
- const webhookModel = new WebhookModel(getStore().db);
188
- const secret = randomToken("whsec_").value;
189
- await webhookModel.create(project.id, {
190
- url,
191
- events,
192
- secret
193
- });
194
- return c.html(await renderSettingsPage(c, "webhooks", { secret }) ?? "", 201);
195
- });
196
- app.post("/projects/:slug/settings/webhooks/:webhookId/delete", async (c) => {
197
- const project = await findProject(c.req.param("slug"));
198
- const webhook = await new WebhookModel(getStore().db).get(project.id, c.req.param("webhookId"));
199
- if (!webhook) notFound("Webhook not found");
200
- await new WebhookModel(getStore().db).remove(project.id, webhook.id);
201
- return hxRedirect(c, `/projects/${project.slug}/settings/webhooks`);
202
- });
203
- app.post("/projects/:slug/settings/members", async (c) => {
204
- const project = await findProject(c.req.param("slug"));
205
- const form = await c.req.formData();
206
- const userId = asString(form.get("userId"));
207
- const role = asString(form.get("role"));
208
- if (!userId || !role) return c.html(await renderSettingsPage(c, "members", { globalError: "User and role are required" }) ?? "", 400);
209
- await new MemberModel(getStore().db).set(project.id, userId, role);
210
- return hxRedirect(c, `/projects/${project.slug}/settings/members`);
211
- });
212
- app.post("/projects/:slug/settings/members/:userId/remove", async (c) => {
213
- const project = await findProject(c.req.param("slug"));
214
- await new MemberModel(getStore().db).remove(project.id, c.req.param("userId"));
215
- return hxRedirect(c, `/projects/${project.slug}/settings/members`);
216
- });
217
- app.post("/projects/:slug/settings/status", async (c) => {
218
- const project = await findProject(c.req.param("slug"));
219
- const form = await c.req.formData();
220
- const providerKey = asString(form.get("provider"));
221
- const token = asString(form.get("token"));
222
- const configRaw = asString(form.get("config")) ?? "{}";
223
- const provider = getStore().gitHosts.find((p) => p.metadata.kind === providerKey);
224
- if (!provider) return c.html(await renderSettingsPage(c, "status", { globalError: "Unknown git provider" }) ?? "", 400);
225
- if (!token) return c.html(await renderSettingsPage(c, "status", { errors: { token: "Token is required" } }) ?? "", 400);
226
- let config;
227
- try {
228
- config = JSON.parse(configRaw);
229
- } catch {
230
- return c.html(await renderSettingsPage(c, "status", { errors: { config: "Config must be valid JSON" } }) ?? "", 400);
231
- }
232
- const parsed = provider.metadata.schema.safeParse(config);
233
- if (!parsed.success) return c.html(await renderSettingsPage(c, "status", { errors: { config: parsed.error.message } }) ?? "", 400);
234
- const { db, config: shelfConfig } = getStore();
235
- await new StatusConfigModel(db, shelfConfig.secret).create(project.id, {
236
- provider: provider.metadata.kind,
237
- config: parsed.data,
238
- token
239
- });
240
- return hxRedirect(c, `/projects/${project.slug}/settings/status`);
241
- });
242
- app.post("/projects/:slug/settings/status/:id/delete", async (c) => {
243
- const project = await findProject(c.req.param("slug"));
244
- const { db, config: shelfConfig } = getStore();
245
- await new StatusConfigModel(db, shelfConfig.secret).remove(project.id, c.req.param("id"));
246
- return hxRedirect(c, `/projects/${project.slug}/settings/status`);
247
- });
248
- }
249
- function asString(value) {
250
- return typeof value === "string" ? value.trim() : void 0;
13
+ registerGeneralSettings(app);
14
+ registerLabelSettings(app);
15
+ registerTokenSettings(app);
16
+ registerWebhookSettings(app);
17
+ registerMemberSettings(app);
18
+ registerStatusSettings(app);
251
19
  }
252
20
  //#endregion
253
- export { registerSettingsPages, renderSettingsPage };
21
+ export { registerSettingsPages };
254
22
 
255
23
  //# sourceMappingURL=settings.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"settings.mjs","names":[],"sources":["../../src/routers/settings.ts"],"sourcesContent":["import type { Context } from \"hono\";\nimport type { GitHostProvider } from \"../adapters/git-host/index.ts\";\nimport type { ShelfApp } from \"../index.tsx\";\n\nimport { LabelModel } from \"../models/label.ts\";\nimport { MemberModel } from \"../models/member.ts\";\nimport { ProjectModel } from \"../models/project.ts\";\nimport { StatusConfigModel } from \"../models/status-config.ts\";\nimport { TokenModel } from \"../models/token.ts\";\nimport { WebhookModel } from \"../models/webhook.ts\";\nimport { renderProjectSettingsPage, type SettingsFormState, type SettingsTab } from \"../pages/project-settings.tsx\";\nimport type { SettingsMember } from \"../pages/settings-members.tsx\";\nimport type { SettingsStatusConfig } from \"../pages/settings-status.tsx\";\nimport type { SettingsWebhook } from \"../pages/settings-webhooks.tsx\";\nimport type { LabelType, Project, Token } from \"../schema.ts\";\nimport { getStore } from \"../store.ts\";\nimport { randomToken } from \"../utils/hash.ts\";\nimport { notFound } from \"./helpers.ts\";\nimport { hxRedirect } from \"./htmx.ts\";\n\ninterface SettingsData {\n project: Project;\n labelTypes: LabelType[];\n tokens: Omit<Token, \"hash\">[];\n members: SettingsMember[];\n webhooks: SettingsWebhook[];\n statusConfigs: SettingsStatusConfig[];\n gitHosts: GitHostProvider[];\n isAdmin: boolean;\n}\n\nasync function loadSettingsData(slug: string): Promise<SettingsData | null> {\n const project = await new ProjectModel(getStore().db).getBySlug(slug);\n if (!project) {\n return null;\n }\n const { db, config, user, authEnabled, gitHosts } = getStore();\n const labelTypes = await new LabelModel(db).listTypes(project.id);\n const tokensDb = await new TokenModel(db).list(project.id);\n const tokens = tokensDb.map(({ hash: _hash, ...rest }) => rest);\n const members = await new MemberModel(db).list(project.id);\n const webhooksDb = await new WebhookModel(db).list(project.id);\n const webhooks = webhooksDb.map((webhook) => ({ id: webhook.id, url: webhook.url, events: WebhookModel.eventsOf(webhook) }));\n const statusConfigsDb = await new StatusConfigModel(db, config.secret).list(project.id);\n const statusConfigs = statusConfigsDb.map((row) => ({\n id: row.id,\n provider: row.provider,\n config: JSON.parse(row.config) as Record<string, unknown>,\n hasToken: row.tokenEncrypted.length > 0,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n }));\n const isAdmin = !authEnabled || user?.role === \"admin\" || members.some((member) => member.userId === user?.id && member.role === \"admin\");\n return { project, labelTypes, tokens, members, webhooks, statusConfigs, gitHosts, isAdmin };\n}\n\nexport async function renderSettingsPage(c: Context, tab: SettingsTab, formState?: SettingsFormState): Promise<string> {\n const slug = c.req.param(\"slug\") ?? \"\";\n const data = await loadSettingsData(slug);\n if (!data) {\n notFound(\"Project not found\");\n }\n return await renderProjectSettingsPage(\n {\n project: data.project,\n activeTab: tab,\n labelTypes: data.labelTypes,\n tokens: data.tokens,\n members: data.members,\n webhooks: data.webhooks,\n statusConfigs: data.statusConfigs,\n gitHosts: data.gitHosts,\n isAdmin: data.isAdmin,\n },\n formState,\n );\n}\n\nasync function settingsPage(c: Context, tab: SettingsTab): Promise<string> {\n const html = await renderSettingsPage(c, tab);\n if (!html) {\n notFound(\"Project not found\");\n }\n return html;\n}\n\nasync function findProject(slug: string): Promise<Project> {\n const project = await new ProjectModel(getStore().db).getBySlug(slug);\n if (!project) {\n notFound(\"Project not found\");\n }\n return project;\n}\n\nexport function registerSettingsPages(app: ShelfApp): void {\n app.get(\"/projects/:slug/settings\", async (c) => c.html(await settingsPage(c, \"general\")));\n app.get(\"/projects/:slug/settings/tests\", async (c) => c.html(await settingsPage(c, \"tests\")));\n app.get(\"/projects/:slug/settings/labels\", async (c) => c.html(await settingsPage(c, \"labels\")));\n app.get(\"/projects/:slug/settings/tokens\", async (c) => c.html(await settingsPage(c, \"tokens\")));\n app.get(\"/projects/:slug/settings/webhooks\", async (c) => c.html(await settingsPage(c, \"webhooks\")));\n app.get(\"/projects/:slug/settings/members\", async (c) => c.html(await settingsPage(c, \"members\")));\n app.get(\"/projects/:slug/settings/status\", async (c) => c.html(await settingsPage(c, \"status\")));\n\n app.post(\"/projects/:slug/settings\", async (c) => {\n const slug = c.req.param(\"slug\");\n const project = await findProject(slug);\n const form = await c.req.formData();\n const name = form.get(\"name\");\n if (typeof name !== \"string\" || name.trim() === \"\") {\n return c.html((await renderSettingsPage(c, \"general\", { errors: { name: \"Name is required\" } })) ?? \"\", 400);\n }\n const gitRepository = asString(form.get(\"gitRepository\"));\n const gitDefaultBranch = asString(form.get(\"gitDefaultBranch\"));\n const pixelThreshold = asString(form.get(\"pixelThreshold\"));\n const maxDiffRatio = asString(form.get(\"maxDiffRatio\"));\n const publicBranchRegex = asString(form.get(\"publicBranchRegex\"));\n try {\n await new ProjectModel(getStore().db).update(project.id, {\n name: name.trim(),\n gitRepository: gitRepository ?? undefined,\n gitDefaultBranch: gitDefaultBranch ?? undefined,\n pixelThreshold: pixelThreshold ? Number(pixelThreshold) : undefined,\n maxDiffRatio: maxDiffRatio ? Number(maxDiffRatio) : undefined,\n publicBranchRegex: publicBranchRegex === \"\" ? null : (publicBranchRegex ?? undefined),\n });\n return hxRedirect(c, `/projects/${slug}/settings`);\n } catch (error) {\n const message = error instanceof Error ? error.message : \"Failed to update\";\n return c.html((await renderSettingsPage(c, \"general\", { globalError: message })) ?? \"\", 400);\n }\n });\n\n app.post(\"/projects/:slug/settings/tests\", async (c) => {\n const slug = c.req.param(\"slug\");\n const project = await findProject(slug);\n const form = await c.req.formData();\n const executePlay = form.get(\"executePlay\") === \"true\";\n const playTimeoutMsRaw = asString(form.get(\"playTimeoutMs\"));\n const playTimeoutMs = playTimeoutMsRaw ? Number(playTimeoutMsRaw) : undefined;\n if (playTimeoutMs !== undefined && (Number.isNaN(playTimeoutMs) || playTimeoutMs < 1000 || playTimeoutMs > 30_000)) {\n return c.html((await renderSettingsPage(c, \"tests\", { globalError: \"Play timeout must be between 1000 and 30000\" })) ?? \"\", 400);\n }\n try {\n await new ProjectModel(getStore().db).update(project.id, {\n executePlay,\n playTimeoutMs: playTimeoutMs ?? undefined,\n });\n return hxRedirect(c, `/projects/${slug}/settings/tests`);\n } catch (error) {\n const message = error instanceof Error ? error.message : \"Failed to update\";\n return c.html((await renderSettingsPage(c, \"tests\", { globalError: message })) ?? \"\", 400);\n }\n });\n\n app.post(\"/projects/:slug/delete\", async (c) => {\n const project = await findProject(c.req.param(\"slug\"));\n await new ProjectModel(getStore().db).remove(project.id);\n return hxRedirect(c, \"/projects\");\n });\n\n app.post(\"/projects/:slug/settings/labels\", async (c) => {\n const project = await findProject(c.req.param(\"slug\"));\n const form = await c.req.formData();\n const key = asString(form.get(\"key\"));\n const labelName = asString(form.get(\"labelName\")) ?? asString(form.get(\"name\"));\n const linkTemplate = asString(form.get(\"linkTemplate\"));\n if (!key || !labelName) {\n return c.html((await renderSettingsPage(c, \"labels\", { globalError: \"Key and name are required\" })) ?? \"\", 400);\n }\n try {\n await new LabelModel(getStore().db).createType(project.id, { key, name: labelName, linkTemplate });\n return hxRedirect(c, `/projects/${project.slug}/settings/labels`);\n } catch (error) {\n const message = error instanceof Error ? error.message : \"Failed to create label\";\n return c.html((await renderSettingsPage(c, \"labels\", { globalError: message })) ?? \"\", 400);\n }\n });\n\n app.post(\"/projects/:slug/settings/labels/:key/delete\", async (c) => {\n const project = await findProject(c.req.param(\"slug\"));\n try {\n await new LabelModel(getStore().db).removeType(project.id, c.req.param(\"key\"));\n } catch {\n return c.html((await renderSettingsPage(c, \"labels\", { globalError: \"Cannot delete built-in label\" })) ?? \"\", 400);\n }\n return hxRedirect(c, `/projects/${project.slug}/settings/labels`);\n });\n\n app.post(\"/projects/:slug/settings/tokens\", async (c) => {\n const project = await findProject(c.req.param(\"slug\"));\n const form = await c.req.formData();\n const tokenName = asString(form.get(\"tokenName\")) ?? asString(form.get(\"name\"));\n if (!tokenName) {\n return c.html((await renderSettingsPage(c, \"tokens\", { globalError: \"Name is required\" })) ?? \"\", 400);\n }\n const token = randomToken(\"shelf_\");\n await new TokenModel(getStore().db).create(project.id, tokenName, token.hash);\n const html = await renderSettingsPage(c, \"tokens\");\n const inject = `<div class=\"alert alert--success\" role=\"alert\"><strong class=\"alert__title\">Token created</strong><div class=\"alert__body\">Copy now — shown once: <code>${token.value}</code></div></div>`;\n const withToken = html.replace('<nav class=\"tabs\"', `${inject}<nav class=\"tabs\"`);\n return c.html(withToken);\n });\n\n app.post(\"/projects/:slug/settings/tokens/:tokenId/delete\", async (c) => {\n const project = await findProject(c.req.param(\"slug\"));\n await new TokenModel(getStore().db).remove(c.req.param(\"tokenId\"));\n return hxRedirect(c, `/projects/${project.slug}/settings/tokens`);\n });\n\n app.post(\"/projects/:slug/settings/webhooks\", async (c) => {\n const project = await findProject(c.req.param(\"slug\"));\n const form = await c.req.formData();\n const url = asString(form.get(\"url\"));\n if (!url) {\n return c.html((await renderSettingsPage(c, \"webhooks\", { errors: { url: \"A valid URL is required\" } })) ?? \"\", 400);\n }\n let parsedUrl: URL;\n try {\n parsedUrl = new URL(url);\n } catch {\n return c.html((await renderSettingsPage(c, \"webhooks\", { errors: { url: \"Enter a valid URL\" } })) ?? \"\", 400);\n }\n if (parsedUrl.protocol !== \"https:\" && parsedUrl.protocol !== \"http:\") {\n return c.html((await renderSettingsPage(c, \"webhooks\", { errors: { url: \"URL must use http or https\" } })) ?? \"\", 400);\n }\n const eventsRaw = asString(form.get(\"events\"));\n const events = eventsRaw ? eventsRaw.split(\",\").map((event) => event.trim()).filter((event) => event.length > 0) : undefined;\n const webhookModel = new WebhookModel(getStore().db);\n const secret = randomToken(\"whsec_\").value;\n await webhookModel.create(project.id, { url, events, secret });\n return c.html((await renderSettingsPage(c, \"webhooks\", { secret })) ?? \"\", 201);\n });\n\n app.post(\"/projects/:slug/settings/webhooks/:webhookId/delete\", async (c) => {\n const project = await findProject(c.req.param(\"slug\"));\n const webhook = await new WebhookModel(getStore().db).get(project.id, c.req.param(\"webhookId\"));\n if (!webhook) {\n notFound(\"Webhook not found\");\n }\n await new WebhookModel(getStore().db).remove(project.id, webhook.id);\n return hxRedirect(c, `/projects/${project.slug}/settings/webhooks`);\n });\n\n app.post(\"/projects/:slug/settings/members\", async (c) => {\n const project = await findProject(c.req.param(\"slug\"));\n const form = await c.req.formData();\n const userId = asString(form.get(\"userId\"));\n const role = asString(form.get(\"role\"));\n if (!userId || !role) {\n return c.html((await renderSettingsPage(c, \"members\", { globalError: \"User and role are required\" })) ?? \"\", 400);\n }\n await new MemberModel(getStore().db).set(project.id, userId, role as never);\n return hxRedirect(c, `/projects/${project.slug}/settings/members`);\n });\n\n app.post(\"/projects/:slug/settings/members/:userId/remove\", async (c) => {\n const project = await findProject(c.req.param(\"slug\"));\n await new MemberModel(getStore().db).remove(project.id, c.req.param(\"userId\"));\n return hxRedirect(c, `/projects/${project.slug}/settings/members`);\n });\n\n app.post(\"/projects/:slug/settings/status\", async (c) => {\n const project = await findProject(c.req.param(\"slug\"));\n const form = await c.req.formData();\n const providerKey = asString(form.get(\"provider\"));\n const token = asString(form.get(\"token\"));\n const configRaw = asString(form.get(\"config\")) ?? \"{}\";\n const providers = getStore().gitHosts;\n const provider = providers.find((p) => p.metadata.kind === providerKey);\n if (!provider) {\n return c.html((await renderSettingsPage(c, \"status\", { globalError: \"Unknown git provider\" })) ?? \"\", 400);\n }\n if (!token) {\n return c.html((await renderSettingsPage(c, \"status\", { errors: { token: \"Token is required\" } })) ?? \"\", 400);\n }\n let config: unknown;\n try {\n config = JSON.parse(configRaw);\n } catch {\n return c.html((await renderSettingsPage(c, \"status\", { errors: { config: \"Config must be valid JSON\" } })) ?? \"\", 400);\n }\n const parsed = provider.metadata.schema.safeParse(config);\n if (!parsed.success) {\n return c.html((await renderSettingsPage(c, \"status\", { errors: { config: parsed.error.message } })) ?? \"\", 400);\n }\n const { db, config: shelfConfig } = getStore();\n await new StatusConfigModel(db, shelfConfig.secret).create(project.id, { provider: provider.metadata.kind, config: parsed.data, token });\n return hxRedirect(c, `/projects/${project.slug}/settings/status`);\n });\n\n app.post(\"/projects/:slug/settings/status/:id/delete\", async (c) => {\n const project = await findProject(c.req.param(\"slug\"));\n const { db, config: shelfConfig } = getStore();\n await new StatusConfigModel(db, shelfConfig.secret).remove(project.id, c.req.param(\"id\"));\n return hxRedirect(c, `/projects/${project.slug}/settings/status`);\n });\n}\n\nfunction asString(value: FormDataEntryValue | null): string | undefined {\n return typeof value === \"string\" ? value.trim() : undefined;\n}"],"mappings":"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"}
1
+ {"version":3,"file":"settings.mjs","names":[],"sources":["../../src/routers/settings.ts"],"sourcesContent":["import type { ShelfApp } from \"../index.tsx\";\nimport { registerGeneralSettings } from \"./settings-general.ts\";\nimport { registerLabelSettings } from \"./settings-labels.ts\";\nimport { registerMemberSettings } from \"./settings-members.ts\";\nimport { registerStatusSettings } from \"./settings-status.ts\";\nimport { registerTokenSettings } from \"./settings-tokens.ts\";\nimport { registerWebhookSettings } from \"./settings-webhooks.ts\";\n\n/** Register the server-rendered project settings pages and their form handlers. */\nexport function registerSettingsPages(app: ShelfApp): void {\n registerGeneralSettings(app);\n registerLabelSettings(app);\n registerTokenSettings(app);\n registerWebhookSettings(app);\n registerMemberSettings(app);\n registerStatusSettings(app);\n}\n"],"mappings":";;;;;;;;;;;AASA,SAAgB,sBAAsB,KAAqB;CACzD,wBAAwB,GAAG;CAC3B,sBAAsB,GAAG;CACzB,sBAAsB,GAAG;CACzB,wBAAwB,GAAG;CAC3B,uBAAuB,GAAG;CAC1B,uBAAuB,GAAG;AAC5B"}
@@ -1,10 +1,10 @@
1
1
  import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
+ import { SnapshotModel } from "../models/snapshot.mjs";
4
5
  import { getStore } from "../store.mjs";
5
6
  import { resolveAuthorizedProject } from "./helpers.mjs";
6
7
  import { notFound, okSchema, snapshotSchema, unauthorized } from "./schemas.mjs";
7
- import { SnapshotModel } from "../models/snapshot.mjs";
8
8
  import { APPROVER_ROLES, VIEW_ROLES, approveSnapshot, buildForProject, refreshBuild, snapshotForBuild } from "./builds.handlers.mjs";
9
9
  import { createRoute, z } from "@hono/zod-openapi";
10
10
  //#region src/routers/snapshots.ts
@@ -86,6 +86,7 @@ const rejectAllRoute = createRoute({
86
86
  ...notFound
87
87
  }
88
88
  });
89
+ /** Register the snapshot list, approve, reject, and bulk-review endpoints. */
89
90
  function registerSnapshots(app) {
90
91
  app.openapi(listSnapshotsRoute, async (c) => {
91
92
  const { slug, buildId } = c.req.valid("param");
@@ -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\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"];
@@ -63,6 +63,7 @@ function toPublic(row) {
63
63
  updatedAt: row.updatedAt
64
64
  };
65
65
  }
66
+ /** Register the git status-config list, create, and delete endpoints. */
66
67
  function registerStatusConfigs(app) {
67
68
  app.openapi(listRoute, async (c) => {
68
69
  const { slug } = c.req.valid("param");
@@ -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\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,11 @@ 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
+ }
63
+ /** Register the published Storybook resolver and static-asset routes. */
59
64
  function registerStorybook(app) {
60
65
  app.get("/projects/:slug/storybook", async (c) => {
61
66
  const slug = c.req.param("slug");
@@ -95,7 +100,10 @@ function registerStorybook(app) {
95
100
  const build = await new BuildModel(getStore().db).get(buildId);
96
101
  if (!build || build.projectId !== project.id) notFound("Build not found");
97
102
  if (!await canViewBuild(build, project)) return c.redirect("/auth/login", 302);
98
- 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
+ }
99
107
  if (rest.split("/").some((segment) => !isSafeSegment(segment))) return c.notFound();
100
108
  const path = posix.join(storybookDir(project.id, build.id), rest);
101
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\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";
@@ -49,6 +49,7 @@ const deleteTokenRoute = createRoute({
49
49
  ...notFound$1
50
50
  }
51
51
  });
52
+ /** Register the project API token list, create, and revoke endpoints. */
52
53
  function registerTokens(app) {
53
54
  app.openapi(listTokensRoute, async (c) => {
54
55
  const project = await resolveAuthorizedProject(c, c.req.valid("param").slug, ...ADMIN_ROLES);
@@ -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\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;AAED,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"}