@headless-lms/server 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (223) hide show
  1. package/README.md +14 -0
  2. package/dist/app/community-mail.d.ts +5 -3
  3. package/dist/app/community-mail.js +60 -12
  4. package/dist/app/community-mail.js.map +1 -1
  5. package/dist/app/container.d.ts +24 -3
  6. package/dist/app/container.js +102 -38
  7. package/dist/app/container.js.map +1 -1
  8. package/dist/app/notification-channels.js +1 -0
  9. package/dist/app/notification-channels.js.map +1 -1
  10. package/dist/app/notification-reads.d.ts +3 -3
  11. package/dist/app/notification-reads.js +7 -28
  12. package/dist/app/notification-reads.js.map +1 -1
  13. package/dist/app/platform.d.ts +14 -0
  14. package/dist/app/platform.js +14 -0
  15. package/dist/app/platform.js.map +1 -0
  16. package/dist/http/config.d.ts +4 -0
  17. package/dist/http/errors.js +9 -4
  18. package/dist/http/errors.js.map +1 -1
  19. package/dist/http/fastify.d.ts +8 -0
  20. package/dist/http/mcp/catalogue.d.ts +11 -9
  21. package/dist/http/mcp/catalogue.js +15 -21
  22. package/dist/http/mcp/catalogue.js.map +1 -1
  23. package/dist/http/mcp/challenge.js +3 -3
  24. package/dist/http/mcp/challenge.js.map +1 -1
  25. package/dist/http/mcp/route.js +7 -7
  26. package/dist/http/mcp/route.js.map +1 -1
  27. package/dist/http/mcp/test-support.d.ts +6 -5
  28. package/dist/http/mcp/test-support.js +32 -11
  29. package/dist/http/mcp/test-support.js.map +1 -1
  30. package/dist/http/mcp/tool.d.ts +3 -9
  31. package/dist/http/mcp/tool.js +14 -6
  32. package/dist/http/mcp/tool.js.map +1 -1
  33. package/dist/http/mcp/tools/automations.js +10 -20
  34. package/dist/http/mcp/tools/automations.js.map +1 -1
  35. package/dist/http/mcp/tools/catalog.js +25 -42
  36. package/dist/http/mcp/tools/catalog.js.map +1 -1
  37. package/dist/http/mcp/tools/comments.js +9 -19
  38. package/dist/http/mcp/tools/comments.js.map +1 -1
  39. package/dist/http/mcp/tools/communities.js +16 -31
  40. package/dist/http/mcp/tools/communities.js.map +1 -1
  41. package/dist/http/mcp/tools/community-members.js +9 -16
  42. package/dist/http/mcp/tools/community-members.js.map +1 -1
  43. package/dist/http/mcp/tools/community-moderation.js +13 -25
  44. package/dist/http/mcp/tools/community-moderation.js.map +1 -1
  45. package/dist/http/mcp/tools/courses.js +11 -21
  46. package/dist/http/mcp/tools/courses.js.map +1 -1
  47. package/dist/http/mcp/tools/events.d.ts +6 -0
  48. package/dist/http/mcp/tools/events.js +170 -0
  49. package/dist/http/mcp/tools/events.js.map +1 -0
  50. package/dist/http/mcp/tools/index.d.ts +5 -1
  51. package/dist/http/mcp/tools/index.js +45 -10
  52. package/dist/http/mcp/tools/index.js.map +1 -1
  53. package/dist/http/mcp/tools/organization.d.ts +6 -0
  54. package/dist/http/mcp/tools/organization.js +214 -0
  55. package/dist/http/mcp/tools/organization.js.map +1 -0
  56. package/dist/http/mcp/tools/people.js +30 -49
  57. package/dist/http/mcp/tools/people.js.map +1 -1
  58. package/dist/http/plugins/auth.js +37 -8
  59. package/dist/http/plugins/auth.js.map +1 -1
  60. package/dist/http/plugins/error-handler.d.ts +5 -1
  61. package/dist/http/plugins/error-handler.js +10 -2
  62. package/dist/http/plugins/error-handler.js.map +1 -1
  63. package/dist/http/plugins/openapi.js +18 -2
  64. package/dist/http/plugins/openapi.js.map +1 -1
  65. package/dist/http/plugins/platform.d.ts +7 -0
  66. package/dist/http/plugins/platform.js +13 -0
  67. package/dist/http/plugins/platform.js.map +1 -0
  68. package/dist/http/routes/access.js +54 -36
  69. package/dist/http/routes/access.js.map +1 -1
  70. package/dist/http/routes/activities.js +45 -45
  71. package/dist/http/routes/activities.js.map +1 -1
  72. package/dist/http/routes/assets.js +30 -65
  73. package/dist/http/routes/assets.js.map +1 -1
  74. package/dist/http/routes/automations.js +37 -42
  75. package/dist/http/routes/automations.js.map +1 -1
  76. package/dist/http/routes/bundles.js +33 -36
  77. package/dist/http/routes/bundles.js.map +1 -1
  78. package/dist/http/routes/communities.js +93 -145
  79. package/dist/http/routes/communities.js.map +1 -1
  80. package/dist/http/routes/community-moderation.js +83 -137
  81. package/dist/http/routes/community-moderation.js.map +1 -1
  82. package/dist/http/routes/course-analytics.js +32 -15
  83. package/dist/http/routes/course-analytics.js.map +1 -1
  84. package/dist/http/routes/courses.js +25 -32
  85. package/dist/http/routes/courses.js.map +1 -1
  86. package/dist/http/routes/dashboard.js +9 -9
  87. package/dist/http/routes/dashboard.js.map +1 -1
  88. package/dist/http/routes/discussion.js +21 -39
  89. package/dist/http/routes/discussion.js.map +1 -1
  90. package/dist/http/routes/downloads.js +41 -47
  91. package/dist/http/routes/downloads.js.map +1 -1
  92. package/dist/http/routes/entitlements.js +19 -23
  93. package/dist/http/routes/entitlements.js.map +1 -1
  94. package/dist/http/routes/events.d.ts +7 -0
  95. package/dist/http/routes/events.js +333 -0
  96. package/dist/http/routes/events.js.map +1 -0
  97. package/dist/http/routes/integrations.js +40 -73
  98. package/dist/http/routes/integrations.js.map +1 -1
  99. package/dist/http/routes/member/assets.js +10 -13
  100. package/dist/http/routes/member/assets.js.map +1 -1
  101. package/dist/http/routes/member/chats.js +170 -47
  102. package/dist/http/routes/member/chats.js.map +1 -1
  103. package/dist/http/routes/member/comments.js +65 -26
  104. package/dist/http/routes/member/comments.js.map +1 -1
  105. package/dist/http/routes/member/community.js +45 -52
  106. package/dist/http/routes/member/community.js.map +1 -1
  107. package/dist/http/routes/member/courses.js +12 -19
  108. package/dist/http/routes/member/courses.js.map +1 -1
  109. package/dist/http/routes/member/devices.js +9 -10
  110. package/dist/http/routes/member/devices.js.map +1 -1
  111. package/dist/http/routes/member/downloads.js +6 -9
  112. package/dist/http/routes/member/downloads.js.map +1 -1
  113. package/dist/http/routes/member/events.d.ts +7 -0
  114. package/dist/http/routes/member/events.js +386 -0
  115. package/dist/http/routes/member/events.js.map +1 -0
  116. package/dist/http/routes/member/files.js +71 -46
  117. package/dist/http/routes/member/files.js.map +1 -1
  118. package/dist/http/routes/member/index.js +2 -0
  119. package/dist/http/routes/member/index.js.map +1 -1
  120. package/dist/http/routes/member/notification-settings.js +15 -23
  121. package/dist/http/routes/member/notification-settings.js.map +1 -1
  122. package/dist/http/routes/member/notifications.js +16 -16
  123. package/dist/http/routes/member/notifications.js.map +1 -1
  124. package/dist/http/routes/member/org.js +5 -15
  125. package/dist/http/routes/member/org.js.map +1 -1
  126. package/dist/http/routes/member/posts.js +67 -68
  127. package/dist/http/routes/member/posts.js.map +1 -1
  128. package/dist/http/routes/member/profile.js +15 -6
  129. package/dist/http/routes/member/profile.js.map +1 -1
  130. package/dist/http/routes/member/progress.js +7 -11
  131. package/dist/http/routes/member/progress.js.map +1 -1
  132. package/dist/http/routes/organizations.js +319 -135
  133. package/dist/http/routes/organizations.js.map +1 -1
  134. package/dist/http/routes/platform.d.ts +7 -0
  135. package/dist/http/routes/platform.js +61 -0
  136. package/dist/http/routes/platform.js.map +1 -0
  137. package/dist/http/routes/students.js +12 -75
  138. package/dist/http/routes/students.js.map +1 -1
  139. package/dist/http/routes/webhooks.js +30 -39
  140. package/dist/http/routes/webhooks.js.map +1 -1
  141. package/dist/http/routes.js +9 -0
  142. package/dist/http/routes.js.map +1 -1
  143. package/dist/http/schemas/access.d.ts +346 -64
  144. package/dist/http/schemas/access.js +39 -12
  145. package/dist/http/schemas/access.js.map +1 -1
  146. package/dist/http/schemas/assets.d.ts +19 -14
  147. package/dist/http/schemas/assets.js +6 -5
  148. package/dist/http/schemas/assets.js.map +1 -1
  149. package/dist/http/schemas/automations.d.ts +4 -4
  150. package/dist/http/schemas/chats.d.ts +211 -32
  151. package/dist/http/schemas/chats.js +13 -4
  152. package/dist/http/schemas/chats.js.map +1 -1
  153. package/dist/http/schemas/community.d.ts +91 -44
  154. package/dist/http/schemas/community.js +21 -8
  155. package/dist/http/schemas/community.js.map +1 -1
  156. package/dist/http/schemas/course-analytics.d.ts +51 -1
  157. package/dist/http/schemas/course-analytics.js +19 -1
  158. package/dist/http/schemas/course-analytics.js.map +1 -1
  159. package/dist/http/schemas/dashboard.d.ts +1 -0
  160. package/dist/http/schemas/dashboard.js +2 -1
  161. package/dist/http/schemas/dashboard.js.map +1 -1
  162. package/dist/http/schemas/discussion.d.ts +22 -72
  163. package/dist/http/schemas/discussion.js +2 -2
  164. package/dist/http/schemas/discussion.js.map +1 -1
  165. package/dist/http/schemas/entitlements.d.ts +9 -12
  166. package/dist/http/schemas/entitlements.js +11 -5
  167. package/dist/http/schemas/entitlements.js.map +1 -1
  168. package/dist/http/schemas/events.d.ts +527 -0
  169. package/dist/http/schemas/events.js +64 -0
  170. package/dist/http/schemas/events.js.map +1 -0
  171. package/dist/http/schemas/files.d.ts +15 -5
  172. package/dist/http/schemas/files.js +10 -2
  173. package/dist/http/schemas/files.js.map +1 -1
  174. package/dist/http/schemas/index.d.ts +11 -9
  175. package/dist/http/schemas/index.js +11 -9
  176. package/dist/http/schemas/integrations.d.ts +4 -4
  177. package/dist/http/schemas/invites.d.ts +3 -47
  178. package/dist/http/schemas/invites.js +4 -25
  179. package/dist/http/schemas/invites.js.map +1 -1
  180. package/dist/http/schemas/member.d.ts +94 -7
  181. package/dist/http/schemas/member.js +7 -3
  182. package/dist/http/schemas/member.js.map +1 -1
  183. package/dist/http/schemas/members.d.ts +333 -69
  184. package/dist/http/schemas/members.js +67 -25
  185. package/dist/http/schemas/members.js.map +1 -1
  186. package/dist/http/schemas/notifications.d.ts +57 -53
  187. package/dist/http/schemas/platform.d.ts +37 -0
  188. package/dist/http/schemas/platform.js +19 -0
  189. package/dist/http/schemas/platform.js.map +1 -0
  190. package/dist/http/schemas/posts.d.ts +55 -35
  191. package/dist/http/schemas/posts.js +4 -4
  192. package/dist/http/schemas/posts.js.map +1 -1
  193. package/dist/http/schemas/profile.d.ts +1 -1
  194. package/dist/http/schemas/shared.d.ts +1 -11
  195. package/dist/http/schemas/shared.js +1 -10
  196. package/dist/http/schemas/shared.js.map +1 -1
  197. package/dist/http/schemas/students.d.ts +25 -3
  198. package/dist/http/schemas/students.js +3 -1
  199. package/dist/http/schemas/students.js.map +1 -1
  200. package/dist/http/scope.d.ts +9 -13
  201. package/dist/http/scope.js +29 -40
  202. package/dist/http/scope.js.map +1 -1
  203. package/dist/http/server.js +9 -0
  204. package/dist/http/server.js.map +1 -1
  205. package/dist/http/shutdown.d.ts +11 -0
  206. package/dist/http/shutdown.js +40 -0
  207. package/dist/http/shutdown.js.map +1 -0
  208. package/dist/http/sse.d.ts +19 -0
  209. package/dist/http/sse.js +104 -0
  210. package/dist/http/sse.js.map +1 -0
  211. package/dist/http/test-caller.d.ts +13 -0
  212. package/dist/http/test-caller.js +14 -0
  213. package/dist/http/test-caller.js.map +1 -0
  214. package/dist/http/well-known.js +2 -2
  215. package/dist/http/well-known.js.map +1 -1
  216. package/dist/index.d.ts +4 -2
  217. package/dist/index.js +4 -2
  218. package/dist/index.js.map +1 -1
  219. package/dist/packages/server/package.js +1 -1
  220. package/package.json +7 -6
  221. package/dist/http/routes/member/actor.d.ts +0 -14
  222. package/dist/http/routes/member/actor.js +0 -11
  223. package/dist/http/routes/member/actor.js.map +0 -1
@@ -0,0 +1,333 @@
1
+ import { PageQuery, refusals } from "../schemas/shared.js";
2
+ import { resolveActor } from "../scope.js";
3
+ import { AddEventAsset, AddEventAttendee, CreateEvent, Event, EventAnalytics, EventAssetParams, EventAttendeeParams, EventAttendeesPage, EventFile, EventIdParam, EventsPage, EventsQuery, RenameEventAsset, ReorderEventAssets, UpdateEvent } from "../schemas/events.js";
4
+ import "../schemas/index.js";
5
+ import { z } from "zod";
6
+ //#region src/http/routes/events.ts
7
+ async function eventsRoutes(app, container) {
8
+ const r = app.withTypeProvider();
9
+ const content = container.content;
10
+ r.route({
11
+ method: "GET",
12
+ url: "/api/events",
13
+ preHandler: app.requireOrgSession,
14
+ schema: {
15
+ operationId: "listEvents",
16
+ tags: ["Events"],
17
+ summary: "List events",
18
+ description: `Requires \`events:read\`. Every event the caller holds \`events:read\` on, drafts and canceled included. \`from\` and \`to\` keep the events that overlap the range.`,
19
+ querystring: EventsQuery,
20
+ response: {
21
+ 200: EventsPage,
22
+ ...refusals(400, 403)
23
+ }
24
+ },
25
+ handler: async (req) => {
26
+ const actor = await resolveActor(container, req);
27
+ return content.listEvents(req.query, actor);
28
+ }
29
+ });
30
+ r.route({
31
+ method: "GET",
32
+ url: "/api/events/:eventId",
33
+ preHandler: app.requireOrgSession,
34
+ schema: {
35
+ operationId: "getEvent",
36
+ tags: ["Events"],
37
+ summary: "Get an event",
38
+ description: "Requires `events:read`.",
39
+ params: EventIdParam,
40
+ response: {
41
+ 200: Event,
42
+ ...refusals(403, 404)
43
+ }
44
+ },
45
+ handler: async (req) => {
46
+ const actor = await resolveActor(container, req);
47
+ return content.getEvent(req.params.eventId, actor);
48
+ }
49
+ });
50
+ r.route({
51
+ method: "POST",
52
+ url: "/api/events",
53
+ preHandler: app.requireOrgSession,
54
+ schema: {
55
+ operationId: "createEvent",
56
+ tags: ["Events"],
57
+ summary: "Create an event",
58
+ description: `Requires \`events:write\`. The event starts as a draft. 400 when it ends before it starts.`,
59
+ body: CreateEvent,
60
+ response: {
61
+ 201: Event,
62
+ ...refusals(400, 403)
63
+ }
64
+ },
65
+ handler: async (req, reply) => {
66
+ const actor = await resolveActor(container, req);
67
+ const event = await content.createEvent(req.body, actor);
68
+ return reply.code(201).send(event);
69
+ }
70
+ });
71
+ r.route({
72
+ method: "PATCH",
73
+ url: "/api/events/:eventId",
74
+ preHandler: app.requireOrgSession,
75
+ schema: {
76
+ operationId: "updateEvent",
77
+ tags: ["Events"],
78
+ summary: "Update an event",
79
+ description: `Requires \`events:write\`. Changes only the fields sent; \`status\` publishes or unpublishes it. \`thumbnailAssetId\` must be a ready library image (409 \`conflict\` otherwise); null clears it. A file it does not already use must be uploaded for it, or the caller needs \`media:read\` (403 otherwise). 409 \`canceled\` when the event is canceled.`,
80
+ params: EventIdParam,
81
+ body: UpdateEvent,
82
+ response: {
83
+ 200: Event,
84
+ ...refusals(400, 403, 404, 409)
85
+ }
86
+ },
87
+ handler: async (req) => {
88
+ const actor = await resolveActor(container, req);
89
+ return content.updateEvent(req.params.eventId, req.body, actor);
90
+ }
91
+ });
92
+ r.route({
93
+ method: "POST",
94
+ url: "/api/events/:eventId/cancel",
95
+ preHandler: app.requireOrgSession,
96
+ schema: {
97
+ operationId: "cancelEvent",
98
+ tags: ["Events"],
99
+ summary: "Cancel an event",
100
+ description: `Requires \`events:write\`. Canceling is final. 409 \`canceled\` when it is already canceled.`,
101
+ params: EventIdParam,
102
+ response: {
103
+ 200: Event,
104
+ ...refusals(403, 404, 409)
105
+ }
106
+ },
107
+ handler: async (req) => {
108
+ const actor = await resolveActor(container, req);
109
+ return content.cancelEvent(req.params.eventId, actor);
110
+ }
111
+ });
112
+ r.route({
113
+ method: "DELETE",
114
+ url: "/api/events/:eventId",
115
+ preHandler: app.requireOrgSession,
116
+ schema: {
117
+ operationId: "deleteEvent",
118
+ tags: ["Events"],
119
+ summary: "Delete an event",
120
+ description: `Requires \`events:delete\`. Its attendees lose it; its library files survive.`,
121
+ params: EventIdParam,
122
+ response: {
123
+ 204: z.void(),
124
+ ...refusals(403, 404)
125
+ }
126
+ },
127
+ handler: async (req, reply) => {
128
+ const actor = await resolveActor(container, req);
129
+ await content.deleteEvent(req.params.eventId, actor);
130
+ return reply.code(204).send();
131
+ }
132
+ });
133
+ r.route({
134
+ method: "GET",
135
+ url: "/api/events/:eventId/assets",
136
+ preHandler: app.requireOrgSession,
137
+ schema: {
138
+ operationId: "listEventAssets",
139
+ tags: ["Events"],
140
+ summary: "List an event's files in order",
141
+ description: "Requires `events:read`.",
142
+ params: EventIdParam,
143
+ response: {
144
+ 200: z.array(EventFile),
145
+ ...refusals(403, 404)
146
+ }
147
+ },
148
+ handler: async (req) => {
149
+ const actor = await resolveActor(container, req);
150
+ return content.listEventAssets(req.params.eventId, actor);
151
+ }
152
+ });
153
+ r.route({
154
+ method: "POST",
155
+ url: "/api/events/:eventId/assets",
156
+ preHandler: app.requireOrgSession,
157
+ schema: {
158
+ operationId: "addEventAsset",
159
+ tags: ["Events"],
160
+ summary: "Attach a file to an event",
161
+ description: `Requires \`events:write\`. Added last. 409 when the file is not a ready library file or is already attached. A file not uploaded for the event needs \`media:read\` (403 otherwise). Returns the event's files.`,
162
+ params: EventIdParam,
163
+ body: AddEventAsset,
164
+ response: {
165
+ 200: z.array(EventFile),
166
+ ...refusals(403, 404, 409)
167
+ }
168
+ },
169
+ handler: async (req) => {
170
+ const actor = await resolveActor(container, req);
171
+ return content.addEventAsset(req.params.eventId, req.body, actor);
172
+ }
173
+ });
174
+ r.route({
175
+ method: "PUT",
176
+ url: "/api/events/:eventId/assets/order",
177
+ preHandler: app.requireOrgSession,
178
+ schema: {
179
+ operationId: "reorderEventAssets",
180
+ tags: ["Events"],
181
+ summary: "Reorder an event's files",
182
+ description: `Requires \`events:write\`. Send every attached file id exactly once (409 otherwise). Returns the event's files.`,
183
+ params: EventIdParam,
184
+ body: ReorderEventAssets,
185
+ response: {
186
+ 200: z.array(EventFile),
187
+ ...refusals(403, 404, 409)
188
+ }
189
+ },
190
+ handler: async (req) => {
191
+ const actor = await resolveActor(container, req);
192
+ return content.reorderEventAssets(req.params.eventId, req.body.assetIds, actor);
193
+ }
194
+ });
195
+ r.route({
196
+ method: "PATCH",
197
+ url: "/api/events/:eventId/assets/:assetId",
198
+ preHandler: app.requireOrgSession,
199
+ schema: {
200
+ operationId: "renameEventAsset",
201
+ tags: ["Events"],
202
+ summary: "Set a file's display name within an event",
203
+ description: `Requires \`events:write\`. Null falls back to the filename. Returns the event's files.`,
204
+ params: EventAssetParams,
205
+ body: RenameEventAsset,
206
+ response: {
207
+ 200: z.array(EventFile),
208
+ ...refusals(403, 404)
209
+ }
210
+ },
211
+ handler: async (req) => {
212
+ const actor = await resolveActor(container, req);
213
+ return content.renameEventAsset(req.params.eventId, req.params.assetId, req.body.displayName, actor);
214
+ }
215
+ });
216
+ r.route({
217
+ method: "DELETE",
218
+ url: "/api/events/:eventId/assets/:assetId",
219
+ preHandler: app.requireOrgSession,
220
+ schema: {
221
+ operationId: "removeEventAsset",
222
+ tags: ["Events"],
223
+ summary: "Remove a file from an event",
224
+ description: `Requires \`events:write\`. The library file itself survives. Returns the event's files.`,
225
+ params: EventAssetParams,
226
+ response: {
227
+ 200: z.array(EventFile),
228
+ ...refusals(403, 404)
229
+ }
230
+ },
231
+ handler: async (req) => {
232
+ const actor = await resolveActor(container, req);
233
+ return content.removeEventAsset(req.params.eventId, req.params.assetId, actor);
234
+ }
235
+ });
236
+ r.route({
237
+ method: "POST",
238
+ url: "/api/events/:eventId/attendees",
239
+ preHandler: app.requireOrgSession,
240
+ schema: {
241
+ operationId: "addEventAttendee",
242
+ tags: ["Events"],
243
+ summary: "Add an attendee to an event",
244
+ description: "Requires `access:write`, or `events:write` on the event or the org. Grants the event to the person, with source `manual`. Adding someone who already attends changes nothing. 404 when the event or the person does not exist; 409 `not_published`, `ended` or `full`.",
245
+ params: EventIdParam,
246
+ body: AddEventAttendee,
247
+ response: {
248
+ 204: z.void(),
249
+ ...refusals(400, 403, 404, 409)
250
+ }
251
+ },
252
+ handler: async (req, reply) => {
253
+ const actor = await resolveActor(container, req);
254
+ await container.entitlements.grant({
255
+ orgUserId: req.body.orgUserId,
256
+ contentId: req.params.eventId,
257
+ contentType: "event",
258
+ expiresAt: null
259
+ }, actor);
260
+ return reply.code(204).send();
261
+ }
262
+ });
263
+ r.route({
264
+ method: "DELETE",
265
+ url: "/api/events/:eventId/attendees/:orgUserId",
266
+ preHandler: app.requireOrgSession,
267
+ schema: {
268
+ operationId: "removeEventAttendee",
269
+ tags: ["Events"],
270
+ summary: "Remove an attendee from an event",
271
+ description: "Requires `access:write`, or `events:write` on the event or the org. Revokes the person's direct grant to the event. 404 when they have no direct active grant; someone who attends only through a bundle has none.",
272
+ params: EventAttendeeParams,
273
+ response: {
274
+ 204: z.void(),
275
+ ...refusals(403, 404)
276
+ }
277
+ },
278
+ handler: async (req, reply) => {
279
+ const actor = await resolveActor(container, req);
280
+ await container.entitlements.revoke({
281
+ orgUserId: req.params.orgUserId,
282
+ contentId: req.params.eventId,
283
+ contentType: "event"
284
+ }, actor);
285
+ return reply.code(204).send();
286
+ }
287
+ });
288
+ r.route({
289
+ method: "GET",
290
+ url: "/api/events/:eventId/attendees",
291
+ preHandler: app.requireOrgSession,
292
+ schema: {
293
+ operationId: "listEventAttendees",
294
+ tags: ["Events"],
295
+ summary: "List an event's attendees",
296
+ description: `Requires \`events:read\`. On the event or the org. The people attending, directly or through a bundle, by grant date. \`source\` is \`bundle\` for someone who attends only through a bundle.`,
297
+ params: EventIdParam,
298
+ querystring: PageQuery,
299
+ response: {
300
+ 200: EventAttendeesPage,
301
+ ...refusals(400, 403, 404)
302
+ }
303
+ },
304
+ handler: async (req) => {
305
+ const actor = await resolveActor(container, req);
306
+ return container.reporting.admin.eventAttendees(req.params.eventId, req.query, actor);
307
+ }
308
+ });
309
+ r.route({
310
+ method: "GET",
311
+ url: "/api/events/:eventId/analytics",
312
+ preHandler: app.requireOrgSession,
313
+ schema: {
314
+ operationId: "getEventAnalytics",
315
+ tags: ["Events"],
316
+ summary: "An event's attendance figures",
317
+ description: `Requires \`events:read\`. On the event or the org. The attendees now and by source, the capacity, the revoked direct grants, and the current direct grants by grant date before the start.`,
318
+ params: EventIdParam,
319
+ response: {
320
+ 200: EventAnalytics,
321
+ ...refusals(403, 404)
322
+ }
323
+ },
324
+ handler: async (req) => {
325
+ const actor = await resolveActor(container, req);
326
+ return container.reporting.admin.eventAnalytics(req.params.eventId, actor);
327
+ }
328
+ });
329
+ }
330
+ //#endregion
331
+ export { eventsRoutes };
332
+
333
+ //# sourceMappingURL=events.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.js","names":[],"sources":["../../../src/http/routes/events.ts"],"sourcesContent":["import type { FastifyInstance } from 'fastify';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport { z } from 'zod';\nimport {\n AddEventAsset,\n AddEventAttendee,\n CreateEvent,\n Event,\n EventAnalytics,\n EventAttendeesPage,\n EventAssetParams,\n EventAttendeeParams,\n EventFile,\n EventIdParam,\n EventsPage,\n EventsQuery,\n PageQuery,\n RenameEventAsset,\n ReorderEventAssets,\n UpdateEvent,\n refusals,\n} from '../schemas/index.js';\nimport type { Container } from '../../app/container.js';\nimport { resolveActor } from '../scope.js';\n\nexport async function eventsRoutes(app: FastifyInstance, container: Container): Promise<void> {\n const r = app.withTypeProvider<ZodTypeProvider>();\n const content = container.content;\n\n r.route({\n method: 'GET',\n url: '/api/events',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listEvents',\n tags: ['Events'],\n summary: 'List events',\n description: `Requires \\`events:read\\`. Every event the caller holds \\`events:read\\` on, drafts and canceled included. \\`from\\` and \\`to\\` keep the events that overlap the range.`,\n querystring: EventsQuery,\n response: { 200: EventsPage, ...refusals(400, 403) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return content.listEvents(req.query, actor);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/events/:eventId',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getEvent',\n tags: ['Events'],\n summary: 'Get an event',\n description: 'Requires `events:read`.',\n params: EventIdParam,\n response: { 200: Event, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return content.getEvent(req.params.eventId, actor);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/events',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'createEvent',\n tags: ['Events'],\n summary: 'Create an event',\n description: `Requires \\`events:write\\`. The event starts as a draft. 400 when it ends before it starts.`,\n body: CreateEvent,\n response: { 201: Event, ...refusals(400, 403) },\n },\n handler: async (req, reply) => {\n const actor = await resolveActor(container, req);\n const event = await content.createEvent(req.body, actor);\n return reply.code(201).send(event);\n },\n });\n\n r.route({\n method: 'PATCH',\n url: '/api/events/:eventId',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'updateEvent',\n tags: ['Events'],\n summary: 'Update an event',\n description: `Requires \\`events:write\\`. Changes only the fields sent; \\`status\\` publishes or unpublishes it. \\`thumbnailAssetId\\` must be a ready library image (409 \\`conflict\\` otherwise); null clears it. A file it does not already use must be uploaded for it, or the caller needs \\`media:read\\` (403 otherwise). 409 \\`canceled\\` when the event is canceled.`,\n params: EventIdParam,\n body: UpdateEvent,\n response: { 200: Event, ...refusals(400, 403, 404, 409) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return content.updateEvent(req.params.eventId, req.body, actor);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/events/:eventId/cancel',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'cancelEvent',\n tags: ['Events'],\n summary: 'Cancel an event',\n description: `Requires \\`events:write\\`. Canceling is final. 409 \\`canceled\\` when it is already canceled.`,\n params: EventIdParam,\n response: { 200: Event, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return content.cancelEvent(req.params.eventId, actor);\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/events/:eventId',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'deleteEvent',\n tags: ['Events'],\n summary: 'Delete an event',\n description: `Requires \\`events:delete\\`. Its attendees lose it; its library files survive.`,\n params: EventIdParam,\n response: { 204: z.void(), ...refusals(403, 404) },\n },\n handler: async (req, reply) => {\n const actor = await resolveActor(container, req);\n await content.deleteEvent(req.params.eventId, actor);\n return reply.code(204).send();\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/events/:eventId/assets',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listEventAssets',\n tags: ['Events'],\n summary: \"List an event's files in order\",\n description: 'Requires `events:read`.',\n params: EventIdParam,\n response: { 200: z.array(EventFile), ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return content.listEventAssets(req.params.eventId, actor);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/events/:eventId/assets',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'addEventAsset',\n tags: ['Events'],\n summary: 'Attach a file to an event',\n description: `Requires \\`events:write\\`. Added last. 409 when the file is not a ready library file or is already attached. A file not uploaded for the event needs \\`media:read\\` (403 otherwise). Returns the event's files.`,\n params: EventIdParam,\n body: AddEventAsset,\n response: { 200: z.array(EventFile), ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return content.addEventAsset(req.params.eventId, req.body, actor);\n },\n });\n\n r.route({\n method: 'PUT',\n url: '/api/events/:eventId/assets/order',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'reorderEventAssets',\n tags: ['Events'],\n summary: \"Reorder an event's files\",\n description: `Requires \\`events:write\\`. Send every attached file id exactly once (409 otherwise). Returns the event's files.`,\n params: EventIdParam,\n body: ReorderEventAssets,\n response: { 200: z.array(EventFile), ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return content.reorderEventAssets(req.params.eventId, req.body.assetIds, actor);\n },\n });\n\n r.route({\n method: 'PATCH',\n url: '/api/events/:eventId/assets/:assetId',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'renameEventAsset',\n tags: ['Events'],\n summary: \"Set a file's display name within an event\",\n description: `Requires \\`events:write\\`. Null falls back to the filename. Returns the event's files.`,\n params: EventAssetParams,\n body: RenameEventAsset,\n response: { 200: z.array(EventFile), ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return content.renameEventAsset(\n req.params.eventId,\n req.params.assetId,\n req.body.displayName,\n actor,\n );\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/events/:eventId/assets/:assetId',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'removeEventAsset',\n tags: ['Events'],\n summary: 'Remove a file from an event',\n description: `Requires \\`events:write\\`. The library file itself survives. Returns the event's files.`,\n params: EventAssetParams,\n response: { 200: z.array(EventFile), ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return content.removeEventAsset(req.params.eventId, req.params.assetId, actor);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/events/:eventId/attendees',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'addEventAttendee',\n tags: ['Events'],\n summary: 'Add an attendee to an event',\n description:\n 'Requires `access:write`, or `events:write` on the event or the org. Grants the event to the person, with source `manual`. Adding someone who already attends changes nothing. 404 when the event or the person does not exist; 409 `not_published`, `ended` or `full`.',\n params: EventIdParam,\n body: AddEventAttendee,\n response: { 204: z.void(), ...refusals(400, 403, 404, 409) },\n },\n handler: async (req, reply) => {\n const actor = await resolveActor(container, req);\n await container.entitlements.grant(\n {\n orgUserId: req.body.orgUserId,\n contentId: req.params.eventId,\n contentType: 'event',\n expiresAt: null,\n },\n actor,\n );\n return reply.code(204).send();\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/events/:eventId/attendees/:orgUserId',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'removeEventAttendee',\n tags: ['Events'],\n summary: 'Remove an attendee from an event',\n description:\n \"Requires `access:write`, or `events:write` on the event or the org. Revokes the person's direct grant to the event. 404 when they have no direct active grant; someone who attends only through a bundle has none.\",\n params: EventAttendeeParams,\n response: { 204: z.void(), ...refusals(403, 404) },\n },\n handler: async (req, reply) => {\n const actor = await resolveActor(container, req);\n await container.entitlements.revoke(\n { orgUserId: req.params.orgUserId, contentId: req.params.eventId, contentType: 'event' },\n actor,\n );\n return reply.code(204).send();\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/events/:eventId/attendees',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listEventAttendees',\n tags: ['Events'],\n summary: \"List an event's attendees\",\n description: `Requires \\`events:read\\`. On the event or the org. The people attending, directly or through a bundle, by grant date. \\`source\\` is \\`bundle\\` for someone who attends only through a bundle.`,\n params: EventIdParam,\n querystring: PageQuery,\n response: { 200: EventAttendeesPage, ...refusals(400, 403, 404) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return container.reporting.admin.eventAttendees(req.params.eventId, req.query, actor);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/events/:eventId/analytics',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getEventAnalytics',\n tags: ['Events'],\n summary: \"An event's attendance figures\",\n description: `Requires \\`events:read\\`. On the event or the org. The attendees now and by source, the capacity, the revoked direct grants, and the current direct grants by grant date before the start.`,\n params: EventIdParam,\n response: { 200: EventAnalytics, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return container.reporting.admin.eventAnalytics(req.params.eventId, actor);\n },\n });\n}\n"],"mappings":"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"}
@@ -1,77 +1,61 @@
1
1
  import { refusals } from "../schemas/shared.js";
2
- import { resolveScope } from "../scope.js";
2
+ import { resolveActor } from "../scope.js";
3
3
  import { ActionInvocationResult, AvailableIntegrationsList, ConfigureRequest, ConnectRequest, Connection, ConnectionActionParams, ConnectionIdParam, ConnectionsList, InvokeActionRequest, ReconnectRequest } from "../schemas/integrations.js";
4
4
  import "../schemas/index.js";
5
5
  import { z } from "zod";
6
- import { NotFoundError } from "@headless-lms/core/shared/errors";
7
6
  //#region src/http/routes/integrations.ts
8
7
  function toApi(connection) {
9
8
  const { credentialRef: _credentialRef, ...rest } = connection;
10
9
  return rest;
11
10
  }
12
- /** Resolve the session's active org to the domain org id, or 400 and return null. */
13
- async function resolveOrgId(req, reply) {
14
- if (!req.orgId) {
15
- await reply.code(400).send({
16
- error: "no_active_org",
17
- message: "No active organization"
18
- });
19
- return null;
20
- }
21
- return req.orgId;
22
- }
23
11
  async function integrationsRoutes(app, container) {
24
12
  const r = app.withTypeProvider();
25
13
  const integrations = container.integrations;
26
14
  const tags = ["Integrations"];
27
- const staff = async (req) => {
28
- await resolveScope(container, req);
29
- };
30
15
  r.route({
31
16
  method: "GET",
32
17
  url: "/api/integrations/available",
33
- preHandler: staff,
34
18
  schema: {
35
19
  operationId: "listAvailableIntegrations",
36
20
  tags,
37
- summary: "List the integrations this deployment supports (staff)",
38
- description: "Staff only (owner, admin, instructor). Each with the JSON Schemas of its config, its secrets and its actions.",
21
+ summary: "List the integrations this deployment supports",
22
+ description: `Requires \`integrations:read\`. Each with the JSON Schemas of its config, its secrets and its actions.`,
39
23
  response: {
40
24
  200: AvailableIntegrationsList,
41
25
  ...refusals(403)
42
26
  }
43
27
  },
44
- handler: async () => integrations.available()
28
+ handler: async (req) => {
29
+ const actor = await resolveActor(container, req);
30
+ return integrations.available(actor);
31
+ }
45
32
  });
46
33
  r.route({
47
34
  method: "GET",
48
35
  url: "/api/integrations",
49
- preHandler: staff,
50
36
  schema: {
51
37
  operationId: "listConnections",
52
38
  tags,
53
- summary: "List the organization's integration connections (staff)",
54
- description: "Staff only (owner, admin, instructor). Secrets are never returned.",
39
+ summary: "List the organization's integration connections",
40
+ description: `Requires \`integrations:read\`. Secrets are never returned.`,
55
41
  response: {
56
42
  200: ConnectionsList,
57
43
  ...refusals(403)
58
44
  }
59
45
  },
60
- handler: async (req, reply) => {
61
- const orgId = await resolveOrgId(req, reply);
62
- if (!orgId) return;
63
- return (await integrations.list(orgId)).map(toApi);
46
+ handler: async (req) => {
47
+ const actor = await resolveActor(container, req);
48
+ return (await integrations.list(actor)).map(toApi);
64
49
  }
65
50
  });
66
51
  r.route({
67
52
  method: "POST",
68
53
  url: "/api/integrations",
69
- preHandler: staff,
70
54
  schema: {
71
55
  operationId: "connectIntegration",
72
56
  tags,
73
- summary: "Connect an integration (staff)",
74
- description: "Staff only (owner, admin, instructor). Stores the secrets encrypted; they are never returned. 400 when the integration is unknown or the config is invalid; 409 when the organization already has a connection to it.",
57
+ summary: "Connect an integration",
58
+ description: `Requires \`integrations:write\`. Stores the secrets encrypted; they are never returned. 400 when the integration is unknown or the config is invalid; 409 when the organization already has a connection to it.`,
75
59
  body: ConnectRequest,
76
60
  response: {
77
61
  201: Connection,
@@ -79,44 +63,38 @@ async function integrationsRoutes(app, container) {
79
63
  }
80
64
  },
81
65
  handler: async (req, reply) => {
82
- const orgId = await resolveOrgId(req, reply);
83
- if (!orgId) return;
84
- const connection = await integrations.connect(orgId, req.body);
66
+ const actor = await resolveActor(container, req);
67
+ const connection = await integrations.connect(req.body, actor);
85
68
  return reply.code(201).send(toApi(connection));
86
69
  }
87
70
  });
88
71
  r.route({
89
72
  method: "GET",
90
73
  url: "/api/integrations/:id",
91
- preHandler: staff,
92
74
  schema: {
93
75
  operationId: "getConnection",
94
76
  tags,
95
- summary: "Get a connection (staff)",
96
- description: "Staff only (owner, admin, instructor). Secrets are never returned.",
77
+ summary: "Get a connection",
78
+ description: `Requires \`integrations:read\`. Secrets are never returned.`,
97
79
  params: ConnectionIdParam,
98
80
  response: {
99
81
  200: Connection,
100
82
  ...refusals(403, 404)
101
83
  }
102
84
  },
103
- handler: async (req, reply) => {
104
- const orgId = await resolveOrgId(req, reply);
105
- if (!orgId) return;
106
- const connection = await integrations.get(orgId, req.params.id);
107
- if (!connection) throw new NotFoundError("Connection", req.params.id);
108
- return toApi(connection);
85
+ handler: async (req) => {
86
+ const actor = await resolveActor(container, req);
87
+ return toApi(await integrations.get(req.params.id, actor));
109
88
  }
110
89
  });
111
90
  r.route({
112
91
  method: "PATCH",
113
92
  url: "/api/integrations/:id",
114
- preHandler: staff,
115
93
  schema: {
116
94
  operationId: "configureConnection",
117
95
  tags,
118
- summary: "Change a connection's configuration or active flag (staff)",
119
- description: "Staff only (owner, admin, instructor). 400 when the config is invalid for the integration.",
96
+ summary: "Change a connection's configuration or active flag",
97
+ description: `Requires \`integrations:write\`. 400 when the config is invalid for the integration.`,
120
98
  params: ConnectionIdParam,
121
99
  body: ConfigureRequest,
122
100
  response: {
@@ -124,23 +102,19 @@ async function integrationsRoutes(app, container) {
124
102
  ...refusals(400, 403, 404)
125
103
  }
126
104
  },
127
- handler: async (req, reply) => {
128
- const orgId = await resolveOrgId(req, reply);
129
- if (!orgId) return;
130
- const connection = await integrations.configure(orgId, req.params.id, req.body);
131
- if (!connection) throw new NotFoundError("Connection", req.params.id);
132
- return toApi(connection);
105
+ handler: async (req) => {
106
+ const actor = await resolveActor(container, req);
107
+ return toApi(await integrations.configure(req.params.id, req.body, actor));
133
108
  }
134
109
  });
135
110
  r.route({
136
111
  method: "POST",
137
112
  url: "/api/integrations/:id/reconnect",
138
- preHandler: staff,
139
113
  schema: {
140
114
  operationId: "reconnectIntegration",
141
115
  tags,
142
- summary: "Replace a connection's secrets (staff)",
143
- description: "Staff only (owner, admin, instructor). The new secrets replace the stored ones.",
116
+ summary: "Replace a connection's secrets",
117
+ description: `Requires \`integrations:write\`. The new secrets replace the stored ones.`,
144
118
  params: ConnectionIdParam,
145
119
  body: ReconnectRequest,
146
120
  response: {
@@ -148,23 +122,19 @@ async function integrationsRoutes(app, container) {
148
122
  ...refusals(403, 404)
149
123
  }
150
124
  },
151
- handler: async (req, reply) => {
152
- const orgId = await resolveOrgId(req, reply);
153
- if (!orgId) return;
154
- const connection = await integrations.reconnect(orgId, req.params.id, req.body.secrets);
155
- if (!connection) throw new NotFoundError("Connection", req.params.id);
156
- return toApi(connection);
125
+ handler: async (req) => {
126
+ const actor = await resolveActor(container, req);
127
+ return toApi(await integrations.reconnect(req.params.id, req.body.secrets, actor));
157
128
  }
158
129
  });
159
130
  r.route({
160
131
  method: "POST",
161
132
  url: "/api/integrations/:id/actions/:actionId",
162
- preHandler: staff,
163
133
  schema: {
164
134
  operationId: "invokeConnectionAction",
165
135
  tags,
166
- summary: "Invoke one of a connection's actions (staff)",
167
- description: "Staff only (owner, admin, instructor). Runs the action, such as a listing that feeds a picker, with `input` checked against its input schema. 400 when the action fails or rejects the input; 404 when the connection or action does not exist; 409 when the connection is inactive.",
136
+ summary: "Invoke one of a connection's actions",
137
+ description: `Requires \`integrations:write\`. Runs the action, such as a listing that feeds a picker, with \`input\` checked against its input schema. 400 when the action fails or rejects the input; 404 when the connection or action does not exist; 409 when the connection is inactive.`,
168
138
  params: ConnectionActionParams,
169
139
  body: InvokeActionRequest,
170
140
  response: {
@@ -172,21 +142,19 @@ async function integrationsRoutes(app, container) {
172
142
  ...refusals(400, 403, 404, 409)
173
143
  }
174
144
  },
175
- handler: async (req, reply) => {
176
- const orgId = await resolveOrgId(req, reply);
177
- if (!orgId) return;
178
- return { output: await integrations.invoke(orgId, req.params.id, req.params.actionId, req.body.input ?? {}) };
145
+ handler: async (req) => {
146
+ const actor = await resolveActor(container, req);
147
+ return { output: await integrations.invoke(req.params.id, req.params.actionId, req.body.input ?? {}, actor) };
179
148
  }
180
149
  });
181
150
  r.route({
182
151
  method: "DELETE",
183
152
  url: "/api/integrations/:id",
184
- preHandler: staff,
185
153
  schema: {
186
154
  operationId: "disconnectIntegration",
187
155
  tags,
188
- summary: "Disconnect an integration (staff)",
189
- description: "Staff only (owner, admin, instructor). Destroys its stored secrets.",
156
+ summary: "Disconnect an integration",
157
+ description: `Requires \`integrations:delete\`. Destroys its stored secrets.`,
190
158
  params: ConnectionIdParam,
191
159
  response: {
192
160
  204: z.void(),
@@ -194,9 +162,8 @@ async function integrationsRoutes(app, container) {
194
162
  }
195
163
  },
196
164
  handler: async (req, reply) => {
197
- const orgId = await resolveOrgId(req, reply);
198
- if (!orgId) return;
199
- if (!await integrations.disconnect(orgId, req.params.id)) throw new NotFoundError("Connection", req.params.id);
165
+ const actor = await resolveActor(container, req);
166
+ await integrations.disconnect(req.params.id, actor);
200
167
  return reply.code(204).send();
201
168
  }
202
169
  });
@@ -1 +1 @@
1
- {"version":3,"file":"integrations.js","names":[],"sources":["../../../src/http/routes/integrations.ts"],"sourcesContent":["// HTTP routes for integrations (an org's connections to external services).\n// All require a session with an active organization. Credentials are\n// write-only: accepted on connect/reconnect, never present in a response —\n// `toApi` strips the credentialRef so not even the reference leaks.\nimport type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport { z } from 'zod';\nimport {\n ActionInvocationResult,\n AvailableIntegrationsList,\n ConfigureRequest,\n ConnectRequest,\n Connection,\n ConnectionActionParams,\n ConnectionIdParam,\n ConnectionsList,\n InvokeActionRequest,\n ReconnectRequest,\n refusals,\n} from '../schemas/index.js';\nimport type { Connection as DomainConnection } from '@headless-lms/core/integrations';\nimport { NotFoundError } from '@headless-lms/core/shared/errors';\nimport type { Container } from '../../app/container.js';\nimport { resolveScope } from '../scope.js';\n\nfunction toApi(connection: DomainConnection): Connection {\n const { credentialRef: _credentialRef, ...rest } = connection;\n return rest;\n}\n\n/** Resolve the session's active org to the domain org id, or 400 and return null. */\nasync function resolveOrgId(req: FastifyRequest, reply: FastifyReply): Promise<string | null> {\n if (!req.orgId) {\n await reply.code(400).send({ error: 'no_active_org', message: 'No active organization' });\n return null;\n }\n return req.orgId;\n}\n\nexport async function integrationsRoutes(\n app: FastifyInstance,\n container: Container,\n): Promise<void> {\n const r = app.withTypeProvider<ZodTypeProvider>();\n const integrations = container.integrations;\n const tags = ['Integrations'];\n const staff = async (req: FastifyRequest) => {\n await resolveScope(container, req);\n };\n\n r.route({\n method: 'GET',\n url: '/api/integrations/available',\n preHandler: staff,\n schema: {\n operationId: 'listAvailableIntegrations',\n tags,\n summary: 'List the integrations this deployment supports (staff)',\n description:\n 'Staff only (owner, admin, instructor). Each with the JSON Schemas of its config, its secrets and its actions.',\n response: { 200: AvailableIntegrationsList, ...refusals(403) },\n },\n handler: async () => integrations.available(),\n });\n\n r.route({\n method: 'GET',\n url: '/api/integrations',\n preHandler: staff,\n schema: {\n operationId: 'listConnections',\n tags,\n summary: \"List the organization's integration connections (staff)\",\n description: 'Staff only (owner, admin, instructor). Secrets are never returned.',\n response: { 200: ConnectionsList, ...refusals(403) },\n },\n handler: async (req, reply) => {\n const orgId = await resolveOrgId(req, reply);\n if (!orgId) {\n return;\n }\n return (await integrations.list(orgId)).map(toApi);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/integrations',\n preHandler: staff,\n schema: {\n operationId: 'connectIntegration',\n tags,\n summary: 'Connect an integration (staff)',\n description:\n 'Staff only (owner, admin, instructor). Stores the secrets encrypted; they are never returned. 400 when the integration is unknown or the config is invalid; 409 when the organization already has a connection to it.',\n body: ConnectRequest,\n response: { 201: Connection, ...refusals(400, 403, 409) },\n },\n handler: async (req, reply) => {\n const orgId = await resolveOrgId(req, reply);\n if (!orgId) {\n return;\n }\n const connection = await integrations.connect(orgId, req.body);\n return reply.code(201).send(toApi(connection));\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/integrations/:id',\n preHandler: staff,\n schema: {\n operationId: 'getConnection',\n tags,\n summary: 'Get a connection (staff)',\n description: 'Staff only (owner, admin, instructor). Secrets are never returned.',\n params: ConnectionIdParam,\n response: { 200: Connection, ...refusals(403, 404) },\n },\n handler: async (req, reply) => {\n const orgId = await resolveOrgId(req, reply);\n if (!orgId) {\n return;\n }\n const connection = await integrations.get(orgId, req.params.id);\n if (!connection) {\n throw new NotFoundError('Connection', req.params.id);\n }\n return toApi(connection);\n },\n });\n\n r.route({\n method: 'PATCH',\n url: '/api/integrations/:id',\n preHandler: staff,\n schema: {\n operationId: 'configureConnection',\n tags,\n summary: \"Change a connection's configuration or active flag (staff)\",\n description:\n 'Staff only (owner, admin, instructor). 400 when the config is invalid for the integration.',\n params: ConnectionIdParam,\n body: ConfigureRequest,\n response: { 200: Connection, ...refusals(400, 403, 404) },\n },\n handler: async (req, reply) => {\n const orgId = await resolveOrgId(req, reply);\n if (!orgId) {\n return;\n }\n const connection = await integrations.configure(orgId, req.params.id, req.body);\n if (!connection) {\n throw new NotFoundError('Connection', req.params.id);\n }\n return toApi(connection);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/integrations/:id/reconnect',\n preHandler: staff,\n schema: {\n operationId: 'reconnectIntegration',\n tags,\n summary: \"Replace a connection's secrets (staff)\",\n description:\n 'Staff only (owner, admin, instructor). The new secrets replace the stored ones.',\n params: ConnectionIdParam,\n body: ReconnectRequest,\n response: { 200: Connection, ...refusals(403, 404) },\n },\n handler: async (req, reply) => {\n const orgId = await resolveOrgId(req, reply);\n if (!orgId) {\n return;\n }\n const connection = await integrations.reconnect(orgId, req.params.id, req.body.secrets);\n if (!connection) {\n throw new NotFoundError('Connection', req.params.id);\n }\n return toApi(connection);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/integrations/:id/actions/:actionId',\n preHandler: staff,\n schema: {\n operationId: 'invokeConnectionAction',\n tags,\n summary: \"Invoke one of a connection's actions (staff)\",\n description:\n 'Staff only (owner, admin, instructor). Runs the action, such as a listing that feeds a picker, with `input` checked against its input schema. 400 when the action fails or rejects the input; 404 when the connection or action does not exist; 409 when the connection is inactive.',\n params: ConnectionActionParams,\n body: InvokeActionRequest,\n response: { 200: ActionInvocationResult, ...refusals(400, 403, 404, 409) },\n },\n handler: async (req, reply) => {\n const orgId = await resolveOrgId(req, reply);\n if (!orgId) {\n return;\n }\n const output = await integrations.invoke(\n orgId,\n req.params.id,\n req.params.actionId,\n req.body.input ?? {},\n );\n return { output: output as Record<string, unknown> };\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/integrations/:id',\n preHandler: staff,\n schema: {\n operationId: 'disconnectIntegration',\n tags,\n summary: 'Disconnect an integration (staff)',\n description: 'Staff only (owner, admin, instructor). Destroys its stored secrets.',\n params: ConnectionIdParam,\n response: { 204: z.void(), ...refusals(403, 404) },\n },\n handler: async (req, reply) => {\n const orgId = await resolveOrgId(req, reply);\n if (!orgId) {\n return;\n }\n const removed = await integrations.disconnect(orgId, req.params.id);\n if (!removed) {\n throw new NotFoundError('Connection', req.params.id);\n }\n return reply.code(204).send();\n },\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"integrations.js","names":[],"sources":["../../../src/http/routes/integrations.ts"],"sourcesContent":["// HTTP routes for integrations (an org's connections to external services).\n// Credentials are write-only: accepted on connect/reconnect, never present in a\n// response — `toApi` strips the credentialRef so not even the reference leaks.\nimport type { FastifyInstance } from 'fastify';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport { z } from 'zod';\nimport {\n ActionInvocationResult,\n AvailableIntegrationsList,\n ConfigureRequest,\n ConnectRequest,\n Connection,\n ConnectionActionParams,\n ConnectionIdParam,\n ConnectionsList,\n InvokeActionRequest,\n ReconnectRequest,\n refusals,\n} from '../schemas/index.js';\nimport type { Connection as DomainConnection } from '@headless-lms/core/integrations';\nimport type { Container } from '../../app/container.js';\nimport { resolveActor } from '../scope.js';\n\nfunction toApi(connection: DomainConnection): Connection {\n const { credentialRef: _credentialRef, ...rest } = connection;\n return rest;\n}\n\nexport async function integrationsRoutes(\n app: FastifyInstance,\n container: Container,\n): Promise<void> {\n const r = app.withTypeProvider<ZodTypeProvider>();\n const integrations = container.integrations;\n const tags = ['Integrations'];\n\n r.route({\n method: 'GET',\n url: '/api/integrations/available',\n schema: {\n operationId: 'listAvailableIntegrations',\n tags,\n summary: 'List the integrations this deployment supports',\n description: `Requires \\`integrations:read\\`. Each with the JSON Schemas of its config, its secrets and its actions.`,\n response: { 200: AvailableIntegrationsList, ...refusals(403) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return integrations.available(actor);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/integrations',\n schema: {\n operationId: 'listConnections',\n tags,\n summary: \"List the organization's integration connections\",\n description: `Requires \\`integrations:read\\`. Secrets are never returned.`,\n response: { 200: ConnectionsList, ...refusals(403) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return (await integrations.list(actor)).map(toApi);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/integrations',\n schema: {\n operationId: 'connectIntegration',\n tags,\n summary: 'Connect an integration',\n description: `Requires \\`integrations:write\\`. Stores the secrets encrypted; they are never returned. 400 when the integration is unknown or the config is invalid; 409 when the organization already has a connection to it.`,\n body: ConnectRequest,\n response: { 201: Connection, ...refusals(400, 403, 409) },\n },\n handler: async (req, reply) => {\n const actor = await resolveActor(container, req);\n const connection = await integrations.connect(req.body, actor);\n return reply.code(201).send(toApi(connection));\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/integrations/:id',\n schema: {\n operationId: 'getConnection',\n tags,\n summary: 'Get a connection',\n description: `Requires \\`integrations:read\\`. Secrets are never returned.`,\n params: ConnectionIdParam,\n response: { 200: Connection, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return toApi(await integrations.get(req.params.id, actor));\n },\n });\n\n r.route({\n method: 'PATCH',\n url: '/api/integrations/:id',\n schema: {\n operationId: 'configureConnection',\n tags,\n summary: \"Change a connection's configuration or active flag\",\n description: `Requires \\`integrations:write\\`. 400 when the config is invalid for the integration.`,\n params: ConnectionIdParam,\n body: ConfigureRequest,\n response: { 200: Connection, ...refusals(400, 403, 404) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return toApi(await integrations.configure(req.params.id, req.body, actor));\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/integrations/:id/reconnect',\n schema: {\n operationId: 'reconnectIntegration',\n tags,\n summary: \"Replace a connection's secrets\",\n description: `Requires \\`integrations:write\\`. The new secrets replace the stored ones.`,\n params: ConnectionIdParam,\n body: ReconnectRequest,\n response: { 200: Connection, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return toApi(await integrations.reconnect(req.params.id, req.body.secrets, actor));\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/integrations/:id/actions/:actionId',\n schema: {\n operationId: 'invokeConnectionAction',\n tags,\n summary: \"Invoke one of a connection's actions\",\n description: `Requires \\`integrations:write\\`. Runs the action, such as a listing that feeds a picker, with \\`input\\` checked against its input schema. 400 when the action fails or rejects the input; 404 when the connection or action does not exist; 409 when the connection is inactive.`,\n params: ConnectionActionParams,\n body: InvokeActionRequest,\n response: { 200: ActionInvocationResult, ...refusals(400, 403, 404, 409) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n const output = await integrations.invoke(\n req.params.id,\n req.params.actionId,\n req.body.input ?? {},\n actor,\n );\n return { output: output as Record<string, unknown> };\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/integrations/:id',\n schema: {\n operationId: 'disconnectIntegration',\n tags,\n summary: 'Disconnect an integration',\n description: `Requires \\`integrations:delete\\`. Destroys its stored secrets.`,\n params: ConnectionIdParam,\n response: { 204: z.void(), ...refusals(403, 404) },\n },\n handler: async (req, reply) => {\n const actor = await resolveActor(container, req);\n await integrations.disconnect(req.params.id, actor);\n return reply.code(204).send();\n },\n });\n}\n"],"mappings":"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"}