@storyshelf/app 0.5.3

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 (268) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +18 -0
  3. package/dist/app-types.d.mts +54 -0
  4. package/dist/asset-manifest.mjs +41 -0
  5. package/dist/asset-manifest.mjs.map +1 -0
  6. package/dist/asset-paths.mjs +23 -0
  7. package/dist/asset-paths.mjs.map +1 -0
  8. package/dist/assets/htmx.min.js +3304 -0
  9. package/dist/assets/mark.svg +1 -0
  10. package/dist/capture-setup.mjs +139 -0
  11. package/dist/capture-setup.mjs.map +1 -0
  12. package/dist/credential-keys.mjs +20 -0
  13. package/dist/credential-keys.mjs.map +1 -0
  14. package/dist/credentials.mjs +56 -0
  15. package/dist/credentials.mjs.map +1 -0
  16. package/dist/index.d.mts +18 -0
  17. package/dist/index.mjs +310 -0
  18. package/dist/index.mjs.map +1 -0
  19. package/dist/lifecycle.mjs +132 -0
  20. package/dist/lifecycle.mjs.map +1 -0
  21. package/dist/middleware/auth-gate.mjs +36 -0
  22. package/dist/middleware/auth-gate.mjs.map +1 -0
  23. package/dist/middleware/csrf.mjs +75 -0
  24. package/dist/middleware/csrf.mjs.map +1 -0
  25. package/dist/middleware/rate-limit.mjs +68 -0
  26. package/dist/middleware/rate-limit.mjs.map +1 -0
  27. package/dist/middleware/request-log.mjs +30 -0
  28. package/dist/middleware/request-log.mjs.map +1 -0
  29. package/dist/middleware/server-timing.mjs +39 -0
  30. package/dist/middleware/server-timing.mjs.map +1 -0
  31. package/dist/middleware/setup-gate.mjs +37 -0
  32. package/dist/middleware/setup-gate.mjs.map +1 -0
  33. package/dist/middleware/store-scope.mjs +33 -0
  34. package/dist/middleware/store-scope.mjs.map +1 -0
  35. package/dist/notify.d.mts +17 -0
  36. package/dist/notify.mjs +141 -0
  37. package/dist/notify.mjs.map +1 -0
  38. package/dist/pages/admin-system.mjs +243 -0
  39. package/dist/pages/admin-system.mjs.map +1 -0
  40. package/dist/pages/build-detail.mjs +392 -0
  41. package/dist/pages/build-detail.mjs.map +1 -0
  42. package/dist/pages/build-diff-actions.mjs +162 -0
  43. package/dist/pages/build-diff-actions.mjs.map +1 -0
  44. package/dist/pages/build-diff-comments.mjs +104 -0
  45. package/dist/pages/build-diff-comments.mjs.map +1 -0
  46. package/dist/pages/build-diff-header.mjs +117 -0
  47. package/dist/pages/build-diff-header.mjs.map +1 -0
  48. package/dist/pages/build-diff-model.mjs +48 -0
  49. package/dist/pages/build-diff-model.mjs.map +1 -0
  50. package/dist/pages/build-diff-nav.mjs +122 -0
  51. package/dist/pages/build-diff-nav.mjs.map +1 -0
  52. package/dist/pages/build-diff-shortcuts.mjs +78 -0
  53. package/dist/pages/build-diff-shortcuts.mjs.map +1 -0
  54. package/dist/pages/build-diff-stale.mjs +70 -0
  55. package/dist/pages/build-diff-stale.mjs.map +1 -0
  56. package/dist/pages/build-diff-viewer.mjs +70 -0
  57. package/dist/pages/build-diff-viewer.mjs.map +1 -0
  58. package/dist/pages/build-diff.mjs +110 -0
  59. package/dist/pages/build-diff.mjs.map +1 -0
  60. package/dist/pages/compute-jobs.mjs +165 -0
  61. package/dist/pages/compute-jobs.mjs.map +1 -0
  62. package/dist/pages/invite.mjs +91 -0
  63. package/dist/pages/invite.mjs.map +1 -0
  64. package/dist/pages/label-detail.mjs +154 -0
  65. package/dist/pages/label-detail.mjs.map +1 -0
  66. package/dist/pages/library.mjs +343 -0
  67. package/dist/pages/library.mjs.map +1 -0
  68. package/dist/pages/login.mjs +117 -0
  69. package/dist/pages/login.mjs.map +1 -0
  70. package/dist/pages/passkey-ceremony.mjs +97 -0
  71. package/dist/pages/passkey-ceremony.mjs.map +1 -0
  72. package/dist/pages/profile.mjs +364 -0
  73. package/dist/pages/profile.mjs.map +1 -0
  74. package/dist/pages/project-builds-table.mjs +151 -0
  75. package/dist/pages/project-builds-table.mjs.map +1 -0
  76. package/dist/pages/project-builds.mjs +143 -0
  77. package/dist/pages/project-builds.mjs.map +1 -0
  78. package/dist/pages/project-create.mjs +90 -0
  79. package/dist/pages/project-create.mjs.map +1 -0
  80. package/dist/pages/project-settings.mjs +95 -0
  81. package/dist/pages/project-settings.mjs.map +1 -0
  82. package/dist/pages/projects-card.mjs +194 -0
  83. package/dist/pages/projects-card.mjs.map +1 -0
  84. package/dist/pages/projects.mjs +129 -0
  85. package/dist/pages/projects.mjs.map +1 -0
  86. package/dist/pages/root.mjs +98 -0
  87. package/dist/pages/root.mjs.map +1 -0
  88. package/dist/pages/settings-general.mjs +138 -0
  89. package/dist/pages/settings-general.mjs.map +1 -0
  90. package/dist/pages/settings-labels.mjs +107 -0
  91. package/dist/pages/settings-labels.mjs.map +1 -0
  92. package/dist/pages/settings-members.mjs +192 -0
  93. package/dist/pages/settings-members.mjs.map +1 -0
  94. package/dist/pages/settings-notifications.mjs +240 -0
  95. package/dist/pages/settings-notifications.mjs.map +1 -0
  96. package/dist/pages/settings-status.mjs +137 -0
  97. package/dist/pages/settings-status.mjs.map +1 -0
  98. package/dist/pages/settings-tests.mjs +111 -0
  99. package/dist/pages/settings-tests.mjs.map +1 -0
  100. package/dist/pages/settings-tokens.mjs +90 -0
  101. package/dist/pages/settings-tokens.mjs.map +1 -0
  102. package/dist/pages/settings-webhooks.mjs +104 -0
  103. package/dist/pages/settings-webhooks.mjs.map +1 -0
  104. package/dist/pages/storybook.mjs +116 -0
  105. package/dist/pages/storybook.mjs.map +1 -0
  106. package/dist/retention-timer.mjs +65 -0
  107. package/dist/retention-timer.mjs.map +1 -0
  108. package/dist/routers/admin-pages.mjs +48 -0
  109. package/dist/routers/admin-pages.mjs.map +1 -0
  110. package/dist/routers/admin.mjs +113 -0
  111. package/dist/routers/admin.mjs.map +1 -0
  112. package/dist/routers/assets.mjs +64 -0
  113. package/dist/routers/assets.mjs.map +1 -0
  114. package/dist/routers/attempts.mjs +96 -0
  115. package/dist/routers/attempts.mjs.map +1 -0
  116. package/dist/routers/auth-sync.mjs +64 -0
  117. package/dist/routers/auth-sync.mjs.map +1 -0
  118. package/dist/routers/auth.mjs +236 -0
  119. package/dist/routers/auth.mjs.map +1 -0
  120. package/dist/routers/baseline-guard.mjs +83 -0
  121. package/dist/routers/baseline-guard.mjs.map +1 -0
  122. package/dist/routers/builds.handlers.mjs +217 -0
  123. package/dist/routers/builds.handlers.mjs.map +1 -0
  124. package/dist/routers/builds.mjs +381 -0
  125. package/dist/routers/builds.mjs.map +1 -0
  126. package/dist/routers/comments.mjs +109 -0
  127. package/dist/routers/comments.mjs.map +1 -0
  128. package/dist/routers/flash.mjs +35 -0
  129. package/dist/routers/flash.mjs.map +1 -0
  130. package/dist/routers/health-report.mjs +159 -0
  131. package/dist/routers/health-report.mjs.map +1 -0
  132. package/dist/routers/health.mjs +46 -0
  133. package/dist/routers/health.mjs.map +1 -0
  134. package/dist/routers/helpers.mjs +110 -0
  135. package/dist/routers/helpers.mjs.map +1 -0
  136. package/dist/routers/htmx.mjs +24 -0
  137. package/dist/routers/htmx.mjs.map +1 -0
  138. package/dist/routers/labels.mjs +118 -0
  139. package/dist/routers/labels.mjs.map +1 -0
  140. package/dist/routers/media.mjs +53 -0
  141. package/dist/routers/media.mjs.map +1 -0
  142. package/dist/routers/members.mjs +177 -0
  143. package/dist/routers/members.mjs.map +1 -0
  144. package/dist/routers/notification-channels.mjs +190 -0
  145. package/dist/routers/notification-channels.mjs.map +1 -0
  146. package/dist/routers/notification-subscriptions.mjs +120 -0
  147. package/dist/routers/notification-subscriptions.mjs.map +1 -0
  148. package/dist/routers/profile.mjs +276 -0
  149. package/dist/routers/profile.mjs.map +1 -0
  150. package/dist/routers/projects.mjs +133 -0
  151. package/dist/routers/projects.mjs.map +1 -0
  152. package/dist/routers/review-next.mjs +47 -0
  153. package/dist/routers/review-next.mjs.map +1 -0
  154. package/dist/routers/schemas.mjs +337 -0
  155. package/dist/routers/schemas.mjs.map +1 -0
  156. package/dist/routers/settings-general.mjs +104 -0
  157. package/dist/routers/settings-general.mjs.map +1 -0
  158. package/dist/routers/settings-labels.mjs +52 -0
  159. package/dist/routers/settings-labels.mjs.map +1 -0
  160. package/dist/routers/settings-members.mjs +58 -0
  161. package/dist/routers/settings-members.mjs.map +1 -0
  162. package/dist/routers/settings-notifications.mjs +108 -0
  163. package/dist/routers/settings-notifications.mjs.map +1 -0
  164. package/dist/routers/settings-status.mjs +77 -0
  165. package/dist/routers/settings-status.mjs.map +1 -0
  166. package/dist/routers/settings-tokens.mjs +40 -0
  167. package/dist/routers/settings-tokens.mjs.map +1 -0
  168. package/dist/routers/settings-webhooks.mjs +67 -0
  169. package/dist/routers/settings-webhooks.mjs.map +1 -0
  170. package/dist/routers/settings.handlers.mjs +149 -0
  171. package/dist/routers/settings.handlers.mjs.map +1 -0
  172. package/dist/routers/settings.mjs +25 -0
  173. package/dist/routers/settings.mjs.map +1 -0
  174. package/dist/routers/snapshots.mjs +240 -0
  175. package/dist/routers/snapshots.mjs.map +1 -0
  176. package/dist/routers/status-configs.mjs +108 -0
  177. package/dist/routers/status-configs.mjs.map +1 -0
  178. package/dist/routers/storybook.mjs +171 -0
  179. package/dist/routers/storybook.mjs.map +1 -0
  180. package/dist/routers/tokens.mjs +91 -0
  181. package/dist/routers/tokens.mjs.map +1 -0
  182. package/dist/routers/ui.mjs +166 -0
  183. package/dist/routers/ui.mjs.map +1 -0
  184. package/dist/routers/webhooks.mjs +97 -0
  185. package/dist/routers/webhooks.mjs.map +1 -0
  186. package/dist/routers/well-known.mjs +43 -0
  187. package/dist/routers/well-known.mjs.map +1 -0
  188. package/dist/runtime.mjs +52 -0
  189. package/dist/runtime.mjs.map +1 -0
  190. package/dist/store.mjs +20 -0
  191. package/dist/store.mjs.map +1 -0
  192. package/dist/ui/avatar.mjs +76 -0
  193. package/dist/ui/avatar.mjs.map +1 -0
  194. package/dist/ui/buttons.mjs +205 -0
  195. package/dist/ui/buttons.mjs.map +1 -0
  196. package/dist/ui/code-block.mjs +47 -0
  197. package/dist/ui/code-block.mjs.map +1 -0
  198. package/dist/ui/compare-stage.mjs +344 -0
  199. package/dist/ui/compare-stage.mjs.map +1 -0
  200. package/dist/ui/csrf-field.mjs +20 -0
  201. package/dist/ui/csrf-field.mjs.map +1 -0
  202. package/dist/ui/css.mjs +36 -0
  203. package/dist/ui/css.mjs.map +1 -0
  204. package/dist/ui/document.mjs +130 -0
  205. package/dist/ui/document.mjs.map +1 -0
  206. package/dist/ui/dropdown.mjs +181 -0
  207. package/dist/ui/dropdown.mjs.map +1 -0
  208. package/dist/ui/feedback.mjs +341 -0
  209. package/dist/ui/feedback.mjs.map +1 -0
  210. package/dist/ui/filter-input.mjs +58 -0
  211. package/dist/ui/filter-input.mjs.map +1 -0
  212. package/dist/ui/forms.mjs +291 -0
  213. package/dist/ui/forms.mjs.map +1 -0
  214. package/dist/ui/icons/icon.mjs +53 -0
  215. package/dist/ui/icons/icon.mjs.map +1 -0
  216. package/dist/ui/icons/paths.mjs +61 -0
  217. package/dist/ui/icons/paths.mjs.map +1 -0
  218. package/dist/ui/icons/sprite.mjs +26 -0
  219. package/dist/ui/icons/sprite.mjs.map +1 -0
  220. package/dist/ui/kbd.mjs +33 -0
  221. package/dist/ui/kbd.mjs.map +1 -0
  222. package/dist/ui/layout.mjs +173 -0
  223. package/dist/ui/layout.mjs.map +1 -0
  224. package/dist/ui/progress.mjs +44 -0
  225. package/dist/ui/progress.mjs.map +1 -0
  226. package/dist/ui/segmented.mjs +77 -0
  227. package/dist/ui/segmented.mjs.map +1 -0
  228. package/dist/ui/shell/account.mjs +161 -0
  229. package/dist/ui/shell/account.mjs.map +1 -0
  230. package/dist/ui/shell/bare.mjs +69 -0
  231. package/dist/ui/shell/bare.mjs.map +1 -0
  232. package/dist/ui/shell/boot-script.mjs +46 -0
  233. package/dist/ui/shell/boot-script.mjs.map +1 -0
  234. package/dist/ui/shell/client-script.mjs +259 -0
  235. package/dist/ui/shell/client-script.mjs.map +1 -0
  236. package/dist/ui/shell/nav.mjs +111 -0
  237. package/dist/ui/shell/nav.mjs.map +1 -0
  238. package/dist/ui/shell/review-script.mjs +195 -0
  239. package/dist/ui/shell/review-script.mjs.map +1 -0
  240. package/dist/ui/shell/sidebar.mjs +323 -0
  241. package/dist/ui/shell/sidebar.mjs.map +1 -0
  242. package/dist/ui/shell/topbar.mjs +268 -0
  243. package/dist/ui/shell/topbar.mjs.map +1 -0
  244. package/dist/ui/stacks.mjs +82 -0
  245. package/dist/ui/stacks.mjs.map +1 -0
  246. package/dist/ui/styles/base.mjs +41 -0
  247. package/dist/ui/styles/base.mjs.map +1 -0
  248. package/dist/ui/styles/behavior.mjs +43 -0
  249. package/dist/ui/styles/behavior.mjs.map +1 -0
  250. package/dist/ui/styles/review-layout.mjs +347 -0
  251. package/dist/ui/styles/review-layout.mjs.map +1 -0
  252. package/dist/ui/styles/review.mjs +86 -0
  253. package/dist/ui/styles/review.mjs.map +1 -0
  254. package/dist/ui/styles/tokens.mjs +132 -0
  255. package/dist/ui/styles/tokens.mjs.map +1 -0
  256. package/dist/ui/styles.mjs +19 -0
  257. package/dist/ui/styles.mjs.map +1 -0
  258. package/dist/ui/subnav.mjs +91 -0
  259. package/dist/ui/subnav.mjs.map +1 -0
  260. package/dist/ui/table.mjs +73 -0
  261. package/dist/ui/table.mjs.map +1 -0
  262. package/dist/ui/theme.mjs +64 -0
  263. package/dist/ui/theme.mjs.map +1 -0
  264. package/dist/ui/thumbnail.mjs +51 -0
  265. package/dist/ui/thumbnail.mjs.map +1 -0
  266. package/dist/ui/time.mjs +50 -0
  267. package/dist/ui/time.mjs.map +1 -0
  268. package/package.json +92 -0
@@ -0,0 +1,91 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { getStore } from "../store.mjs";
5
+ import { notFound, resolveAuthorizedProject } from "./helpers.mjs";
6
+ import { notFound as notFound$1, tokenCreateSchema, tokenCreatedSchema, tokenPublicSchema, unauthorized } from "./schemas.mjs";
7
+ import { createRoute, z } from "@hono/zod-openapi";
8
+ import { TokenModel } from "@storyshelf/core/models";
9
+ import { randomToken } from "@storyshelf/core/utils";
10
+ //#region src/routers/tokens.ts
11
+ const ADMIN_ROLES = ["admin"];
12
+ const listTokensRoute = createRoute({
13
+ method: "get",
14
+ tags: ["Tokens"],
15
+ summary: "List API tokens",
16
+ path: "/api/v1/projects/{slug}/tokens",
17
+ request: { params: z.object({ slug: z.string() }) },
18
+ responses: {
19
+ 200: {
20
+ content: { "application/json": { schema: tokenPublicSchema.array() } },
21
+ description: "List API tokens"
22
+ },
23
+ ...notFound$1,
24
+ ...unauthorized
25
+ }
26
+ });
27
+ const createTokenRoute = createRoute({
28
+ method: "post",
29
+ tags: ["Tokens"],
30
+ summary: "Create an API token",
31
+ path: "/api/v1/projects/{slug}/tokens",
32
+ request: {
33
+ params: z.object({ slug: z.string() }),
34
+ body: { content: { "application/json": { schema: tokenCreateSchema } } }
35
+ },
36
+ responses: {
37
+ 201: {
38
+ content: { "application/json": { schema: tokenCreatedSchema } },
39
+ description: "Created token"
40
+ },
41
+ ...notFound$1
42
+ }
43
+ });
44
+ const deleteTokenRoute = createRoute({
45
+ method: "delete",
46
+ tags: ["Tokens"],
47
+ summary: "Delete an API token",
48
+ path: "/api/v1/projects/{slug}/tokens/{tokenId}",
49
+ request: { params: z.object({
50
+ slug: z.string(),
51
+ tokenId: z.string()
52
+ }) },
53
+ responses: {
54
+ 204: { description: "Deleted token" },
55
+ ...notFound$1
56
+ }
57
+ });
58
+ /** Register the project API token list, create, and revoke endpoints. */
59
+ function registerTokens(app) {
60
+ app.openapi(listTokensRoute, async (c) => {
61
+ const project = await resolveAuthorizedProject(c, c.req.valid("param").slug, ...ADMIN_ROLES);
62
+ const tokens = await new TokenModel(getStore().db).list(project.id);
63
+ return c.json(tokens.map(({ hash: _hash, ...rest }) => rest));
64
+ });
65
+ app.openapi(createTokenRoute, async (c) => {
66
+ const project = await resolveAuthorizedProject(c, c.req.valid("param").slug, ...ADMIN_ROLES);
67
+ const body = c.req.valid("json");
68
+ const token = randomToken("shelf_");
69
+ await new TokenModel(getStore().db).create(project.id, {
70
+ name: body.name,
71
+ hash: token.hash,
72
+ userId: getStore().user?.id ?? null
73
+ });
74
+ return c.json({
75
+ ...body,
76
+ token: token.value
77
+ }, 201);
78
+ });
79
+ app.openapi(deleteTokenRoute, async (c) => {
80
+ const { slug, tokenId } = c.req.valid("param");
81
+ const project = await resolveAuthorizedProject(c, slug, ...ADMIN_ROLES);
82
+ const found = await new TokenModel(getStore().db).get(project.id, tokenId);
83
+ if (!found) notFound("Token not found");
84
+ await new TokenModel(getStore().db).remove(found.id);
85
+ return c.body(null, 204);
86
+ });
87
+ }
88
+ //#endregion
89
+ export { registerTokens };
90
+
91
+ //# sourceMappingURL=tokens.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokens.mjs","names":["notFoundResponse"],"sources":["../../src/routers/tokens.ts"],"sourcesContent":["import { createRoute, z } from \"@hono/zod-openapi\";\nimport { TokenModel } from \"@storyshelf/core/models\";\nimport type { ProjectRole } from \"@storyshelf/core/types\";\nimport { randomToken } from \"@storyshelf/core/utils\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { getStore } from \"../store.ts\";\nimport { resolveAuthorizedProject, notFound } from \"./helpers.ts\";\nimport {\n notFound as notFoundResponse,\n tokenCreatedSchema,\n tokenCreateSchema,\n tokenPublicSchema,\n unauthorized,\n} from \"./schemas.ts\";\nconst ADMIN_ROLES: readonly ProjectRole[] = [\"admin\"];\n\nconst listTokensRoute = createRoute({\n method: \"get\",\n tags: [\"Tokens\"],\n summary: \"List API tokens\",\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 tags: [\"Tokens\"],\n summary: \"Create an API token\",\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 tags: [\"Tokens\"],\n summary: \"Delete an API token\",\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: ShelfRouter): 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, {\n name: body.name,\n hash: token.hash,\n userId: getStore().user?.id ?? null,\n });\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":";;;;;;;;;;AAcA,MAAM,cAAsC,CAAC,OAAO;AAEpD,MAAM,kBAAkB,YAAY;CAClC,QAAQ;CACR,MAAM,CAAC,QAAQ;CACf,SAAS;CACT,MAAM;CACN,SAAS,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,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,CAAC,QAAQ;CACf,SAAS;CACT,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,CAAC,QAAQ;CACf,SAAS;CACT,MAAM;CACN,SAAS,EAAE,QAAQ,EAAE,OAAO;EAAE,MAAM,EAAE,OAAO;EAAG,SAAS,EAAE,OAAO;CAAE,CAAC,EAAE;CACvE,WAAW;EACT,KAAK,EAAE,aAAa,gBAAgB;EACpC,GAAGA;CACL;AACF,CAAC;;AAGD,SAAgB,eAAe,KAAwB;CACrD,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;GACrD,MAAM,KAAK;GACX,MAAM,MAAM;GACZ,QAAQ,SAAS,CAAC,CAAC,MAAM,MAAM;EACjC,CAAC;EACD,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"}
@@ -0,0 +1,166 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { getStore } from "../store.mjs";
5
+ import { hxRedirect } from "./htmx.mjs";
6
+ import { flash } from "./flash.mjs";
7
+ import { currentProjectRole } from "./helpers.mjs";
8
+ import { loadBaselineView } from "./baseline-guard.mjs";
9
+ import { renderBuildDetailPage } from "../pages/build-detail.mjs";
10
+ import { renderBuildDiffPage } from "../pages/build-diff.mjs";
11
+ import { renderActiveQueue, renderComputeJobsPage } from "../pages/compute-jobs.mjs";
12
+ import { renderLabelDetailPage, renderLabelsPage } from "../pages/label-detail.mjs";
13
+ import { renderLibraryPage } from "../pages/library.mjs";
14
+ import { renderProjectBuildsPage } from "../pages/project-builds.mjs";
15
+ import { renderProjectCreatePage } from "../pages/project-create.mjs";
16
+ import { renderProjectsPage } from "../pages/projects.mjs";
17
+ import { renderRootPage } from "../pages/root.mjs";
18
+ import { registerSettingsPages } from "./settings.mjs";
19
+ import { BuildModel, CommentModel, LabelModel, ProjectModel, SnapshotModel } from "@storyshelf/core/models";
20
+ //#region src/routers/ui.ts
21
+ function asString(value) {
22
+ return typeof value === "string" ? value.trim() : void 0;
23
+ }
24
+ function decodeRest(value) {
25
+ try {
26
+ return decodeURIComponent(value);
27
+ } catch {
28
+ return "";
29
+ }
30
+ }
31
+ async function getQueueView() {
32
+ const queue = getStore().captureQueue;
33
+ if (!queue) return [];
34
+ return (await queue.active()).map((entry) => ({
35
+ buildId: entry.buildId,
36
+ status: entry.status,
37
+ queuedAt: entry.queuedAt,
38
+ startedAt: entry.startedAt,
39
+ finishedAt: entry.finishedAt,
40
+ error: entry.error
41
+ }));
42
+ }
43
+ async function canManageJobs(projectId) {
44
+ const { authEnabled } = getStore();
45
+ if (!authEnabled) return true;
46
+ const roles = [
47
+ "developer",
48
+ "approver",
49
+ "admin"
50
+ ];
51
+ const role = await currentProjectRole(projectId);
52
+ return Boolean(role && roles.includes(role));
53
+ }
54
+ /** Register the server-rendered HTML pages (projects, builds, diffs, labels, settings). */
55
+ function registerUiPages(app) {
56
+ app.get("/", async (c) => c.html(await renderRootPage()));
57
+ app.get("/projects", async (c) => c.html(await renderProjectsPage()));
58
+ app.get("/projects/new", async (c) => c.html(await renderProjectCreatePage()));
59
+ app.post("/projects/new", async (c) => {
60
+ const form = await c.req.formData();
61
+ const name = asString(form.get("name"));
62
+ const gitRepository = asString(form.get("gitRepository"));
63
+ const gitDefaultBranch = asString(form.get("gitDefaultBranch"));
64
+ if (!name) return c.html(renderProjectCreatePage({
65
+ values: {
66
+ name,
67
+ gitRepository,
68
+ gitDefaultBranch
69
+ },
70
+ errors: { name: "Name is required" }
71
+ }), 400);
72
+ try {
73
+ const project = await new ProjectModel(getStore().db).create({
74
+ name,
75
+ gitRepository,
76
+ gitDefaultBranch
77
+ });
78
+ await new LabelModel(getStore().db).seedFor(project.id);
79
+ flash(c, `Created ${project.name}`);
80
+ return hxRedirect(c, `/projects/${project.slug}/library`);
81
+ } catch (error) {
82
+ const message = error instanceof Error ? error.message : "Failed to create project";
83
+ return c.html(renderProjectCreatePage({
84
+ values: {
85
+ name,
86
+ gitRepository,
87
+ gitDefaultBranch
88
+ },
89
+ globalError: message
90
+ }), 400);
91
+ }
92
+ });
93
+ app.get("/projects/:slug", (c) => c.redirect(`/projects/${c.req.param("slug")}/library`, 302));
94
+ app.get("/projects/:slug/builds", async (c) => {
95
+ const html = await renderProjectBuildsPage(c.req.param("slug"), {
96
+ status: c.req.query("status"),
97
+ branch: c.req.query("branch")
98
+ });
99
+ if (!html) return c.notFound();
100
+ return c.html(html);
101
+ });
102
+ app.get("/projects/:slug/builds/:buildId", async (c) => {
103
+ const html = await renderBuildDetailPage(c.req.param("buildId"));
104
+ if (!html) return c.notFound();
105
+ return c.html(html);
106
+ });
107
+ app.get("/projects/:slug/jobs", async (c) => {
108
+ const slug = c.req.param("slug");
109
+ const project = await new ProjectModel(getStore().db).getBySlug(slug);
110
+ if (!project) return c.notFound();
111
+ const queueView = await getQueueView();
112
+ const canRetry = await canManageJobs(project.id);
113
+ if (c.req.query("partial") === "queue") return c.html(renderActiveQueue(slug, queueView));
114
+ const html = await renderComputeJobsPage(slug, queueView, canRetry, getStore().captureQueue?.metadata.kind === "memory");
115
+ if (!html) return c.notFound();
116
+ return c.html(html);
117
+ });
118
+ app.get("/projects/:slug/builds/:buildId/diff", async (c) => {
119
+ const slug = c.req.param("slug");
120
+ const buildId = c.req.param("buildId");
121
+ const snapshotId = c.req.query("snapshot");
122
+ const project = await new ProjectModel(getStore().db).getBySlug(slug);
123
+ if (!project) return c.notFound();
124
+ const build = await new BuildModel(getStore().db).get(buildId);
125
+ if (!build || build.projectId !== project.id) return c.notFound();
126
+ const snapshots = await new SnapshotModel(getStore().db).listByBuild(build.id);
127
+ const comments = await new CommentModel(getStore().db).listByBuild(build.id);
128
+ const { hasBaseline, drifted } = await loadBaselineView(project, build, snapshots);
129
+ const { user, authEnabled } = getStore();
130
+ const canReview = !authEnabled || Boolean(user);
131
+ return c.html(renderBuildDiffPage({
132
+ project,
133
+ build,
134
+ snapshots,
135
+ comments,
136
+ selectedId: snapshotId,
137
+ canReview,
138
+ hasBaseline,
139
+ drifted
140
+ }));
141
+ });
142
+ app.get("/projects/:slug/labels", async (c) => {
143
+ const html = await renderLabelsPage(c.req.param("slug"));
144
+ if (!html) return c.notFound();
145
+ return c.html(html);
146
+ });
147
+ app.get("/projects/:slug/labels/:key/*", async (c) => {
148
+ const slug = c.req.param("slug");
149
+ const key = c.req.param("key");
150
+ const base = `/projects/${slug}/labels/${key}/`;
151
+ const value = c.req.path.startsWith(base) ? decodeRest(c.req.path.slice(base.length)) : "";
152
+ const html = await renderLabelDetailPage(slug, key, value);
153
+ if (!html) return c.notFound();
154
+ return c.html(html);
155
+ });
156
+ app.get("/projects/:slug/library", async (c) => {
157
+ const html = await renderLibraryPage(c.req.param("slug"), c.req.query("branch"));
158
+ if (!html) return c.notFound();
159
+ return c.html(html);
160
+ });
161
+ registerSettingsPages(app);
162
+ }
163
+ //#endregion
164
+ export { registerUiPages };
165
+
166
+ //# sourceMappingURL=ui.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ui.mjs","names":[],"sources":["../../src/routers/ui.ts"],"sourcesContent":["import { BuildModel } from \"@storyshelf/core/models\";\nimport { CommentModel } from \"@storyshelf/core/models\";\nimport { LabelModel } from \"@storyshelf/core/models\";\nimport { ProjectModel } from \"@storyshelf/core/models\";\nimport { SnapshotModel } from \"@storyshelf/core/models\";\nimport type { ProjectRole } from \"@storyshelf/core/types\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { renderBuildDetailPage } from \"../pages/build-detail.tsx\";\nimport { renderBuildDiffPage } from \"../pages/build-diff.tsx\";\nimport { renderComputeJobsPage, renderActiveQueue } from \"../pages/compute-jobs.tsx\";\nimport { renderLabelDetailPage, renderLabelsPage } from \"../pages/label-detail.tsx\";\nimport { renderLibraryPage } from \"../pages/library.tsx\";\nimport { renderProjectBuildsPage } from \"../pages/project-builds.tsx\";\nimport { renderProjectCreatePage } from \"../pages/project-create.tsx\";\nimport { renderProjectsPage } from \"../pages/projects.tsx\";\nimport { renderRootPage } from \"../pages/root.tsx\";\nimport { getStore } from \"../store.ts\";\nimport { loadBaselineView } from \"./baseline-guard.ts\";\nimport { flash } from \"./flash.ts\";\nimport { currentProjectRole } from \"./helpers.ts\";\nimport { hxRedirect } from \"./htmx.ts\";\nimport { registerSettingsPages } from \"./settings.ts\";\nfunction asString(value: FormDataEntryValue | null): string | undefined {\n return typeof value === \"string\" ? value.trim() : undefined;\n}\n\nfunction decodeRest(value: string): string {\n try {\n return decodeURIComponent(value);\n } catch {\n return \"\";\n }\n}\n\ninterface QueueView {\n buildId: string;\n status: string;\n queuedAt: string;\n startedAt?: string;\n finishedAt?: string;\n error?: string;\n}\n\nasync function getQueueView(): Promise<QueueView[]> {\n const queue = getStore().captureQueue;\n if (!queue) {\n return [];\n }\n const active = await queue.active();\n return active.map((entry) => ({\n buildId: entry.buildId,\n status: entry.status,\n queuedAt: entry.queuedAt,\n startedAt: entry.startedAt,\n finishedAt: entry.finishedAt,\n error: entry.error,\n }));\n}\n\nasync function canManageJobs(projectId: string): Promise<boolean> {\n const { authEnabled } = getStore();\n if (!authEnabled) {\n return true;\n }\n const roles: readonly ProjectRole[] = [\"developer\", \"approver\", \"admin\"];\n const role = await currentProjectRole(projectId);\n return Boolean(role && roles.includes(role));\n}\n\n/** Register the server-rendered HTML pages (projects, builds, diffs, labels, settings). */\nexport function registerUiPages(app: ShelfRouter): void {\n // eslint-disable-next-line promise-function-async -- renderRootPage returns RenderedContent (string | Promise<string>)\n app.get(\"/\", async (c) => c.html(await renderRootPage()));\n app.get(\"/projects\", async (c) => c.html(await renderProjectsPage()));\n // eslint-disable-next-line promise-function-async -- renderProjectCreatePage returns RenderedContent (string | Promise<string>)\n app.get(\"/projects/new\", async (c) => c.html(await renderProjectCreatePage()));\n\n app.post(\"/projects/new\", async (c) => {\n const form = await c.req.formData();\n const name = asString(form.get(\"name\"));\n const gitRepository = asString(form.get(\"gitRepository\"));\n const gitDefaultBranch = asString(form.get(\"gitDefaultBranch\"));\n if (!name) {\n return c.html(\n renderProjectCreatePage({\n values: { name, gitRepository, gitDefaultBranch },\n errors: { name: \"Name is required\" },\n }),\n 400,\n );\n }\n try {\n const project = await new ProjectModel(getStore().db).create({\n name,\n gitRepository,\n gitDefaultBranch,\n });\n await new LabelModel(getStore().db).seedFor(project.id);\n flash(c, `Created ${project.name}`);\n return hxRedirect(c, `/projects/${project.slug}/library`);\n } catch (error) {\n const message = error instanceof Error ? error.message : \"Failed to create project\";\n return c.html(\n renderProjectCreatePage({\n values: { name, gitRepository, gitDefaultBranch },\n globalError: message,\n }),\n 400,\n );\n }\n });\n\n app.get(\"/projects/:slug\", (c) => c.redirect(`/projects/${c.req.param(\"slug\")}/library`, 302));\n\n app.get(\"/projects/:slug/builds\", async (c) => {\n const html = await renderProjectBuildsPage(c.req.param(\"slug\"), {\n status: c.req.query(\"status\"),\n branch: c.req.query(\"branch\"),\n });\n if (!html) {\n return c.notFound();\n }\n return c.html(html);\n });\n\n app.get(\"/projects/:slug/builds/:buildId\", async (c) => {\n const html = await renderBuildDetailPage(c.req.param(\"buildId\"));\n if (!html) {\n return c.notFound();\n }\n return c.html(html);\n });\n\n app.get(\"/projects/:slug/jobs\", async (c) => {\n const slug = c.req.param(\"slug\");\n const project = await new ProjectModel(getStore().db).getBySlug(slug);\n if (!project) {\n return c.notFound();\n }\n const queueView = await getQueueView();\n const canRetry = await canManageJobs(project.id);\n if (c.req.query(\"partial\") === \"queue\") {\n return c.html(renderActiveQueue(slug, queueView));\n }\n const html = await renderComputeJobsPage(\n slug,\n queueView,\n canRetry,\n getStore().captureQueue?.metadata.kind === \"memory\",\n );\n if (!html) {\n return c.notFound();\n }\n return c.html(html);\n });\n\n app.get(\"/projects/:slug/builds/:buildId/diff\", async (c) => {\n const slug = c.req.param(\"slug\");\n const buildId = c.req.param(\"buildId\");\n const snapshotId = c.req.query(\"snapshot\");\n const project = await new ProjectModel(getStore().db).getBySlug(slug);\n if (!project) {\n return c.notFound();\n }\n const build = await new BuildModel(getStore().db).get(buildId);\n if (!build || build.projectId !== project.id) {\n return c.notFound();\n }\n const snapshots = await new SnapshotModel(getStore().db).listByBuild(build.id);\n const comments = await new CommentModel(getStore().db).listByBuild(build.id);\n const { hasBaseline, drifted } = await loadBaselineView(project, build, snapshots);\n const { user, authEnabled } = getStore();\n const canReview = !authEnabled || Boolean(user);\n return c.html(\n renderBuildDiffPage({\n project,\n build,\n snapshots,\n comments,\n selectedId: snapshotId,\n canReview,\n hasBaseline,\n drifted,\n }),\n );\n });\n\n app.get(\"/projects/:slug/labels\", async (c) => {\n const html = await renderLabelsPage(c.req.param(\"slug\"));\n if (!html) {\n return c.notFound();\n }\n return c.html(html);\n });\n\n app.get(\"/projects/:slug/labels/:key/*\", async (c) => {\n const slug = c.req.param(\"slug\");\n const key = c.req.param(\"key\");\n // Hono's bare `/*` wildcard is not exposed through `param()`, so derive the\n // label value from the raw request path and decode it ourselves.\n const base = `/projects/${slug}/labels/${key}/`;\n const value = c.req.path.startsWith(base) ? decodeRest(c.req.path.slice(base.length)) : \"\";\n const html = await renderLabelDetailPage(slug, key, value);\n if (!html) {\n return c.notFound();\n }\n return c.html(html);\n });\n\n app.get(\"/projects/:slug/library\", async (c) => {\n const html = await renderLibraryPage(c.req.param(\"slug\"), c.req.query(\"branch\"));\n if (!html) {\n return c.notFound();\n }\n return c.html(html);\n });\n\n registerSettingsPages(app);\n}\n"],"mappings":"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"}
@@ -0,0 +1,97 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { credentialKeys } from "../credential-keys.mjs";
5
+ import { getStore } from "../store.mjs";
6
+ import { notFound, resolveAuthorizedProject } from "./helpers.mjs";
7
+ import { notFound as notFound$1, unauthorized, webhookCreateSchema, webhookCreatedSchema, webhookPublicSchema } from "./schemas.mjs";
8
+ import { createRoute, z } from "@hono/zod-openapi";
9
+ import { WebhookModel } from "@storyshelf/core/models";
10
+ import { randomToken } from "@storyshelf/core/utils";
11
+ //#region src/routers/webhooks.ts
12
+ const ADMIN_ROLES = ["admin"];
13
+ const listWebhooksRoute = createRoute({
14
+ method: "get",
15
+ tags: ["Webhooks"],
16
+ summary: "List webhooks",
17
+ path: "/api/v1/projects/{slug}/webhooks",
18
+ request: { params: z.object({ slug: z.string() }) },
19
+ responses: {
20
+ 200: {
21
+ content: { "application/json": { schema: webhookPublicSchema.array() } },
22
+ description: "List webhooks"
23
+ },
24
+ ...notFound$1,
25
+ ...unauthorized
26
+ }
27
+ });
28
+ const createWebhookRoute = createRoute({
29
+ method: "post",
30
+ tags: ["Webhooks"],
31
+ summary: "Create a webhook",
32
+ path: "/api/v1/projects/{slug}/webhooks",
33
+ request: {
34
+ params: z.object({ slug: z.string() }),
35
+ body: { content: { "application/json": { schema: webhookCreateSchema } } }
36
+ },
37
+ responses: {
38
+ 201: {
39
+ content: { "application/json": { schema: webhookCreatedSchema } },
40
+ description: "Created webhook"
41
+ },
42
+ ...notFound$1
43
+ }
44
+ });
45
+ const deleteWebhookRoute = createRoute({
46
+ method: "delete",
47
+ tags: ["Webhooks"],
48
+ summary: "Delete a webhook",
49
+ path: "/api/v1/projects/{slug}/webhooks/{webhookId}",
50
+ request: { params: z.object({
51
+ slug: z.string(),
52
+ webhookId: z.string()
53
+ }) },
54
+ responses: {
55
+ 204: { description: "Deleted webhook" },
56
+ ...notFound$1
57
+ }
58
+ });
59
+ /** Register the project webhook list, create, and delete endpoints. */
60
+ function registerWebhooks(app) {
61
+ app.openapi(listWebhooksRoute, async (c) => {
62
+ const project = await resolveAuthorizedProject(c, c.req.valid("param").slug, ...ADMIN_ROLES);
63
+ const webhooks = await new WebhookModel(getStore().db, void 0, credentialKeys(getStore().config)).list(project.id);
64
+ return c.json(webhooks.map((webhook) => ({
65
+ id: webhook.id,
66
+ url: webhook.url,
67
+ events: WebhookModel.eventsOf(webhook)
68
+ })));
69
+ });
70
+ app.openapi(createWebhookRoute, async (c) => {
71
+ const project = await resolveAuthorizedProject(c, c.req.valid("param").slug, ...ADMIN_ROLES);
72
+ const body = c.req.valid("json");
73
+ const secret = randomToken("whsec_").value;
74
+ const webhook = await new WebhookModel(getStore().db, void 0, credentialKeys(getStore().config)).create(project.id, {
75
+ ...body,
76
+ secret
77
+ });
78
+ return c.json({
79
+ id: webhook.id,
80
+ url: webhook.url,
81
+ events: body.events ?? [],
82
+ secret
83
+ }, 201);
84
+ });
85
+ app.openapi(deleteWebhookRoute, async (c) => {
86
+ const { slug, webhookId } = c.req.valid("param");
87
+ const project = await resolveAuthorizedProject(c, slug, ...ADMIN_ROLES);
88
+ const webhook = await new WebhookModel(getStore().db, void 0, credentialKeys(getStore().config)).get(project.id, webhookId);
89
+ if (!webhook) notFound("Webhook not found");
90
+ await new WebhookModel(getStore().db, void 0, credentialKeys(getStore().config)).remove(project.id, webhook.id);
91
+ return c.body(null, 204);
92
+ });
93
+ }
94
+ //#endregion
95
+ export { registerWebhooks };
96
+
97
+ //# sourceMappingURL=webhooks.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhooks.mjs","names":["notFoundResponse"],"sources":["../../src/routers/webhooks.ts"],"sourcesContent":["import { createRoute, z } from \"@hono/zod-openapi\";\nimport { WebhookModel } from \"@storyshelf/core/models\";\nimport type { ProjectRole } from \"@storyshelf/core/types\";\nimport { randomToken } from \"@storyshelf/core/utils\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { credentialKeys } from \"../credential-keys.ts\";\nimport { getStore } from \"../store.ts\";\nimport { resolveAuthorizedProject, notFound } from \"./helpers.ts\";\nimport {\n notFound as notFoundResponse,\n unauthorized,\n webhookCreatedSchema,\n webhookCreateSchema,\n webhookPublicSchema,\n} from \"./schemas.ts\";\nconst ADMIN_ROLES: readonly ProjectRole[] = [\"admin\"];\n\nconst listWebhooksRoute = createRoute({\n method: \"get\",\n tags: [\"Webhooks\"],\n summary: \"List webhooks\",\n path: \"/api/v1/projects/{slug}/webhooks\",\n request: { params: z.object({ slug: z.string() }) },\n responses: {\n 200: {\n content: { \"application/json\": { schema: webhookPublicSchema.array() } },\n description: \"List webhooks\",\n },\n ...notFoundResponse,\n ...unauthorized,\n },\n});\n\nconst createWebhookRoute = createRoute({\n method: \"post\",\n tags: [\"Webhooks\"],\n summary: \"Create a webhook\",\n path: \"/api/v1/projects/{slug}/webhooks\",\n request: {\n params: z.object({ slug: z.string() }),\n body: { content: { \"application/json\": { schema: webhookCreateSchema } } },\n },\n responses: {\n 201: {\n content: { \"application/json\": { schema: webhookCreatedSchema } },\n description: \"Created webhook\",\n },\n ...notFoundResponse,\n },\n});\n\nconst deleteWebhookRoute = createRoute({\n method: \"delete\",\n tags: [\"Webhooks\"],\n summary: \"Delete a webhook\",\n path: \"/api/v1/projects/{slug}/webhooks/{webhookId}\",\n request: { params: z.object({ slug: z.string(), webhookId: z.string() }) },\n responses: {\n 204: { description: \"Deleted webhook\" },\n ...notFoundResponse,\n },\n});\n\n/** Register the project webhook list, create, and delete endpoints. */\nexport function registerWebhooks(app: ShelfRouter): void {\n app.openapi(listWebhooksRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...ADMIN_ROLES);\n const webhooks = await new WebhookModel(\n getStore().db,\n undefined,\n credentialKeys(getStore().config),\n ).list(project.id);\n return c.json(\n webhooks.map((webhook) => ({\n id: webhook.id,\n url: webhook.url,\n events: WebhookModel.eventsOf(webhook),\n })),\n );\n });\n\n app.openapi(createWebhookRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...ADMIN_ROLES);\n const body = c.req.valid(\"json\");\n const secret = randomToken(\"whsec_\").value;\n const webhook = await new WebhookModel(\n getStore().db,\n undefined,\n credentialKeys(getStore().config),\n ).create(project.id, { ...body, secret });\n return c.json({ id: webhook.id, url: webhook.url, events: body.events ?? [], secret }, 201);\n });\n\n app.openapi(deleteWebhookRoute, async (c) => {\n const { slug, webhookId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...ADMIN_ROLES);\n const webhook = await new WebhookModel(\n getStore().db,\n undefined,\n credentialKeys(getStore().config),\n ).get(project.id, webhookId);\n if (!webhook) {\n notFound(\"Webhook not found\");\n }\n await new WebhookModel(getStore().db, undefined, credentialKeys(getStore().config)).remove(\n project.id,\n webhook.id,\n );\n return c.body(null, 204);\n });\n}\n"],"mappings":"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"}
@@ -0,0 +1,43 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { getStore } from "../store.mjs";
5
+ //#region src/routers/well-known.ts
6
+ /** SSO-capable login methods (oauth buttons + SSO) with IdP reply URLs. */
7
+ function ssoProviders(auth, origin) {
8
+ return auth.loginMethods().filter((method) => method.kind === "oauth" || method.kind === "sso").map((method) => ({
9
+ id: method.id,
10
+ label: method.label,
11
+ redirectUris: method.kind === "sso" ? [`${origin}/api/auth/sso/callback/${method.id}`, `${origin}/api/auth/sso/saml2/sp/acs/${method.id}`] : [`${origin}/api/auth/callback/${method.id}`]
12
+ }));
13
+ }
14
+ function requestOrigin(c) {
15
+ return getStore().config.publicBaseUrl ?? new URL(c.req.url).origin;
16
+ }
17
+ /** Register public relying-party helper documents (no auth required). */
18
+ function registerWellKnown(app, auth) {
19
+ app.get("/.well-known/openid-configuration", (c) => {
20
+ const origin = requestOrigin(c);
21
+ const providers = ssoProviders(auth, origin);
22
+ return c.json({
23
+ issuer: origin,
24
+ note: "StoryShelf is an OpenID Connect relying party, not an identity provider. This document helps operators register redirect URIs with their provider; it is not provider metadata.",
25
+ response_types_supported: ["code"],
26
+ scopes_supported: [
27
+ "openid",
28
+ "email",
29
+ "profile"
30
+ ],
31
+ redirect_uris: providers.flatMap((provider) => provider.redirectUris),
32
+ providers: providers.map((method) => ({
33
+ id: method.id,
34
+ label: method.label
35
+ }))
36
+ }, 200, { "Cache-Control": "public, max-age=300" });
37
+ });
38
+ app.get("/.well-known/change-password", (c) => c.redirect("/profile", 302));
39
+ }
40
+ //#endregion
41
+ export { registerWellKnown };
42
+
43
+ //# sourceMappingURL=well-known.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"well-known.mjs","names":[],"sources":["../../src/routers/well-known.ts"],"sourcesContent":["import type { Auth } from \"@storyshelf/auth\";\nimport type { Context } from \"hono\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { getStore } from \"../store.ts\";\n\n/** One SSO-capable login method with its IdP reply URLs. */\ninterface SsoProviderLink {\n id: string;\n label: string;\n redirectUris: string[];\n}\n\n/** SSO-capable login methods (oauth buttons + SSO) with IdP reply URLs. */\nfunction ssoProviders(auth: Auth, origin: string): SsoProviderLink[] {\n return auth\n .loginMethods()\n .filter((method) => method.kind === \"oauth\" || method.kind === \"sso\")\n .map((method) => ({\n id: method.id,\n label: method.label,\n redirectUris:\n method.kind === \"sso\"\n ? [\n `${origin}/api/auth/sso/callback/${method.id}`,\n `${origin}/api/auth/sso/saml2/sp/acs/${method.id}`,\n ]\n : [`${origin}/api/auth/callback/${method.id}`],\n }));\n}\n\nfunction requestOrigin(c: Context): string {\n return getStore().config.publicBaseUrl ?? new URL(c.req.url).origin;\n}\n\n/** Register public relying-party helper documents (no auth required). */\nexport function registerWellKnown(app: ShelfRouter, auth: Auth): void {\n app.get(\"/.well-known/openid-configuration\", (c) => {\n const origin = requestOrigin(c);\n const providers = ssoProviders(auth, origin);\n return c.json(\n {\n issuer: origin,\n note: \"StoryShelf is an OpenID Connect relying party, not an identity provider. This document helps operators register redirect URIs with their provider; it is not provider metadata.\",\n response_types_supported: [\"code\"],\n scopes_supported: [\"openid\", \"email\", \"profile\"],\n redirect_uris: providers.flatMap((provider) => provider.redirectUris),\n providers: providers.map((method) => ({ id: method.id, label: method.label })),\n },\n 200,\n { \"Cache-Control\": \"public, max-age=300\" },\n );\n });\n\n // oxlint-disable-next-line typescript/promise-function-async -- Hono handler may return Response directly\n app.get(\"/.well-known/change-password\", (c) => c.redirect(\"/profile\", 302));\n}\n"],"mappings":";;;;;;AAaA,SAAS,aAAa,MAAY,QAAmC;CACnE,OAAO,KACJ,aAAa,CAAC,CACd,QAAQ,WAAW,OAAO,SAAS,WAAW,OAAO,SAAS,KAAK,CAAC,CACpE,KAAK,YAAY;EAChB,IAAI,OAAO;EACX,OAAO,OAAO;EACd,cACE,OAAO,SAAS,QACZ,CACE,GAAG,OAAO,yBAAyB,OAAO,MAC1C,GAAG,OAAO,6BAA6B,OAAO,IAChD,IACA,CAAC,GAAG,OAAO,qBAAqB,OAAO,IAAI;CACnD,EAAE;AACN;AAEA,SAAS,cAAc,GAAoB;CACzC,OAAO,SAAS,CAAC,CAAC,OAAO,iBAAiB,IAAI,IAAI,EAAE,IAAI,GAAG,CAAC,CAAC;AAC/D;;AAGA,SAAgB,kBAAkB,KAAkB,MAAkB;CACpE,IAAI,IAAI,sCAAsC,MAAM;EAClD,MAAM,SAAS,cAAc,CAAC;EAC9B,MAAM,YAAY,aAAa,MAAM,MAAM;EAC3C,OAAO,EAAE,KACP;GACE,QAAQ;GACR,MAAM;GACN,0BAA0B,CAAC,MAAM;GACjC,kBAAkB;IAAC;IAAU;IAAS;GAAS;GAC/C,eAAe,UAAU,SAAS,aAAa,SAAS,YAAY;GACpE,WAAW,UAAU,KAAK,YAAY;IAAE,IAAI,OAAO;IAAI,OAAO,OAAO;GAAM,EAAE;EAC/E,GACA,KACA,EAAE,iBAAiB,sBAAsB,CAC3C;CACF,CAAC;CAGD,IAAI,IAAI,iCAAiC,MAAM,EAAE,SAAS,YAAY,GAAG,CAAC;AAC5E"}
@@ -0,0 +1,52 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { validateConfig, validateUiConfig } from "@storyshelf/core/config";
5
+ import { createShelfLogger } from "@storyshelf/core/logger";
6
+ //#region src/runtime.ts
7
+ function addOptionalSnapshots(snap, options) {
8
+ if (options.captureRunner) snap["captureRunner"] = options.captureRunner.metadata;
9
+ if (options.captureQueue) snap["captureQueue"] = options.captureQueue.metadata;
10
+ }
11
+ /**
12
+ * Build a snapshot of adapter metadata for the `/api/v1/health` and docs.
13
+ * Collects the name/version of every wired adapter so operators can verify
14
+ * the running configuration at a glance.
15
+ *
16
+ * @param options - Router options containing the wired adapters
17
+ * @returns Metadata map keyed by adapter category (e.g. `database`, `git:github`)
18
+ */
19
+ function buildAdapterSnapshot(options) {
20
+ const snap = {
21
+ database: options.database.metadata,
22
+ storage: options.storage.metadata
23
+ };
24
+ addOptionalSnapshots(snap, options);
25
+ for (const p of options.gitHosts ?? []) snap[`git:${p.metadata.kind}`] = p.metadata;
26
+ for (const n of options.notifiers ?? []) snap[`notify:${n.metadata.kind}`] = n.metadata;
27
+ return snap;
28
+ }
29
+ /** Validate config/ui and derive runtime singletons from options. */
30
+ function resolveRuntime(options) {
31
+ const rawConfig = options.config ? validateConfig(options.config) : {};
32
+ const ui = options.ui ? validateUiConfig(options.ui) : {};
33
+ const logger = options.logger ?? createShelfLogger();
34
+ const authEnabled = options.auth !== void 0;
35
+ const gitHosts = options.gitHosts ?? [];
36
+ const notifiers = options.notifiers ?? [];
37
+ return {
38
+ config: rawConfig.adapters ? rawConfig : {
39
+ ...rawConfig,
40
+ adapters: buildAdapterSnapshot(options)
41
+ },
42
+ ui,
43
+ logger,
44
+ authEnabled,
45
+ gitHosts,
46
+ notifiers
47
+ };
48
+ }
49
+ //#endregion
50
+ export { buildAdapterSnapshot, resolveRuntime };
51
+
52
+ //# sourceMappingURL=runtime.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime.mjs","names":[],"sources":["../src/runtime.ts"],"sourcesContent":["import type { GitHostProvider } from \"@storyshelf/core/adapter/git-host\";\nimport type { AdapterMetadata, GitAdapterMetadata } from \"@storyshelf/core/adapter/metadata\";\nimport type { NotifierProvider } from \"@storyshelf/core/adapter/notifier\";\nimport type { ShelfConfig, ShelfOptions, UIConfig } from \"@storyshelf/core/config\";\nimport { validateConfig, validateUiConfig } from \"@storyshelf/core/config\";\nimport type { Logger } from \"@storyshelf/core/logger\";\nimport { createShelfLogger } from \"@storyshelf/core/logger\";\n\n/**\n * Resolved server runtime derived from {@link ShelfOptions}.\n * Holds the validated config, UI branding, structured logger, and\n * computed helpers like whether auth is enabled and which git hosts are wired.\n */\nexport interface ServerRuntime {\n config: ShelfConfig;\n ui: UIConfig;\n logger: Logger;\n authEnabled: boolean;\n gitHosts: GitHostProvider[];\n notifiers: NotifierProvider[];\n}\n\nfunction addOptionalSnapshots(\n snap: Record<string, AdapterMetadata | GitAdapterMetadata>,\n options: ShelfOptions,\n): void {\n if (options.captureRunner) {\n snap[\"captureRunner\"] = options.captureRunner.metadata;\n }\n if (options.captureQueue) {\n snap[\"captureQueue\"] = options.captureQueue.metadata;\n }\n}\n\n/**\n * Build a snapshot of adapter metadata for the `/api/v1/health` and docs.\n * Collects the name/version of every wired adapter so operators can verify\n * the running configuration at a glance.\n *\n * @param options - Router options containing the wired adapters\n * @returns Metadata map keyed by adapter category (e.g. `database`, `git:github`)\n */\nexport function buildAdapterSnapshot(\n options: ShelfOptions,\n): Record<string, AdapterMetadata | GitAdapterMetadata> {\n const snap: Record<string, AdapterMetadata | GitAdapterMetadata> = {\n database: options.database.metadata,\n storage: options.storage.metadata,\n };\n addOptionalSnapshots(snap, options);\n for (const p of options.gitHosts ?? []) {\n snap[`git:${p.metadata.kind}`] = p.metadata;\n }\n for (const n of options.notifiers ?? []) {\n snap[`notify:${n.metadata.kind}`] = n.metadata;\n }\n return snap;\n}\n\n/** Validate config/ui and derive runtime singletons from options. */\nexport function resolveRuntime(options: ShelfOptions): ServerRuntime {\n // eslint-disable-next-line typescript/no-unnecessary-type-assertion -- ShelfConfig lacks index signature\n const rawConfig = options.config\n ? validateConfig(options.config as unknown as Record<string, unknown>)\n : {};\n // eslint-disable-next-line typescript/no-unnecessary-type-assertion -- UIConfig lacks index signature\n const ui = options.ui ? validateUiConfig(options.ui as unknown as Record<string, unknown>) : {};\n const logger = options.logger ?? createShelfLogger();\n const authEnabled = options.auth !== undefined;\n const gitHosts = options.gitHosts ?? [];\n const notifiers = options.notifiers ?? [];\n // Adapter introspection — auto-populate config.adapters if not supplied\n const config: ShelfConfig = rawConfig.adapters\n ? rawConfig\n : {\n ...rawConfig,\n adapters: buildAdapterSnapshot(options),\n };\n return { config, ui, logger, authEnabled, gitHosts, notifiers };\n}\n"],"mappings":";;;;;;AAsBA,SAAS,qBACP,MACA,SACM;CACN,IAAI,QAAQ,eACV,KAAK,mBAAmB,QAAQ,cAAc;CAEhD,IAAI,QAAQ,cACV,KAAK,kBAAkB,QAAQ,aAAa;AAEhD;;;;;;;;;AAUA,SAAgB,qBACd,SACsD;CACtD,MAAM,OAA6D;EACjE,UAAU,QAAQ,SAAS;EAC3B,SAAS,QAAQ,QAAQ;CAC3B;CACA,qBAAqB,MAAM,OAAO;CAClC,KAAK,MAAM,KAAK,QAAQ,YAAY,CAAC,GACnC,KAAK,OAAO,EAAE,SAAS,UAAU,EAAE;CAErC,KAAK,MAAM,KAAK,QAAQ,aAAa,CAAC,GACpC,KAAK,UAAU,EAAE,SAAS,UAAU,EAAE;CAExC,OAAO;AACT;;AAGA,SAAgB,eAAe,SAAsC;CAEnE,MAAM,YAAY,QAAQ,SACtB,eAAe,QAAQ,MAA4C,IACnE,CAAC;CAEL,MAAM,KAAK,QAAQ,KAAK,iBAAiB,QAAQ,EAAwC,IAAI,CAAC;CAC9F,MAAM,SAAS,QAAQ,UAAU,kBAAkB;CACnD,MAAM,cAAc,QAAQ,SAAS,KAAA;CACrC,MAAM,WAAW,QAAQ,YAAY,CAAC;CACtC,MAAM,YAAY,QAAQ,aAAa,CAAC;CAQxC,OAAO;EAAE,QANmB,UAAU,WAClC,YACA;GACE,GAAG;GACH,UAAU,qBAAqB,OAAO;EACxC;EACa;EAAI;EAAQ;EAAa;EAAU;CAAU;AAChE"}
package/dist/store.mjs ADDED
@@ -0,0 +1,20 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { AsyncLocalStorage } from "node:async_hooks";
5
+ //#region src/store.ts
6
+ const storage = new AsyncLocalStorage();
7
+ /** Run a function with the given request store in scope. */
8
+ function runWithStore(store, fn) {
9
+ return storage.run(store, fn);
10
+ }
11
+ /** Return the current request store, throwing outside a request scope. */
12
+ function getStore() {
13
+ const store = storage.getStore();
14
+ if (!store) throw new Error("Store not available. Use runWithStore().");
15
+ return store;
16
+ }
17
+ //#endregion
18
+ export { getStore, runWithStore };
19
+
20
+ //# sourceMappingURL=store.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.mjs","names":[],"sources":["../src/store.ts"],"sourcesContent":["import type { CaptureQueue } from \"@storyshelf/core/adapter/capture-queue\";\nimport type { DatabaseAdapter } from \"@storyshelf/core/adapter/database\";\nimport type { GitHostProvider } from \"@storyshelf/core/adapter/git-host\";\nimport type { NotifierProvider } from \"@storyshelf/core/adapter/notifier\";\nimport type { StorageAdapter } from \"@storyshelf/core/adapter/storage\";\nimport type { ShelfConfig, UIConfig } from \"@storyshelf/core/config\";\nimport type { Logger } from \"@storyshelf/core/logger\";\nimport type { AuthUser } from \"@storyshelf/core/types\";\nimport { AsyncLocalStorage } from \"node:async_hooks\";\n\n/** Request-scoped dependencies and session state. */\nexport interface Store {\n db: DatabaseAdapter;\n storage: StorageAdapter;\n config: ShelfConfig;\n ui: UIConfig;\n logger: Logger;\n user: AuthUser | null;\n authEnabled: boolean;\n /** Raw session cookie value (\"default\" when anonymous); binds CSRF tokens. */\n sessionId: string;\n enqueueCapture?: (buildId: string, reqId?: string) => Promise<void>;\n captureQueue?: CaptureQueue | null;\n gitHosts: GitHostProvider[];\n notifiers: NotifierProvider[];\n}\n\nconst storage = new AsyncLocalStorage<Store>();\n\n/** Run a function with the given request store in scope. */\nexport function runWithStore<T>(store: Store, fn: () => T): T {\n return storage.run(store, fn);\n}\n\n/** Return the current request store, throwing outside a request scope. */\nexport function getStore(): Store {\n const store = storage.getStore();\n if (!store) {\n throw new Error(\"Store not available. Use runWithStore().\");\n }\n return store;\n}\n"],"mappings":";;;;;AA2BA,MAAM,UAAU,IAAI,kBAAyB;;AAG7C,SAAgB,aAAgB,OAAc,IAAgB;CAC5D,OAAO,QAAQ,IAAI,OAAO,EAAE;AAC9B;;AAGA,SAAgB,WAAkB;CAChC,MAAM,QAAQ,QAAQ,SAAS;CAC/B,IAAI,CAAC,OACH,MAAM,IAAI,MAAM,0CAA0C;CAE5D,OAAO;AACT"}
@@ -0,0 +1,76 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { css } from "./css.mjs";
5
+ import { jsx } from "hono/jsx/jsx-runtime";
6
+ //#region src/ui/avatar.tsx
7
+ const avatarBase = css`
8
+ /* avatar */
9
+ flex: none;
10
+ display: inline-grid;
11
+ place-items: center;
12
+ width: 1.75rem;
13
+ height: 1.75rem;
14
+ border-radius: var(--radius-pill);
15
+ background: var(--accent-subtle);
16
+ color: var(--accent-fg);
17
+ border: 1px solid var(--accent-border);
18
+ font-size: var(--text-xs);
19
+ font-weight: 650;
20
+ line-height: 1;
21
+ overflow: hidden;
22
+ object-fit: cover;
23
+ user-select: none;
24
+ `;
25
+ const avatarSizes = {
26
+ sm: css`
27
+ /* avatar-sm */
28
+ ${avatarBase}
29
+ width: 1.375rem;
30
+ height: 1.375rem;
31
+ font-size: 0.65rem;
32
+ `,
33
+ md: avatarBase,
34
+ lg: css`
35
+ /* avatar-lg */
36
+ ${avatarBase}
37
+ width: 2.5rem;
38
+ height: 2.5rem;
39
+ font-size: var(--text-base);
40
+ `
41
+ };
42
+ css`
43
+ /* avatar-group */
44
+ display: inline-flex;
45
+ & > * + * {
46
+ margin-left: -0.4rem;
47
+ box-shadow: 0 0 0 2px var(--surface-card);
48
+ }
49
+ `;
50
+ /** Up to two initials from a display name or email. */
51
+ function initials(name) {
52
+ const parts = name.replace(/@.*$/u, "").split(/[\s._-]+/u).filter((part) => part.length > 0);
53
+ const first = parts[0];
54
+ const last = parts.at(-1);
55
+ if (first === void 0 || last === void 0) return "";
56
+ return (parts.length > 1 ? `${first.slice(0, 1)}${last.slice(0, 1)}` : first.slice(0, 2)).toUpperCase();
57
+ }
58
+ /** Round user image, falling back to tinted initials (the brand accent shows subtly). */
59
+ const Avatar = ({ name, src, size = "md" }) => {
60
+ if (src) return /* @__PURE__ */ jsx("img", {
61
+ class: avatarSizes[size],
62
+ src,
63
+ alt: name,
64
+ loading: "lazy"
65
+ });
66
+ return /* @__PURE__ */ jsx("span", {
67
+ class: avatarSizes[size],
68
+ role: "img",
69
+ "aria-label": name,
70
+ children: initials(name) || "?"
71
+ });
72
+ };
73
+ //#endregion
74
+ export { Avatar, initials };
75
+
76
+ //# sourceMappingURL=avatar.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"avatar.mjs","names":[],"sources":["../../src/ui/avatar.tsx"],"sourcesContent":["import type { FC } from \"hono/jsx\";\nimport { css } from \"./css.ts\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\ntype AvatarSize = \"sm\" | \"md\" | \"lg\";\n\nconst avatarBase = css`\n /* avatar */\n flex: none;\n display: inline-grid;\n place-items: center;\n width: 1.75rem;\n height: 1.75rem;\n border-radius: var(--radius-pill);\n background: var(--accent-subtle);\n color: var(--accent-fg);\n border: 1px solid var(--accent-border);\n font-size: var(--text-xs);\n font-weight: 650;\n line-height: 1;\n overflow: hidden;\n object-fit: cover;\n user-select: none;\n`;\n\nconst avatarSm = css`\n /* avatar-sm */\n ${avatarBase}\n width: 1.375rem;\n height: 1.375rem;\n font-size: 0.65rem;\n`;\n\nconst avatarLg = css`\n /* avatar-lg */\n ${avatarBase}\n width: 2.5rem;\n height: 2.5rem;\n font-size: var(--text-base);\n`;\n\nconst avatarSizes: Record<AvatarSize, Promise<string>> = {\n sm: avatarSm,\n md: avatarBase,\n lg: avatarLg,\n};\n\nconst avatarGroup = css`\n /* avatar-group */\n display: inline-flex;\n & > * + * {\n margin-left: -0.4rem;\n box-shadow: 0 0 0 2px var(--surface-card);\n }\n`;\n\n/** Up to two initials from a display name or email. */\nexport function initials(name: string): string {\n const parts = name\n .replace(/@.*$/u, \"\")\n .split(/[\\s._-]+/u)\n .filter((part) => part.length > 0);\n const first = parts[0];\n const last = parts.at(-1);\n if (first === undefined || last === undefined) {\n return \"\";\n }\n const letters = parts.length > 1 ? `${first.slice(0, 1)}${last.slice(0, 1)}` : first.slice(0, 2);\n return letters.toUpperCase();\n}\n\n/** Round user image, falling back to tinted initials (the brand accent shows subtly). */\nexport const Avatar: FC<{ name: string; src?: string | null; size?: AvatarSize }> = ({\n name,\n src,\n size = \"md\",\n}) => {\n if (src) {\n return <img class={avatarSizes[size]} src={src} alt={name} loading=\"lazy\" />;\n }\n return (\n <span class={avatarSizes[size]} role=\"img\" aria-label={name}>\n {initials(name) || \"?\"}\n </span>\n );\n};\n\n/** Overlapping avatars for a list of people. */\nexport const AvatarGroup: FC<{ children?: unknown }> = ({ children }) => {\n return <span class={avatarGroup}>{children}</span>;\n};\n"],"mappings":";;;;;;AAOA,MAAM,aAAa,GAAG;;;;;;;;;;;;;;;;;;AAmCtB,MAAM,cAAmD;CACvD,IAAI,GAjBc;;IAEhB,WAAW;;;;;CAgBb,IAAI;CACJ,IAAI,GAXc;;IAEhB,WAAW;;;;;AAUf;AAEoB,GAAG;;;;;;;;;AAUvB,SAAgB,SAAS,MAAsB;CAC7C,MAAM,QAAQ,KACX,QAAQ,SAAS,EAAE,CAAC,CACpB,MAAM,WAAW,CAAC,CAClB,QAAQ,SAAS,KAAK,SAAS,CAAC;CACnC,MAAM,QAAQ,MAAM;CACpB,MAAM,OAAO,MAAM,GAAG,EAAE;CACxB,IAAI,UAAU,KAAA,KAAa,SAAS,KAAA,GAClC,OAAO;CAGT,QADgB,MAAM,SAAS,IAAI,GAAG,MAAM,MAAM,GAAG,CAAC,IAAI,KAAK,MAAM,GAAG,CAAC,MAAM,MAAM,MAAM,GAAG,CAAC,EAAA,CAChF,YAAY;AAC7B;;AAGA,MAAa,UAAwE,EACnF,MACA,KACA,OAAO,WACH;CACJ,IAAI,KACF,OAAO,oBAAC,OAAD;EAAK,OAAO,YAAY;EAAY;EAAK,KAAK;EAAM,SAAQ;CAAQ,CAAA;CAE7E,OACE,oBAAC,QAAD;EAAM,OAAO,YAAY;EAAO,MAAK;EAAM,cAAY;EACpD,UAAA,SAAS,IAAI,KAAK;CACf,CAAA;AAEV"}