@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,177 @@
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 { groupMappingCreateSchema, groupMappingSchema, memberRoleSchema, memberSchema, memberSetSchema, notFound, unauthorized } from "./schemas.mjs";
7
+ import { createRoute, z } from "@hono/zod-openapi";
8
+ import { MemberModel, ProjectGroupMappingModel } from "@storyshelf/core/models";
9
+ import { HTTPException } from "hono/http-exception";
10
+ //#region src/routers/members.ts
11
+ const VIEW_ROLES = [
12
+ "viewer",
13
+ "developer",
14
+ "approver",
15
+ "admin"
16
+ ];
17
+ const ADMIN_ROLES = ["admin"];
18
+ const listMembersRoute = createRoute({
19
+ method: "get",
20
+ tags: ["Members"],
21
+ summary: "List members",
22
+ path: "/api/v1/projects/{slug}/members",
23
+ request: { params: z.object({ slug: z.string() }) },
24
+ responses: {
25
+ 200: {
26
+ content: { "application/json": { schema: memberSchema.array() } },
27
+ description: "List project members"
28
+ },
29
+ ...notFound,
30
+ ...unauthorized
31
+ }
32
+ });
33
+ const setMemberRoute = createRoute({
34
+ method: "post",
35
+ tags: ["Members"],
36
+ summary: "Add or update a member",
37
+ path: "/api/v1/projects/{slug}/members",
38
+ request: {
39
+ params: z.object({ slug: z.string() }),
40
+ body: { content: { "application/json": { schema: memberSetSchema } } }
41
+ },
42
+ responses: {
43
+ 201: {
44
+ content: { "application/json": { schema: memberSchema } },
45
+ description: "Added or updated member"
46
+ },
47
+ ...notFound
48
+ }
49
+ });
50
+ const updateMemberRoute = createRoute({
51
+ method: "patch",
52
+ tags: ["Members"],
53
+ summary: "Change a member's role",
54
+ path: "/api/v1/projects/{slug}/members/{userId}",
55
+ request: {
56
+ params: z.object({
57
+ slug: z.string(),
58
+ userId: z.string()
59
+ }),
60
+ body: { content: { "application/json": { schema: memberRoleSchema } } }
61
+ },
62
+ responses: {
63
+ 200: {
64
+ content: { "application/json": { schema: memberSchema } },
65
+ description: "Updated member role"
66
+ },
67
+ ...notFound
68
+ }
69
+ });
70
+ const deleteMemberRoute = createRoute({
71
+ method: "delete",
72
+ tags: ["Members"],
73
+ summary: "Remove a member",
74
+ path: "/api/v1/projects/{slug}/members/{userId}",
75
+ request: { params: z.object({
76
+ slug: z.string(),
77
+ userId: z.string()
78
+ }) },
79
+ responses: {
80
+ 204: { description: "Removed member" },
81
+ ...notFound
82
+ }
83
+ });
84
+ const listGroupMappingsRoute = createRoute({
85
+ method: "get",
86
+ tags: ["Members"],
87
+ summary: "List group mappings",
88
+ path: "/api/v1/projects/{slug}/group-mappings",
89
+ request: { params: z.object({ slug: z.string() }) },
90
+ responses: {
91
+ 200: {
92
+ content: { "application/json": { schema: groupMappingSchema.array() } },
93
+ description: "List identity-provider group mappings"
94
+ },
95
+ ...notFound,
96
+ ...unauthorized
97
+ }
98
+ });
99
+ const createGroupMappingRoute = createRoute({
100
+ method: "post",
101
+ tags: ["Members"],
102
+ summary: "Create a group mapping",
103
+ path: "/api/v1/projects/{slug}/group-mappings",
104
+ request: {
105
+ params: z.object({ slug: z.string() }),
106
+ body: { content: { "application/json": { schema: groupMappingCreateSchema } } }
107
+ },
108
+ responses: {
109
+ 201: {
110
+ content: { "application/json": { schema: groupMappingSchema } },
111
+ description: "Created group mapping (exact group-name match)"
112
+ },
113
+ ...notFound
114
+ }
115
+ });
116
+ const deleteGroupMappingRoute = createRoute({
117
+ method: "delete",
118
+ tags: ["Members"],
119
+ summary: "Remove a group mapping",
120
+ path: "/api/v1/projects/{slug}/group-mappings/{mappingId}",
121
+ request: { params: z.object({
122
+ slug: z.string(),
123
+ mappingId: z.string()
124
+ }) },
125
+ responses: {
126
+ 204: { description: "Removed group mapping" },
127
+ ...notFound
128
+ }
129
+ });
130
+ /** Register the project member list, upsert, and removal endpoints. */
131
+ function registerMembers(app) {
132
+ app.openapi(listMembersRoute, async (c) => {
133
+ const project = await resolveAuthorizedProject(c, c.req.valid("param").slug, ...VIEW_ROLES);
134
+ return c.json(await new MemberModel(getStore().db).list(project.id));
135
+ });
136
+ app.openapi(setMemberRoute, async (c) => {
137
+ const project = await resolveAuthorizedProject(c, c.req.valid("param").slug, ...ADMIN_ROLES);
138
+ const body = c.req.valid("json");
139
+ return c.json(await new MemberModel(getStore().db).set(project.id, body.userId, body.role), 201);
140
+ });
141
+ app.openapi(updateMemberRoute, async (c) => {
142
+ const { slug, userId } = c.req.valid("param");
143
+ const project = await resolveAuthorizedProject(c, slug, ...ADMIN_ROLES);
144
+ const body = c.req.valid("json");
145
+ return c.json(await new MemberModel(getStore().db).set(project.id, userId, body.role));
146
+ });
147
+ app.openapi(deleteMemberRoute, async (c) => {
148
+ const { slug, userId } = c.req.valid("param");
149
+ const project = await resolveAuthorizedProject(c, slug, ...ADMIN_ROLES);
150
+ await new MemberModel(getStore().db).remove(project.id, userId);
151
+ return c.body(null, 204);
152
+ });
153
+ registerGroupMappingRoutes(app);
154
+ }
155
+ /** Register identity-provider group mapping endpoints. */
156
+ function registerGroupMappingRoutes(app) {
157
+ app.openapi(listGroupMappingsRoute, async (c) => {
158
+ const project = await resolveAuthorizedProject(c, c.req.valid("param").slug, ...VIEW_ROLES);
159
+ return c.json(await new ProjectGroupMappingModel(getStore().db).list(project.id));
160
+ });
161
+ app.openapi(createGroupMappingRoute, async (c) => {
162
+ const project = await resolveAuthorizedProject(c, c.req.valid("param").slug, ...ADMIN_ROLES);
163
+ const body = c.req.valid("json");
164
+ if (body.groupName.includes("*")) throw new HTTPException(400, { message: "Group names match exactly; wildcards are not expanded. A broad pattern can silently grant org-wide access." });
165
+ return c.json(await new ProjectGroupMappingModel(getStore().db).create(project.id, body.groupName, body.role), 201);
166
+ });
167
+ app.openapi(deleteGroupMappingRoute, async (c) => {
168
+ const { slug, mappingId } = c.req.valid("param");
169
+ const project = await resolveAuthorizedProject(c, slug, ...ADMIN_ROLES);
170
+ await new ProjectGroupMappingModel(getStore().db).remove(project.id, mappingId);
171
+ return c.body(null, 204);
172
+ });
173
+ }
174
+ //#endregion
175
+ export { registerMembers };
176
+
177
+ //# sourceMappingURL=members.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"members.mjs","names":[],"sources":["../../src/routers/members.ts"],"sourcesContent":["import { createRoute, z } from \"@hono/zod-openapi\";\nimport { MemberModel, ProjectGroupMappingModel } from \"@storyshelf/core/models\";\nimport type { ProjectRole } from \"@storyshelf/core/types\";\nimport { HTTPException } from \"hono/http-exception\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { getStore } from \"../store.ts\";\nimport { resolveAuthorizedProject } from \"./helpers.ts\";\nimport {\n groupMappingCreateSchema,\n groupMappingSchema,\n memberRoleSchema,\n memberSchema,\n memberSetSchema,\n notFound,\n unauthorized,\n} from \"./schemas.ts\";\nconst VIEW_ROLES: readonly ProjectRole[] = [\"viewer\", \"developer\", \"approver\", \"admin\"];\nconst ADMIN_ROLES: readonly ProjectRole[] = [\"admin\"];\n\nconst listMembersRoute = createRoute({\n method: \"get\",\n tags: [\"Members\"],\n summary: \"List members\",\n path: \"/api/v1/projects/{slug}/members\",\n request: { params: z.object({ slug: z.string() }) },\n responses: {\n 200: {\n content: { \"application/json\": { schema: memberSchema.array() } },\n description: \"List project members\",\n },\n ...notFound,\n ...unauthorized,\n },\n});\n\nconst setMemberRoute = createRoute({\n method: \"post\",\n tags: [\"Members\"],\n summary: \"Add or update a member\",\n path: \"/api/v1/projects/{slug}/members\",\n request: {\n params: z.object({ slug: z.string() }),\n body: { content: { \"application/json\": { schema: memberSetSchema } } },\n },\n responses: {\n 201: {\n content: { \"application/json\": { schema: memberSchema } },\n description: \"Added or updated member\",\n },\n ...notFound,\n },\n});\n\nconst updateMemberRoute = createRoute({\n method: \"patch\",\n tags: [\"Members\"],\n summary: \"Change a member's role\",\n path: \"/api/v1/projects/{slug}/members/{userId}\",\n request: {\n params: z.object({ slug: z.string(), userId: z.string() }),\n body: { content: { \"application/json\": { schema: memberRoleSchema } } },\n },\n responses: {\n 200: {\n content: { \"application/json\": { schema: memberSchema } },\n description: \"Updated member role\",\n },\n ...notFound,\n },\n});\n\nconst deleteMemberRoute = createRoute({\n method: \"delete\",\n tags: [\"Members\"],\n summary: \"Remove a member\",\n path: \"/api/v1/projects/{slug}/members/{userId}\",\n request: { params: z.object({ slug: z.string(), userId: z.string() }) },\n responses: {\n 204: { description: \"Removed member\" },\n ...notFound,\n },\n});\n\nconst listGroupMappingsRoute = createRoute({\n method: \"get\",\n tags: [\"Members\"],\n summary: \"List group mappings\",\n path: \"/api/v1/projects/{slug}/group-mappings\",\n request: { params: z.object({ slug: z.string() }) },\n responses: {\n 200: {\n content: { \"application/json\": { schema: groupMappingSchema.array() } },\n description: \"List identity-provider group mappings\",\n },\n ...notFound,\n ...unauthorized,\n },\n});\n\nconst createGroupMappingRoute = createRoute({\n method: \"post\",\n tags: [\"Members\"],\n summary: \"Create a group mapping\",\n path: \"/api/v1/projects/{slug}/group-mappings\",\n request: {\n params: z.object({ slug: z.string() }),\n body: { content: { \"application/json\": { schema: groupMappingCreateSchema } } },\n },\n responses: {\n 201: {\n content: { \"application/json\": { schema: groupMappingSchema } },\n description: \"Created group mapping (exact group-name match)\",\n },\n ...notFound,\n },\n});\n\nconst deleteGroupMappingRoute = createRoute({\n method: \"delete\",\n tags: [\"Members\"],\n summary: \"Remove a group mapping\",\n path: \"/api/v1/projects/{slug}/group-mappings/{mappingId}\",\n request: { params: z.object({ slug: z.string(), mappingId: z.string() }) },\n responses: {\n 204: { description: \"Removed group mapping\" },\n ...notFound,\n },\n});\n\n/** Register the project member list, upsert, and removal endpoints. */\nexport function registerMembers(app: ShelfRouter): void {\n app.openapi(listMembersRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...VIEW_ROLES);\n return c.json(await new MemberModel(getStore().db).list(project.id));\n });\n\n app.openapi(setMemberRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...ADMIN_ROLES);\n const body = c.req.valid(\"json\");\n return c.json(\n await new MemberModel(getStore().db).set(project.id, body.userId, body.role),\n 201,\n );\n });\n\n app.openapi(updateMemberRoute, async (c) => {\n const { slug, userId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...ADMIN_ROLES);\n const body = c.req.valid(\"json\");\n return c.json(await new MemberModel(getStore().db).set(project.id, userId, body.role));\n });\n\n app.openapi(deleteMemberRoute, async (c) => {\n const { slug, userId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...ADMIN_ROLES);\n await new MemberModel(getStore().db).remove(project.id, userId);\n return c.body(null, 204);\n });\n\n registerGroupMappingRoutes(app);\n}\n\n/** Register identity-provider group mapping endpoints. */\nfunction registerGroupMappingRoutes(app: ShelfRouter): void {\n app.openapi(listGroupMappingsRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...VIEW_ROLES);\n return c.json(await new ProjectGroupMappingModel(getStore().db).list(project.id));\n });\n\n app.openapi(createGroupMappingRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...ADMIN_ROLES);\n const body = c.req.valid(\"json\");\n if (body.groupName.includes(\"*\")) {\n throw new HTTPException(400, {\n message:\n \"Group names match exactly; wildcards are not expanded. A broad pattern can silently grant org-wide access.\",\n });\n }\n return c.json(\n await new ProjectGroupMappingModel(getStore().db).create(\n project.id,\n body.groupName,\n body.role,\n ),\n 201,\n );\n });\n\n app.openapi(deleteGroupMappingRoute, async (c) => {\n const { slug, mappingId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...ADMIN_ROLES);\n await new ProjectGroupMappingModel(getStore().db).remove(project.id, mappingId);\n return c.body(null, 204);\n });\n}\n"],"mappings":"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"}
@@ -0,0 +1,190 @@
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, requireSiteAdmin, resolveAuthorizedProject } from "./helpers.mjs";
7
+ import { notFound as notFound$1, notificationChannelCreateSchema, notificationChannelSchema, unauthorized } from "./schemas.mjs";
8
+ import { createRoute, z } from "@hono/zod-openapi";
9
+ import { NotificationChannelModel } from "@storyshelf/core/models";
10
+ //#region src/routers/notification-channels.ts
11
+ const ADMIN_ROLES = ["admin"];
12
+ /** Public channel view (secret redacted, config/events decoded). */
13
+ function toPublic(row) {
14
+ return {
15
+ id: row.id,
16
+ projectId: row.projectId,
17
+ provider: row.provider,
18
+ config: NotificationChannelModel.configOf(row),
19
+ hasSecret: row.secretEncrypted !== null,
20
+ events: NotificationChannelModel.eventsOf(row),
21
+ enabled: row.enabled,
22
+ createdAt: row.createdAt,
23
+ updatedAt: row.updatedAt
24
+ };
25
+ }
26
+ /** Validate channel input against the provider descriptor. */
27
+ function validateChannelInput(provider, config) {
28
+ const descriptor = getStore().notifiers.find((candidate) => candidate.metadata.kind === provider);
29
+ if (!descriptor) return {
30
+ ok: false,
31
+ message: `Unknown provider: ${provider}`
32
+ };
33
+ const parsed = descriptor.metadata.schema.safeParse(config);
34
+ if (!parsed.success) return {
35
+ ok: false,
36
+ message: parsed.error.message
37
+ };
38
+ return { ok: true };
39
+ }
40
+ const listChannelsRoute = createRoute({
41
+ method: "get",
42
+ tags: ["Notifications"],
43
+ summary: "List notification channels",
44
+ path: "/api/v1/projects/{slug}/notification-channels",
45
+ request: { params: z.object({ slug: z.string() }) },
46
+ responses: {
47
+ 200: {
48
+ content: { "application/json": { schema: notificationChannelSchema.array() } },
49
+ description: "List notification channels"
50
+ },
51
+ ...notFound$1,
52
+ ...unauthorized
53
+ }
54
+ });
55
+ const createChannelRoute = createRoute({
56
+ method: "post",
57
+ tags: ["Notifications"],
58
+ summary: "Create a notification channel",
59
+ path: "/api/v1/projects/{slug}/notification-channels",
60
+ request: {
61
+ params: z.object({ slug: z.string() }),
62
+ body: { content: { "application/json": { schema: notificationChannelCreateSchema } } }
63
+ },
64
+ responses: {
65
+ 201: {
66
+ content: { "application/json": { schema: notificationChannelSchema } },
67
+ description: "Created notification channel"
68
+ },
69
+ 400: {
70
+ content: { "application/json": { schema: z.object({ message: z.string() }) } },
71
+ description: "Bad request"
72
+ },
73
+ ...notFound$1
74
+ }
75
+ });
76
+ const deleteChannelRoute = createRoute({
77
+ method: "delete",
78
+ tags: ["Notifications"],
79
+ summary: "Delete a notification channel",
80
+ path: "/api/v1/projects/{slug}/notification-channels/{channelId}",
81
+ request: { params: z.object({
82
+ slug: z.string(),
83
+ channelId: z.string()
84
+ }) },
85
+ responses: {
86
+ 204: { description: "Deleted notification channel" },
87
+ ...notFound$1
88
+ }
89
+ });
90
+ /** Register the project notification-channel list, create, and delete endpoints. */
91
+ function registerNotificationChannels(app) {
92
+ app.openapi(listChannelsRoute, async (c) => {
93
+ const project = await resolveAuthorizedProject(c, c.req.valid("param").slug, ...ADMIN_ROLES);
94
+ const rows = await new NotificationChannelModel(getStore().db, void 0, credentialKeys(getStore().config)).list(project.id);
95
+ return c.json(rows.map((row) => toPublic(row)));
96
+ });
97
+ app.openapi(createChannelRoute, async (c) => {
98
+ const project = await resolveAuthorizedProject(c, c.req.valid("param").slug, ...ADMIN_ROLES);
99
+ const body = c.req.valid("json");
100
+ const valid = validateChannelInput(body.provider, body.config);
101
+ if (!valid.ok) return c.json({ message: valid.message }, 400);
102
+ const row = await new NotificationChannelModel(getStore().db, void 0, credentialKeys(getStore().config)).create({
103
+ projectId: project.id,
104
+ ...body
105
+ });
106
+ return c.json(toPublic(row), 201);
107
+ });
108
+ app.openapi(deleteChannelRoute, async (c) => {
109
+ const { slug, channelId } = c.req.valid("param");
110
+ const project = await resolveAuthorizedProject(c, slug, ...ADMIN_ROLES);
111
+ const model = new NotificationChannelModel(getStore().db, void 0, credentialKeys(getStore().config));
112
+ if ((await model.get(channelId))?.projectId !== project.id) notFound("Notification channel not found");
113
+ await model.remove(channelId);
114
+ return c.body(null, 204);
115
+ });
116
+ }
117
+ const listSystemRoute = createRoute({
118
+ method: "get",
119
+ tags: ["Notifications"],
120
+ summary: "List site-wide channels",
121
+ path: "/api/v1/admin/notification-channels",
122
+ request: {},
123
+ responses: {
124
+ 200: {
125
+ content: { "application/json": { schema: notificationChannelSchema.array() } },
126
+ description: "List site-wide notification channels"
127
+ },
128
+ ...unauthorized
129
+ }
130
+ });
131
+ const createSystemRoute = createRoute({
132
+ method: "post",
133
+ tags: ["Notifications"],
134
+ summary: "Create a site-wide channel",
135
+ path: "/api/v1/admin/notification-channels",
136
+ request: { body: { content: { "application/json": { schema: notificationChannelCreateSchema } } } },
137
+ responses: {
138
+ 201: {
139
+ content: { "application/json": { schema: notificationChannelSchema } },
140
+ description: "Created site-wide notification channel"
141
+ },
142
+ 400: {
143
+ content: { "application/json": { schema: z.object({ message: z.string() }) } },
144
+ description: "Bad request"
145
+ },
146
+ ...unauthorized
147
+ }
148
+ });
149
+ const deleteSystemRoute = createRoute({
150
+ method: "delete",
151
+ tags: ["Notifications"],
152
+ summary: "Delete a site-wide channel",
153
+ path: "/api/v1/admin/notification-channels/{channelId}",
154
+ request: { params: z.object({ channelId: z.string() }) },
155
+ responses: {
156
+ 204: { description: "Deleted site-wide notification channel" },
157
+ ...notFound$1
158
+ }
159
+ });
160
+ /** Register the site-admin (project-less, `sys:*`) channel endpoints. */
161
+ function registerSystemChannels(app) {
162
+ app.openapi(listSystemRoute, async (c) => {
163
+ requireSiteAdmin(c);
164
+ const rows = await new NotificationChannelModel(getStore().db, void 0, credentialKeys(getStore().config)).listSystem();
165
+ return c.json(rows.map((row) => toPublic(row)));
166
+ });
167
+ app.openapi(createSystemRoute, async (c) => {
168
+ requireSiteAdmin(c);
169
+ const body = c.req.valid("json");
170
+ const valid = validateChannelInput(body.provider, body.config);
171
+ if (!valid.ok) return c.json({ message: valid.message }, 400);
172
+ const row = await new NotificationChannelModel(getStore().db, void 0, credentialKeys(getStore().config)).create({
173
+ projectId: null,
174
+ ...body
175
+ });
176
+ return c.json(toPublic(row), 201);
177
+ });
178
+ app.openapi(deleteSystemRoute, async (c) => {
179
+ requireSiteAdmin(c);
180
+ const { channelId } = c.req.valid("param");
181
+ const model = new NotificationChannelModel(getStore().db, void 0, credentialKeys(getStore().config));
182
+ if ((await model.get(channelId))?.projectId !== null) notFound("Notification channel not found");
183
+ await model.remove(channelId);
184
+ return c.body(null, 204);
185
+ });
186
+ }
187
+ //#endregion
188
+ export { registerNotificationChannels, registerSystemChannels };
189
+
190
+ //# sourceMappingURL=notification-channels.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notification-channels.mjs","names":["notFoundResponse"],"sources":["../../src/routers/notification-channels.ts"],"sourcesContent":["import { createRoute, z } from \"@hono/zod-openapi\";\nimport { NotificationChannelModel } from \"@storyshelf/core/models\";\nimport type { NotificationChannelRow } from \"@storyshelf/core/schema\";\nimport type { ProjectRole } from \"@storyshelf/core/types\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { credentialKeys } from \"../credential-keys.ts\";\nimport { getStore } from \"../store.ts\";\nimport { notFound, requireSiteAdmin, resolveAuthorizedProject } from \"./helpers.ts\";\nimport {\n notFound as notFoundResponse,\n notificationChannelCreateSchema,\n notificationChannelSchema,\n unauthorized,\n} from \"./schemas.ts\";\nconst ADMIN_ROLES: readonly ProjectRole[] = [\"admin\"];\n\n/** Public channel view (secret redacted, config/events decoded). */\nfunction toPublic(row: NotificationChannelRow): {\n id: string;\n projectId: string | null;\n provider: string;\n config: Record<string, unknown>;\n hasSecret: boolean;\n events: string[];\n enabled: boolean;\n createdAt: string;\n updatedAt: string;\n} {\n return {\n id: row.id,\n projectId: row.projectId,\n provider: row.provider,\n config: NotificationChannelModel.configOf(row),\n hasSecret: row.secretEncrypted !== null,\n events: NotificationChannelModel.eventsOf(row),\n enabled: row.enabled,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n };\n}\n\n/** Validate channel input against the provider descriptor. */\nfunction validateChannelInput(\n provider: string,\n config: unknown,\n): { ok: true } | { ok: false; message: string } {\n const descriptor = getStore().notifiers.find((candidate) => candidate.metadata.kind === provider);\n if (!descriptor) {\n return { ok: false, message: `Unknown provider: ${provider}` };\n }\n const parsed = descriptor.metadata.schema.safeParse(config);\n if (!parsed.success) {\n return { ok: false, message: parsed.error.message };\n }\n return { ok: true };\n}\n\nconst listChannelsRoute = createRoute({\n method: \"get\",\n tags: [\"Notifications\"],\n summary: \"List notification channels\",\n path: \"/api/v1/projects/{slug}/notification-channels\",\n request: { params: z.object({ slug: z.string() }) },\n responses: {\n 200: {\n content: { \"application/json\": { schema: notificationChannelSchema.array() } },\n description: \"List notification channels\",\n },\n ...notFoundResponse,\n ...unauthorized,\n },\n});\n\nconst createChannelRoute = createRoute({\n method: \"post\",\n tags: [\"Notifications\"],\n summary: \"Create a notification channel\",\n path: \"/api/v1/projects/{slug}/notification-channels\",\n request: {\n params: z.object({ slug: z.string() }),\n body: { content: { \"application/json\": { schema: notificationChannelCreateSchema } } },\n },\n responses: {\n 201: {\n content: { \"application/json\": { schema: notificationChannelSchema } },\n description: \"Created notification channel\",\n },\n 400: {\n content: { \"application/json\": { schema: z.object({ message: z.string() }) } },\n description: \"Bad request\",\n },\n ...notFoundResponse,\n },\n});\n\nconst deleteChannelRoute = createRoute({\n method: \"delete\",\n tags: [\"Notifications\"],\n summary: \"Delete a notification channel\",\n path: \"/api/v1/projects/{slug}/notification-channels/{channelId}\",\n request: { params: z.object({ slug: z.string(), channelId: z.string() }) },\n responses: {\n 204: { description: \"Deleted notification channel\" },\n ...notFoundResponse,\n },\n});\n\n/** Register the project notification-channel list, create, and delete endpoints. */\nexport function registerNotificationChannels(app: ShelfRouter): void {\n app.openapi(listChannelsRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...ADMIN_ROLES);\n const rows = await new NotificationChannelModel(\n getStore().db,\n undefined,\n credentialKeys(getStore().config),\n ).list(project.id);\n return c.json(rows.map((row) => toPublic(row)));\n });\n\n app.openapi(createChannelRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...ADMIN_ROLES);\n const body = c.req.valid(\"json\");\n const valid = validateChannelInput(body.provider, body.config);\n if (!valid.ok) {\n return c.json({ message: valid.message }, 400);\n }\n const row = await new NotificationChannelModel(\n getStore().db,\n undefined,\n credentialKeys(getStore().config),\n ).create({ projectId: project.id, ...body });\n return c.json(toPublic(row), 201);\n });\n\n app.openapi(deleteChannelRoute, async (c) => {\n const { slug, channelId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...ADMIN_ROLES);\n const model = new NotificationChannelModel(\n getStore().db,\n undefined,\n credentialKeys(getStore().config),\n );\n const existing = await model.get(channelId);\n if (existing?.projectId !== project.id) {\n notFound(\"Notification channel not found\");\n }\n await model.remove(channelId);\n return c.body(null, 204);\n });\n}\n\nconst listSystemRoute = createRoute({\n method: \"get\",\n tags: [\"Notifications\"],\n summary: \"List site-wide channels\",\n path: \"/api/v1/admin/notification-channels\",\n request: {},\n responses: {\n 200: {\n content: { \"application/json\": { schema: notificationChannelSchema.array() } },\n description: \"List site-wide notification channels\",\n },\n ...unauthorized,\n },\n});\n\nconst createSystemRoute = createRoute({\n method: \"post\",\n tags: [\"Notifications\"],\n summary: \"Create a site-wide channel\",\n path: \"/api/v1/admin/notification-channels\",\n request: {\n body: { content: { \"application/json\": { schema: notificationChannelCreateSchema } } },\n },\n responses: {\n 201: {\n content: { \"application/json\": { schema: notificationChannelSchema } },\n description: \"Created site-wide notification channel\",\n },\n 400: {\n content: { \"application/json\": { schema: z.object({ message: z.string() }) } },\n description: \"Bad request\",\n },\n ...unauthorized,\n },\n});\n\nconst deleteSystemRoute = createRoute({\n method: \"delete\",\n tags: [\"Notifications\"],\n summary: \"Delete a site-wide channel\",\n path: \"/api/v1/admin/notification-channels/{channelId}\",\n request: { params: z.object({ channelId: z.string() }) },\n responses: {\n 204: { description: \"Deleted site-wide notification channel\" },\n ...notFoundResponse,\n },\n});\n\n/** Register the site-admin (project-less, `sys:*`) channel endpoints. */\nexport function registerSystemChannels(app: ShelfRouter): void {\n app.openapi(listSystemRoute, async (c) => {\n requireSiteAdmin(c);\n const rows = await new NotificationChannelModel(\n getStore().db,\n undefined,\n credentialKeys(getStore().config),\n ).listSystem();\n return c.json(rows.map((row) => toPublic(row)));\n });\n\n app.openapi(createSystemRoute, async (c) => {\n requireSiteAdmin(c);\n const body = c.req.valid(\"json\");\n const valid = validateChannelInput(body.provider, body.config);\n if (!valid.ok) {\n return c.json({ message: valid.message }, 400);\n }\n const row = await new NotificationChannelModel(\n getStore().db,\n undefined,\n credentialKeys(getStore().config),\n ).create({ projectId: null, ...body });\n return c.json(toPublic(row), 201);\n });\n\n app.openapi(deleteSystemRoute, async (c) => {\n requireSiteAdmin(c);\n const { channelId } = c.req.valid(\"param\");\n const model = new NotificationChannelModel(\n getStore().db,\n undefined,\n credentialKeys(getStore().config),\n );\n const existing = await model.get(channelId);\n if (existing?.projectId !== null) {\n notFound(\"Notification channel not found\");\n }\n await model.remove(channelId);\n return c.body(null, 204);\n });\n}\n"],"mappings":"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"}
@@ -0,0 +1,120 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { getStore } from "../store.mjs";
5
+ import { resolveAuthorizedProject, unauthorized } from "./helpers.mjs";
6
+ import { notFound, notificationSubscriptionPutSchema, notificationSubscriptionSchema, unauthorized as unauthorized$1 } from "./schemas.mjs";
7
+ import { createRoute, z } from "@hono/zod-openapi";
8
+ import { NotificationSubscriptionModel } from "@storyshelf/core/models";
9
+ //#region src/routers/notification-subscriptions.ts
10
+ const VIEW_ROLES = [
11
+ "viewer",
12
+ "developer",
13
+ "approver",
14
+ "admin"
15
+ ];
16
+ const ADMIN_ROLES = ["admin"];
17
+ /** Public subscription view (event/via lists decoded). */
18
+ function toPublic(row) {
19
+ return {
20
+ projectId: row.projectId,
21
+ userId: row.userId,
22
+ events: NotificationSubscriptionModel.eventsOf(row),
23
+ via: NotificationSubscriptionModel.viaOf(row),
24
+ enabled: row.enabled
25
+ };
26
+ }
27
+ /** Require a session user (subscriptions are meaningless without identity). */
28
+ function currentUserId() {
29
+ const user = getStore().user;
30
+ if (!user) unauthorized();
31
+ return user.id;
32
+ }
33
+ const getSubscriptionRoute = createRoute({
34
+ method: "get",
35
+ tags: ["Notifications"],
36
+ summary: "Get my notification subscription",
37
+ path: "/api/v1/projects/{slug}/notifications/me",
38
+ request: { params: z.object({ slug: z.string() }) },
39
+ responses: {
40
+ 200: {
41
+ content: { "application/json": { schema: notificationSubscriptionSchema.nullable() } },
42
+ description: "Your notification subscription (null when never opted in)"
43
+ },
44
+ ...notFound,
45
+ ...unauthorized$1
46
+ }
47
+ });
48
+ const putSubscriptionRoute = createRoute({
49
+ method: "put",
50
+ tags: ["Notifications"],
51
+ summary: "Save my notification subscription",
52
+ path: "/api/v1/projects/{slug}/notifications/me",
53
+ request: {
54
+ params: z.object({ slug: z.string() }),
55
+ body: { content: { "application/json": { schema: notificationSubscriptionPutSchema } } }
56
+ },
57
+ responses: {
58
+ 200: {
59
+ content: { "application/json": { schema: notificationSubscriptionSchema } },
60
+ description: "Saved notification subscription"
61
+ },
62
+ ...notFound,
63
+ ...unauthorized$1
64
+ }
65
+ });
66
+ const deleteSubscriptionRoute = createRoute({
67
+ method: "delete",
68
+ tags: ["Notifications"],
69
+ summary: "Remove my notification subscription",
70
+ path: "/api/v1/projects/{slug}/notifications/me",
71
+ request: { params: z.object({ slug: z.string() }) },
72
+ responses: {
73
+ 204: { description: "Removed notification subscription (opted out)" },
74
+ ...notFound,
75
+ ...unauthorized$1
76
+ }
77
+ });
78
+ const listSubscriptionsRoute = createRoute({
79
+ method: "get",
80
+ tags: ["Notifications"],
81
+ summary: "List notification subscriptions",
82
+ path: "/api/v1/projects/{slug}/notifications",
83
+ request: { params: z.object({ slug: z.string() }) },
84
+ responses: {
85
+ 200: {
86
+ content: { "application/json": { schema: notificationSubscriptionSchema.array() } },
87
+ description: "List notification subscriptions (admin audit)"
88
+ },
89
+ ...notFound,
90
+ ...unauthorized$1
91
+ }
92
+ });
93
+ /** Register the self-service subscription endpoints plus the admin audit list. */
94
+ function registerNotificationSubscriptions(app) {
95
+ app.openapi(getSubscriptionRoute, async (c) => {
96
+ const project = await resolveAuthorizedProject(c, c.req.valid("param").slug, ...VIEW_ROLES);
97
+ const row = await new NotificationSubscriptionModel(getStore().db).getFor(project.id, currentUserId());
98
+ return c.json(row ? toPublic(row) : null);
99
+ });
100
+ app.openapi(putSubscriptionRoute, async (c) => {
101
+ const project = await resolveAuthorizedProject(c, c.req.valid("param").slug, ...VIEW_ROLES);
102
+ const body = c.req.valid("json");
103
+ const row = await new NotificationSubscriptionModel(getStore().db).upsert(project.id, currentUserId(), body);
104
+ return c.json(toPublic(row));
105
+ });
106
+ app.openapi(deleteSubscriptionRoute, async (c) => {
107
+ const project = await resolveAuthorizedProject(c, c.req.valid("param").slug, ...VIEW_ROLES);
108
+ await new NotificationSubscriptionModel(getStore().db).remove(project.id, currentUserId());
109
+ return c.body(null, 204);
110
+ });
111
+ app.openapi(listSubscriptionsRoute, async (c) => {
112
+ const project = await resolveAuthorizedProject(c, c.req.valid("param").slug, ...ADMIN_ROLES);
113
+ const rows = await new NotificationSubscriptionModel(getStore().db).list(project.id);
114
+ return c.json(rows.map((row) => toPublic(row)));
115
+ });
116
+ }
117
+ //#endregion
118
+ export { registerNotificationSubscriptions };
119
+
120
+ //# sourceMappingURL=notification-subscriptions.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notification-subscriptions.mjs","names":["notFoundResponse","unauthorizedResponse"],"sources":["../../src/routers/notification-subscriptions.ts"],"sourcesContent":["import { createRoute, z } from \"@hono/zod-openapi\";\nimport { NotificationSubscriptionModel } from \"@storyshelf/core/models\";\nimport type { NotificationSubscriptionRow } from \"@storyshelf/core/schema\";\nimport type { ProjectRole } from \"@storyshelf/core/types\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { getStore } from \"../store.ts\";\nimport { resolveAuthorizedProject, unauthorized } from \"./helpers.ts\";\nimport {\n notFound as notFoundResponse,\n notificationSubscriptionPutSchema,\n notificationSubscriptionSchema,\n unauthorized as unauthorizedResponse,\n} from \"./schemas.ts\";\n\nconst VIEW_ROLES: readonly ProjectRole[] = [\"viewer\", \"developer\", \"approver\", \"admin\"];\nconst ADMIN_ROLES: readonly ProjectRole[] = [\"admin\"];\n\n/** Public subscription view (event/via lists decoded). */\nfunction toPublic(row: NotificationSubscriptionRow): {\n projectId: string;\n userId: string;\n events: string[];\n via: string[];\n enabled: boolean;\n} {\n return {\n projectId: row.projectId,\n userId: row.userId,\n events: NotificationSubscriptionModel.eventsOf(row),\n via: NotificationSubscriptionModel.viaOf(row),\n enabled: row.enabled,\n };\n}\n\n/** Require a session user (subscriptions are meaningless without identity). */\nfunction currentUserId(): string {\n const user = getStore().user;\n if (!user) {\n unauthorized();\n }\n return user.id;\n}\n\nconst getSubscriptionRoute = createRoute({\n method: \"get\",\n tags: [\"Notifications\"],\n summary: \"Get my notification subscription\",\n path: \"/api/v1/projects/{slug}/notifications/me\",\n request: { params: z.object({ slug: z.string() }) },\n responses: {\n 200: {\n content: { \"application/json\": { schema: notificationSubscriptionSchema.nullable() } },\n description: \"Your notification subscription (null when never opted in)\",\n },\n ...notFoundResponse,\n ...unauthorizedResponse,\n },\n});\n\nconst putSubscriptionRoute = createRoute({\n method: \"put\",\n tags: [\"Notifications\"],\n summary: \"Save my notification subscription\",\n path: \"/api/v1/projects/{slug}/notifications/me\",\n request: {\n params: z.object({ slug: z.string() }),\n body: { content: { \"application/json\": { schema: notificationSubscriptionPutSchema } } },\n },\n responses: {\n 200: {\n content: { \"application/json\": { schema: notificationSubscriptionSchema } },\n description: \"Saved notification subscription\",\n },\n ...notFoundResponse,\n ...unauthorizedResponse,\n },\n});\n\nconst deleteSubscriptionRoute = createRoute({\n method: \"delete\",\n tags: [\"Notifications\"],\n summary: \"Remove my notification subscription\",\n path: \"/api/v1/projects/{slug}/notifications/me\",\n request: { params: z.object({ slug: z.string() }) },\n responses: {\n 204: { description: \"Removed notification subscription (opted out)\" },\n ...notFoundResponse,\n ...unauthorizedResponse,\n },\n});\n\nconst listSubscriptionsRoute = createRoute({\n method: \"get\",\n tags: [\"Notifications\"],\n summary: \"List notification subscriptions\",\n path: \"/api/v1/projects/{slug}/notifications\",\n request: { params: z.object({ slug: z.string() }) },\n responses: {\n 200: {\n content: { \"application/json\": { schema: notificationSubscriptionSchema.array() } },\n description: \"List notification subscriptions (admin audit)\",\n },\n ...notFoundResponse,\n ...unauthorizedResponse,\n },\n});\n\n/** Register the self-service subscription endpoints plus the admin audit list. */\nexport function registerNotificationSubscriptions(app: ShelfRouter): void {\n app.openapi(getSubscriptionRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...VIEW_ROLES);\n const row = await new NotificationSubscriptionModel(getStore().db).getFor(\n project.id,\n currentUserId(),\n );\n return c.json(row ? toPublic(row) : null);\n });\n\n app.openapi(putSubscriptionRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...VIEW_ROLES);\n const body = c.req.valid(\"json\");\n const row = await new NotificationSubscriptionModel(getStore().db).upsert(\n project.id,\n currentUserId(),\n body,\n );\n return c.json(toPublic(row));\n });\n\n app.openapi(deleteSubscriptionRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...VIEW_ROLES);\n await new NotificationSubscriptionModel(getStore().db).remove(project.id, currentUserId());\n return c.body(null, 204);\n });\n\n app.openapi(listSubscriptionsRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...ADMIN_ROLES);\n const rows = await new NotificationSubscriptionModel(getStore().db).list(project.id);\n return c.json(rows.map((row) => toPublic(row)));\n });\n}\n"],"mappings":"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"}