@headless-lms/server 0.3.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 (228) hide show
  1. package/README.md +14 -0
  2. package/dist/app/community-mail.d.ts +10 -10
  3. package/dist/app/community-mail.js +72 -28
  4. package/dist/app/community-mail.js.map +1 -1
  5. package/dist/app/container.d.ts +48 -10
  6. package/dist/app/container.js +268 -48
  7. package/dist/app/container.js.map +1 -1
  8. package/dist/app/notification-channels.d.ts +47 -0
  9. package/dist/app/notification-channels.js +175 -0
  10. package/dist/app/notification-channels.js.map +1 -0
  11. package/dist/app/notification-reads.d.ts +23 -0
  12. package/dist/app/notification-reads.js +74 -0
  13. package/dist/app/notification-reads.js.map +1 -0
  14. package/dist/app/platform.d.ts +14 -0
  15. package/dist/app/platform.js +14 -0
  16. package/dist/app/platform.js.map +1 -0
  17. package/dist/http/config.d.ts +4 -0
  18. package/dist/http/errors.d.ts +1 -0
  19. package/dist/http/errors.js +24 -4
  20. package/dist/http/errors.js.map +1 -1
  21. package/dist/http/fastify.d.ts +8 -0
  22. package/dist/http/mcp/catalogue.d.ts +11 -9
  23. package/dist/http/mcp/catalogue.js +15 -21
  24. package/dist/http/mcp/catalogue.js.map +1 -1
  25. package/dist/http/mcp/challenge.js +3 -3
  26. package/dist/http/mcp/challenge.js.map +1 -1
  27. package/dist/http/mcp/route.js +7 -7
  28. package/dist/http/mcp/route.js.map +1 -1
  29. package/dist/http/mcp/test-support.d.ts +6 -5
  30. package/dist/http/mcp/test-support.js +32 -11
  31. package/dist/http/mcp/test-support.js.map +1 -1
  32. package/dist/http/mcp/tool.d.ts +3 -9
  33. package/dist/http/mcp/tool.js +14 -6
  34. package/dist/http/mcp/tool.js.map +1 -1
  35. package/dist/http/mcp/tools/automations.js +10 -20
  36. package/dist/http/mcp/tools/automations.js.map +1 -1
  37. package/dist/http/mcp/tools/catalog.js +25 -42
  38. package/dist/http/mcp/tools/catalog.js.map +1 -1
  39. package/dist/http/mcp/tools/comments.js +9 -19
  40. package/dist/http/mcp/tools/comments.js.map +1 -1
  41. package/dist/http/mcp/tools/communities.js +16 -31
  42. package/dist/http/mcp/tools/communities.js.map +1 -1
  43. package/dist/http/mcp/tools/community-members.js +9 -16
  44. package/dist/http/mcp/tools/community-members.js.map +1 -1
  45. package/dist/http/mcp/tools/community-moderation.js +13 -25
  46. package/dist/http/mcp/tools/community-moderation.js.map +1 -1
  47. package/dist/http/mcp/tools/courses.js +11 -21
  48. package/dist/http/mcp/tools/courses.js.map +1 -1
  49. package/dist/http/mcp/tools/events.d.ts +6 -0
  50. package/dist/http/mcp/tools/events.js +170 -0
  51. package/dist/http/mcp/tools/events.js.map +1 -0
  52. package/dist/http/mcp/tools/index.d.ts +5 -1
  53. package/dist/http/mcp/tools/index.js +45 -10
  54. package/dist/http/mcp/tools/index.js.map +1 -1
  55. package/dist/http/mcp/tools/organization.d.ts +6 -0
  56. package/dist/http/mcp/tools/organization.js +214 -0
  57. package/dist/http/mcp/tools/organization.js.map +1 -0
  58. package/dist/http/mcp/tools/people.js +30 -49
  59. package/dist/http/mcp/tools/people.js.map +1 -1
  60. package/dist/http/plugins/auth.js +37 -8
  61. package/dist/http/plugins/auth.js.map +1 -1
  62. package/dist/http/plugins/error-handler.d.ts +5 -1
  63. package/dist/http/plugins/error-handler.js +13 -3
  64. package/dist/http/plugins/error-handler.js.map +1 -1
  65. package/dist/http/plugins/openapi.js +18 -2
  66. package/dist/http/plugins/openapi.js.map +1 -1
  67. package/dist/http/plugins/platform.d.ts +7 -0
  68. package/dist/http/plugins/platform.js +13 -0
  69. package/dist/http/plugins/platform.js.map +1 -0
  70. package/dist/http/routes/access.js +54 -36
  71. package/dist/http/routes/access.js.map +1 -1
  72. package/dist/http/routes/activities.js +45 -45
  73. package/dist/http/routes/activities.js.map +1 -1
  74. package/dist/http/routes/assets.js +30 -65
  75. package/dist/http/routes/assets.js.map +1 -1
  76. package/dist/http/routes/automations.js +37 -42
  77. package/dist/http/routes/automations.js.map +1 -1
  78. package/dist/http/routes/bundles.js +33 -36
  79. package/dist/http/routes/bundles.js.map +1 -1
  80. package/dist/http/routes/communities.js +144 -136
  81. package/dist/http/routes/communities.js.map +1 -1
  82. package/dist/http/routes/community-moderation.js +83 -137
  83. package/dist/http/routes/community-moderation.js.map +1 -1
  84. package/dist/http/routes/course-analytics.js +32 -15
  85. package/dist/http/routes/course-analytics.js.map +1 -1
  86. package/dist/http/routes/courses.js +25 -32
  87. package/dist/http/routes/courses.js.map +1 -1
  88. package/dist/http/routes/dashboard.js +9 -9
  89. package/dist/http/routes/dashboard.js.map +1 -1
  90. package/dist/http/routes/discussion.js +21 -39
  91. package/dist/http/routes/discussion.js.map +1 -1
  92. package/dist/http/routes/downloads.js +41 -47
  93. package/dist/http/routes/downloads.js.map +1 -1
  94. package/dist/http/routes/entitlements.js +19 -23
  95. package/dist/http/routes/entitlements.js.map +1 -1
  96. package/dist/http/routes/events.d.ts +7 -0
  97. package/dist/http/routes/events.js +333 -0
  98. package/dist/http/routes/events.js.map +1 -0
  99. package/dist/http/routes/integrations.js +40 -73
  100. package/dist/http/routes/integrations.js.map +1 -1
  101. package/dist/http/routes/member/assets.js +10 -13
  102. package/dist/http/routes/member/assets.js.map +1 -1
  103. package/dist/http/routes/member/chats.js +177 -75
  104. package/dist/http/routes/member/chats.js.map +1 -1
  105. package/dist/http/routes/member/comments.js +65 -26
  106. package/dist/http/routes/member/comments.js.map +1 -1
  107. package/dist/http/routes/member/community.js +68 -51
  108. package/dist/http/routes/member/community.js.map +1 -1
  109. package/dist/http/routes/member/courses.js +12 -19
  110. package/dist/http/routes/member/courses.js.map +1 -1
  111. package/dist/http/routes/member/devices.d.ts +7 -0
  112. package/dist/http/routes/member/devices.js +97 -0
  113. package/dist/http/routes/member/devices.js.map +1 -0
  114. package/dist/http/routes/member/downloads.js +6 -9
  115. package/dist/http/routes/member/downloads.js.map +1 -1
  116. package/dist/http/routes/member/events.d.ts +7 -0
  117. package/dist/http/routes/member/events.js +386 -0
  118. package/dist/http/routes/member/events.js.map +1 -0
  119. package/dist/http/routes/member/files.js +71 -46
  120. package/dist/http/routes/member/files.js.map +1 -1
  121. package/dist/http/routes/member/index.js +6 -0
  122. package/dist/http/routes/member/index.js.map +1 -1
  123. package/dist/http/routes/member/notification-settings.d.ts +7 -0
  124. package/dist/http/routes/member/notification-settings.js +100 -0
  125. package/dist/http/routes/member/notification-settings.js.map +1 -0
  126. package/dist/http/routes/member/notifications.js +69 -45
  127. package/dist/http/routes/member/notifications.js.map +1 -1
  128. package/dist/http/routes/member/org.js +5 -15
  129. package/dist/http/routes/member/org.js.map +1 -1
  130. package/dist/http/routes/member/posts.js +94 -95
  131. package/dist/http/routes/member/posts.js.map +1 -1
  132. package/dist/http/routes/member/profile.js +15 -6
  133. package/dist/http/routes/member/profile.js.map +1 -1
  134. package/dist/http/routes/member/progress.js +7 -11
  135. package/dist/http/routes/member/progress.js.map +1 -1
  136. package/dist/http/routes/organizations.js +415 -110
  137. package/dist/http/routes/organizations.js.map +1 -1
  138. package/dist/http/routes/platform.d.ts +7 -0
  139. package/dist/http/routes/platform.js +61 -0
  140. package/dist/http/routes/platform.js.map +1 -0
  141. package/dist/http/routes/students.js +12 -75
  142. package/dist/http/routes/students.js.map +1 -1
  143. package/dist/http/routes/webhooks.js +30 -39
  144. package/dist/http/routes/webhooks.js.map +1 -1
  145. package/dist/http/routes.js +9 -0
  146. package/dist/http/routes.js.map +1 -1
  147. package/dist/http/schemas/access.d.ts +346 -64
  148. package/dist/http/schemas/access.js +39 -12
  149. package/dist/http/schemas/access.js.map +1 -1
  150. package/dist/http/schemas/assets.d.ts +13 -8
  151. package/dist/http/schemas/assets.js +6 -5
  152. package/dist/http/schemas/assets.js.map +1 -1
  153. package/dist/http/schemas/automations.d.ts +4 -8
  154. package/dist/http/schemas/chats.d.ts +238 -36
  155. package/dist/http/schemas/chats.js +14 -7
  156. package/dist/http/schemas/chats.js.map +1 -1
  157. package/dist/http/schemas/community.d.ts +140 -33
  158. package/dist/http/schemas/community.js +25 -9
  159. package/dist/http/schemas/community.js.map +1 -1
  160. package/dist/http/schemas/course-analytics.d.ts +51 -1
  161. package/dist/http/schemas/course-analytics.js +19 -1
  162. package/dist/http/schemas/course-analytics.js.map +1 -1
  163. package/dist/http/schemas/dashboard.d.ts +1 -0
  164. package/dist/http/schemas/dashboard.js +2 -1
  165. package/dist/http/schemas/dashboard.js.map +1 -1
  166. package/dist/http/schemas/discussion.d.ts +45 -73
  167. package/dist/http/schemas/discussion.js +16 -4
  168. package/dist/http/schemas/discussion.js.map +1 -1
  169. package/dist/http/schemas/entitlements.d.ts +9 -12
  170. package/dist/http/schemas/entitlements.js +11 -5
  171. package/dist/http/schemas/entitlements.js.map +1 -1
  172. package/dist/http/schemas/events.d.ts +527 -0
  173. package/dist/http/schemas/events.js +64 -0
  174. package/dist/http/schemas/events.js.map +1 -0
  175. package/dist/http/schemas/files.d.ts +15 -5
  176. package/dist/http/schemas/files.js +10 -2
  177. package/dist/http/schemas/files.js.map +1 -1
  178. package/dist/http/schemas/index.d.ts +14 -12
  179. package/dist/http/schemas/index.js +14 -12
  180. package/dist/http/schemas/invites.d.ts +3 -47
  181. package/dist/http/schemas/invites.js +4 -25
  182. package/dist/http/schemas/invites.js.map +1 -1
  183. package/dist/http/schemas/member.d.ts +94 -7
  184. package/dist/http/schemas/member.js +7 -3
  185. package/dist/http/schemas/member.js.map +1 -1
  186. package/dist/http/schemas/members.d.ts +329 -65
  187. package/dist/http/schemas/members.js +67 -25
  188. package/dist/http/schemas/members.js.map +1 -1
  189. package/dist/http/schemas/notifications.d.ts +502 -175
  190. package/dist/http/schemas/notifications.js +148 -28
  191. package/dist/http/schemas/notifications.js.map +1 -1
  192. package/dist/http/schemas/platform.d.ts +37 -0
  193. package/dist/http/schemas/platform.js +19 -0
  194. package/dist/http/schemas/platform.js.map +1 -0
  195. package/dist/http/schemas/posts.d.ts +92 -38
  196. package/dist/http/schemas/posts.js +4 -4
  197. package/dist/http/schemas/posts.js.map +1 -1
  198. package/dist/http/schemas/profile.d.ts +1 -1
  199. package/dist/http/schemas/shared.d.ts +10 -13
  200. package/dist/http/schemas/shared.js +3 -11
  201. package/dist/http/schemas/shared.js.map +1 -1
  202. package/dist/http/schemas/students.d.ts +22 -0
  203. package/dist/http/schemas/students.js +3 -1
  204. package/dist/http/schemas/students.js.map +1 -1
  205. package/dist/http/scope.d.ts +9 -12
  206. package/dist/http/scope.js +30 -35
  207. package/dist/http/scope.js.map +1 -1
  208. package/dist/http/server.js +9 -0
  209. package/dist/http/server.js.map +1 -1
  210. package/dist/http/shutdown.d.ts +11 -0
  211. package/dist/http/shutdown.js +40 -0
  212. package/dist/http/shutdown.js.map +1 -0
  213. package/dist/http/sse.d.ts +19 -0
  214. package/dist/http/sse.js +104 -0
  215. package/dist/http/sse.js.map +1 -0
  216. package/dist/http/test-caller.d.ts +13 -0
  217. package/dist/http/test-caller.js +14 -0
  218. package/dist/http/test-caller.js.map +1 -0
  219. package/dist/http/well-known.js +2 -2
  220. package/dist/http/well-known.js.map +1 -1
  221. package/dist/index.d.ts +5 -2
  222. package/dist/index.js +4 -2
  223. package/dist/index.js.map +1 -1
  224. package/dist/packages/server/package.js +1 -1
  225. package/package.json +7 -6
  226. package/dist/http/routes/member/actor.d.ts +0 -14
  227. package/dist/http/routes/member/actor.js +0 -11
  228. package/dist/http/routes/member/actor.js.map +0 -1
@@ -1,10 +1,10 @@
1
1
  import { refusals } from "../schemas/shared.js";
2
- import { resolveScope } from "../scope.js";
3
- import { CommunitiesPage, CommunitiesQuery, Community, CommunityAnalytics, CommunityIdParam, CommunitySpaceParam, CreateCommunity, CreateSpace, OrderedIds, PatchDmSettings, SetSpaceTopics, SpaceDetail, SpaceLinkBody, SpaceLinkParam, UpdateCommunity, UpdateSpace } from "../schemas/community.js";
2
+ import { resolveActor } from "../scope.js";
3
+ import { CommunitiesPage, CommunitiesQuery, Community, CommunityAnalytics, CommunityIdParam, CommunitySpaceParam, CreateCommunity, CreateSpace, OrderedIds, PatchCommunitySettings, SetSpaceTopics, SpaceDetail, SpaceLinkBody, SpaceLinkParam, UpdateCommunity, UpdateSpace } from "../schemas/community.js";
4
4
  import { MessageQueue } from "../schemas/chats.js";
5
+ import { NotificationCategoryList, ReplaceNotificationCategories } from "../schemas/notifications.js";
5
6
  import "../schemas/index.js";
6
7
  import { z } from "zod";
7
- import { NotFoundError } from "@headless-lms/core/shared/errors";
8
8
  //#region src/http/routes/communities.ts
9
9
  async function communitiesRoutes(app, container) {
10
10
  const r = app.withTypeProvider();
@@ -16,8 +16,8 @@ async function communitiesRoutes(app, container) {
16
16
  schema: {
17
17
  operationId: "listCommunities",
18
18
  tags,
19
- summary: "List communities (staff)",
20
- description: "Staff only (owner, admin, instructor). Pages every community in the organization, drafts included, filtered by `status` and searched by title.",
19
+ summary: "List communities",
20
+ description: `Requires one of \`communities:read\`, \`communities:moderate\`. Pages every community in the organization, drafts included, filtered by \`status\` and searched by title.`,
21
21
  querystring: CommunitiesQuery,
22
22
  response: {
23
23
  200: CommunitiesPage,
@@ -25,8 +25,8 @@ async function communitiesRoutes(app, container) {
25
25
  }
26
26
  },
27
27
  handler: async (req) => {
28
- const scope = await resolveScope(container, req);
29
- return content.listCommunities(scope.orgId, req.query);
28
+ const actor = await resolveActor(container, req);
29
+ return content.listCommunities(req.query, actor);
30
30
  }
31
31
  });
32
32
  r.route({
@@ -35,8 +35,8 @@ async function communitiesRoutes(app, container) {
35
35
  schema: {
36
36
  operationId: "createCommunity",
37
37
  tags,
38
- summary: "Create a community (staff)",
39
- description: "Owner or admin only. The community starts as a draft with the default direct-message settings and no spaces. Members see it once it is published and they hold it.",
38
+ summary: "Create a community",
39
+ description: `Requires \`communities:write\`. The community starts as a draft with the default direct-message settings and no spaces. Members see it once it is published and they hold it.`,
40
40
  body: CreateCommunity,
41
41
  response: {
42
42
  201: Community,
@@ -44,11 +44,8 @@ async function communitiesRoutes(app, container) {
44
44
  }
45
45
  },
46
46
  handler: async (req, reply) => {
47
- const scope = await resolveScope(container, req);
48
- const community = await content.createCommunity(scope.orgId, req.body, {
49
- id: scope.orgUserId,
50
- role: scope.role
51
- });
47
+ const actor = await resolveActor(container, req);
48
+ const community = await content.createCommunity(req.body, actor);
52
49
  req.log.info({ communityId: community.id }, "community created");
53
50
  return reply.code(201).send(community);
54
51
  }
@@ -59,8 +56,8 @@ async function communitiesRoutes(app, container) {
59
56
  schema: {
60
57
  operationId: "getCommunity",
61
58
  tags,
62
- summary: "Get a community (staff)",
63
- description: "Staff only (owner, admin, instructor). Any community of the organization, drafts included.",
59
+ summary: "Get a community",
60
+ description: `Requires one of \`communities:read\`, \`communities:moderate\`. Any community of the organization, drafts included.`,
64
61
  params: CommunityIdParam,
65
62
  response: {
66
63
  200: Community,
@@ -68,10 +65,8 @@ async function communitiesRoutes(app, container) {
68
65
  }
69
66
  },
70
67
  handler: async (req) => {
71
- const scope = await resolveScope(container, req);
72
- const community = await content.getCommunity(scope.orgId, req.params.communityId);
73
- if (!community) throw new NotFoundError("Community", req.params.communityId);
74
- return community;
68
+ const actor = await resolveActor(container, req);
69
+ return content.getCommunity(req.params.communityId, actor);
75
70
  }
76
71
  });
77
72
  r.route({
@@ -80,8 +75,8 @@ async function communitiesRoutes(app, container) {
80
75
  schema: {
81
76
  operationId: "updateCommunity",
82
77
  tags,
83
- summary: "Update a community (staff)",
84
- description: "Owner or admin only. Changes only the fields sent. `thumbnailAssetId` must be a ready library image (409 otherwise); null clears it.",
78
+ summary: "Update a community",
79
+ description: `Requires \`communities:write\`. Changes only the fields sent. \`thumbnailAssetId\` must be a ready library image (409 otherwise); null clears it. A file it does not already use must be uploaded for it, or the caller needs \`media:read\` (403 otherwise).`,
85
80
  params: CommunityIdParam,
86
81
  body: UpdateCommunity,
87
82
  response: {
@@ -90,11 +85,8 @@ async function communitiesRoutes(app, container) {
90
85
  }
91
86
  },
92
87
  handler: async (req) => {
93
- const scope = await resolveScope(container, req);
94
- return content.updateCommunity(scope.orgId, req.params.communityId, req.body, {
95
- id: scope.orgUserId,
96
- role: scope.role
97
- });
88
+ const actor = await resolveActor(container, req);
89
+ return content.updateCommunity(req.params.communityId, req.body, actor);
98
90
  }
99
91
  });
100
92
  r.route({
@@ -103,8 +95,8 @@ async function communitiesRoutes(app, container) {
103
95
  schema: {
104
96
  operationId: "deleteCommunity",
105
97
  tags,
106
- summary: "Delete a community (staff)",
107
- description: "Owner or admin only. Links to the community from other spaces are removed with it.",
98
+ summary: "Delete a community",
99
+ description: `Requires \`communities:delete\`. Links to the community from other spaces are removed with it.`,
108
100
  params: CommunityIdParam,
109
101
  response: {
110
102
  204: z.void(),
@@ -112,11 +104,8 @@ async function communitiesRoutes(app, container) {
112
104
  }
113
105
  },
114
106
  handler: async (req, reply) => {
115
- const scope = await resolveScope(container, req);
116
- await content.deleteCommunity(scope.orgId, req.params.communityId, {
117
- id: scope.orgUserId,
118
- role: scope.role
119
- });
107
+ const actor = await resolveActor(container, req);
108
+ await content.deleteCommunity(req.params.communityId, actor);
120
109
  return reply.code(204).send();
121
110
  }
122
111
  });
@@ -124,23 +113,79 @@ async function communitiesRoutes(app, container) {
124
113
  method: "PATCH",
125
114
  url: "/api/communities/:communityId/settings",
126
115
  schema: {
127
- operationId: "patchCommunityDmSettings",
116
+ operationId: "patchCommunitySettings",
128
117
  tags,
129
- summary: "Change direct-message settings for a community (staff)",
130
- description: "Owner or admin only. Changes only the settings sent; the rest keep their values. They decide who may message whom from then on.",
118
+ summary: "Change the direct-message and digest settings of a community",
119
+ description: `Requires \`communities:write\`. Changes only the settings sent; the rest keep their values. The direct-message settings decide who may message whom from then on. \`digest\` sets when the community digest goes out, or \`null\` stops it; the first digest after a change goes out at the next slot.`,
131
120
  params: CommunityIdParam,
132
- body: PatchDmSettings,
121
+ body: PatchCommunitySettings,
133
122
  response: {
134
123
  200: Community,
135
124
  ...refusals(403, 404)
136
125
  }
137
126
  },
138
127
  handler: async (req) => {
139
- const scope = await resolveScope(container, req);
140
- return content.patchDmSettings(scope.orgId, req.params.communityId, req.body, {
141
- id: scope.orgUserId,
142
- role: scope.role
143
- });
128
+ const actor = await resolveActor(container, req);
129
+ return content.patchCommunitySettings(req.params.communityId, req.body, actor);
130
+ }
131
+ });
132
+ r.route({
133
+ method: "GET",
134
+ url: "/api/communities/:communityId/notification-categories",
135
+ schema: {
136
+ operationId: "getCommunityNotificationCategories",
137
+ tags: ["Notifications"],
138
+ summary: "Get a community's notification categories",
139
+ description: `Requires \`communities:read\`. On the community. The list in use for the community, with \`source\` saying whether it is the default or a stored list.`,
140
+ params: CommunityIdParam,
141
+ response: {
142
+ 200: NotificationCategoryList,
143
+ ...refusals(403, 404)
144
+ }
145
+ },
146
+ handler: async (req) => {
147
+ const actor = await resolveActor(container, req);
148
+ return container.notificationCategories.community.get(req.params.communityId, actor);
149
+ }
150
+ });
151
+ r.route({
152
+ method: "PUT",
153
+ url: "/api/communities/:communityId/notification-categories",
154
+ schema: {
155
+ operationId: "replaceCommunityNotificationCategories",
156
+ tags: ["Notifications"],
157
+ summary: "Replace a community's notification categories",
158
+ description: `Requires \`communities:write\`. On the community. Stores the whole list for this community; other communities keep theirs. 422 when a key repeats, a type cannot be in a community list, a reason is not allowed for its type, a channel is not configured, \`perSpace\` is set on a type that cannot occur in a space, \`spaceDefaults\` appears without \`perSpace\` or names a channel the category does not offer, or earlier categories already cover everything a category covers.`,
159
+ params: CommunityIdParam,
160
+ body: ReplaceNotificationCategories,
161
+ response: {
162
+ 200: NotificationCategoryList,
163
+ ...refusals(400, 403, 404, 422)
164
+ }
165
+ },
166
+ handler: async (req) => {
167
+ const actor = await resolveActor(container, req);
168
+ return container.notificationCategories.community.replace(req.params.communityId, req.body.categories, actor);
169
+ }
170
+ });
171
+ r.route({
172
+ method: "DELETE",
173
+ url: "/api/communities/:communityId/notification-categories",
174
+ schema: {
175
+ operationId: "resetCommunityNotificationCategories",
176
+ tags: ["Notifications"],
177
+ summary: "Go back to the default notification categories for a community",
178
+ description: `Requires \`communities:write\`. On the community. Removes the stored list; the default applies again.`,
179
+ params: CommunityIdParam,
180
+ response: {
181
+ 204: z.void(),
182
+ ...refusals(403, 404)
183
+ }
184
+ },
185
+ handler: async (req, reply) => {
186
+ const actor = await resolveActor(container, req);
187
+ await container.notificationCategories.community.reset(req.params.communityId, actor);
188
+ return reply.code(204).send();
144
189
  }
145
190
  });
146
191
  r.route({
@@ -149,8 +194,8 @@ async function communitiesRoutes(app, container) {
149
194
  schema: {
150
195
  operationId: "publishCommunity",
151
196
  tags,
152
- summary: "Publish a community (staff)",
153
- description: "Owner or admin only. Makes the community visible to the people who hold it.",
197
+ summary: "Publish a community",
198
+ description: `Requires \`communities:write\`. Makes the community visible to the people who hold it.`,
154
199
  params: CommunityIdParam,
155
200
  response: {
156
201
  200: Community,
@@ -158,11 +203,8 @@ async function communitiesRoutes(app, container) {
158
203
  }
159
204
  },
160
205
  handler: async (req) => {
161
- const scope = await resolveScope(container, req);
162
- return content.publishCommunity(scope.orgId, req.params.communityId, {
163
- id: scope.orgUserId,
164
- role: scope.role
165
- });
206
+ const actor = await resolveActor(container, req);
207
+ return content.publishCommunity(req.params.communityId, actor);
166
208
  }
167
209
  });
168
210
  r.route({
@@ -171,8 +213,8 @@ async function communitiesRoutes(app, container) {
171
213
  schema: {
172
214
  operationId: "unpublishCommunity",
173
215
  tags,
174
- summary: "Unpublish a community (staff)",
175
- description: "Owner or admin only. Hides the community from members again; nothing in it is deleted.",
216
+ summary: "Unpublish a community",
217
+ description: `Requires \`communities:write\`. Hides the community from members again; nothing in it is deleted.`,
176
218
  params: CommunityIdParam,
177
219
  response: {
178
220
  200: Community,
@@ -180,11 +222,8 @@ async function communitiesRoutes(app, container) {
180
222
  }
181
223
  },
182
224
  handler: async (req) => {
183
- const scope = await resolveScope(container, req);
184
- return content.unpublishCommunity(scope.orgId, req.params.communityId, {
185
- id: scope.orgUserId,
186
- role: scope.role
187
- });
225
+ const actor = await resolveActor(container, req);
226
+ return content.unpublishCommunity(req.params.communityId, actor);
188
227
  }
189
228
  });
190
229
  r.route({
@@ -193,8 +232,8 @@ async function communitiesRoutes(app, container) {
193
232
  schema: {
194
233
  operationId: "getCommunityAnalytics",
195
234
  tags,
196
- summary: "Staff analytics for a community",
197
- description: "Staff only (owner, admin, instructor). Membership, activity and moderation counts for one community.",
235
+ summary: "Analytics for a community",
236
+ description: `Requires \`communities:read\`. Membership, activity and moderation counts for one community.`,
198
237
  params: CommunityIdParam,
199
238
  response: {
200
239
  200: CommunityAnalytics,
@@ -202,10 +241,8 @@ async function communitiesRoutes(app, container) {
202
241
  }
203
242
  },
204
243
  handler: async (req) => {
205
- const scope = await resolveScope(container, req);
206
- const stats = await container.reporting.admin.communityAnalytics(scope.orgId, req.params.communityId);
207
- if (!stats) throw new NotFoundError("Community", req.params.communityId);
208
- return stats;
244
+ const actor = await resolveActor(container, req);
245
+ return container.reporting.admin.communityAnalytics(req.params.communityId, actor);
209
246
  }
210
247
  });
211
248
  r.route({
@@ -214,8 +251,8 @@ async function communitiesRoutes(app, container) {
214
251
  schema: {
215
252
  operationId: "getCommunityMessageQueue",
216
253
  tags,
217
- summary: "Open message flags in a community (staff)",
218
- description: "Staff only (owner, admin, instructor). The messages in the community's chats with open flags, with the flags and the chat each is in. Holds at most the 100 oldest open flags; working the queue brings in the next ones.",
254
+ summary: "Open message flags in a community",
255
+ description: `Requires \`communities:moderate\`. The messages in the community's chats with open flags, with the flags and the chat each is in. Holds at most the 100 oldest open flags; working the queue brings in the next ones.`,
219
256
  params: CommunityIdParam,
220
257
  response: {
221
258
  200: MessageQueue,
@@ -223,11 +260,8 @@ async function communitiesRoutes(app, container) {
223
260
  }
224
261
  },
225
262
  handler: async (req) => {
226
- const scope = await resolveScope(container, req);
227
- return container.chats.messageQueue(scope.orgId, {
228
- id: scope.orgUserId,
229
- role: scope.role
230
- }, req.params.communityId);
263
+ const actor = await resolveActor(container, req);
264
+ return container.chats.messageQueue(req.params.communityId, actor);
231
265
  }
232
266
  });
233
267
  r.route({
@@ -236,8 +270,8 @@ async function communitiesRoutes(app, container) {
236
270
  schema: {
237
271
  operationId: "listCommunitySpaces",
238
272
  tags,
239
- summary: "List the spaces in a community (staff)",
240
- description: "Staff only (owner, admin, instructor). Every space in order, with its topics and links.",
273
+ summary: "List the spaces in a community",
274
+ description: `Requires one of \`communities:read\`, \`communities:moderate\`. Every space in order, with its topics and links.`,
241
275
  params: CommunityIdParam,
242
276
  response: {
243
277
  200: z.array(SpaceDetail),
@@ -245,8 +279,8 @@ async function communitiesRoutes(app, container) {
245
279
  }
246
280
  },
247
281
  handler: async (req) => {
248
- const scope = await resolveScope(container, req);
249
- return content.listSpaces(scope.orgId, req.params.communityId);
282
+ const actor = await resolveActor(container, req);
283
+ return content.listSpaces(req.params.communityId, actor);
250
284
  }
251
285
  });
252
286
  r.route({
@@ -255,8 +289,8 @@ async function communitiesRoutes(app, container) {
255
289
  schema: {
256
290
  operationId: "createSpace",
257
291
  tags,
258
- summary: "Create a space (staff)",
259
- description: "Owner or admin only. The space is added last. An `all_members` space cannot have a capacity (409).",
292
+ summary: "Create a space",
293
+ description: `Requires \`communities:write\`. The space is added last. An \`all_members\` space cannot have a capacity (409).`,
260
294
  params: CommunityIdParam,
261
295
  body: CreateSpace,
262
296
  response: {
@@ -265,11 +299,8 @@ async function communitiesRoutes(app, container) {
265
299
  }
266
300
  },
267
301
  handler: async (req, reply) => {
268
- const scope = await resolveScope(container, req);
269
- const space = await content.createSpace(scope.orgId, req.params.communityId, req.body, {
270
- id: scope.orgUserId,
271
- role: scope.role
272
- });
302
+ const actor = await resolveActor(container, req);
303
+ const space = await content.createSpace(req.params.communityId, req.body, actor);
273
304
  return reply.code(201).send(space);
274
305
  }
275
306
  });
@@ -279,8 +310,8 @@ async function communitiesRoutes(app, container) {
279
310
  schema: {
280
311
  operationId: "reorderSpaces",
281
312
  tags,
282
- summary: "Reorder the spaces in a community (staff)",
283
- description: "Owner or admin only. Send every space id of the community exactly once (409 otherwise).",
313
+ summary: "Reorder the spaces in a community",
314
+ description: `Requires \`communities:write\`. Send every space id of the community exactly once (409 otherwise).`,
284
315
  params: CommunityIdParam,
285
316
  body: OrderedIds,
286
317
  response: {
@@ -289,11 +320,8 @@ async function communitiesRoutes(app, container) {
289
320
  }
290
321
  },
291
322
  handler: async (req) => {
292
- const scope = await resolveScope(container, req);
293
- return content.reorderSpaces(scope.orgId, req.params.communityId, req.body.ids, {
294
- id: scope.orgUserId,
295
- role: scope.role
296
- });
323
+ const actor = await resolveActor(container, req);
324
+ return content.reorderSpaces(req.params.communityId, req.body.ids, actor);
297
325
  }
298
326
  });
299
327
  r.route({
@@ -302,8 +330,8 @@ async function communitiesRoutes(app, container) {
302
330
  schema: {
303
331
  operationId: "getSpace",
304
332
  tags,
305
- summary: "Get a space (staff)",
306
- description: "Staff only (owner, admin, instructor). The space with its topics and links.",
333
+ summary: "Get a space",
334
+ description: `Requires \`communities:read\`. The space with its topics and links.`,
307
335
  params: CommunitySpaceParam,
308
336
  response: {
309
337
  200: SpaceDetail,
@@ -311,10 +339,8 @@ async function communitiesRoutes(app, container) {
311
339
  }
312
340
  },
313
341
  handler: async (req) => {
314
- const scope = await resolveScope(container, req);
315
- const space = await content.getCommunitySpace(scope.orgId, req.params.communityId, req.params.spaceId);
316
- if (!space) throw new NotFoundError("Space", req.params.spaceId);
317
- return space;
342
+ const actor = await resolveActor(container, req);
343
+ return content.getCommunitySpace(req.params.communityId, req.params.spaceId, actor);
318
344
  }
319
345
  });
320
346
  r.route({
@@ -323,8 +349,8 @@ async function communitiesRoutes(app, container) {
323
349
  schema: {
324
350
  operationId: "updateSpace",
325
351
  tags,
326
- summary: "Update a space (staff)",
327
- description: "Owner or admin only. Changes only the fields sent; `settings` merges into the current settings. An `all_members` space cannot have a capacity (409).",
352
+ summary: "Update a space",
353
+ description: `Requires \`communities:write\`. Changes only the fields sent; \`settings\` merges into the current settings. An \`all_members\` space cannot have a capacity (409).`,
328
354
  params: CommunitySpaceParam,
329
355
  body: UpdateSpace,
330
356
  response: {
@@ -333,11 +359,8 @@ async function communitiesRoutes(app, container) {
333
359
  }
334
360
  },
335
361
  handler: async (req) => {
336
- const scope = await resolveScope(container, req);
337
- return content.updateSpace(scope.orgId, req.params.communityId, req.params.spaceId, req.body, {
338
- id: scope.orgUserId,
339
- role: scope.role
340
- });
362
+ const actor = await resolveActor(container, req);
363
+ return content.updateSpace(req.params.communityId, req.params.spaceId, req.body, actor);
341
364
  }
342
365
  });
343
366
  r.route({
@@ -346,8 +369,8 @@ async function communitiesRoutes(app, container) {
346
369
  schema: {
347
370
  operationId: "deleteSpace",
348
371
  tags,
349
- summary: "Delete a space (staff)",
350
- description: "Owner or admin only. The space and everything in it go.",
372
+ summary: "Delete a space",
373
+ description: `Requires \`communities:write\`. The space and everything in it go.`,
351
374
  params: CommunitySpaceParam,
352
375
  response: {
353
376
  204: z.void(),
@@ -355,11 +378,8 @@ async function communitiesRoutes(app, container) {
355
378
  }
356
379
  },
357
380
  handler: async (req, reply) => {
358
- const scope = await resolveScope(container, req);
359
- await content.deleteSpace(scope.orgId, req.params.communityId, req.params.spaceId, {
360
- id: scope.orgUserId,
361
- role: scope.role
362
- });
381
+ const actor = await resolveActor(container, req);
382
+ await content.deleteSpace(req.params.communityId, req.params.spaceId, actor);
363
383
  return reply.code(204).send();
364
384
  }
365
385
  });
@@ -369,8 +389,8 @@ async function communitiesRoutes(app, container) {
369
389
  schema: {
370
390
  operationId: "setSpaceTopics",
371
391
  tags,
372
- summary: "Replace the topics on a space (staff)",
373
- description: "Owner or admin only. The list replaces the current topics: an entry with an `id` keeps and renames that topic, one without adds a topic, and a topic left out is deleted. Names must be unique (409), and a topic that posts use cannot be deleted (409).",
392
+ summary: "Replace the topics on a space",
393
+ description: `Requires \`communities:write\`. The list replaces the current topics: an entry with an \`id\` keeps and renames that topic, one without adds a topic, and a topic left out is deleted. Names must be unique (409), and a topic that posts use cannot be deleted (409).`,
374
394
  params: CommunitySpaceParam,
375
395
  body: SetSpaceTopics,
376
396
  response: {
@@ -379,11 +399,8 @@ async function communitiesRoutes(app, container) {
379
399
  }
380
400
  },
381
401
  handler: async (req) => {
382
- const scope = await resolveScope(container, req);
383
- return content.setSpaceTopics(scope.orgId, req.params.communityId, req.params.spaceId, req.body, {
384
- id: scope.orgUserId,
385
- role: scope.role
386
- });
402
+ const actor = await resolveActor(container, req);
403
+ return content.setSpaceTopics(req.params.communityId, req.params.spaceId, req.body, actor);
387
404
  }
388
405
  });
389
406
  r.route({
@@ -392,8 +409,8 @@ async function communitiesRoutes(app, container) {
392
409
  schema: {
393
410
  operationId: "addSpaceLink",
394
411
  tags,
395
- summary: "Link a course, download, or community into a space (staff)",
396
- description: "Owner or admin only. The link is added last. Linking the same content twice is a 409.",
412
+ summary: "Link a course, download, or community into a space",
413
+ description: `Requires \`communities:write\`. The link is added last. Linking the same content twice is a 409.`,
397
414
  params: CommunitySpaceParam,
398
415
  body: SpaceLinkBody,
399
416
  response: {
@@ -402,11 +419,8 @@ async function communitiesRoutes(app, container) {
402
419
  }
403
420
  },
404
421
  handler: async (req, reply) => {
405
- const scope = await resolveScope(container, req);
406
- const space = await content.addSpaceLink(scope.orgId, req.params.communityId, req.params.spaceId, req.body.contentId, {
407
- id: scope.orgUserId,
408
- role: scope.role
409
- });
422
+ const actor = await resolveActor(container, req);
423
+ const space = await content.addSpaceLink(req.params.communityId, req.params.spaceId, req.body.contentId, actor);
410
424
  return reply.code(201).send(space);
411
425
  }
412
426
  });
@@ -416,8 +430,8 @@ async function communitiesRoutes(app, container) {
416
430
  schema: {
417
431
  operationId: "removeSpaceLink",
418
432
  tags,
419
- summary: "Remove a link from a space (staff)",
420
- description: "Owner or admin only. The linked content itself is untouched.",
433
+ summary: "Remove a link from a space",
434
+ description: `Requires \`communities:write\`. The linked content itself is untouched.`,
421
435
  params: SpaceLinkParam,
422
436
  response: {
423
437
  200: SpaceDetail,
@@ -425,11 +439,8 @@ async function communitiesRoutes(app, container) {
425
439
  }
426
440
  },
427
441
  handler: async (req) => {
428
- const scope = await resolveScope(container, req);
429
- return content.removeSpaceLink(scope.orgId, req.params.communityId, req.params.spaceId, req.params.linkId, {
430
- id: scope.orgUserId,
431
- role: scope.role
432
- });
442
+ const actor = await resolveActor(container, req);
443
+ return content.removeSpaceLink(req.params.communityId, req.params.spaceId, req.params.linkId, actor);
433
444
  }
434
445
  });
435
446
  r.route({
@@ -438,8 +449,8 @@ async function communitiesRoutes(app, container) {
438
449
  schema: {
439
450
  operationId: "reorderSpaceLinks",
440
451
  tags,
441
- summary: "Reorder the links in a space (staff)",
442
- description: "Owner or admin only. Send every link id of the space exactly once (409 otherwise).",
452
+ summary: "Reorder the links in a space",
453
+ description: `Requires \`communities:write\`. Send every link id of the space exactly once (409 otherwise).`,
443
454
  params: CommunitySpaceParam,
444
455
  body: OrderedIds,
445
456
  response: {
@@ -448,11 +459,8 @@ async function communitiesRoutes(app, container) {
448
459
  }
449
460
  },
450
461
  handler: async (req) => {
451
- const scope = await resolveScope(container, req);
452
- return content.reorderSpaceLinks(scope.orgId, req.params.communityId, req.params.spaceId, req.body.ids, {
453
- id: scope.orgUserId,
454
- role: scope.role
455
- });
462
+ const actor = await resolveActor(container, req);
463
+ return content.reorderSpaceLinks(req.params.communityId, req.params.spaceId, req.body.ids, actor);
456
464
  }
457
465
  });
458
466
  }