@storyshelf/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. package/README.md +127 -0
  2. package/dist/adapter/auth.d.mts +50 -0
  3. package/dist/adapter/auth.mjs +10 -0
  4. package/dist/adapter/auth.mjs.map +1 -0
  5. package/dist/adapter/capture-runner.d.mts +70 -0
  6. package/dist/adapter/capture-runner.mjs +4 -0
  7. package/dist/adapter/database.d.mts +43 -0
  8. package/dist/adapter/database.mjs +4 -0
  9. package/dist/adapter/git-host/helpers.d.mts +12 -0
  10. package/dist/adapter/git-host/helpers.mjs +29 -0
  11. package/dist/adapter/git-host/helpers.mjs.map +1 -0
  12. package/dist/adapter/git-host.d.mts +46 -0
  13. package/dist/adapter/git-host.mjs +4 -0
  14. package/dist/adapter/storage.d.mts +22 -0
  15. package/dist/adapter/storage.mjs +4 -0
  16. package/dist/adapters/capture-queue.d.mts +49 -0
  17. package/dist/adapters/metadata.d.mts +24 -0
  18. package/dist/adapters/webhook-events.mjs +39 -0
  19. package/dist/adapters/webhook-events.mjs.map +1 -0
  20. package/dist/capture/adapter.d.mts +29 -0
  21. package/dist/capture/adapter.mjs +13 -0
  22. package/dist/capture/adapter.mjs.map +1 -0
  23. package/dist/capture/orchestrator.d.mts +23 -0
  24. package/dist/capture/orchestrator.mjs +103 -0
  25. package/dist/capture/orchestrator.mjs.map +1 -0
  26. package/dist/capture/pipeline.d.mts +41 -0
  27. package/dist/capture/pipeline.mjs +119 -0
  28. package/dist/capture/pipeline.mjs.map +1 -0
  29. package/dist/capture/queue.d.mts +35 -0
  30. package/dist/capture/queue.mjs +96 -0
  31. package/dist/capture/queue.mjs.map +1 -0
  32. package/dist/capture/status-fanout.mjs +66 -0
  33. package/dist/capture/status-fanout.mjs.map +1 -0
  34. package/dist/capture/storybook.d.mts +14 -0
  35. package/dist/capture/storybook.mjs +40 -0
  36. package/dist/capture/storybook.mjs.map +1 -0
  37. package/dist/config.d.mts +68 -0
  38. package/dist/config.mjs +69 -0
  39. package/dist/config.mjs.map +1 -0
  40. package/dist/ddl.d.mts +7 -0
  41. package/dist/ddl.mjs +155 -0
  42. package/dist/ddl.mjs.map +1 -0
  43. package/dist/diff/engine.d.mts +8 -0
  44. package/dist/diff/engine.mjs +79 -0
  45. package/dist/diff/engine.mjs.map +1 -0
  46. package/dist/diff/options.d.mts +40 -0
  47. package/dist/diff/options.mjs +15 -0
  48. package/dist/diff/options.mjs.map +1 -0
  49. package/dist/index.d.mts +48 -0
  50. package/dist/index.mjs +327 -0
  51. package/dist/index.mjs.map +1 -0
  52. package/dist/logger.d.mts +39 -0
  53. package/dist/logger.mjs +35 -0
  54. package/dist/logger.mjs.map +1 -0
  55. package/dist/middleware/csrf.mjs +39 -0
  56. package/dist/middleware/csrf.mjs.map +1 -0
  57. package/dist/middleware/rate-limit.mjs +38 -0
  58. package/dist/middleware/rate-limit.mjs.map +1 -0
  59. package/dist/models/baseline.d.mts +20 -0
  60. package/dist/models/baseline.mjs +89 -0
  61. package/dist/models/baseline.mjs.map +1 -0
  62. package/dist/models/build.d.mts +35 -0
  63. package/dist/models/build.mjs +77 -0
  64. package/dist/models/build.mjs.map +1 -0
  65. package/dist/models/comment.d.mts +20 -0
  66. package/dist/models/comment.mjs +42 -0
  67. package/dist/models/comment.mjs.map +1 -0
  68. package/dist/models/label.d.mts +43 -0
  69. package/dist/models/label.mjs +107 -0
  70. package/dist/models/label.mjs.map +1 -0
  71. package/dist/models/member.d.mts +27 -0
  72. package/dist/models/member.mjs +54 -0
  73. package/dist/models/member.mjs.map +1 -0
  74. package/dist/models/project.d.mts +42 -0
  75. package/dist/models/project.mjs +78 -0
  76. package/dist/models/project.mjs.map +1 -0
  77. package/dist/models/snapshot.d.mts +46 -0
  78. package/dist/models/snapshot.mjs +75 -0
  79. package/dist/models/snapshot.mjs.map +1 -0
  80. package/dist/models/status-config.mjs +61 -0
  81. package/dist/models/status-config.mjs.map +1 -0
  82. package/dist/models/token.d.mts +33 -0
  83. package/dist/models/token.mjs +60 -0
  84. package/dist/models/token.mjs.map +1 -0
  85. package/dist/models/webhook.d.mts +37 -0
  86. package/dist/models/webhook.mjs +67 -0
  87. package/dist/models/webhook.mjs.map +1 -0
  88. package/dist/pages/build-detail.mjs +333 -0
  89. package/dist/pages/build-detail.mjs.map +1 -0
  90. package/dist/pages/build-diff.mjs +437 -0
  91. package/dist/pages/build-diff.mjs.map +1 -0
  92. package/dist/pages/compute-jobs.mjs +196 -0
  93. package/dist/pages/compute-jobs.mjs.map +1 -0
  94. package/dist/pages/login.mjs +77 -0
  95. package/dist/pages/login.mjs.map +1 -0
  96. package/dist/pages/project-builds.mjs +228 -0
  97. package/dist/pages/project-builds.mjs.map +1 -0
  98. package/dist/pages/project-create.mjs +168 -0
  99. package/dist/pages/project-create.mjs.map +1 -0
  100. package/dist/pages/project-settings.mjs +111 -0
  101. package/dist/pages/project-settings.mjs.map +1 -0
  102. package/dist/pages/projects.mjs +178 -0
  103. package/dist/pages/projects.mjs.map +1 -0
  104. package/dist/pages/root.mjs +90 -0
  105. package/dist/pages/root.mjs.map +1 -0
  106. package/dist/pages/settings-general.mjs +191 -0
  107. package/dist/pages/settings-general.mjs.map +1 -0
  108. package/dist/pages/settings-labels.mjs +149 -0
  109. package/dist/pages/settings-labels.mjs.map +1 -0
  110. package/dist/pages/settings-members.mjs +124 -0
  111. package/dist/pages/settings-members.mjs.map +1 -0
  112. package/dist/pages/settings-status.mjs +150 -0
  113. package/dist/pages/settings-status.mjs.map +1 -0
  114. package/dist/pages/settings-tokens.mjs +95 -0
  115. package/dist/pages/settings-tokens.mjs.map +1 -0
  116. package/dist/pages/settings-webhooks.mjs +149 -0
  117. package/dist/pages/settings-webhooks.mjs.map +1 -0
  118. package/dist/retention/purge.d.mts +28 -0
  119. package/dist/retention/purge.mjs +66 -0
  120. package/dist/retention/purge.mjs.map +1 -0
  121. package/dist/routers/admin.mjs +41 -0
  122. package/dist/routers/admin.mjs.map +1 -0
  123. package/dist/routers/assets.mjs +24 -0
  124. package/dist/routers/assets.mjs.map +1 -0
  125. package/dist/routers/auth.mjs +77 -0
  126. package/dist/routers/auth.mjs.map +1 -0
  127. package/dist/routers/builds.handlers.mjs +87 -0
  128. package/dist/routers/builds.handlers.mjs.map +1 -0
  129. package/dist/routers/builds.mjs +162 -0
  130. package/dist/routers/builds.mjs.map +1 -0
  131. package/dist/routers/comments.mjs +89 -0
  132. package/dist/routers/comments.mjs.map +1 -0
  133. package/dist/routers/helpers.mjs +76 -0
  134. package/dist/routers/helpers.mjs.map +1 -0
  135. package/dist/routers/htmx.mjs +18 -0
  136. package/dist/routers/htmx.mjs.map +1 -0
  137. package/dist/routers/labels.mjs +77 -0
  138. package/dist/routers/labels.mjs.map +1 -0
  139. package/dist/routers/media.mjs +54 -0
  140. package/dist/routers/media.mjs.map +1 -0
  141. package/dist/routers/members.mjs +101 -0
  142. package/dist/routers/members.mjs.map +1 -0
  143. package/dist/routers/projects.mjs +121 -0
  144. package/dist/routers/projects.mjs.map +1 -0
  145. package/dist/routers/schemas.mjs +188 -0
  146. package/dist/routers/schemas.mjs.map +1 -0
  147. package/dist/routers/settings.mjs +235 -0
  148. package/dist/routers/settings.mjs.map +1 -0
  149. package/dist/routers/snapshots.mjs +143 -0
  150. package/dist/routers/snapshots.mjs.map +1 -0
  151. package/dist/routers/status-configs.mjs +100 -0
  152. package/dist/routers/status-configs.mjs.map +1 -0
  153. package/dist/routers/tokens.mjs +80 -0
  154. package/dist/routers/tokens.mjs.map +1 -0
  155. package/dist/routers/ui.mjs +142 -0
  156. package/dist/routers/ui.mjs.map +1 -0
  157. package/dist/routers/webhooks.mjs +89 -0
  158. package/dist/routers/webhooks.mjs.map +1 -0
  159. package/dist/schema.d.mts +4372 -0
  160. package/dist/schema.mjs +158 -0
  161. package/dist/schema.mjs.map +1 -0
  162. package/dist/store.mjs +18 -0
  163. package/dist/store.mjs.map +1 -0
  164. package/dist/types.d.mts +18 -0
  165. package/dist/types.mjs +49 -0
  166. package/dist/types.mjs.map +1 -0
  167. package/dist/ui/components.mjs +127 -0
  168. package/dist/ui/components.mjs.map +1 -0
  169. package/dist/ui/document.d.mts +8 -0
  170. package/dist/ui/document.mjs +338 -0
  171. package/dist/ui/document.mjs.map +1 -0
  172. package/dist/ui/styles.mjs +173 -0
  173. package/dist/ui/styles.mjs.map +1 -0
  174. package/dist/ui/theme.mjs +42 -0
  175. package/dist/ui/theme.mjs.map +1 -0
  176. package/dist/urls.d.mts +19 -0
  177. package/dist/urls.mjs +23 -0
  178. package/dist/urls.mjs.map +1 -0
  179. package/dist/utils/encrypt.mjs +49 -0
  180. package/dist/utils/encrypt.mjs.map +1 -0
  181. package/dist/utils/hash.mjs +22 -0
  182. package/dist/utils/hash.mjs.map +1 -0
  183. package/dist/utils/paths.d.mts +11 -0
  184. package/dist/utils/paths.mjs +23 -0
  185. package/dist/utils/paths.mjs.map +1 -0
  186. package/dist/utils/ulid.d.mts +8 -0
  187. package/dist/utils/ulid.mjs +41 -0
  188. package/dist/utils/ulid.mjs.map +1 -0
  189. package/package.json +162 -0
@@ -0,0 +1,235 @@
1
+ import "node:path";
2
+ import "node:url";
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";
15
+ //#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
+ }
70
+ function registerSettingsPages(app) {
71
+ app.get("/projects/:slug/settings", async (c) => c.html(await settingsPage(c, "general")));
72
+ app.get("/projects/:slug/settings/labels", async (c) => c.html(await settingsPage(c, "labels")));
73
+ app.get("/projects/:slug/settings/tokens", async (c) => c.html(await settingsPage(c, "tokens")));
74
+ app.get("/projects/:slug/settings/webhooks", async (c) => c.html(await settingsPage(c, "webhooks")));
75
+ app.get("/projects/:slug/settings/members", async (c) => c.html(await settingsPage(c, "members")));
76
+ app.get("/projects/:slug/settings/status", async (c) => c.html(await settingsPage(c, "status")));
77
+ app.post("/projects/:slug/settings", async (c) => {
78
+ const slug = c.req.param("slug");
79
+ const project = await findProject(slug);
80
+ const form = await c.req.formData();
81
+ const name = form.get("name");
82
+ if (typeof name !== "string" || name.trim() === "") return c.html(await renderSettingsPage(c, "general", { errors: { name: "Name is required" } }) ?? "", 400);
83
+ const gitRepository = asString(form.get("gitRepository"));
84
+ const gitDefaultBranch = asString(form.get("gitDefaultBranch"));
85
+ const pixelThreshold = asString(form.get("pixelThreshold"));
86
+ const maxDiffRatio = asString(form.get("maxDiffRatio"));
87
+ const publicBranchRegex = asString(form.get("publicBranchRegex"));
88
+ try {
89
+ await new ProjectModel(getStore().db).update(project.id, {
90
+ name: name.trim(),
91
+ gitRepository: gitRepository ?? void 0,
92
+ gitDefaultBranch: gitDefaultBranch ?? void 0,
93
+ pixelThreshold: pixelThreshold ? Number(pixelThreshold) : void 0,
94
+ maxDiffRatio: maxDiffRatio ? Number(maxDiffRatio) : void 0,
95
+ publicBranchRegex: publicBranchRegex === "" ? null : publicBranchRegex ?? void 0
96
+ });
97
+ return hxRedirect(c, `/projects/${slug}/settings`);
98
+ } catch (error) {
99
+ const message = error instanceof Error ? error.message : "Failed to update";
100
+ return c.html(await renderSettingsPage(c, "general", { globalError: message }) ?? "", 400);
101
+ }
102
+ });
103
+ app.post("/projects/:slug/delete", async (c) => {
104
+ const project = await findProject(c.req.param("slug"));
105
+ await new ProjectModel(getStore().db).remove(project.id);
106
+ return hxRedirect(c, "/projects");
107
+ });
108
+ app.post("/projects/:slug/settings/labels", async (c) => {
109
+ const project = await findProject(c.req.param("slug"));
110
+ const form = await c.req.formData();
111
+ const key = asString(form.get("key"));
112
+ const labelName = asString(form.get("labelName")) ?? asString(form.get("name"));
113
+ const linkTemplate = asString(form.get("linkTemplate"));
114
+ if (!key || !labelName) return c.html(await renderSettingsPage(c, "labels", { globalError: "Key and name are required" }) ?? "", 400);
115
+ try {
116
+ await new LabelModel(getStore().db).createType(project.id, {
117
+ key,
118
+ name: labelName,
119
+ linkTemplate
120
+ });
121
+ return hxRedirect(c, `/projects/${project.slug}/settings/labels`);
122
+ } catch (error) {
123
+ const message = error instanceof Error ? error.message : "Failed to create label";
124
+ return c.html(await renderSettingsPage(c, "labels", { globalError: message }) ?? "", 400);
125
+ }
126
+ });
127
+ app.post("/projects/:slug/settings/labels/:key/delete", async (c) => {
128
+ const project = await findProject(c.req.param("slug"));
129
+ try {
130
+ await new LabelModel(getStore().db).removeType(project.id, c.req.param("key"));
131
+ } catch {
132
+ return c.html(await renderSettingsPage(c, "labels", { globalError: "Cannot delete built-in label" }) ?? "", 400);
133
+ }
134
+ return hxRedirect(c, `/projects/${project.slug}/settings/labels`);
135
+ });
136
+ app.post("/projects/:slug/settings/tokens", async (c) => {
137
+ const project = await findProject(c.req.param("slug"));
138
+ const form = await c.req.formData();
139
+ const tokenName = asString(form.get("tokenName")) ?? asString(form.get("name"));
140
+ if (!tokenName) return c.html(await renderSettingsPage(c, "tokens", { globalError: "Name is required" }) ?? "", 400);
141
+ const token = randomToken("shelf_");
142
+ await new TokenModel(getStore().db).create(project.id, tokenName, token.hash);
143
+ const html = await renderSettingsPage(c, "tokens");
144
+ 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>`;
145
+ const withToken = html.replace("<nav class=\"tabs\"", `${inject}<nav class="tabs"`);
146
+ return c.html(withToken);
147
+ });
148
+ app.post("/projects/:slug/settings/tokens/:tokenId/delete", async (c) => {
149
+ const project = await findProject(c.req.param("slug"));
150
+ await new TokenModel(getStore().db).remove(c.req.param("tokenId"));
151
+ return hxRedirect(c, `/projects/${project.slug}/settings/tokens`);
152
+ });
153
+ app.post("/projects/:slug/settings/webhooks", async (c) => {
154
+ const project = await findProject(c.req.param("slug"));
155
+ const form = await c.req.formData();
156
+ const url = asString(form.get("url"));
157
+ if (!url) return c.html(await renderSettingsPage(c, "webhooks", { errors: { url: "A valid URL is required" } }) ?? "", 400);
158
+ let parsedUrl;
159
+ try {
160
+ parsedUrl = new URL(url);
161
+ } catch {
162
+ return c.html(await renderSettingsPage(c, "webhooks", { errors: { url: "Enter a valid URL" } }) ?? "", 400);
163
+ }
164
+ if (parsedUrl.protocol !== "https:" && parsedUrl.protocol !== "http:") return c.html(await renderSettingsPage(c, "webhooks", { errors: { url: "URL must use http or https" } }) ?? "", 400);
165
+ const eventsRaw = asString(form.get("events"));
166
+ const events = eventsRaw ? eventsRaw.split(",").map((event) => event.trim()).filter((event) => event.length > 0) : void 0;
167
+ const webhookModel = new WebhookModel(getStore().db);
168
+ const secret = randomToken("whsec_").value;
169
+ await webhookModel.create(project.id, {
170
+ url,
171
+ events,
172
+ secret
173
+ });
174
+ return c.html(await renderSettingsPage(c, "webhooks", { secret }) ?? "", 201);
175
+ });
176
+ app.post("/projects/:slug/settings/webhooks/:webhookId/delete", async (c) => {
177
+ const project = await findProject(c.req.param("slug"));
178
+ const webhook = await new WebhookModel(getStore().db).get(project.id, c.req.param("webhookId"));
179
+ if (!webhook) notFound("Webhook not found");
180
+ await new WebhookModel(getStore().db).remove(project.id, webhook.id);
181
+ return hxRedirect(c, `/projects/${project.slug}/settings/webhooks`);
182
+ });
183
+ app.post("/projects/:slug/settings/members", async (c) => {
184
+ const project = await findProject(c.req.param("slug"));
185
+ const form = await c.req.formData();
186
+ const userId = asString(form.get("userId"));
187
+ const role = asString(form.get("role"));
188
+ if (!userId || !role) return c.html(await renderSettingsPage(c, "members", { globalError: "User and role are required" }) ?? "", 400);
189
+ await new MemberModel(getStore().db).set(project.id, userId, role);
190
+ return hxRedirect(c, `/projects/${project.slug}/settings/members`);
191
+ });
192
+ app.post("/projects/:slug/settings/members/:userId/remove", async (c) => {
193
+ const project = await findProject(c.req.param("slug"));
194
+ await new MemberModel(getStore().db).remove(project.id, c.req.param("userId"));
195
+ return hxRedirect(c, `/projects/${project.slug}/settings/members`);
196
+ });
197
+ app.post("/projects/:slug/settings/status", async (c) => {
198
+ const project = await findProject(c.req.param("slug"));
199
+ const form = await c.req.formData();
200
+ const providerKey = asString(form.get("provider"));
201
+ const token = asString(form.get("token"));
202
+ const configRaw = asString(form.get("config")) ?? "{}";
203
+ const provider = getStore().gitHosts.find((p) => p.metadata.kind === providerKey);
204
+ if (!provider) return c.html(await renderSettingsPage(c, "status", { globalError: "Unknown git provider" }) ?? "", 400);
205
+ if (!token) return c.html(await renderSettingsPage(c, "status", { errors: { token: "Token is required" } }) ?? "", 400);
206
+ let config;
207
+ try {
208
+ config = JSON.parse(configRaw);
209
+ } catch {
210
+ return c.html(await renderSettingsPage(c, "status", { errors: { config: "Config must be valid JSON" } }) ?? "", 400);
211
+ }
212
+ const parsed = provider.metadata.schema.safeParse(config);
213
+ if (!parsed.success) return c.html(await renderSettingsPage(c, "status", { errors: { config: parsed.error.message } }) ?? "", 400);
214
+ const { db, config: shelfConfig } = getStore();
215
+ await new StatusConfigModel(db, shelfConfig.secret).create(project.id, {
216
+ provider: provider.metadata.kind,
217
+ config: parsed.data,
218
+ token
219
+ });
220
+ return hxRedirect(c, `/projects/${project.slug}/settings/status`);
221
+ });
222
+ app.post("/projects/:slug/settings/status/:id/delete", async (c) => {
223
+ const project = await findProject(c.req.param("slug"));
224
+ const { db, config: shelfConfig } = getStore();
225
+ await new StatusConfigModel(db, shelfConfig.secret).remove(project.id, c.req.param("id"));
226
+ return hxRedirect(c, `/projects/${project.slug}/settings/status`);
227
+ });
228
+ }
229
+ function asString(value) {
230
+ return typeof value === "string" ? value.trim() : void 0;
231
+ }
232
+ //#endregion
233
+ export { registerSettingsPages, renderSettingsPage };
234
+
235
+ //# sourceMappingURL=settings.mjs.map
@@ -0,0 +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: Array<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/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/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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"}
@@ -0,0 +1,143 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { getStore } from "../store.mjs";
5
+ import { resolveAuthorizedProject } from "./helpers.mjs";
6
+ import { notFound, okSchema, snapshotSchema, unauthorized } from "./schemas.mjs";
7
+ import { SnapshotModel } from "../models/snapshot.mjs";
8
+ import { APPROVER_ROLES, VIEW_ROLES, approveSnapshot, buildForProject, refreshBuild, snapshotForBuild } from "./builds.handlers.mjs";
9
+ import { createRoute, z } from "@hono/zod-openapi";
10
+ //#region src/routers/snapshots.ts
11
+ const listSnapshotsRoute = createRoute({
12
+ method: "get",
13
+ path: "/api/v1/projects/{slug}/builds/{buildId}/snapshots",
14
+ request: { params: z.object({
15
+ slug: z.string(),
16
+ buildId: z.string()
17
+ }) },
18
+ responses: {
19
+ 200: {
20
+ content: { "application/json": { schema: snapshotSchema.array() } },
21
+ description: "List snapshots for a build"
22
+ },
23
+ ...notFound,
24
+ ...unauthorized
25
+ }
26
+ });
27
+ const approveSnapshotRoute = createRoute({
28
+ method: "post",
29
+ path: "/api/v1/projects/{slug}/builds/{buildId}/snapshots/{snapshotId}/approve",
30
+ request: { params: z.object({
31
+ slug: z.string(),
32
+ buildId: z.string(),
33
+ snapshotId: z.string()
34
+ }) },
35
+ responses: {
36
+ 200: {
37
+ content: { "application/json": { schema: okSchema } },
38
+ description: "Snapshot approved"
39
+ },
40
+ ...notFound
41
+ }
42
+ });
43
+ const rejectSnapshotRoute = createRoute({
44
+ method: "post",
45
+ path: "/api/v1/projects/{slug}/builds/{buildId}/snapshots/{snapshotId}/reject",
46
+ request: { params: z.object({
47
+ slug: z.string(),
48
+ buildId: z.string(),
49
+ snapshotId: z.string()
50
+ }) },
51
+ responses: {
52
+ 200: {
53
+ content: { "application/json": { schema: okSchema } },
54
+ description: "Snapshot rejected"
55
+ },
56
+ ...notFound
57
+ }
58
+ });
59
+ const approveAllRoute = createRoute({
60
+ method: "post",
61
+ path: "/api/v1/projects/{slug}/builds/{buildId}/approve-all",
62
+ request: { params: z.object({
63
+ slug: z.string(),
64
+ buildId: z.string()
65
+ }) },
66
+ responses: {
67
+ 200: {
68
+ content: { "application/json": { schema: okSchema } },
69
+ description: "All snapshots approved"
70
+ },
71
+ ...notFound
72
+ }
73
+ });
74
+ const rejectAllRoute = createRoute({
75
+ method: "post",
76
+ path: "/api/v1/projects/{slug}/builds/{buildId}/reject-all",
77
+ request: { params: z.object({
78
+ slug: z.string(),
79
+ buildId: z.string()
80
+ }) },
81
+ responses: {
82
+ 200: {
83
+ content: { "application/json": { schema: okSchema } },
84
+ description: "All snapshots rejected"
85
+ },
86
+ ...notFound
87
+ }
88
+ });
89
+ function registerSnapshots(app) {
90
+ app.openapi(listSnapshotsRoute, async (c) => {
91
+ const { slug, buildId } = c.req.valid("param");
92
+ const project = await resolveAuthorizedProject(c, slug, ...VIEW_ROLES);
93
+ const build = await buildForProject(project.id, buildId);
94
+ const snapshots = new SnapshotModel(getStore().db).listByBuild(build.id);
95
+ return c.json(await snapshots);
96
+ });
97
+ app.openapi(approveSnapshotRoute, async (c) => {
98
+ const { slug, buildId, snapshotId } = c.req.valid("param");
99
+ const project = await resolveAuthorizedProject(c, slug, ...APPROVER_ROLES);
100
+ const build = await buildForProject(project.id, buildId);
101
+ await snapshotForBuild(build, snapshotId);
102
+ const userId = getStore().user?.id ?? "anonymous";
103
+ await approveSnapshot(snapshotId, userId);
104
+ return c.json({ ok: true });
105
+ });
106
+ app.openapi(rejectSnapshotRoute, async (c) => {
107
+ const { slug, buildId, snapshotId } = c.req.valid("param");
108
+ const project = await resolveAuthorizedProject(c, slug, ...APPROVER_ROLES);
109
+ const build = await buildForProject(project.id, buildId);
110
+ const snapshot = await snapshotForBuild(build, snapshotId);
111
+ const userId = getStore().user?.id ?? "anonymous";
112
+ await new SnapshotModel(getStore().db).review(snapshot.id, "rejected", userId);
113
+ await refreshBuild(build.id);
114
+ return c.json({ ok: true });
115
+ });
116
+ app.openapi(approveAllRoute, async (c) => {
117
+ const { slug, buildId } = c.req.valid("param");
118
+ const project = await resolveAuthorizedProject(c, slug, ...APPROVER_ROLES);
119
+ const build = await buildForProject(project.id, buildId);
120
+ const snapshots = await new SnapshotModel(getStore().db).listByBuild(build.id);
121
+ const userId = getStore().user?.id ?? "anonymous";
122
+ await Promise.all(snapshots.filter((snapshot) => snapshot.status === "new" || snapshot.status === "changed").map(async (snapshot) => {
123
+ await approveSnapshot(snapshot.id, userId);
124
+ }));
125
+ return c.json({ ok: true });
126
+ });
127
+ app.openapi(rejectAllRoute, async (c) => {
128
+ const { slug, buildId } = c.req.valid("param");
129
+ const project = await resolveAuthorizedProject(c, slug, ...APPROVER_ROLES);
130
+ const build = await buildForProject(project.id, buildId);
131
+ const snapshots = await new SnapshotModel(getStore().db).listByBuild(build.id);
132
+ const userId = getStore().user?.id ?? "anonymous";
133
+ await Promise.all(snapshots.filter((snapshot) => snapshot.status === "new" || snapshot.status === "changed").map(async (snapshot) => {
134
+ await new SnapshotModel(getStore().db).review(snapshot.id, "rejected", userId);
135
+ }));
136
+ await refreshBuild(build.id);
137
+ return c.json({ ok: true });
138
+ });
139
+ }
140
+ //#endregion
141
+ export { registerSnapshots };
142
+
143
+ //# sourceMappingURL=snapshots.mjs.map
@@ -0,0 +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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"}
@@ -0,0 +1,100 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { getStore } from "../store.mjs";
5
+ import { resolveAuthorizedProject } from "./helpers.mjs";
6
+ import { notFound, statusConfigCreateSchema, statusConfigSchema, unauthorized } from "./schemas.mjs";
7
+ import { StatusConfigModel } from "../models/status-config.mjs";
8
+ import { createRoute, z } from "@hono/zod-openapi";
9
+ //#region src/routers/status-configs.ts
10
+ const ADMIN_ROLES = ["admin"];
11
+ const listRoute = createRoute({
12
+ method: "get",
13
+ path: "/api/v1/projects/{slug}/status-configs",
14
+ request: { params: z.object({ slug: z.string() }) },
15
+ responses: {
16
+ 200: {
17
+ content: { "application/json": { schema: statusConfigSchema.array() } },
18
+ description: "List status configs"
19
+ },
20
+ ...notFound,
21
+ ...unauthorized
22
+ }
23
+ });
24
+ const createRouteDef = createRoute({
25
+ method: "post",
26
+ path: "/api/v1/projects/{slug}/status-configs",
27
+ request: {
28
+ params: z.object({ slug: z.string() }),
29
+ body: { content: { "application/json": { schema: statusConfigCreateSchema } } }
30
+ },
31
+ responses: {
32
+ 201: {
33
+ content: { "application/json": { schema: statusConfigSchema } },
34
+ description: "Created status config"
35
+ },
36
+ 400: {
37
+ content: { "application/json": { schema: z.object({ message: z.string() }) } },
38
+ description: "Bad request"
39
+ },
40
+ ...notFound
41
+ }
42
+ });
43
+ const deleteRoute = createRoute({
44
+ method: "delete",
45
+ path: "/api/v1/projects/{slug}/status-configs/{id}",
46
+ request: { params: z.object({
47
+ slug: z.string(),
48
+ id: z.string()
49
+ }) },
50
+ responses: {
51
+ 204: { description: "Deleted status config" },
52
+ ...notFound
53
+ }
54
+ });
55
+ function toPublic(row) {
56
+ return {
57
+ id: row.id,
58
+ projectId: row.projectId,
59
+ provider: row.provider,
60
+ config: JSON.parse(row.config),
61
+ hasToken: true,
62
+ createdAt: row.createdAt,
63
+ updatedAt: row.updatedAt
64
+ };
65
+ }
66
+ function registerStatusConfigs(app) {
67
+ app.openapi(listRoute, async (c) => {
68
+ const { slug } = c.req.valid("param");
69
+ const project = await resolveAuthorizedProject(c, slug, "admin");
70
+ const rows = await new StatusConfigModel(getStore().db, getStore().config.secret).list(project.id);
71
+ return c.json(rows.map((row) => toPublic(row)));
72
+ });
73
+ app.openapi(createRouteDef, async (c) => {
74
+ const { slug } = c.req.valid("param");
75
+ const project = await resolveAuthorizedProject(c, slug, ...ADMIN_ROLES);
76
+ const body = c.req.valid("json");
77
+ const provider = getStore().gitHosts.find((p) => p.metadata.kind === body.provider);
78
+ if (!provider) return c.json({ message: `Unknown provider: ${body.provider}` }, 400);
79
+ const parsed = provider.metadata.schema.safeParse(body.config);
80
+ if (!parsed.success) return c.json({ message: parsed.error.message }, 400);
81
+ const row = await new StatusConfigModel(getStore().db, getStore().config.secret).create(project.id, {
82
+ provider: body.provider,
83
+ config: parsed.data,
84
+ token: body.token
85
+ });
86
+ return c.json(toPublic(row), 201);
87
+ });
88
+ app.openapi(deleteRoute, async (c) => {
89
+ const { slug, id } = c.req.valid("param");
90
+ const project = await resolveAuthorizedProject(c, slug, ...ADMIN_ROLES);
91
+ const model = new StatusConfigModel(getStore().db, getStore().config.secret);
92
+ if (!await model.get(project.id, id)) return c.json({ message: "Status config not found" }, 404);
93
+ await model.remove(project.id, id);
94
+ return c.body(null, 204);
95
+ });
96
+ }
97
+ //#endregion
98
+ export { registerStatusConfigs };
99
+
100
+ //# sourceMappingURL=status-configs.mjs.map
@@ -0,0 +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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"}
@@ -0,0 +1,80 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { getStore } from "../store.mjs";
5
+ import { TokenModel } from "../models/token.mjs";
6
+ import { randomToken } from "../utils/hash.mjs";
7
+ import { notFound, resolveAuthorizedProject } from "./helpers.mjs";
8
+ import { notFound as notFound$1, tokenCreateSchema, tokenCreatedSchema, tokenPublicSchema, unauthorized } from "./schemas.mjs";
9
+ import { createRoute, z } from "@hono/zod-openapi";
10
+ //#region src/routers/tokens.ts
11
+ const ADMIN_ROLES = ["admin"];
12
+ const listTokensRoute = createRoute({
13
+ method: "get",
14
+ path: "/api/v1/projects/{slug}/tokens",
15
+ request: { params: z.object({ slug: z.string() }) },
16
+ responses: {
17
+ 200: {
18
+ content: { "application/json": { schema: tokenPublicSchema.array() } },
19
+ description: "List API tokens"
20
+ },
21
+ ...notFound$1,
22
+ ...unauthorized
23
+ }
24
+ });
25
+ const createTokenRoute = createRoute({
26
+ method: "post",
27
+ path: "/api/v1/projects/{slug}/tokens",
28
+ request: {
29
+ params: z.object({ slug: z.string() }),
30
+ body: { content: { "application/json": { schema: tokenCreateSchema } } }
31
+ },
32
+ responses: {
33
+ 201: {
34
+ content: { "application/json": { schema: tokenCreatedSchema } },
35
+ description: "Created token"
36
+ },
37
+ ...notFound$1
38
+ }
39
+ });
40
+ const deleteTokenRoute = createRoute({
41
+ method: "delete",
42
+ path: "/api/v1/projects/{slug}/tokens/{tokenId}",
43
+ request: { params: z.object({
44
+ slug: z.string(),
45
+ tokenId: z.string()
46
+ }) },
47
+ responses: {
48
+ 204: { description: "Deleted token" },
49
+ ...notFound$1
50
+ }
51
+ });
52
+ function registerTokens(app) {
53
+ app.openapi(listTokensRoute, async (c) => {
54
+ const project = await resolveAuthorizedProject(c, c.req.valid("param").slug, ...ADMIN_ROLES);
55
+ const tokens = await new TokenModel(getStore().db).list(project.id);
56
+ return c.json(tokens.map(({ hash: _hash, ...rest }) => rest));
57
+ });
58
+ app.openapi(createTokenRoute, async (c) => {
59
+ const project = await resolveAuthorizedProject(c, c.req.valid("param").slug, ...ADMIN_ROLES);
60
+ const body = c.req.valid("json");
61
+ const token = randomToken("shelf_");
62
+ await new TokenModel(getStore().db).create(project.id, body.name, token.hash);
63
+ return c.json({
64
+ ...body,
65
+ token: token.value
66
+ }, 201);
67
+ });
68
+ app.openapi(deleteTokenRoute, async (c) => {
69
+ const { slug, tokenId } = c.req.valid("param");
70
+ const project = await resolveAuthorizedProject(c, slug, ...ADMIN_ROLES);
71
+ const found = await new TokenModel(getStore().db).get(project.id, tokenId);
72
+ if (!found) notFound("Token not found");
73
+ await new TokenModel(getStore().db).remove(found.id);
74
+ return c.body(null, 204);
75
+ });
76
+ }
77
+ //#endregion
78
+ export { registerTokens };
79
+
80
+ //# sourceMappingURL=tokens.mjs.map