@headless-lms/core 0.4.0 → 0.6.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 (249) hide show
  1. package/dist/assets/events.d.ts +15 -9
  2. package/dist/assets/file-urls.d.ts +14 -7
  3. package/dist/assets/file-urls.js +15 -4
  4. package/dist/assets/file-urls.js.map +1 -1
  5. package/dist/assets/index.d.ts +4 -4
  6. package/dist/assets/index.js +3 -3
  7. package/dist/assets/ports.d.ts +34 -15
  8. package/dist/assets/service.d.ts +30 -14
  9. package/dist/assets/service.js +233 -54
  10. package/dist/assets/service.js.map +1 -1
  11. package/dist/automations/actions.js.map +1 -1
  12. package/dist/automations/catalog.js +68 -3
  13. package/dist/automations/catalog.js.map +1 -1
  14. package/dist/automations/community-derivations.js +16 -16
  15. package/dist/automations/community-derivations.js.map +1 -1
  16. package/dist/automations/community-mail.d.ts +34 -4
  17. package/dist/automations/community-mail.js +32 -4
  18. package/dist/automations/community-mail.js.map +1 -1
  19. package/dist/automations/event-permissions.d.ts +12 -0
  20. package/dist/automations/event-permissions.js +38 -0
  21. package/dist/automations/event-permissions.js.map +1 -0
  22. package/dist/automations/index.d.ts +2 -1
  23. package/dist/automations/index.js +2 -1
  24. package/dist/automations/notifications/applicable.d.ts +8 -0
  25. package/dist/automations/notifications/applicable.js +12 -0
  26. package/dist/automations/notifications/applicable.js.map +1 -0
  27. package/dist/automations/notifications/debounce.js +1 -1
  28. package/dist/automations/notifications/debounce.js.map +1 -1
  29. package/dist/automations/notifications/defaults.js +33 -3
  30. package/dist/automations/notifications/defaults.js.map +1 -1
  31. package/dist/automations/notifications/processor.js +1 -1
  32. package/dist/automations/notifications/processor.js.map +1 -1
  33. package/dist/automations/notifications/rules/content.js +60 -3
  34. package/dist/automations/notifications/rules/content.js.map +1 -1
  35. package/dist/automations/notifications/rules/discussion.js +29 -29
  36. package/dist/automations/notifications/rules/discussion.js.map +1 -1
  37. package/dist/automations/notifications/rules/entitlements.js +36 -9
  38. package/dist/automations/notifications/rules/entitlements.js.map +1 -1
  39. package/dist/automations/notifications/rules/events.d.ts +9 -0
  40. package/dist/automations/notifications/rules/events.js +27 -0
  41. package/dist/automations/notifications/rules/events.js.map +1 -0
  42. package/dist/automations/notifications/rules/organizations.js +17 -10
  43. package/dist/automations/notifications/rules/organizations.js.map +1 -1
  44. package/dist/automations/notifications/rules/recipients.js +0 -1
  45. package/dist/automations/notifications/rules/recipients.js.map +1 -1
  46. package/dist/automations/notifications/service.d.ts +8 -6
  47. package/dist/automations/notifications/service.js +12 -12
  48. package/dist/automations/notifications/service.js.map +1 -1
  49. package/dist/automations/notifications/types.js +94 -7
  50. package/dist/automations/notifications/types.js.map +1 -1
  51. package/dist/automations/ports.d.ts +30 -31
  52. package/dist/automations/service.d.ts +20 -17
  53. package/dist/automations/service.js +90 -61
  54. package/dist/automations/service.js.map +1 -1
  55. package/dist/content/event-sight.d.ts +11 -0
  56. package/dist/content/event-sight.js +11 -0
  57. package/dist/content/event-sight.js.map +1 -0
  58. package/dist/content/events.d.ts +465 -88
  59. package/dist/content/events.js +34 -1
  60. package/dist/content/events.js.map +1 -1
  61. package/dist/content/index.d.ts +6 -4
  62. package/dist/content/index.js +3 -1
  63. package/dist/content/jobs.d.ts +16 -0
  64. package/dist/content/jobs.js +37 -0
  65. package/dist/content/jobs.js.map +1 -0
  66. package/dist/content/model.d.ts +2 -2
  67. package/dist/content/notification-categories.d.ts +5 -4
  68. package/dist/content/notification-categories.js +15 -6
  69. package/dist/content/notification-categories.js.map +1 -1
  70. package/dist/content/ports.d.ts +113 -68
  71. package/dist/content/service.d.ts +103 -67
  72. package/dist/content/service.js +739 -152
  73. package/dist/content/service.js.map +1 -1
  74. package/dist/content/titles.d.ts +6 -3
  75. package/dist/content/titles.js +37 -24
  76. package/dist/content/titles.js.map +1 -1
  77. package/dist/content/types.d.ts +2 -2
  78. package/dist/discussion/authors.d.ts +4 -2
  79. package/dist/discussion/authors.js +9 -5
  80. package/dist/discussion/authors.js.map +1 -1
  81. package/dist/discussion/chats.d.ts +56 -37
  82. package/dist/discussion/chats.js +392 -160
  83. package/dist/discussion/chats.js.map +1 -1
  84. package/dist/discussion/events.d.ts +85 -47
  85. package/dist/discussion/events.js +41 -3
  86. package/dist/discussion/events.js.map +1 -1
  87. package/dist/discussion/index.d.ts +8 -5
  88. package/dist/discussion/index.js +5 -3
  89. package/dist/discussion/live.d.ts +106 -0
  90. package/dist/discussion/live.js +642 -0
  91. package/dist/discussion/live.js.map +1 -0
  92. package/dist/discussion/mentions.js +2 -2
  93. package/dist/discussion/mentions.js.map +1 -1
  94. package/dist/discussion/ports.d.ts +182 -100
  95. package/dist/discussion/ports.js.map +1 -1
  96. package/dist/discussion/posts.d.ts +45 -39
  97. package/dist/discussion/posts.js +266 -152
  98. package/dist/discussion/posts.js.map +1 -1
  99. package/dist/discussion/rules.d.ts +18 -0
  100. package/dist/discussion/rules.js +22 -0
  101. package/dist/discussion/rules.js.map +1 -0
  102. package/dist/discussion/service.d.ts +37 -31
  103. package/dist/discussion/service.js +250 -186
  104. package/dist/discussion/service.js.map +1 -1
  105. package/dist/discussion/subscriptions.d.ts +8 -8
  106. package/dist/discussion/subscriptions.js +28 -12
  107. package/dist/discussion/subscriptions.js.map +1 -1
  108. package/dist/discussion/titles.d.ts +4 -2
  109. package/dist/discussion/titles.js +27 -13
  110. package/dist/discussion/titles.js.map +1 -1
  111. package/dist/entitlements/events.d.ts +58 -58
  112. package/dist/entitlements/index.d.ts +4 -4
  113. package/dist/entitlements/index.js +2 -2
  114. package/dist/entitlements/model.d.ts +2 -2
  115. package/dist/entitlements/ports.d.ts +65 -39
  116. package/dist/entitlements/rules.d.ts +3 -6
  117. package/dist/entitlements/rules.js +5 -10
  118. package/dist/entitlements/rules.js.map +1 -1
  119. package/dist/entitlements/service.d.ts +40 -28
  120. package/dist/entitlements/service.js +369 -162
  121. package/dist/entitlements/service.js.map +1 -1
  122. package/dist/identity/devices.d.ts +3 -3
  123. package/dist/identity/devices.js +6 -4
  124. package/dist/identity/devices.js.map +1 -1
  125. package/dist/identity/index.d.ts +2 -2
  126. package/dist/identity/index.js +2 -2
  127. package/dist/identity/notification-settings.d.ts +1 -1
  128. package/dist/identity/notification-settings.js +2 -1
  129. package/dist/identity/notification-settings.js.map +1 -1
  130. package/dist/identity/ports.d.ts +7 -7
  131. package/dist/identity/service.d.ts +8 -7
  132. package/dist/identity/service.js +16 -12
  133. package/dist/identity/service.js.map +1 -1
  134. package/dist/integrations/events.d.ts +3 -3
  135. package/dist/integrations/ports.d.ts +9 -8
  136. package/dist/integrations/service.d.ts +9 -8
  137. package/dist/integrations/service.js +31 -14
  138. package/dist/integrations/service.js.map +1 -1
  139. package/dist/organizations/default-roles.d.ts +22 -0
  140. package/dist/organizations/default-roles.js +89 -0
  141. package/dist/organizations/default-roles.js.map +1 -0
  142. package/dist/organizations/effective-permissions.d.ts +47 -0
  143. package/dist/organizations/effective-permissions.js +118 -0
  144. package/dist/organizations/effective-permissions.js.map +1 -0
  145. package/dist/organizations/events.d.ts +326 -181
  146. package/dist/organizations/events.js +90 -18
  147. package/dist/organizations/events.js.map +1 -1
  148. package/dist/organizations/index.d.ts +14 -10
  149. package/dist/organizations/index.js +7 -3
  150. package/dist/organizations/members.d.ts +66 -8
  151. package/dist/organizations/members.js.map +1 -1
  152. package/dist/organizations/model.d.ts +2 -2
  153. package/dist/organizations/notification-catalog.d.ts +31 -0
  154. package/dist/organizations/notification-catalog.js +32 -0
  155. package/dist/organizations/notification-catalog.js.map +1 -0
  156. package/dist/organizations/notification-categories.d.ts +6 -4
  157. package/dist/organizations/notification-categories.js +25 -10
  158. package/dist/organizations/notification-categories.js.map +1 -1
  159. package/dist/organizations/org-roles.d.ts +33 -0
  160. package/dist/organizations/org-roles.js +49 -0
  161. package/dist/organizations/org-roles.js.map +1 -0
  162. package/dist/organizations/permissions.d.ts +311 -0
  163. package/dist/organizations/permissions.js +327 -0
  164. package/dist/organizations/permissions.js.map +1 -0
  165. package/dist/organizations/ports.d.ts +187 -65
  166. package/dist/organizations/service.d.ts +102 -35
  167. package/dist/organizations/service.js +1254 -327
  168. package/dist/organizations/service.js.map +1 -1
  169. package/dist/organizations/testing.d.ts +18 -0
  170. package/dist/organizations/testing.js +37 -0
  171. package/dist/organizations/testing.js.map +1 -0
  172. package/dist/organizations/types.d.ts +5 -16
  173. package/dist/progress/events.d.ts +14 -14
  174. package/dist/progress/index.d.ts +2 -2
  175. package/dist/progress/ports.d.ts +11 -3
  176. package/dist/progress/service.d.ts +6 -5
  177. package/dist/progress/service.js +21 -6
  178. package/dist/progress/service.js.map +1 -1
  179. package/dist/reporting/admin/index.d.ts +6 -4
  180. package/dist/reporting/admin/model.d.ts +36 -3
  181. package/dist/reporting/admin/ports.d.ts +39 -14
  182. package/dist/reporting/admin/service.d.ts +32 -11
  183. package/dist/reporting/admin/service.js +102 -18
  184. package/dist/reporting/admin/service.js.map +1 -1
  185. package/dist/reporting/events/index.d.ts +27 -0
  186. package/dist/reporting/events/index.js +20 -0
  187. package/dist/reporting/events/index.js.map +1 -0
  188. package/dist/reporting/member/calendar.d.ts +11 -0
  189. package/dist/reporting/member/calendar.js +75 -0
  190. package/dist/reporting/member/calendar.js.map +1 -0
  191. package/dist/reporting/member/index.d.ts +3 -2
  192. package/dist/reporting/member/model.d.ts +29 -5
  193. package/dist/reporting/member/ports.d.ts +49 -29
  194. package/dist/reporting/member/service.d.ts +57 -29
  195. package/dist/reporting/member/service.js +345 -114
  196. package/dist/reporting/member/service.js.map +1 -1
  197. package/dist/shared/actor.d.ts +23 -3
  198. package/dist/shared/errors.d.ts +2 -1
  199. package/dist/shared/errors.js +3 -1
  200. package/dist/shared/errors.js.map +1 -1
  201. package/dist/shared/id.d.ts +4 -0
  202. package/dist/shared/id.js +4 -0
  203. package/dist/shared/id.js.map +1 -1
  204. package/dist/shared/logger.d.ts +2 -1
  205. package/dist/shared/logger.js +10 -1
  206. package/dist/shared/logger.js.map +1 -1
  207. package/dist/shared/ports.d.ts +25 -5
  208. package/dist/types/content.d.ts +2 -2
  209. package/dist/types/discussion.d.ts +4 -5
  210. package/dist/types/email-templates.d.ts +22 -3
  211. package/dist/types/entitlements.d.ts +2 -2
  212. package/dist/types/identity.d.ts +13 -16
  213. package/dist/types/index.d.ts +9 -9
  214. package/dist/types/index.js +2 -1
  215. package/dist/types/notifications.d.ts +4 -4
  216. package/dist/types/organizations.d.ts +3 -2
  217. package/dist/types/ports.d.ts +8 -1
  218. package/dist/types/ports.js +7 -1
  219. package/dist/types/ports.js.map +1 -0
  220. package/dist/types/schemas/assets.d.ts +5 -5
  221. package/dist/types/schemas/assets.js +4 -3
  222. package/dist/types/schemas/assets.js.map +1 -1
  223. package/dist/types/schemas/content.d.ts +599 -13
  224. package/dist/types/schemas/content.js +219 -8
  225. package/dist/types/schemas/content.js.map +1 -1
  226. package/dist/types/schemas/discussion.d.ts +280 -57
  227. package/dist/types/schemas/discussion.js +77 -3
  228. package/dist/types/schemas/discussion.js.map +1 -1
  229. package/dist/types/schemas/entitlements.d.ts +21 -8
  230. package/dist/types/schemas/entitlements.js +11 -4
  231. package/dist/types/schemas/entitlements.js.map +1 -1
  232. package/dist/types/schemas/index.d.ts +5 -5
  233. package/dist/types/schemas/index.js +5 -5
  234. package/dist/types/schemas/notifications.d.ts +11 -10
  235. package/dist/types/schemas/notifications.js +3 -2
  236. package/dist/types/schemas/notifications.js.map +1 -1
  237. package/dist/types/schemas/organizations.d.ts +99 -60
  238. package/dist/types/schemas/organizations.js +80 -27
  239. package/dist/types/schemas/organizations.js.map +1 -1
  240. package/dist/types/schemas/progress.d.ts +3 -4
  241. package/dist/types/schemas/progress.js +0 -1
  242. package/dist/types/schemas/progress.js.map +1 -1
  243. package/package.json +1 -1
  244. package/dist/organizations/access-scopes.d.ts +0 -19
  245. package/dist/organizations/access-scopes.js +0 -89
  246. package/dist/organizations/access-scopes.js.map +0 -1
  247. package/dist/organizations/roles.d.ts +0 -19
  248. package/dist/organizations/roles.js +0 -82
  249. package/dist/organizations/roles.js.map +0 -1
@@ -2,6 +2,8 @@ import { spaceSettingsDefaults } from "../types/schemas/content.js";
2
2
  import { ConflictError, ForbiddenError, InvalidInputError, NotFoundError } from "../shared/errors.js";
3
3
  import { noopLogger } from "../shared/logger.js";
4
4
  import { contentEvents } from "./events.js";
5
+ import { EVENT_LEADS, EVENT_STARTING_SOON_JOB } from "./jobs.js";
6
+ import { isDeepStrictEqual } from "node:util";
5
7
  //#region src/content/service.ts
6
8
  /** URL-safe slug derived from a title. Domain rule owned by the service. */
7
9
  function slugify(title) {
@@ -11,10 +13,35 @@ function isPublishedActivity(settings) {
11
13
  return settings?.published !== false;
12
14
  }
13
15
  function actorMetadata(actor) {
14
- return { actorOrgUserId: actor.id };
16
+ return { actorId: actor.id };
15
17
  }
16
- function assertConfigurator(actor) {
17
- if (actor.role !== "owner" && actor.role !== "admin") throw new ForbiddenError("Only an owner or an admin can configure a community");
18
+ const COMMUNITY_READERS = ["communities:read", "communities:moderate"];
19
+ function eventChange(actor, previous, next) {
20
+ return {
21
+ actorId: actor.id,
22
+ previousStatus: previous.status,
23
+ previousType: previous.settings.type,
24
+ previousStartsAt: previous.startsAt.toISOString(),
25
+ previousEndsAt: previous.endsAt.toISOString(),
26
+ locationChanged: !isDeepStrictEqual(previous.location, next.location)
27
+ };
28
+ }
29
+ function assertEventTimes(startsAt, endsAt) {
30
+ if (endsAt.getTime() <= startsAt.getTime()) throw new InvalidInputError("An event must end after it starts");
31
+ }
32
+ function startingSoonKey(eventId, lead) {
33
+ return `${eventId}:${lead}`;
34
+ }
35
+ const EVENT_LEAD_KEYS = Object.keys(EVENT_LEADS);
36
+ function shownPublicly(item) {
37
+ return item.status === "published" || item.type === "event" && item.status === "canceled";
38
+ }
39
+ function spaceImageIds(space) {
40
+ return [
41
+ space.coverAssetId,
42
+ space.backgroundAssetId,
43
+ space.iconAssetId
44
+ ].filter((id) => id !== null);
18
45
  }
19
46
  function sameIdSet(current, requested) {
20
47
  if (current.length !== requested.length) return false;
@@ -27,6 +54,7 @@ var ContentService = class {
27
54
  libraryAssets;
28
55
  files;
29
56
  fileUrls;
57
+ scheduler;
30
58
  logger;
31
59
  constructor(params) {
32
60
  this.repo = params.repo;
@@ -34,14 +62,67 @@ var ContentService = class {
34
62
  this.libraryAssets = params.libraryAssets;
35
63
  this.files = params.files;
36
64
  this.fileUrls = params.fileUrls;
65
+ this.scheduler = params.scheduler;
37
66
  this.logger = params.logger ?? noopLogger;
38
67
  }
68
+ require(actor, permission, on) {
69
+ this.requireAny(actor, [permission], on);
70
+ }
71
+ requireAny(actor, permissions, on) {
72
+ if (!permissions.some((permission) => actor.permissions.has(permission, on))) {
73
+ this.logger.warn("content action refused", {
74
+ orgId: actor.orgId,
75
+ actorId: actor.id,
76
+ permission: permissions.join(" or "),
77
+ contentId: on
78
+ });
79
+ throw new ForbiddenError(`You need ${permissions.join(" or ")}`);
80
+ }
81
+ }
82
+ visibleIds(actor, permissions, what) {
83
+ const wheres = permissions.map((permission) => actor.permissions.where(permission));
84
+ if (wheres.some((where) => where.org)) return;
85
+ const ids = [...new Set(wheres.flatMap((where) => where.content))];
86
+ if (ids.length === 0) {
87
+ this.logger.warn("content list refused", {
88
+ orgId: actor.orgId,
89
+ actorId: actor.id,
90
+ permission: permissions.join(" or ")
91
+ });
92
+ throw new ForbiddenError(`You cannot see ${what}`);
93
+ }
94
+ return ids;
95
+ }
39
96
  withThumbnail(item) {
40
97
  return {
41
98
  ...item,
42
- thumbnailUrl: item.thumbnailAssetId ? this.fileUrls.thumbnail(item.type, item.id) : null
99
+ thumbnailUrl: !item.thumbnailAssetId ? null : shownPublicly(item) ? this.fileUrls.publicImage(item.orgId, item.id, item.thumbnailAssetId) : this.fileUrls.thumbnail(item.type, item.id)
43
100
  };
44
101
  }
102
+ syncThumbnail(item) {
103
+ return this.files.syncPublicImages(item.orgId, item.id, shownPublicly(item) && item.thumbnailAssetId ? [item.thumbnailAssetId] : []);
104
+ }
105
+ withdrawThumbnail(orgId, id) {
106
+ return this.files.syncPublicImages(orgId, id, []);
107
+ }
108
+ spaceView(space, published) {
109
+ const url = (image, assetId) => !assetId ? null : published ? this.fileUrls.publicImage(space.orgId, space.id, assetId) : this.fileUrls.spaceImage(space.id, image);
110
+ return {
111
+ ...space,
112
+ coverUrl: url("cover", space.coverAssetId),
113
+ backgroundUrl: url("background", space.backgroundAssetId),
114
+ iconUrl: url("icon", space.iconAssetId)
115
+ };
116
+ }
117
+ async communityPublished(orgId, communityId) {
118
+ return (await this.repo.findCommunityById(orgId, communityId))?.status === "published";
119
+ }
120
+ async oneSpaceView(space) {
121
+ return this.spaceView(space, await this.communityPublished(space.orgId, space.communityId));
122
+ }
123
+ syncSpaceImages(space, published) {
124
+ return this.files.syncPublicImages(space.orgId, space.id, published ? spaceImageIds(space) : []);
125
+ }
45
126
  pageWithThumbnails(page) {
46
127
  return {
47
128
  ...page,
@@ -59,14 +140,76 @@ var ContentService = class {
59
140
  }] : [];
60
141
  });
61
142
  }
62
- async listCourses(orgId, query) {
63
- return this.pageWithThumbnails(await this.repo.listCourses(orgId, query));
143
+ async eventFiles(orgId, links) {
144
+ const ready = await this.files.readyFiles(orgId, links.map((link) => link.assetId));
145
+ return links.flatMap((link) => {
146
+ const file = ready[link.assetId];
147
+ return file ? [{
148
+ ...link,
149
+ ...file,
150
+ url: this.fileUrls.eventFile(link.eventId, link.assetId)
151
+ }] : [];
152
+ });
153
+ }
154
+ async assertAttachable(actor, itemId, requested, attached, user) {
155
+ const ids = [...new Set(requested.filter((id) => typeof id === "string"))];
156
+ if (ids.length === 0) return;
157
+ const held = new Set(await attached());
158
+ const added = ids.filter((id) => !held.has(id));
159
+ if (added.length === 0) return;
160
+ const orgId = actor.orgId;
161
+ const found = new Map((await this.libraryAssets.libraryAssetsFor(orgId, added)).map((asset) => [asset.id, asset]));
162
+ const missing = added.filter((id) => !found.has(id));
163
+ if (missing.length > 0) {
164
+ this.logger.warn("library assets refused", {
165
+ orgId,
166
+ contentId: itemId,
167
+ assetIds: missing
168
+ });
169
+ throw new ConflictError(`${user} can only use ready files from the media library. Not available: ${missing.join(", ")}`);
170
+ }
171
+ if (actor.permissions.has("media:read")) return;
172
+ const foreign = added.filter((id) => found.get(id)?.contentId !== itemId);
173
+ if (foreign.length > 0) {
174
+ this.logger.warn("library assets refused — not uploaded for this item", {
175
+ orgId,
176
+ actorId: actor.id,
177
+ contentId: itemId,
178
+ assetIds: foreign
179
+ });
180
+ throw new ForbiddenError(`You need media:read to use files that were not uploaded for this item: ${foreign.join(", ")}`);
181
+ }
182
+ }
183
+ async courseAssetIds(orgId, courseId) {
184
+ const [course, links] = await Promise.all([this.repo.findCourseById(orgId, courseId), this.repo.listActivityAssetsForCourse(orgId, courseId)]);
185
+ return [course?.thumbnailAssetId ?? null, ...links.map((link) => link.assetId)];
186
+ }
187
+ async downloadAssetIds(orgId, downloadId) {
188
+ const [download, links] = await Promise.all([this.repo.getDownload(orgId, downloadId), this.repo.listDownloadAssets(orgId, downloadId)]);
189
+ return [download?.thumbnailAssetId ?? null, ...links.map((link) => link.assetId)];
190
+ }
191
+ async eventAssetIds(orgId, eventId) {
192
+ const [event, links] = await Promise.all([this.repo.findEventById(orgId, eventId), this.repo.listEventAssets(orgId, eventId)]);
193
+ if (!event) throw new NotFoundError("Event", eventId);
194
+ return [event.thumbnailAssetId, ...links.map((link) => link.assetId)];
195
+ }
196
+ async listCourses(query, actor) {
197
+ const ids = this.visibleIds(actor, ["courses:read"], "courses");
198
+ return this.pageWithThumbnails(await this.repo.listCourses(actor.orgId, query, ids));
199
+ }
200
+ async getCourse(id, actor) {
201
+ const course = await this.findCourse(id, actor);
202
+ if (!course) throw new NotFoundError("Course", id);
203
+ return course;
64
204
  }
65
- async getCourse(orgId, id) {
66
- const course = await this.repo.findCourseById(orgId, id);
205
+ async findCourse(id, actor) {
206
+ this.require(actor, "courses:read", id);
207
+ const course = await this.repo.findCourseById(actor.orgId, id);
67
208
  return course ? this.withThumbnail(course) : null;
68
209
  }
69
- async createCourse(orgId, input) {
210
+ async createCourse(input, actor) {
211
+ this.require(actor, "courses:write");
212
+ const orgId = actor.orgId;
70
213
  const course = await this.uow.run(async ({ content, outbox }) => {
71
214
  const created = await content.createCourse(orgId, input, slugify(input.title));
72
215
  await outbox.append([contentEvents.courseCreated.make({
@@ -78,12 +221,15 @@ var ContentService = class {
78
221
  this.logger.info("course created", {
79
222
  orgId,
80
223
  courseId: course.id,
81
- title: course.title
224
+ title: course.title,
225
+ actorId: actor.id
82
226
  });
83
227
  return this.withThumbnail(course);
84
228
  }
85
- async updateCourse(orgId, id, patch) {
86
- await this.assertLibraryAssets(orgId, [patch.thumbnailAssetId], "A course");
229
+ async updateCourse(id, patch, actor) {
230
+ this.require(actor, "courses:write", id);
231
+ const orgId = actor.orgId;
232
+ await this.assertAttachable(actor, id, [patch.thumbnailAssetId], () => this.courseAssetIds(orgId, id), "A course");
87
233
  const course = await this.uow.run(async ({ content, outbox }) => {
88
234
  const updated = await content.updateCourse(orgId, id, patch);
89
235
  if (!updated) {
@@ -102,11 +248,15 @@ var ContentService = class {
102
248
  this.logger.info("course updated", {
103
249
  orgId,
104
250
  courseId: id,
105
- fields: Object.keys(patch)
251
+ fields: Object.keys(patch),
252
+ actorId: actor.id
106
253
  });
254
+ await this.syncThumbnail(course);
107
255
  return this.withThumbnail(course);
108
256
  }
109
- async patchCourseSettings(orgId, id, value) {
257
+ async patchCourseSettings(id, value, actor) {
258
+ this.require(actor, "courses:write", id);
259
+ const orgId = actor.orgId;
110
260
  const merged = await this.uow.run(async ({ content, outbox }) => {
111
261
  const course = await content.findCourseById(orgId, id);
112
262
  if (!course) {
@@ -129,11 +279,14 @@ var ContentService = class {
129
279
  this.logger.info("course settings patched", {
130
280
  orgId,
131
281
  courseId: id,
132
- value
282
+ value,
283
+ actorId: actor.id
133
284
  });
134
285
  return merged;
135
286
  }
136
- async deleteCourse(orgId, id, actor) {
287
+ async deleteCourse(id, actor) {
288
+ this.require(actor, "courses:delete", id);
289
+ const orgId = actor.orgId;
137
290
  await this.uow.run(async ({ content, outbox }) => {
138
291
  const course = await content.findCourseById(orgId, id);
139
292
  if (!course) {
@@ -152,32 +305,44 @@ var ContentService = class {
152
305
  });
153
306
  this.logger.info("course deleted", {
154
307
  orgId,
155
- courseId: id
308
+ courseId: id,
309
+ actorId: actor.id
156
310
  });
311
+ await this.withdrawThumbnail(orgId, id);
157
312
  }
158
- listCourseModules(orgId, courseId) {
159
- return this.repo.listCourseModules(orgId, courseId);
313
+ async listCourseModules(courseId, actor) {
314
+ this.require(actor, "courses:read", courseId);
315
+ return this.repo.listCourseModules(actor.orgId, courseId);
160
316
  }
161
- listCourseActivities(orgId, courseId) {
162
- return this.repo.listActivitiesForCourse(orgId, courseId);
317
+ async listCourseActivities(courseId, actor) {
318
+ this.require(actor, "courses:read", courseId);
319
+ return this.repo.listActivitiesForCourse(actor.orgId, courseId);
163
320
  }
164
- async listCourseActivityViews(orgId, courseId) {
165
- const [activities, links] = await Promise.all([this.repo.listActivitiesForCourse(orgId, courseId), this.repo.listActivityAssetsForCourse(orgId, courseId)]);
321
+ async listCourseActivityViews(courseId, actor) {
322
+ this.require(actor, "courses:read", courseId);
323
+ const [activities, links] = await Promise.all([this.repo.listActivitiesForCourse(actor.orgId, courseId), this.repo.listActivityAssetsForCourse(actor.orgId, courseId)]);
166
324
  return activities.map((activity) => ({
167
325
  ...activity,
168
326
  assetUrls: Object.fromEntries(links.filter((link) => link.activityId === activity.id).map((link) => [link.assetId, this.fileUrls.activityAsset(activity.id, link.assetId)]))
169
327
  }));
170
328
  }
171
- listCourseActivityAssets(orgId, courseId) {
172
- return this.repo.listActivityAssetsForCourse(orgId, courseId);
329
+ async listCourseActivityAssets(courseId, actor) {
330
+ this.require(actor, "courses:read", courseId);
331
+ return this.repo.listActivityAssetsForCourse(actor.orgId, courseId);
173
332
  }
174
- getActivity(orgId, activityId) {
175
- return this.repo.findActivity(orgId, activityId);
333
+ async getActivity(activityId, actor) {
334
+ const activity = await this.repo.findActivity(actor.orgId, activityId);
335
+ if (activity) this.require(actor, "courses:read", activity.courseId);
336
+ return activity;
176
337
  }
177
- getModule(orgId, moduleId) {
178
- return this.repo.findModule(orgId, moduleId);
338
+ async getModule(moduleId, actor) {
339
+ const module = await this.repo.findModule(actor.orgId, moduleId);
340
+ if (module) this.require(actor, "courses:read", module.courseId);
341
+ return module;
179
342
  }
180
- async reorderModules(orgId, courseId, orderedIds) {
343
+ async reorderModules(courseId, orderedIds, actor) {
344
+ this.require(actor, "courses:write", courseId);
345
+ const orgId = actor.orgId;
181
346
  const modules = await this.uow.run(async ({ content, outbox }) => {
182
347
  const reordered = await content.reorderModules(orgId, courseId, orderedIds);
183
348
  await outbox.append([contentEvents.modulesReordered.make({
@@ -193,7 +358,9 @@ var ContentService = class {
193
358
  });
194
359
  return modules;
195
360
  }
196
- async createModule(orgId, courseId, title) {
361
+ async createModule(courseId, title, actor) {
362
+ this.require(actor, "courses:write", courseId);
363
+ const orgId = actor.orgId;
197
364
  const { modules, created } = await this.uow.run(async ({ content, outbox }) => {
198
365
  const all = await content.createModule(orgId, courseId, title);
199
366
  const module = all.at(-1);
@@ -211,11 +378,14 @@ var ContentService = class {
211
378
  orgId,
212
379
  courseId,
213
380
  moduleId: created.id,
214
- title
381
+ title,
382
+ actorId: actor.id
215
383
  });
216
384
  return modules;
217
385
  }
218
- async updateModule(orgId, courseId, moduleId, title) {
386
+ async updateModule(courseId, moduleId, title, actor) {
387
+ this.require(actor, "courses:write", courseId);
388
+ const orgId = actor.orgId;
219
389
  const modules = await this.uow.run(async ({ content, outbox }) => {
220
390
  const existing = await content.findModule(orgId, moduleId);
221
391
  if (!existing || existing.courseId !== courseId) {
@@ -239,11 +409,14 @@ var ContentService = class {
239
409
  orgId,
240
410
  courseId,
241
411
  moduleId,
242
- title
412
+ title,
413
+ actorId: actor.id
243
414
  });
244
415
  return modules;
245
416
  }
246
- async deleteModule(orgId, courseId, moduleId) {
417
+ async deleteModule(courseId, moduleId, actor) {
418
+ this.require(actor, "courses:write", courseId);
419
+ const orgId = actor.orgId;
247
420
  const modules = await this.uow.run(async ({ content, outbox }) => {
248
421
  const module = await content.findModule(orgId, moduleId);
249
422
  if (!module || module.courseId !== courseId) {
@@ -264,11 +437,14 @@ var ContentService = class {
264
437
  this.logger.info("module deleted", {
265
438
  orgId,
266
439
  courseId,
267
- moduleId
440
+ moduleId,
441
+ actorId: actor.id
268
442
  });
269
443
  return modules;
270
444
  }
271
- async reorderActivities(orgId, courseId, moduleId, orderedIds) {
445
+ async reorderActivities(courseId, moduleId, orderedIds, actor) {
446
+ this.require(actor, "courses:write", courseId);
447
+ const orgId = actor.orgId;
272
448
  const modules = await this.uow.run(async ({ content, outbox }) => {
273
449
  const reordered = await content.reorderActivities(orgId, courseId, moduleId, orderedIds);
274
450
  const module = reordered.find((m) => m.id === moduleId);
@@ -286,21 +462,10 @@ var ContentService = class {
286
462
  });
287
463
  return modules;
288
464
  }
289
- async assertLibraryAssets(orgId, assetIds, user = "An activity") {
290
- const ids = assetIds.filter((id) => typeof id === "string");
291
- if (ids.length === 0) return;
292
- const library = new Set(await this.libraryAssets.libraryAssetsAmong(orgId, ids));
293
- const refused = ids.filter((id) => !library.has(id));
294
- if (refused.length > 0) {
295
- this.logger.warn("library assets refused", {
296
- orgId,
297
- assetIds: refused
298
- });
299
- throw new ConflictError(`${user} can only use ready files from the media library. Not available: ${refused.join(", ")}`);
300
- }
301
- }
302
- async saveActivity(orgId, courseId, moduleId, input, activityId) {
303
- await this.assertLibraryAssets(orgId, input.assetIds ?? []);
465
+ async saveActivity(courseId, moduleId, input, activityId, actor) {
466
+ this.require(actor, "courses:write", courseId);
467
+ const orgId = actor.orgId;
468
+ await this.assertAttachable(actor, courseId, input.assetIds ?? [], () => this.courseAssetIds(orgId, courseId), "An activity");
304
469
  const { modules, activity } = await this.uow.run(async ({ content, outbox }) => {
305
470
  let wasPublished = false;
306
471
  if (activityId) {
@@ -326,7 +491,7 @@ var ContentService = class {
326
491
  throw new NotFoundError("Module", moduleId);
327
492
  }
328
493
  }
329
- const { modules: all, activity: saved } = await content.saveActivity(orgId, courseId, moduleId, input, activityId);
494
+ const { modules: all, activity: saved } = await content.saveActivity(orgId, courseId, moduleId, input, activityId ?? void 0);
330
495
  const events = [activityId ? contentEvents.activityUpdated.make({
331
496
  orgId,
332
497
  data: saved
@@ -354,11 +519,14 @@ var ContentService = class {
354
519
  orgId,
355
520
  courseId,
356
521
  moduleId,
357
- activityId: activity.id
522
+ activityId: activity.id,
523
+ actorId: actor.id
358
524
  });
359
525
  return modules;
360
526
  }
361
- async deleteActivity(orgId, courseId, moduleId, activityId) {
527
+ async deleteActivity(courseId, moduleId, activityId, actor) {
528
+ this.require(actor, "courses:write", courseId);
529
+ const orgId = actor.orgId;
362
530
  const modules = await this.uow.run(async ({ content, outbox }) => {
363
531
  const activity = await content.findActivity(orgId, activityId);
364
532
  if (!activity || activity.moduleId !== moduleId || activity.courseId !== courseId) {
@@ -381,18 +549,28 @@ var ContentService = class {
381
549
  orgId,
382
550
  courseId,
383
551
  moduleId,
384
- activityId
552
+ activityId,
553
+ actorId: actor.id
385
554
  });
386
555
  return modules;
387
556
  }
388
- async listDownloads(orgId, query) {
389
- return this.pageWithThumbnails(await this.repo.listDownloads(orgId, query));
557
+ async listDownloads(query, actor) {
558
+ const ids = this.visibleIds(actor, ["downloads:read"], "downloads");
559
+ return this.pageWithThumbnails(await this.repo.listDownloads(actor.orgId, query, ids));
560
+ }
561
+ async getDownload(id, actor) {
562
+ const download = await this.findDownload(id, actor);
563
+ if (!download) throw new NotFoundError("Download", id);
564
+ return download;
390
565
  }
391
- async getDownload(orgId, id) {
392
- const download = await this.repo.getDownload(orgId, id);
566
+ async findDownload(id, actor) {
567
+ this.require(actor, "downloads:read", id);
568
+ const download = await this.repo.getDownload(actor.orgId, id);
393
569
  return download ? this.withThumbnail(download) : null;
394
570
  }
395
- async createDownload(orgId, input) {
571
+ async createDownload(input, actor) {
572
+ this.require(actor, "downloads:write");
573
+ const orgId = actor.orgId;
396
574
  const download = await this.uow.run(async ({ content, outbox }) => {
397
575
  const created = await content.createDownload(orgId, input, slugify(input.title));
398
576
  await outbox.append([contentEvents.downloadCreated.make({
@@ -404,12 +582,15 @@ var ContentService = class {
404
582
  this.logger.info("download created", {
405
583
  orgId,
406
584
  downloadId: download.id,
407
- title: download.title
585
+ title: download.title,
586
+ actorId: actor.id
408
587
  });
409
588
  return this.withThumbnail(download);
410
589
  }
411
- async updateDownload(orgId, id, patch) {
412
- await this.assertLibraryAssets(orgId, [patch.thumbnailAssetId], "A download");
590
+ async updateDownload(id, patch, actor) {
591
+ this.require(actor, "downloads:write", id);
592
+ const orgId = actor.orgId;
593
+ await this.assertAttachable(actor, id, [patch.thumbnailAssetId], () => this.downloadAssetIds(orgId, id), "A download");
413
594
  const download = await this.uow.run(async ({ content, outbox }) => {
414
595
  const updated = await content.updateDownload(orgId, id, patch);
415
596
  if (!updated) {
@@ -428,11 +609,15 @@ var ContentService = class {
428
609
  this.logger.info("download updated", {
429
610
  orgId,
430
611
  downloadId: id,
431
- fields: Object.keys(patch)
612
+ fields: Object.keys(patch),
613
+ actorId: actor.id
432
614
  });
615
+ await this.syncThumbnail(download);
433
616
  return this.withThumbnail(download);
434
617
  }
435
- async removeDownload(orgId, id, actor) {
618
+ async removeDownload(id, actor) {
619
+ this.require(actor, "downloads:delete", id);
620
+ const orgId = actor.orgId;
436
621
  await this.uow.run(async ({ content, outbox }) => {
437
622
  const download = await content.getDownload(orgId, id);
438
623
  if (!download) {
@@ -451,14 +636,19 @@ var ContentService = class {
451
636
  });
452
637
  this.logger.info("download deleted", {
453
638
  orgId,
454
- downloadId: id
639
+ downloadId: id,
640
+ actorId: actor.id
455
641
  });
642
+ await this.withdrawThumbnail(orgId, id);
456
643
  }
457
- async listDownloadAssets(orgId, downloadId) {
458
- return this.downloadFiles(orgId, await this.repo.listDownloadAssets(orgId, downloadId));
644
+ async listDownloadAssets(downloadId, actor) {
645
+ this.require(actor, "downloads:read", downloadId);
646
+ return this.downloadFiles(actor.orgId, await this.repo.listDownloadAssets(actor.orgId, downloadId));
459
647
  }
460
- async addDownloadAsset(orgId, downloadId, input) {
461
- await this.assertLibraryAssets(orgId, [input.assetId], "A download");
648
+ async addDownloadAsset(downloadId, input, actor) {
649
+ this.require(actor, "downloads:write", downloadId);
650
+ const orgId = actor.orgId;
651
+ await this.assertAttachable(actor, downloadId, [input.assetId], () => this.downloadAssetIds(orgId, downloadId), "A download");
462
652
  const assets = await this.uow.run(async ({ content, outbox }) => {
463
653
  const assets = await content.addDownloadAsset(orgId, downloadId, input);
464
654
  await outbox.append([contentEvents.downloadAssetAdded.make({
@@ -473,11 +663,14 @@ var ContentService = class {
473
663
  this.logger.info("download asset added", {
474
664
  orgId,
475
665
  downloadId,
476
- assetId: input.assetId
666
+ assetId: input.assetId,
667
+ actorId: actor.id
477
668
  });
478
669
  return this.downloadFiles(orgId, assets);
479
670
  }
480
- async removeDownloadAsset(orgId, downloadId, assetId) {
671
+ async removeDownloadAsset(downloadId, assetId, actor) {
672
+ this.require(actor, "downloads:write", downloadId);
673
+ const orgId = actor.orgId;
481
674
  const assets = await this.uow.run(async ({ content, outbox }) => {
482
675
  const assets = await content.removeDownloadAsset(orgId, downloadId, assetId);
483
676
  await outbox.append([contentEvents.downloadAssetRemoved.make({
@@ -492,11 +685,14 @@ var ContentService = class {
492
685
  this.logger.info("download asset removed", {
493
686
  orgId,
494
687
  downloadId,
495
- assetId
688
+ assetId,
689
+ actorId: actor.id
496
690
  });
497
691
  return this.downloadFiles(orgId, assets);
498
692
  }
499
- async renameDownloadAsset(orgId, downloadId, assetId, displayName) {
693
+ async renameDownloadAsset(downloadId, assetId, displayName, actor) {
694
+ this.require(actor, "downloads:write", downloadId);
695
+ const orgId = actor.orgId;
500
696
  const assets = await this.uow.run(async ({ content, outbox }) => {
501
697
  const assets = await content.renameDownloadAsset(orgId, downloadId, assetId, displayName);
502
698
  await outbox.append([contentEvents.downloadAssetRenamed.make({
@@ -511,13 +707,16 @@ var ContentService = class {
511
707
  this.logger.info("download asset renamed", {
512
708
  orgId,
513
709
  downloadId,
514
- assetId
710
+ assetId,
711
+ actorId: actor.id
515
712
  });
516
713
  return this.downloadFiles(orgId, assets);
517
714
  }
518
715
  /** The caller must send the COMPLETE ordered set — a partial list would
519
716
  * silently unlink whatever it omitted. */
520
- async reorderDownloadAssets(orgId, downloadId, assetIds) {
717
+ async reorderDownloadAssets(downloadId, assetIds, actor) {
718
+ this.require(actor, "downloads:write", downloadId);
719
+ const orgId = actor.orgId;
521
720
  const assets = await this.uow.run(async ({ content, outbox }) => {
522
721
  const current = await content.listDownloadAssets(orgId, downloadId);
523
722
  const currentIds = new Set(current.map((a) => a.assetId));
@@ -547,13 +746,330 @@ var ContentService = class {
547
746
  });
548
747
  return this.downloadFiles(orgId, assets);
549
748
  }
550
- listBundles(orgId, query) {
551
- return this.repo.listBundles(orgId, query);
749
+ async listEvents(query, actor) {
750
+ const ids = this.visibleIds(actor, ["events:read"], "events");
751
+ return this.pageWithThumbnails(await this.repo.listEvents(actor.orgId, query, ids));
752
+ }
753
+ async getEvent(id, actor) {
754
+ const event = await this.findEvent(id, actor);
755
+ if (!event) throw new NotFoundError("Event", id);
756
+ return event;
757
+ }
758
+ async findEvent(id, actor) {
759
+ this.require(actor, "events:read", id);
760
+ const event = await this.repo.findEventById(actor.orgId, id);
761
+ return event ? this.withThumbnail(event) : null;
762
+ }
763
+ async findEvents(ids, actor) {
764
+ for (const id of ids) this.require(actor, "events:read", id);
765
+ if (ids.length === 0) return [];
766
+ return (await this.repo.findEventsByIds(actor.orgId, ids)).map((event) => this.withThumbnail(event));
767
+ }
768
+ async createEvent(input, actor) {
769
+ this.require(actor, "events:write");
770
+ assertEventTimes(input.startsAt, input.endsAt);
771
+ const orgId = actor.orgId;
772
+ const { settings, ...fields } = input;
773
+ const event = await this.uow.run(async ({ content, outbox }) => {
774
+ const created = await content.createEvent(orgId, fields);
775
+ const event = settings ? {
776
+ ...created,
777
+ settings: await content.patchEventSettings(orgId, created.id, settings)
778
+ } : created;
779
+ await outbox.append([contentEvents.eventCreated.make({
780
+ orgId,
781
+ data: event,
782
+ metadata: actorMetadata(actor)
783
+ })]);
784
+ return event;
785
+ });
786
+ this.logger.info("event created", {
787
+ orgId,
788
+ eventId: event.id,
789
+ title: event.title,
790
+ actorId: actor.id
791
+ });
792
+ return this.withThumbnail(event);
793
+ }
794
+ async updateEvent(id, patch, actor) {
795
+ this.require(actor, "events:write", id);
796
+ const orgId = actor.orgId;
797
+ await this.assertAttachable(actor, id, [patch.thumbnailAssetId], () => this.eventAssetIds(orgId, id), "An event");
798
+ const { settings, ...fields } = patch;
799
+ const { previous, event } = await this.uow.run(async ({ content, outbox }) => {
800
+ const previous = await content.findEventById(orgId, id);
801
+ if (!previous) {
802
+ this.logger.warn("event update rejected — not found", {
803
+ orgId,
804
+ eventId: id
805
+ });
806
+ throw new NotFoundError("Event", id);
807
+ }
808
+ if (previous.status === "canceled") {
809
+ this.logger.warn("event update rejected — canceled", {
810
+ orgId,
811
+ eventId: id
812
+ });
813
+ throw new ConflictError("The event is canceled", "canceled");
814
+ }
815
+ assertEventTimes(fields.startsAt ?? previous.startsAt, fields.endsAt ?? previous.endsAt);
816
+ const updated = await content.updateEvent(orgId, id, fields);
817
+ if (!updated) throw new NotFoundError("Event", id);
818
+ const event = settings ? {
819
+ ...updated,
820
+ settings: await content.patchEventSettings(orgId, id, settings)
821
+ } : updated;
822
+ await outbox.append([contentEvents.eventUpdated.make({
823
+ orgId,
824
+ data: event,
825
+ metadata: eventChange(actor, previous, event)
826
+ })]);
827
+ return {
828
+ previous,
829
+ event
830
+ };
831
+ });
832
+ this.logger.info("event updated", {
833
+ orgId,
834
+ eventId: id,
835
+ fields: Object.keys(patch),
836
+ actorId: actor.id
837
+ });
838
+ await this.scheduleStartingSoon(orgId, previous, event);
839
+ await this.syncThumbnail(event);
840
+ return this.withThumbnail(event);
841
+ }
842
+ async cancelEvent(id, actor) {
843
+ this.require(actor, "events:write", id);
844
+ const orgId = actor.orgId;
845
+ const event = await this.uow.run(async ({ content, outbox }) => {
846
+ const previous = await content.findEventById(orgId, id);
847
+ if (!previous) {
848
+ this.logger.warn("event cancel rejected — not found", {
849
+ orgId,
850
+ eventId: id
851
+ });
852
+ throw new NotFoundError("Event", id);
853
+ }
854
+ if (previous.status === "canceled") {
855
+ this.logger.warn("event cancel rejected — already canceled", {
856
+ orgId,
857
+ eventId: id
858
+ });
859
+ throw new ConflictError("The event is already canceled", "canceled");
860
+ }
861
+ const updated = await content.updateEvent(orgId, id, { status: "canceled" });
862
+ if (!updated) throw new NotFoundError("Event", id);
863
+ await outbox.append([contentEvents.eventUpdated.make({
864
+ orgId,
865
+ data: updated,
866
+ metadata: eventChange(actor, previous, updated)
867
+ })]);
868
+ return updated;
869
+ });
870
+ this.logger.info("event canceled", {
871
+ orgId,
872
+ eventId: id,
873
+ actorId: actor.id
874
+ });
875
+ await this.cancelStartingSoon(orgId, id);
876
+ await this.syncThumbnail(event);
877
+ return this.withThumbnail(event);
878
+ }
879
+ async deleteEvent(id, actor) {
880
+ this.require(actor, "events:delete", id);
881
+ const orgId = actor.orgId;
882
+ await this.uow.run(async ({ content, outbox }) => {
883
+ const event = await content.findEventById(orgId, id);
884
+ if (!event) {
885
+ this.logger.warn("event delete rejected — not found", {
886
+ orgId,
887
+ eventId: id
888
+ });
889
+ throw new NotFoundError("Event", id);
890
+ }
891
+ const linked = await content.spacesLinkingContent(orgId, id);
892
+ if (!await content.deleteEvent(orgId, id)) throw new NotFoundError("Event", id);
893
+ await content.deleteEventSettings(orgId, id);
894
+ await outbox.append([contentEvents.eventDeleted.make({
895
+ orgId,
896
+ data: event,
897
+ metadata: actorMetadata(actor)
898
+ }), ...await this.spaceUpdates(content, orgId, linked, actor)]);
899
+ });
900
+ this.logger.info("event deleted", {
901
+ orgId,
902
+ eventId: id,
903
+ actorId: actor.id
904
+ });
905
+ await this.withdrawThumbnail(orgId, id);
906
+ await this.cancelStartingSoon(orgId, id);
907
+ }
908
+ async startingSoon(orgId, eventId, lead, now) {
909
+ const event = await this.repo.findEventById(orgId, eventId);
910
+ if (!event || event.status !== "published") {
911
+ this.logger.debug("event no longer published", {
912
+ orgId,
913
+ eventId,
914
+ lead
915
+ });
916
+ return;
917
+ }
918
+ const due = new Date(event.startsAt.getTime() - EVENT_LEADS[lead]);
919
+ if (due > now) {
920
+ await this.scheduler.at(EVENT_STARTING_SOON_JOB, due, startingSoonKey(eventId, lead), {
921
+ orgId,
922
+ eventId,
923
+ lead
924
+ });
925
+ this.logger.info("event starting soon timer moved", {
926
+ orgId,
927
+ eventId,
928
+ lead,
929
+ due: due.toISOString()
930
+ });
931
+ return;
932
+ }
933
+ await this.uow.run(({ outbox }) => outbox.append([contentEvents.eventStartingSoon.make({
934
+ orgId,
935
+ data: {
936
+ eventId,
937
+ lead,
938
+ startsAt: event.startsAt
939
+ }
940
+ })]));
941
+ this.logger.info("event starting soon", {
942
+ orgId,
943
+ eventId,
944
+ lead
945
+ });
946
+ }
947
+ async scheduleStartingSoon(orgId, previous, event) {
948
+ if (event.status !== "published") {
949
+ if (previous.status === "published") await this.cancelStartingSoon(orgId, event.id);
950
+ return;
951
+ }
952
+ if (previous.status === "published" && previous.startsAt.getTime() === event.startsAt.getTime()) return;
953
+ const now = Date.now();
954
+ for (const lead of EVENT_LEAD_KEYS) {
955
+ const key = startingSoonKey(event.id, lead);
956
+ const runAt = new Date(event.startsAt.getTime() - EVENT_LEADS[lead]);
957
+ try {
958
+ if (runAt.getTime() > now) await this.scheduler.at(EVENT_STARTING_SOON_JOB, runAt, key, {
959
+ orgId,
960
+ eventId: event.id,
961
+ lead
962
+ });
963
+ else await this.scheduler.cancel(EVENT_STARTING_SOON_JOB, key);
964
+ } catch (err) {
965
+ this.logger.error("event starting soon timer not set", {
966
+ orgId,
967
+ eventId: event.id,
968
+ lead,
969
+ err
970
+ });
971
+ }
972
+ }
552
973
  }
553
- getBundle(orgId, id) {
554
- return this.repo.findBundleById(orgId, id);
974
+ async cancelStartingSoon(orgId, eventId) {
975
+ for (const lead of EVENT_LEAD_KEYS) try {
976
+ await this.scheduler.cancel(EVENT_STARTING_SOON_JOB, startingSoonKey(eventId, lead));
977
+ } catch (err) {
978
+ this.logger.warn("event starting soon timer not cancelled", {
979
+ orgId,
980
+ eventId,
981
+ lead,
982
+ err
983
+ });
984
+ }
985
+ }
986
+ async listEventAssets(eventId, actor) {
987
+ this.require(actor, "events:read", eventId);
988
+ const orgId = actor.orgId;
989
+ if (!await this.repo.findEventById(orgId, eventId)) throw new NotFoundError("Event", eventId);
990
+ return this.eventFiles(orgId, await this.repo.listEventAssets(orgId, eventId));
991
+ }
992
+ async eventFilesFor(eventIds, actor) {
993
+ for (const id of eventIds) this.require(actor, "events:read", id);
994
+ const byEvent = Object.fromEntries(eventIds.map((id) => [id, []]));
995
+ if (eventIds.length === 0) return byEvent;
996
+ const files = await this.eventFiles(actor.orgId, await this.repo.listEventAssetsFor(actor.orgId, eventIds));
997
+ for (const file of files) byEvent[file.eventId]?.push(file);
998
+ return byEvent;
999
+ }
1000
+ async addEventAsset(eventId, input, actor) {
1001
+ this.require(actor, "events:write", eventId);
1002
+ const orgId = actor.orgId;
1003
+ await this.assertAttachable(actor, eventId, [input.assetId], () => this.eventAssetIds(orgId, eventId), "An event");
1004
+ const assets = await this.uow.run(({ content }) => content.addEventAsset(orgId, eventId, input));
1005
+ this.logger.info("event asset added", {
1006
+ orgId,
1007
+ eventId,
1008
+ assetId: input.assetId,
1009
+ actorId: actor.id
1010
+ });
1011
+ return this.eventFiles(orgId, assets);
1012
+ }
1013
+ async removeEventAsset(eventId, assetId, actor) {
1014
+ this.require(actor, "events:write", eventId);
1015
+ const orgId = actor.orgId;
1016
+ const assets = await this.uow.run(({ content }) => content.removeEventAsset(orgId, eventId, assetId));
1017
+ this.logger.info("event asset removed", {
1018
+ orgId,
1019
+ eventId,
1020
+ assetId,
1021
+ actorId: actor.id
1022
+ });
1023
+ return this.eventFiles(orgId, assets);
1024
+ }
1025
+ async renameEventAsset(eventId, assetId, displayName, actor) {
1026
+ this.require(actor, "events:write", eventId);
1027
+ const orgId = actor.orgId;
1028
+ const assets = await this.uow.run(({ content }) => content.renameEventAsset(orgId, eventId, assetId, displayName));
1029
+ this.logger.info("event asset renamed", {
1030
+ orgId,
1031
+ eventId,
1032
+ assetId,
1033
+ actorId: actor.id
1034
+ });
1035
+ return this.eventFiles(orgId, assets);
1036
+ }
1037
+ async reorderEventAssets(eventId, assetIds, actor) {
1038
+ this.require(actor, "events:write", eventId);
1039
+ const orgId = actor.orgId;
1040
+ const assets = await this.uow.run(async ({ content }) => {
1041
+ if (!sameIdSet((await content.listEventAssets(orgId, eventId)).map((link) => link.assetId), assetIds)) {
1042
+ this.logger.warn("event asset reorder rejected — id set mismatch", {
1043
+ orgId,
1044
+ eventId,
1045
+ assetIds
1046
+ });
1047
+ throw new ConflictError("Ordered asset ids does not match the event's current assets");
1048
+ }
1049
+ return content.reorderEventAssets(orgId, eventId, assetIds);
1050
+ });
1051
+ this.logger.debug("event assets reordered", {
1052
+ orgId,
1053
+ eventId
1054
+ });
1055
+ return this.eventFiles(orgId, assets);
1056
+ }
1057
+ async listBundles(query, actor) {
1058
+ this.require(actor, "bundles:read");
1059
+ return this.repo.listBundles(actor.orgId, query);
555
1060
  }
556
- async createBundle(orgId, input) {
1061
+ async getBundle(id, actor) {
1062
+ const bundle = await this.findBundle(id, actor);
1063
+ if (!bundle) throw new NotFoundError("Bundle", id);
1064
+ return bundle;
1065
+ }
1066
+ async findBundle(id, actor) {
1067
+ this.require(actor, "bundles:read");
1068
+ return this.repo.findBundleById(actor.orgId, id);
1069
+ }
1070
+ async createBundle(input, actor) {
1071
+ this.require(actor, "bundles:write");
1072
+ const orgId = actor.orgId;
557
1073
  const { bundle, items } = await this.uow.run(async ({ content, outbox }) => {
558
1074
  const created = await content.createBundle(orgId, input);
559
1075
  const initial = input.contentIds?.length ? await content.listBundleItems(orgId, created.id) : [];
@@ -576,11 +1092,14 @@ var ContentService = class {
576
1092
  orgId,
577
1093
  bundleId: bundle.id,
578
1094
  name: bundle.name,
579
- itemCount: items.length
1095
+ itemCount: items.length,
1096
+ actorId: actor.id
580
1097
  });
581
1098
  return bundle;
582
1099
  }
583
- async updateBundle(orgId, id, patch) {
1100
+ async updateBundle(id, patch, actor) {
1101
+ this.require(actor, "bundles:write");
1102
+ const orgId = actor.orgId;
584
1103
  const bundle = await this.uow.run(async ({ content, outbox }) => {
585
1104
  const updated = await content.updateBundle(orgId, id, patch);
586
1105
  if (!updated) {
@@ -599,11 +1118,14 @@ var ContentService = class {
599
1118
  this.logger.info("bundle updated", {
600
1119
  orgId,
601
1120
  bundleId: id,
602
- fields: Object.keys(patch)
1121
+ fields: Object.keys(patch),
1122
+ actorId: actor.id
603
1123
  });
604
1124
  return bundle;
605
1125
  }
606
- async deleteBundle(orgId, id) {
1126
+ async deleteBundle(id, actor) {
1127
+ this.require(actor, "bundles:delete");
1128
+ const orgId = actor.orgId;
607
1129
  await this.uow.run(async ({ content, outbox }) => {
608
1130
  const bundle = await content.findBundleById(orgId, id);
609
1131
  if (!bundle) {
@@ -621,13 +1143,17 @@ var ContentService = class {
621
1143
  });
622
1144
  this.logger.info("bundle deleted", {
623
1145
  orgId,
624
- bundleId: id
1146
+ bundleId: id,
1147
+ actorId: actor.id
625
1148
  });
626
1149
  }
627
- listBundleItems(orgId, bundleId) {
628
- return this.repo.listBundleItems(orgId, bundleId);
1150
+ async listBundleItems(bundleId, actor) {
1151
+ this.require(actor, "bundles:read");
1152
+ return this.repo.listBundleItems(actor.orgId, bundleId);
629
1153
  }
630
- async addBundleItem(orgId, bundleId, contentId) {
1154
+ async addBundleItem(bundleId, contentId, actor) {
1155
+ this.require(actor, "bundles:write");
1156
+ const orgId = actor.orgId;
631
1157
  const items = await this.uow.run(async ({ content, outbox }) => {
632
1158
  const all = await content.addBundleItem(orgId, bundleId, contentId);
633
1159
  await outbox.append([contentEvents.bundleItemAdded.make({
@@ -642,11 +1168,14 @@ var ContentService = class {
642
1168
  this.logger.info("bundle item added", {
643
1169
  orgId,
644
1170
  bundleId,
645
- contentId
1171
+ contentId,
1172
+ actorId: actor.id
646
1173
  });
647
1174
  return items;
648
1175
  }
649
- async removeBundleItem(orgId, bundleId, contentId) {
1176
+ async removeBundleItem(bundleId, contentId, actor) {
1177
+ this.require(actor, "bundles:write");
1178
+ const orgId = actor.orgId;
650
1179
  const items = await this.uow.run(async ({ content, outbox }) => {
651
1180
  const all = await content.removeBundleItem(orgId, bundleId, contentId);
652
1181
  await outbox.append([contentEvents.bundleItemRemoved.make({
@@ -661,7 +1190,8 @@ var ContentService = class {
661
1190
  this.logger.info("bundle item removed", {
662
1191
  orgId,
663
1192
  bundleId,
664
- contentId
1193
+ contentId,
1194
+ actorId: actor.id
665
1195
  });
666
1196
  return items;
667
1197
  }
@@ -694,35 +1224,60 @@ var ContentService = class {
694
1224
  settings
695
1225
  };
696
1226
  }
697
- async listCommunities(orgId, query) {
698
- return this.pageWithThumbnails(await this.repo.listCommunities(orgId, query));
1227
+ async listCommunities(query, actor) {
1228
+ const ids = this.visibleIds(actor, COMMUNITY_READERS, "communities");
1229
+ return this.pageWithThumbnails(await this.repo.listCommunities(actor.orgId, query, ids));
699
1230
  }
700
- async getCommunity(orgId, id) {
701
- const community = await this.repo.findCommunityById(orgId, id);
1231
+ async getCommunity(id, actor) {
1232
+ const community = await this.findCommunity(id, actor);
1233
+ if (!community) throw new NotFoundError("Community", id);
1234
+ return community;
1235
+ }
1236
+ async findCommunity(id, actor) {
1237
+ this.requireAny(actor, COMMUNITY_READERS, id);
1238
+ const community = await this.repo.findCommunityById(actor.orgId, id);
702
1239
  return community ? this.withThumbnail(community) : null;
703
1240
  }
704
1241
  communitiesWithDigest() {
705
1242
  return this.repo.communitiesWithDigest();
706
1243
  }
707
- listCommunitiesById(orgId, ids) {
708
- return ids.length ? this.repo.findCommunitiesByIds(orgId, ids) : Promise.resolve([]);
709
- }
710
- getSpace(orgId, spaceId) {
711
- return this.repo.findSpace(orgId, spaceId);
712
- }
713
- async getCommunitySpace(orgId, communityId, spaceId) {
714
- const space = await this.repo.findSpace(orgId, spaceId);
715
- return space?.communityId === communityId ? space : null;
716
- }
717
- async listSpaces(orgId, communityId) {
718
- if (!await this.getCommunity(orgId, communityId)) throw new NotFoundError("Community", communityId);
719
- return this.repo.listSpaces(orgId, communityId);
720
- }
721
- async getDmSettings(orgId, communityId) {
722
- return (await this.repo.findCommunityById(orgId, communityId))?.settings.dm ?? null;
723
- }
724
- async createCommunity(orgId, input, actor) {
725
- assertConfigurator(actor);
1244
+ async listCommunitiesById(ids, actor) {
1245
+ if (!actor.permissions.anywhere("communities:read")) {
1246
+ this.logger.warn("content list refused", {
1247
+ orgId: actor.orgId,
1248
+ actorId: actor.id,
1249
+ permission: "communities:read"
1250
+ });
1251
+ throw new ForbiddenError("You cannot see communities");
1252
+ }
1253
+ const visible = ids.filter((id) => actor.permissions.has("communities:read", id));
1254
+ return visible.length ? this.repo.findCommunitiesByIds(actor.orgId, visible) : [];
1255
+ }
1256
+ async getSpace(spaceId, actor) {
1257
+ const space = await this.repo.findSpace(actor.orgId, spaceId);
1258
+ if (!space) return null;
1259
+ this.require(actor, "communities:read", space.communityId);
1260
+ return this.oneSpaceView(space);
1261
+ }
1262
+ async getCommunitySpace(communityId, spaceId, actor) {
1263
+ this.require(actor, "communities:read", communityId);
1264
+ const space = await this.repo.findSpace(actor.orgId, spaceId);
1265
+ if (space?.communityId !== communityId) throw new NotFoundError("Space", spaceId);
1266
+ return this.oneSpaceView(space);
1267
+ }
1268
+ async listSpaces(communityId, actor) {
1269
+ this.requireAny(actor, COMMUNITY_READERS, communityId);
1270
+ const community = await this.repo.findCommunityById(actor.orgId, communityId);
1271
+ if (!community) throw new NotFoundError("Community", communityId);
1272
+ return (await this.repo.listSpaces(actor.orgId, communityId)).map((space) => this.spaceView(space, community.status === "published"));
1273
+ }
1274
+ async getDmSettings(communityId, actor) {
1275
+ this.require(actor, "communities:read", communityId);
1276
+ return (await this.repo.findCommunityById(actor.orgId, communityId))?.settings.dm ?? null;
1277
+ }
1278
+ async createCommunity(input, actor) {
1279
+ this.require(actor, "communities:write");
1280
+ const orgId = actor.orgId;
726
1281
  const community = await this.uow.run(async ({ content, outbox }) => {
727
1282
  const created = await content.createCommunity(orgId, input, slugify(input.title));
728
1283
  await outbox.append([contentEvents.communityCreated.make({
@@ -739,9 +1294,10 @@ var ContentService = class {
739
1294
  });
740
1295
  return this.withThumbnail(community);
741
1296
  }
742
- async updateCommunity(orgId, id, patch, actor) {
743
- assertConfigurator(actor);
744
- await this.assertLibraryAssets(orgId, [patch.thumbnailAssetId], "A community");
1297
+ async updateCommunity(id, patch, actor) {
1298
+ this.require(actor, "communities:write", id);
1299
+ const orgId = actor.orgId;
1300
+ await this.assertAttachable(actor, id, [patch.thumbnailAssetId], async () => [(await this.repo.findCommunityById(orgId, id))?.thumbnailAssetId ?? null], "A community");
745
1301
  const community = await this.uow.run(async ({ content, outbox }) => {
746
1302
  const updated = await content.updateCommunity(orgId, id, patch);
747
1303
  if (!updated) {
@@ -764,17 +1320,23 @@ var ContentService = class {
764
1320
  communityId: id,
765
1321
  actorId: actor.id
766
1322
  });
1323
+ await this.syncThumbnail(community);
1324
+ if (patch.status !== void 0) {
1325
+ const spaces = await this.repo.listSpaces(orgId, id);
1326
+ await Promise.all(spaces.map((space) => this.syncSpaceImages(space, community.status === "published")));
1327
+ }
767
1328
  return this.withThumbnail(community);
768
1329
  }
769
- publishCommunity(orgId, id, actor) {
770
- return this.updateCommunity(orgId, id, { status: "published" }, actor);
1330
+ publishCommunity(id, actor) {
1331
+ return this.updateCommunity(id, { status: "published" }, actor);
771
1332
  }
772
- unpublishCommunity(orgId, id, actor) {
773
- return this.updateCommunity(orgId, id, { status: "draft" }, actor);
1333
+ unpublishCommunity(id, actor) {
1334
+ return this.updateCommunity(id, { status: "draft" }, actor);
774
1335
  }
775
- async deleteCommunity(orgId, id, actor) {
776
- assertConfigurator(actor);
777
- await this.uow.run(async ({ content, outbox }) => {
1336
+ async deleteCommunity(id, actor) {
1337
+ this.require(actor, "communities:delete", id);
1338
+ const orgId = actor.orgId;
1339
+ const spaceIds = await this.uow.run(async ({ content, outbox }) => {
778
1340
  const community = await content.findCommunityById(orgId, id);
779
1341
  if (!community) {
780
1342
  this.logger.warn("community delete rejected — not found", {
@@ -785,6 +1347,7 @@ var ContentService = class {
785
1347
  throw new NotFoundError("Community", id);
786
1348
  }
787
1349
  const linked = await content.spacesLinkingContent(orgId, id);
1350
+ const own = await content.listSpaces(orgId, id);
788
1351
  if (!await content.deleteCommunity(orgId, id)) throw new NotFoundError("Community", id);
789
1352
  await content.deleteCommunitySettings(orgId, id);
790
1353
  await outbox.append([contentEvents.communityDeleted.make({
@@ -792,15 +1355,19 @@ var ContentService = class {
792
1355
  data: community,
793
1356
  metadata: actorMetadata(actor)
794
1357
  }), ...await this.spaceUpdates(content, orgId, linked, actor)]);
1358
+ return own.map((space) => space.id);
795
1359
  });
796
1360
  this.logger.info("community deleted", {
797
1361
  orgId,
798
1362
  communityId: id,
799
1363
  actorId: actor.id
800
1364
  });
1365
+ await this.withdrawThumbnail(orgId, id);
1366
+ await Promise.all(spaceIds.map((spaceId) => this.files.syncPublicImages(orgId, spaceId, [])));
801
1367
  }
802
- async patchCommunitySettings(orgId, id, patch, actor) {
803
- assertConfigurator(actor);
1368
+ async patchCommunitySettings(id, patch, actor) {
1369
+ this.require(actor, "communities:write", id);
1370
+ const orgId = actor.orgId;
804
1371
  const community = await this.uow.run(async ({ content, outbox }) => {
805
1372
  const updated = await content.patchCommunitySettings(orgId, id, patch);
806
1373
  if (!updated) {
@@ -825,8 +1392,9 @@ var ContentService = class {
825
1392
  });
826
1393
  return this.withThumbnail(community);
827
1394
  }
828
- async createSpace(orgId, communityId, input, actor) {
829
- assertConfigurator(actor);
1395
+ async createSpace(communityId, input, actor) {
1396
+ this.require(actor, "communities:write", communityId);
1397
+ const orgId = actor.orgId;
830
1398
  const resolved = this.resolvedSpace(input);
831
1399
  const space = await this.uow.run(async ({ content, outbox }) => {
832
1400
  if (!await content.findCommunityById(orgId, communityId)) {
@@ -856,10 +1424,19 @@ var ContentService = class {
856
1424
  spaceId: space.id,
857
1425
  actorId: actor.id
858
1426
  });
859
- return space;
860
- }
861
- async updateSpace(orgId, communityId, spaceId, patch, actor) {
862
- assertConfigurator(actor);
1427
+ return this.oneSpaceView(space);
1428
+ }
1429
+ async updateSpace(communityId, spaceId, patch, actor) {
1430
+ this.require(actor, "communities:write", communityId);
1431
+ const orgId = actor.orgId;
1432
+ await this.assertAttachable(actor, communityId, [
1433
+ patch.coverAssetId,
1434
+ patch.backgroundAssetId,
1435
+ patch.iconAssetId
1436
+ ], async () => {
1437
+ const current = await this.repo.findSpace(orgId, spaceId);
1438
+ return current ? spaceImageIds(current) : [];
1439
+ }, "A space");
863
1440
  const space = await this.uow.run(async ({ content, outbox }) => {
864
1441
  const current = await content.findSpace(orgId, spaceId);
865
1442
  if (!current || current.communityId !== communityId) {
@@ -892,10 +1469,13 @@ var ContentService = class {
892
1469
  spaceId,
893
1470
  actorId: actor.id
894
1471
  });
895
- return space;
1472
+ const published = await this.communityPublished(orgId, communityId);
1473
+ await this.syncSpaceImages(space, published);
1474
+ return this.spaceView(space, published);
896
1475
  }
897
- async deleteSpace(orgId, communityId, spaceId, actor) {
898
- assertConfigurator(actor);
1476
+ async deleteSpace(communityId, spaceId, actor) {
1477
+ this.require(actor, "communities:write", communityId);
1478
+ const orgId = actor.orgId;
899
1479
  await this.uow.run(async ({ content, outbox }) => {
900
1480
  const space = await content.findSpace(orgId, spaceId);
901
1481
  if (!space || space.communityId !== communityId) {
@@ -920,9 +1500,11 @@ var ContentService = class {
920
1500
  spaceId,
921
1501
  actorId: actor.id
922
1502
  });
1503
+ await this.files.syncPublicImages(orgId, spaceId, []);
923
1504
  }
924
- async reorderSpaces(orgId, communityId, orderedIds, actor) {
925
- assertConfigurator(actor);
1505
+ async reorderSpaces(communityId, orderedIds, actor) {
1506
+ this.require(actor, "communities:write", communityId);
1507
+ const orgId = actor.orgId;
926
1508
  const spaces = await this.uow.run(async ({ content, outbox }) => {
927
1509
  if (!await content.findCommunityById(orgId, communityId)) {
928
1510
  this.logger.warn("spaces reorder rejected — community not found", {
@@ -946,10 +1528,12 @@ var ContentService = class {
946
1528
  communityId,
947
1529
  actorId: actor.id
948
1530
  });
949
- return spaces;
1531
+ const published = await this.communityPublished(orgId, communityId);
1532
+ return spaces.map((space) => this.spaceView(space, published));
950
1533
  }
951
- async setSpaceTopics(orgId, communityId, spaceId, input, actor) {
952
- assertConfigurator(actor);
1534
+ async setSpaceTopics(communityId, spaceId, input, actor) {
1535
+ this.require(actor, "communities:write", communityId);
1536
+ const orgId = actor.orgId;
953
1537
  const names = input.topics.map((topic) => topic.name);
954
1538
  if (new Set(names).size !== names.length) throw new InvalidInputError("A space topic name is already used");
955
1539
  const space = await this.uow.run(async ({ content, outbox }) => {
@@ -982,10 +1566,11 @@ var ContentService = class {
982
1566
  spaceId,
983
1567
  actorId: actor.id
984
1568
  });
985
- return space;
1569
+ return this.oneSpaceView(space);
986
1570
  }
987
- async addSpaceLink(orgId, communityId, spaceId, contentId, actor) {
988
- assertConfigurator(actor);
1571
+ async addSpaceLink(communityId, spaceId, contentId, actor) {
1572
+ this.require(actor, "communities:write", communityId);
1573
+ const orgId = actor.orgId;
989
1574
  const space = await this.uow.run(async ({ content, outbox }) => {
990
1575
  const current = await content.findSpace(orgId, spaceId);
991
1576
  if (!current || current.communityId !== communityId) {
@@ -1013,10 +1598,11 @@ var ContentService = class {
1013
1598
  contentId,
1014
1599
  actorId: actor.id
1015
1600
  });
1016
- return space;
1601
+ return this.oneSpaceView(space);
1017
1602
  }
1018
- async removeSpaceLink(orgId, communityId, spaceId, linkId, actor) {
1019
- assertConfigurator(actor);
1603
+ async removeSpaceLink(communityId, spaceId, linkId, actor) {
1604
+ this.require(actor, "communities:write", communityId);
1605
+ const orgId = actor.orgId;
1020
1606
  const space = await this.uow.run(async ({ content, outbox }) => {
1021
1607
  const current = await content.findSpace(orgId, spaceId);
1022
1608
  if (!current || current.communityId !== communityId) {
@@ -1043,10 +1629,11 @@ var ContentService = class {
1043
1629
  linkId,
1044
1630
  actorId: actor.id
1045
1631
  });
1046
- return space;
1632
+ return this.oneSpaceView(space);
1047
1633
  }
1048
- async reorderSpaceLinks(orgId, communityId, spaceId, orderedIds, actor) {
1049
- assertConfigurator(actor);
1634
+ async reorderSpaceLinks(communityId, spaceId, orderedIds, actor) {
1635
+ this.require(actor, "communities:write", communityId);
1636
+ const orgId = actor.orgId;
1050
1637
  const space = await this.uow.run(async ({ content, outbox }) => {
1051
1638
  const current = await content.findSpace(orgId, spaceId);
1052
1639
  if (!current || current.communityId !== communityId) {
@@ -1073,7 +1660,7 @@ var ContentService = class {
1073
1660
  spaceId,
1074
1661
  actorId: actor.id
1075
1662
  });
1076
- return space;
1663
+ return this.oneSpaceView(space);
1077
1664
  }
1078
1665
  };
1079
1666
  //#endregion