@headless-lms/core 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 (307) hide show
  1. package/dist/assets/events.d.ts +24 -18
  2. package/dist/assets/file-urls.d.ts +11 -7
  3. package/dist/assets/file-urls.js +14 -4
  4. package/dist/assets/file-urls.js.map +1 -1
  5. package/dist/assets/index.d.ts +5 -4
  6. package/dist/assets/index.js +4 -3
  7. package/dist/assets/jobs.d.ts +15 -0
  8. package/dist/assets/jobs.js +30 -0
  9. package/dist/assets/jobs.js.map +1 -0
  10. package/dist/assets/ports.d.ts +37 -17
  11. package/dist/assets/service.d.ts +30 -15
  12. package/dist/assets/service.js +229 -59
  13. package/dist/assets/service.js.map +1 -1
  14. package/dist/automations/actions.js +2 -63
  15. package/dist/automations/actions.js.map +1 -1
  16. package/dist/automations/catalog.d.ts +1 -2
  17. package/dist/automations/catalog.js +137 -65
  18. package/dist/automations/catalog.js.map +1 -1
  19. package/dist/automations/community-derivations.js +20 -20
  20. package/dist/automations/community-derivations.js.map +1 -1
  21. package/dist/automations/community-mail.d.ts +36 -20
  22. package/dist/automations/community-mail.js +32 -24
  23. package/dist/automations/community-mail.js.map +1 -1
  24. package/dist/automations/event-permissions.d.ts +12 -0
  25. package/dist/automations/event-permissions.js +38 -0
  26. package/dist/automations/event-permissions.js.map +1 -0
  27. package/dist/automations/events.d.ts +0 -10
  28. package/dist/automations/index.d.ts +12 -4
  29. package/dist/automations/index.js +9 -1
  30. package/dist/automations/notifications/applicable.d.ts +8 -0
  31. package/dist/automations/notifications/applicable.js +12 -0
  32. package/dist/automations/notifications/applicable.js.map +1 -0
  33. package/dist/automations/notifications/debounce.d.ts +25 -0
  34. package/dist/automations/notifications/debounce.js +93 -0
  35. package/dist/automations/notifications/debounce.js.map +1 -0
  36. package/dist/automations/notifications/defaults.d.ts +8 -0
  37. package/dist/automations/notifications/defaults.js +346 -0
  38. package/dist/automations/notifications/defaults.js.map +1 -0
  39. package/dist/automations/notifications/delivery.d.ts +28 -0
  40. package/dist/automations/notifications/delivery.js +88 -0
  41. package/dist/automations/notifications/delivery.js.map +1 -0
  42. package/dist/automations/notifications/processor.d.ts +64 -0
  43. package/dist/automations/notifications/processor.js +270 -0
  44. package/dist/automations/notifications/processor.js.map +1 -0
  45. package/dist/automations/notifications/rules/content.d.ts +6 -0
  46. package/dist/automations/notifications/rules/content.js +96 -0
  47. package/dist/automations/notifications/rules/content.js.map +1 -0
  48. package/dist/automations/notifications/rules/discussion.d.ts +7 -0
  49. package/dist/automations/notifications/rules/discussion.js +678 -0
  50. package/dist/automations/notifications/rules/discussion.js.map +1 -0
  51. package/dist/automations/notifications/rules/entitlements.d.ts +6 -0
  52. package/dist/automations/notifications/rules/entitlements.js +276 -0
  53. package/dist/automations/notifications/rules/entitlements.js.map +1 -0
  54. package/dist/automations/notifications/rules/events.d.ts +9 -0
  55. package/dist/automations/notifications/rules/events.js +27 -0
  56. package/dist/automations/notifications/rules/events.js.map +1 -0
  57. package/dist/automations/notifications/rules/index.d.ts +6 -0
  58. package/dist/automations/notifications/rules/index.js +15 -0
  59. package/dist/automations/notifications/rules/index.js.map +1 -0
  60. package/dist/automations/notifications/rules/organizations.d.ts +6 -0
  61. package/dist/automations/notifications/rules/organizations.js +35 -0
  62. package/dist/automations/notifications/rules/organizations.js.map +1 -0
  63. package/dist/automations/notifications/rules/recipients.d.ts +32 -0
  64. package/dist/automations/notifications/rules/recipients.js +98 -0
  65. package/dist/automations/notifications/rules/recipients.js.map +1 -0
  66. package/dist/automations/notifications/service.d.ts +29 -0
  67. package/dist/automations/notifications/service.js +45 -0
  68. package/dist/automations/notifications/service.js.map +1 -0
  69. package/dist/automations/notifications/types.d.ts +7 -0
  70. package/dist/automations/notifications/types.js +560 -0
  71. package/dist/automations/notifications/types.js.map +1 -0
  72. package/dist/automations/ports.d.ts +134 -21
  73. package/dist/automations/service.d.ts +30 -30
  74. package/dist/automations/service.js +111 -144
  75. package/dist/automations/service.js.map +1 -1
  76. package/dist/content/event-sight.d.ts +11 -0
  77. package/dist/content/event-sight.js +11 -0
  78. package/dist/content/event-sight.js.map +1 -0
  79. package/dist/content/events.d.ts +437 -46
  80. package/dist/content/events.js +45 -1
  81. package/dist/content/events.js.map +1 -1
  82. package/dist/content/index.d.ts +8 -4
  83. package/dist/content/index.js +5 -1
  84. package/dist/content/jobs.d.ts +16 -0
  85. package/dist/content/jobs.js +37 -0
  86. package/dist/content/jobs.js.map +1 -0
  87. package/dist/content/model.d.ts +2 -2
  88. package/dist/content/notification-categories.d.ts +37 -0
  89. package/dist/content/notification-categories.js +73 -0
  90. package/dist/content/notification-categories.js.map +1 -0
  91. package/dist/content/ports.d.ts +132 -68
  92. package/dist/content/service.d.ts +100 -68
  93. package/dist/content/service.js +703 -146
  94. package/dist/content/service.js.map +1 -1
  95. package/dist/content/titles.d.ts +25 -0
  96. package/dist/content/titles.js +82 -0
  97. package/dist/content/titles.js.map +1 -0
  98. package/dist/content/types.d.ts +2 -2
  99. package/dist/discussion/authors.d.ts +4 -2
  100. package/dist/discussion/authors.js +9 -5
  101. package/dist/discussion/authors.js.map +1 -1
  102. package/dist/discussion/chats.d.ts +63 -39
  103. package/dist/discussion/chats.js +455 -190
  104. package/dist/discussion/chats.js.map +1 -1
  105. package/dist/discussion/digests.d.ts +24 -0
  106. package/dist/discussion/digests.js +104 -0
  107. package/dist/discussion/digests.js.map +1 -0
  108. package/dist/discussion/events.d.ts +227 -23
  109. package/dist/discussion/events.js +114 -19
  110. package/dist/discussion/events.js.map +1 -1
  111. package/dist/discussion/index.d.ts +12 -4
  112. package/dist/discussion/index.js +9 -2
  113. package/dist/discussion/jobs.d.ts +16 -0
  114. package/dist/discussion/jobs.js +40 -0
  115. package/dist/discussion/jobs.js.map +1 -0
  116. package/dist/discussion/live.d.ts +106 -0
  117. package/dist/discussion/live.js +642 -0
  118. package/dist/discussion/live.js.map +1 -0
  119. package/dist/discussion/mentions.d.ts +11 -0
  120. package/dist/discussion/mentions.js +35 -0
  121. package/dist/discussion/mentions.js.map +1 -0
  122. package/dist/discussion/ports.d.ts +279 -114
  123. package/dist/discussion/ports.js.map +1 -1
  124. package/dist/discussion/posts.d.ts +60 -44
  125. package/dist/discussion/posts.js +453 -287
  126. package/dist/discussion/posts.js.map +1 -1
  127. package/dist/discussion/rules.d.ts +18 -0
  128. package/dist/discussion/rules.js +22 -0
  129. package/dist/discussion/rules.js.map +1 -0
  130. package/dist/discussion/service.d.ts +50 -35
  131. package/dist/discussion/service.js +333 -205
  132. package/dist/discussion/service.js.map +1 -1
  133. package/dist/discussion/subscriptions.d.ts +37 -0
  134. package/dist/discussion/subscriptions.js +115 -0
  135. package/dist/discussion/subscriptions.js.map +1 -0
  136. package/dist/discussion/titles.d.ts +28 -0
  137. package/dist/discussion/titles.js +98 -0
  138. package/dist/discussion/titles.js.map +1 -0
  139. package/dist/entitlements/events.d.ts +20 -20
  140. package/dist/entitlements/index.d.ts +5 -4
  141. package/dist/entitlements/index.js +3 -2
  142. package/dist/entitlements/jobs.d.ts +12 -0
  143. package/dist/entitlements/jobs.js +18 -0
  144. package/dist/entitlements/jobs.js.map +1 -0
  145. package/dist/entitlements/model.d.ts +2 -2
  146. package/dist/entitlements/ports.d.ts +68 -38
  147. package/dist/entitlements/rules.d.ts +3 -6
  148. package/dist/entitlements/rules.js +5 -10
  149. package/dist/entitlements/rules.js.map +1 -1
  150. package/dist/entitlements/service.d.ts +43 -28
  151. package/dist/entitlements/service.js +387 -160
  152. package/dist/entitlements/service.js.map +1 -1
  153. package/dist/identity/devices.d.ts +30 -0
  154. package/dist/identity/devices.js +109 -0
  155. package/dist/identity/devices.js.map +1 -0
  156. package/dist/identity/index.d.ts +6 -5
  157. package/dist/identity/index.js +4 -3
  158. package/dist/identity/model.d.ts +2 -2
  159. package/dist/identity/notification-settings.d.ts +28 -0
  160. package/dist/identity/notification-settings.js +114 -0
  161. package/dist/identity/notification-settings.js.map +1 -0
  162. package/dist/identity/ports.d.ts +80 -56
  163. package/dist/identity/service.d.ts +8 -7
  164. package/dist/identity/service.js +17 -13
  165. package/dist/identity/service.js.map +1 -1
  166. package/dist/integrations/ports.d.ts +9 -8
  167. package/dist/integrations/service.d.ts +9 -8
  168. package/dist/integrations/service.js +33 -16
  169. package/dist/integrations/service.js.map +1 -1
  170. package/dist/organizations/default-roles.d.ts +22 -0
  171. package/dist/organizations/default-roles.js +89 -0
  172. package/dist/organizations/default-roles.js.map +1 -0
  173. package/dist/organizations/effective-permissions.d.ts +45 -0
  174. package/dist/organizations/effective-permissions.js +113 -0
  175. package/dist/organizations/effective-permissions.js.map +1 -0
  176. package/dist/organizations/events.d.ts +320 -181
  177. package/dist/organizations/events.js +90 -18
  178. package/dist/organizations/events.js.map +1 -1
  179. package/dist/organizations/index.d.ts +15 -10
  180. package/dist/organizations/index.js +8 -3
  181. package/dist/organizations/members.d.ts +66 -8
  182. package/dist/organizations/members.js.map +1 -1
  183. package/dist/organizations/notification-catalog.d.ts +31 -0
  184. package/dist/organizations/notification-catalog.js +32 -0
  185. package/dist/organizations/notification-catalog.js.map +1 -0
  186. package/dist/organizations/notification-categories.d.ts +34 -0
  187. package/dist/organizations/notification-categories.js +66 -0
  188. package/dist/organizations/notification-categories.js.map +1 -0
  189. package/dist/organizations/org-roles.d.ts +33 -0
  190. package/dist/organizations/org-roles.js +49 -0
  191. package/dist/organizations/org-roles.js.map +1 -0
  192. package/dist/organizations/permissions.d.ts +311 -0
  193. package/dist/organizations/permissions.js +327 -0
  194. package/dist/organizations/permissions.js.map +1 -0
  195. package/dist/organizations/ports.d.ts +187 -65
  196. package/dist/organizations/service.d.ts +104 -35
  197. package/dist/organizations/service.js +1247 -323
  198. package/dist/organizations/service.js.map +1 -1
  199. package/dist/organizations/testing.d.ts +18 -0
  200. package/dist/organizations/testing.js +37 -0
  201. package/dist/organizations/testing.js.map +1 -0
  202. package/dist/organizations/types.d.ts +5 -16
  203. package/dist/progress/index.d.ts +2 -2
  204. package/dist/progress/ports.d.ts +11 -3
  205. package/dist/progress/service.d.ts +6 -5
  206. package/dist/progress/service.js +23 -8
  207. package/dist/progress/service.js.map +1 -1
  208. package/dist/reporting/admin/index.d.ts +6 -4
  209. package/dist/reporting/admin/model.d.ts +36 -3
  210. package/dist/reporting/admin/ports.d.ts +39 -14
  211. package/dist/reporting/admin/service.d.ts +32 -11
  212. package/dist/reporting/admin/service.js +102 -18
  213. package/dist/reporting/admin/service.js.map +1 -1
  214. package/dist/reporting/events/index.d.ts +27 -0
  215. package/dist/reporting/events/index.js +20 -0
  216. package/dist/reporting/events/index.js.map +1 -0
  217. package/dist/reporting/member/calendar.d.ts +11 -0
  218. package/dist/reporting/member/calendar.js +75 -0
  219. package/dist/reporting/member/calendar.js.map +1 -0
  220. package/dist/reporting/member/index.d.ts +3 -2
  221. package/dist/reporting/member/model.d.ts +80 -26
  222. package/dist/reporting/member/ports.d.ts +50 -28
  223. package/dist/reporting/member/service.d.ts +81 -35
  224. package/dist/reporting/member/service.js +438 -145
  225. package/dist/reporting/member/service.js.map +1 -1
  226. package/dist/shared/actor.d.ts +22 -3
  227. package/dist/shared/errors.d.ts +15 -2
  228. package/dist/shared/errors.js +21 -2
  229. package/dist/shared/errors.js.map +1 -1
  230. package/dist/shared/id.d.ts +7 -0
  231. package/dist/shared/id.js +8 -1
  232. package/dist/shared/id.js.map +1 -1
  233. package/dist/shared/logger.d.ts +2 -1
  234. package/dist/shared/logger.js +10 -1
  235. package/dist/shared/logger.js.map +1 -1
  236. package/dist/shared/mailer.d.ts +11 -1
  237. package/dist/shared/mailer.js +32 -5
  238. package/dist/shared/mailer.js.map +1 -1
  239. package/dist/shared/notification-categories.d.ts +16 -0
  240. package/dist/shared/notification-categories.js +94 -0
  241. package/dist/shared/notification-categories.js.map +1 -0
  242. package/dist/shared/notification-settings.d.ts +14 -0
  243. package/dist/shared/notification-settings.js +61 -0
  244. package/dist/shared/notification-settings.js.map +1 -0
  245. package/dist/shared/ports.d.ts +68 -2
  246. package/dist/shared/settings.d.ts +2 -1
  247. package/dist/shared/settings.js +1 -0
  248. package/dist/shared/settings.js.map +1 -1
  249. package/dist/types/automations.d.ts +8 -2
  250. package/dist/types/content.d.ts +2 -2
  251. package/dist/types/discussion.d.ts +4 -5
  252. package/dist/types/email-templates.d.ts +119 -6
  253. package/dist/types/entitlements.d.ts +2 -2
  254. package/dist/types/identity.d.ts +14 -17
  255. package/dist/types/index.d.ts +13 -12
  256. package/dist/types/index.js +2 -1
  257. package/dist/types/notifications.d.ts +157 -0
  258. package/dist/types/notifications.js +1 -0
  259. package/dist/types/organizations.d.ts +3 -2
  260. package/dist/types/ports.d.ts +8 -1
  261. package/dist/types/ports.js +7 -1
  262. package/dist/types/ports.js.map +1 -0
  263. package/dist/types/schemas/assets.d.ts +5 -5
  264. package/dist/types/schemas/assets.js +4 -3
  265. package/dist/types/schemas/assets.js.map +1 -1
  266. package/dist/types/schemas/automations.d.ts +1 -10
  267. package/dist/types/schemas/automations.js +1 -3
  268. package/dist/types/schemas/automations.js.map +1 -1
  269. package/dist/types/schemas/content.d.ts +596 -11
  270. package/dist/types/schemas/content.js +225 -9
  271. package/dist/types/schemas/content.js.map +1 -1
  272. package/dist/types/schemas/discussion.d.ts +362 -66
  273. package/dist/types/schemas/discussion.js +123 -15
  274. package/dist/types/schemas/discussion.js.map +1 -1
  275. package/dist/types/schemas/entitlements.d.ts +14 -1
  276. package/dist/types/schemas/entitlements.js +11 -4
  277. package/dist/types/schemas/entitlements.js.map +1 -1
  278. package/dist/types/schemas/identity.d.ts +1 -85
  279. package/dist/types/schemas/identity.js +1 -42
  280. package/dist/types/schemas/identity.js.map +1 -1
  281. package/dist/types/schemas/index.d.ts +8 -7
  282. package/dist/types/schemas/index.js +8 -7
  283. package/dist/types/schemas/notifications.d.ts +96 -0
  284. package/dist/types/schemas/notifications.js +57 -0
  285. package/dist/types/schemas/notifications.js.map +1 -0
  286. package/dist/types/schemas/organizations.d.ts +96 -60
  287. package/dist/types/schemas/organizations.js +78 -27
  288. package/dist/types/schemas/organizations.js.map +1 -1
  289. package/dist/types/schemas/progress.d.ts +0 -1
  290. package/dist/types/schemas/progress.js +0 -1
  291. package/dist/types/schemas/progress.js.map +1 -1
  292. package/package.json +1 -1
  293. package/dist/automations/community-notifications.d.ts +0 -16
  294. package/dist/automations/community-notifications.js +0 -84
  295. package/dist/automations/community-notifications.js.map +0 -1
  296. package/dist/automations/seeds.d.ts +0 -17
  297. package/dist/automations/seeds.js +0 -235
  298. package/dist/automations/seeds.js.map +0 -1
  299. package/dist/identity/notifications.d.ts +0 -27
  300. package/dist/identity/notifications.js +0 -101
  301. package/dist/identity/notifications.js.map +0 -1
  302. package/dist/organizations/access-scopes.d.ts +0 -19
  303. package/dist/organizations/access-scopes.js +0 -89
  304. package/dist/organizations/access-scopes.js.map +0 -1
  305. package/dist/organizations/roles.d.ts +0 -19
  306. package/dist/organizations/roles.js +0 -82
  307. package/dist/organizations/roles.js.map +0 -1
@@ -1,17 +1,40 @@
1
1
  import { spaceSettingsDefaults } from "../types/schemas/content.js";
2
- import { ConflictError, ForbiddenError, InvalidInputError, NotFoundError } from "../shared/errors.js";
3
2
  import { noopLogger } from "../shared/logger.js";
3
+ import { ConflictError, ForbiddenError, InvalidInputError, NotFoundError } from "../shared/errors.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) {
8
10
  return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
9
11
  }
12
+ function isPublishedActivity(settings) {
13
+ return settings?.published !== false;
14
+ }
10
15
  function actorMetadata(actor) {
11
- return { actorOrgUserId: actor.id };
16
+ return { actorId: actor.id };
17
+ }
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}`;
12
34
  }
13
- function assertConfigurator(actor) {
14
- if (actor.role !== "owner" && actor.role !== "admin") throw new ForbiddenError("Only an owner or an admin can configure a community");
35
+ const EVENT_LEAD_KEYS = Object.keys(EVENT_LEADS);
36
+ function shownPublicly(item) {
37
+ return item.status === "published" || item.type === "event" && item.status === "canceled";
15
38
  }
16
39
  function sameIdSet(current, requested) {
17
40
  if (current.length !== requested.length) return false;
@@ -24,6 +47,7 @@ var ContentService = class {
24
47
  libraryAssets;
25
48
  files;
26
49
  fileUrls;
50
+ scheduler;
27
51
  logger;
28
52
  constructor(params) {
29
53
  this.repo = params.repo;
@@ -31,14 +55,49 @@ var ContentService = class {
31
55
  this.libraryAssets = params.libraryAssets;
32
56
  this.files = params.files;
33
57
  this.fileUrls = params.fileUrls;
58
+ this.scheduler = params.scheduler;
34
59
  this.logger = params.logger ?? noopLogger;
35
60
  }
61
+ require(actor, permission, on) {
62
+ this.requireAny(actor, [permission], on);
63
+ }
64
+ requireAny(actor, permissions, on) {
65
+ if (!permissions.some((permission) => actor.permissions.has(permission, on))) {
66
+ this.logger.warn("content action refused", {
67
+ orgId: actor.orgId,
68
+ actorId: actor.id,
69
+ permission: permissions.join(" or "),
70
+ contentId: on
71
+ });
72
+ throw new ForbiddenError(`You need ${permissions.join(" or ")}`);
73
+ }
74
+ }
75
+ visibleIds(actor, permissions, what) {
76
+ const wheres = permissions.map((permission) => actor.permissions.where(permission));
77
+ if (wheres.some((where) => where.org)) return;
78
+ const ids = [...new Set(wheres.flatMap((where) => where.content))];
79
+ if (ids.length === 0) {
80
+ this.logger.warn("content list refused", {
81
+ orgId: actor.orgId,
82
+ actorId: actor.id,
83
+ permission: permissions.join(" or ")
84
+ });
85
+ throw new ForbiddenError(`You cannot see ${what}`);
86
+ }
87
+ return ids;
88
+ }
36
89
  withThumbnail(item) {
37
90
  return {
38
91
  ...item,
39
- thumbnailUrl: item.thumbnailAssetId ? this.fileUrls.thumbnail(item.type, item.id) : null
92
+ thumbnailUrl: !item.thumbnailAssetId ? null : shownPublicly(item) ? this.fileUrls.publicImage(item.orgId, item.id, item.thumbnailAssetId) : this.fileUrls.thumbnail(item.type, item.id)
40
93
  };
41
94
  }
95
+ syncThumbnail(item) {
96
+ return this.files.syncPublicImage(item.orgId, item.id, shownPublicly(item) ? item.thumbnailAssetId : null);
97
+ }
98
+ withdrawThumbnail(orgId, id) {
99
+ return this.files.syncPublicImage(orgId, id, null);
100
+ }
42
101
  pageWithThumbnails(page) {
43
102
  return {
44
103
  ...page,
@@ -56,14 +115,76 @@ var ContentService = class {
56
115
  }] : [];
57
116
  });
58
117
  }
59
- async listCourses(orgId, query) {
60
- return this.pageWithThumbnails(await this.repo.listCourses(orgId, query));
118
+ async eventFiles(orgId, links) {
119
+ const ready = await this.files.readyFiles(orgId, links.map((link) => link.assetId));
120
+ return links.flatMap((link) => {
121
+ const file = ready[link.assetId];
122
+ return file ? [{
123
+ ...link,
124
+ ...file,
125
+ url: this.fileUrls.eventFile(link.eventId, link.assetId)
126
+ }] : [];
127
+ });
128
+ }
129
+ async assertAttachable(actor, itemId, requested, attached, user) {
130
+ const ids = [...new Set(requested.filter((id) => typeof id === "string"))];
131
+ if (ids.length === 0) return;
132
+ const held = new Set(await attached());
133
+ const added = ids.filter((id) => !held.has(id));
134
+ if (added.length === 0) return;
135
+ const orgId = actor.orgId;
136
+ const found = new Map((await this.libraryAssets.libraryAssetsFor(orgId, added)).map((asset) => [asset.id, asset]));
137
+ const missing = added.filter((id) => !found.has(id));
138
+ if (missing.length > 0) {
139
+ this.logger.warn("library assets refused", {
140
+ orgId,
141
+ contentId: itemId,
142
+ assetIds: missing
143
+ });
144
+ throw new ConflictError(`${user} can only use ready files from the media library. Not available: ${missing.join(", ")}`);
145
+ }
146
+ if (actor.permissions.has("media:read")) return;
147
+ const foreign = added.filter((id) => found.get(id)?.contentId !== itemId);
148
+ if (foreign.length > 0) {
149
+ this.logger.warn("library assets refused — not uploaded for this item", {
150
+ orgId,
151
+ actorId: actor.id,
152
+ contentId: itemId,
153
+ assetIds: foreign
154
+ });
155
+ throw new ForbiddenError(`You need media:read to use files that were not uploaded for this item: ${foreign.join(", ")}`);
156
+ }
157
+ }
158
+ async courseAssetIds(orgId, courseId) {
159
+ const [course, links] = await Promise.all([this.repo.findCourseById(orgId, courseId), this.repo.listActivityAssetsForCourse(orgId, courseId)]);
160
+ return [course?.thumbnailAssetId ?? null, ...links.map((link) => link.assetId)];
61
161
  }
62
- async getCourse(orgId, id) {
63
- const course = await this.repo.findCourseById(orgId, id);
162
+ async downloadAssetIds(orgId, downloadId) {
163
+ const [download, links] = await Promise.all([this.repo.getDownload(orgId, downloadId), this.repo.listDownloadAssets(orgId, downloadId)]);
164
+ return [download?.thumbnailAssetId ?? null, ...links.map((link) => link.assetId)];
165
+ }
166
+ async eventAssetIds(orgId, eventId) {
167
+ const [event, links] = await Promise.all([this.repo.findEventById(orgId, eventId), this.repo.listEventAssets(orgId, eventId)]);
168
+ if (!event) throw new NotFoundError("Event", eventId);
169
+ return [event.thumbnailAssetId, ...links.map((link) => link.assetId)];
170
+ }
171
+ async listCourses(query, actor) {
172
+ const ids = this.visibleIds(actor, ["courses:read"], "courses");
173
+ return this.pageWithThumbnails(await this.repo.listCourses(actor.orgId, query, ids));
174
+ }
175
+ async getCourse(id, actor) {
176
+ const course = await this.findCourse(id, actor);
177
+ if (!course) throw new NotFoundError("Course", id);
178
+ return course;
179
+ }
180
+ async findCourse(id, actor) {
181
+ this.require(actor, "courses:read", id);
182
+ const course = await this.repo.findCourseById(actor.orgId, id);
64
183
  return course ? this.withThumbnail(course) : null;
65
184
  }
66
- async createCourse(orgId, input) {
185
+ async createCourse(input, actor) {
186
+ this.require(actor, "courses:write");
187
+ const orgId = actor.orgId;
67
188
  const course = await this.uow.run(async ({ content, outbox }) => {
68
189
  const created = await content.createCourse(orgId, input, slugify(input.title));
69
190
  await outbox.append([contentEvents.courseCreated.make({
@@ -75,12 +196,15 @@ var ContentService = class {
75
196
  this.logger.info("course created", {
76
197
  orgId,
77
198
  courseId: course.id,
78
- title: course.title
199
+ title: course.title,
200
+ actorId: actor.id
79
201
  });
80
202
  return this.withThumbnail(course);
81
203
  }
82
- async updateCourse(orgId, id, patch) {
83
- await this.assertLibraryAssets(orgId, [patch.thumbnailAssetId], "A course");
204
+ async updateCourse(id, patch, actor) {
205
+ this.require(actor, "courses:write", id);
206
+ const orgId = actor.orgId;
207
+ await this.assertAttachable(actor, id, [patch.thumbnailAssetId], () => this.courseAssetIds(orgId, id), "A course");
84
208
  const course = await this.uow.run(async ({ content, outbox }) => {
85
209
  const updated = await content.updateCourse(orgId, id, patch);
86
210
  if (!updated) {
@@ -99,11 +223,15 @@ var ContentService = class {
99
223
  this.logger.info("course updated", {
100
224
  orgId,
101
225
  courseId: id,
102
- fields: Object.keys(patch)
226
+ fields: Object.keys(patch),
227
+ actorId: actor.id
103
228
  });
229
+ await this.syncThumbnail(course);
104
230
  return this.withThumbnail(course);
105
231
  }
106
- async patchCourseSettings(orgId, id, value) {
232
+ async patchCourseSettings(id, value, actor) {
233
+ this.require(actor, "courses:write", id);
234
+ const orgId = actor.orgId;
107
235
  const merged = await this.uow.run(async ({ content, outbox }) => {
108
236
  const course = await content.findCourseById(orgId, id);
109
237
  if (!course) {
@@ -126,11 +254,14 @@ var ContentService = class {
126
254
  this.logger.info("course settings patched", {
127
255
  orgId,
128
256
  courseId: id,
129
- value
257
+ value,
258
+ actorId: actor.id
130
259
  });
131
260
  return merged;
132
261
  }
133
- async deleteCourse(orgId, id, actor) {
262
+ async deleteCourse(id, actor) {
263
+ this.require(actor, "courses:delete", id);
264
+ const orgId = actor.orgId;
134
265
  await this.uow.run(async ({ content, outbox }) => {
135
266
  const course = await content.findCourseById(orgId, id);
136
267
  if (!course) {
@@ -149,32 +280,44 @@ var ContentService = class {
149
280
  });
150
281
  this.logger.info("course deleted", {
151
282
  orgId,
152
- courseId: id
283
+ courseId: id,
284
+ actorId: actor.id
153
285
  });
286
+ await this.withdrawThumbnail(orgId, id);
154
287
  }
155
- listCourseModules(orgId, courseId) {
156
- return this.repo.listCourseModules(orgId, courseId);
288
+ async listCourseModules(courseId, actor) {
289
+ this.require(actor, "courses:read", courseId);
290
+ return this.repo.listCourseModules(actor.orgId, courseId);
157
291
  }
158
- listCourseActivities(orgId, courseId) {
159
- return this.repo.listActivitiesForCourse(orgId, courseId);
292
+ async listCourseActivities(courseId, actor) {
293
+ this.require(actor, "courses:read", courseId);
294
+ return this.repo.listActivitiesForCourse(actor.orgId, courseId);
160
295
  }
161
- async listCourseActivityViews(orgId, courseId) {
162
- const [activities, links] = await Promise.all([this.repo.listActivitiesForCourse(orgId, courseId), this.repo.listActivityAssetsForCourse(orgId, courseId)]);
296
+ async listCourseActivityViews(courseId, actor) {
297
+ this.require(actor, "courses:read", courseId);
298
+ const [activities, links] = await Promise.all([this.repo.listActivitiesForCourse(actor.orgId, courseId), this.repo.listActivityAssetsForCourse(actor.orgId, courseId)]);
163
299
  return activities.map((activity) => ({
164
300
  ...activity,
165
301
  assetUrls: Object.fromEntries(links.filter((link) => link.activityId === activity.id).map((link) => [link.assetId, this.fileUrls.activityAsset(activity.id, link.assetId)]))
166
302
  }));
167
303
  }
168
- listCourseActivityAssets(orgId, courseId) {
169
- return this.repo.listActivityAssetsForCourse(orgId, courseId);
304
+ async listCourseActivityAssets(courseId, actor) {
305
+ this.require(actor, "courses:read", courseId);
306
+ return this.repo.listActivityAssetsForCourse(actor.orgId, courseId);
170
307
  }
171
- getActivity(orgId, activityId) {
172
- return this.repo.findActivity(orgId, activityId);
308
+ async getActivity(activityId, actor) {
309
+ const activity = await this.repo.findActivity(actor.orgId, activityId);
310
+ if (activity) this.require(actor, "courses:read", activity.courseId);
311
+ return activity;
173
312
  }
174
- getModule(orgId, moduleId) {
175
- return this.repo.findModule(orgId, moduleId);
313
+ async getModule(moduleId, actor) {
314
+ const module = await this.repo.findModule(actor.orgId, moduleId);
315
+ if (module) this.require(actor, "courses:read", module.courseId);
316
+ return module;
176
317
  }
177
- async reorderModules(orgId, courseId, orderedIds) {
318
+ async reorderModules(courseId, orderedIds, actor) {
319
+ this.require(actor, "courses:write", courseId);
320
+ const orgId = actor.orgId;
178
321
  const modules = await this.uow.run(async ({ content, outbox }) => {
179
322
  const reordered = await content.reorderModules(orgId, courseId, orderedIds);
180
323
  await outbox.append([contentEvents.modulesReordered.make({
@@ -190,7 +333,9 @@ var ContentService = class {
190
333
  });
191
334
  return modules;
192
335
  }
193
- async createModule(orgId, courseId, title) {
336
+ async createModule(courseId, title, actor) {
337
+ this.require(actor, "courses:write", courseId);
338
+ const orgId = actor.orgId;
194
339
  const { modules, created } = await this.uow.run(async ({ content, outbox }) => {
195
340
  const all = await content.createModule(orgId, courseId, title);
196
341
  const module = all.at(-1);
@@ -208,11 +353,14 @@ var ContentService = class {
208
353
  orgId,
209
354
  courseId,
210
355
  moduleId: created.id,
211
- title
356
+ title,
357
+ actorId: actor.id
212
358
  });
213
359
  return modules;
214
360
  }
215
- async updateModule(orgId, courseId, moduleId, title) {
361
+ async updateModule(courseId, moduleId, title, actor) {
362
+ this.require(actor, "courses:write", courseId);
363
+ const orgId = actor.orgId;
216
364
  const modules = await this.uow.run(async ({ content, outbox }) => {
217
365
  const existing = await content.findModule(orgId, moduleId);
218
366
  if (!existing || existing.courseId !== courseId) {
@@ -236,11 +384,14 @@ var ContentService = class {
236
384
  orgId,
237
385
  courseId,
238
386
  moduleId,
239
- title
387
+ title,
388
+ actorId: actor.id
240
389
  });
241
390
  return modules;
242
391
  }
243
- async deleteModule(orgId, courseId, moduleId) {
392
+ async deleteModule(courseId, moduleId, actor) {
393
+ this.require(actor, "courses:write", courseId);
394
+ const orgId = actor.orgId;
244
395
  const modules = await this.uow.run(async ({ content, outbox }) => {
245
396
  const module = await content.findModule(orgId, moduleId);
246
397
  if (!module || module.courseId !== courseId) {
@@ -261,11 +412,14 @@ var ContentService = class {
261
412
  this.logger.info("module deleted", {
262
413
  orgId,
263
414
  courseId,
264
- moduleId
415
+ moduleId,
416
+ actorId: actor.id
265
417
  });
266
418
  return modules;
267
419
  }
268
- async reorderActivities(orgId, courseId, moduleId, orderedIds) {
420
+ async reorderActivities(courseId, moduleId, orderedIds, actor) {
421
+ this.require(actor, "courses:write", courseId);
422
+ const orgId = actor.orgId;
269
423
  const modules = await this.uow.run(async ({ content, outbox }) => {
270
424
  const reordered = await content.reorderActivities(orgId, courseId, moduleId, orderedIds);
271
425
  const module = reordered.find((m) => m.id === moduleId);
@@ -283,22 +437,12 @@ var ContentService = class {
283
437
  });
284
438
  return modules;
285
439
  }
286
- async assertLibraryAssets(orgId, assetIds, user = "An activity") {
287
- const ids = assetIds.filter((id) => typeof id === "string");
288
- if (ids.length === 0) return;
289
- const library = new Set(await this.libraryAssets.libraryAssetsAmong(orgId, ids));
290
- const refused = ids.filter((id) => !library.has(id));
291
- if (refused.length > 0) {
292
- this.logger.warn("library assets refused", {
293
- orgId,
294
- assetIds: refused
295
- });
296
- throw new ConflictError(`${user} can only use ready files from the media library. Not available: ${refused.join(", ")}`);
297
- }
298
- }
299
- async saveActivity(orgId, courseId, moduleId, input, activityId) {
300
- await this.assertLibraryAssets(orgId, input.assetIds ?? []);
440
+ async saveActivity(courseId, moduleId, input, activityId, actor) {
441
+ this.require(actor, "courses:write", courseId);
442
+ const orgId = actor.orgId;
443
+ await this.assertAttachable(actor, courseId, input.assetIds ?? [], () => this.courseAssetIds(orgId, courseId), "An activity");
301
444
  const { modules, activity } = await this.uow.run(async ({ content, outbox }) => {
445
+ let wasPublished = false;
302
446
  if (activityId) {
303
447
  const existing = await content.findActivity(orgId, activityId);
304
448
  if (!existing || existing.moduleId !== moduleId || existing.courseId !== courseId) {
@@ -310,6 +454,7 @@ var ContentService = class {
310
454
  });
311
455
  throw new NotFoundError("Activity", activityId);
312
456
  }
457
+ wasPublished = isPublishedActivity(existing.settings);
313
458
  } else {
314
459
  const module = await content.findModule(orgId, moduleId);
315
460
  if (!module || module.courseId !== courseId) {
@@ -321,14 +466,25 @@ var ContentService = class {
321
466
  throw new NotFoundError("Module", moduleId);
322
467
  }
323
468
  }
324
- const { modules: all, activity: saved } = await content.saveActivity(orgId, courseId, moduleId, input, activityId);
325
- await outbox.append([activityId ? contentEvents.activityUpdated.make({
469
+ const { modules: all, activity: saved } = await content.saveActivity(orgId, courseId, moduleId, input, activityId ?? void 0);
470
+ const events = [activityId ? contentEvents.activityUpdated.make({
326
471
  orgId,
327
472
  data: saved
328
473
  }) : contentEvents.activityCreated.make({
329
474
  orgId,
330
475
  data: saved
331
- })]);
476
+ })];
477
+ if (!wasPublished && isPublishedActivity(saved.settings)) {
478
+ if ((await content.findCourseById(orgId, courseId))?.status === "published") events.push(contentEvents.activityPublished.make({
479
+ orgId,
480
+ data: {
481
+ activityId: saved.id,
482
+ moduleId,
483
+ courseId
484
+ }
485
+ }));
486
+ }
487
+ await outbox.append(events);
332
488
  return {
333
489
  modules: all,
334
490
  activity: saved
@@ -338,11 +494,14 @@ var ContentService = class {
338
494
  orgId,
339
495
  courseId,
340
496
  moduleId,
341
- activityId: activity.id
497
+ activityId: activity.id,
498
+ actorId: actor.id
342
499
  });
343
500
  return modules;
344
501
  }
345
- async deleteActivity(orgId, courseId, moduleId, activityId) {
502
+ async deleteActivity(courseId, moduleId, activityId, actor) {
503
+ this.require(actor, "courses:write", courseId);
504
+ const orgId = actor.orgId;
346
505
  const modules = await this.uow.run(async ({ content, outbox }) => {
347
506
  const activity = await content.findActivity(orgId, activityId);
348
507
  if (!activity || activity.moduleId !== moduleId || activity.courseId !== courseId) {
@@ -365,18 +524,28 @@ var ContentService = class {
365
524
  orgId,
366
525
  courseId,
367
526
  moduleId,
368
- activityId
527
+ activityId,
528
+ actorId: actor.id
369
529
  });
370
530
  return modules;
371
531
  }
372
- async listDownloads(orgId, query) {
373
- return this.pageWithThumbnails(await this.repo.listDownloads(orgId, query));
532
+ async listDownloads(query, actor) {
533
+ const ids = this.visibleIds(actor, ["downloads:read"], "downloads");
534
+ return this.pageWithThumbnails(await this.repo.listDownloads(actor.orgId, query, ids));
535
+ }
536
+ async getDownload(id, actor) {
537
+ const download = await this.findDownload(id, actor);
538
+ if (!download) throw new NotFoundError("Download", id);
539
+ return download;
374
540
  }
375
- async getDownload(orgId, id) {
376
- const download = await this.repo.getDownload(orgId, id);
541
+ async findDownload(id, actor) {
542
+ this.require(actor, "downloads:read", id);
543
+ const download = await this.repo.getDownload(actor.orgId, id);
377
544
  return download ? this.withThumbnail(download) : null;
378
545
  }
379
- async createDownload(orgId, input) {
546
+ async createDownload(input, actor) {
547
+ this.require(actor, "downloads:write");
548
+ const orgId = actor.orgId;
380
549
  const download = await this.uow.run(async ({ content, outbox }) => {
381
550
  const created = await content.createDownload(orgId, input, slugify(input.title));
382
551
  await outbox.append([contentEvents.downloadCreated.make({
@@ -388,12 +557,15 @@ var ContentService = class {
388
557
  this.logger.info("download created", {
389
558
  orgId,
390
559
  downloadId: download.id,
391
- title: download.title
560
+ title: download.title,
561
+ actorId: actor.id
392
562
  });
393
563
  return this.withThumbnail(download);
394
564
  }
395
- async updateDownload(orgId, id, patch) {
396
- await this.assertLibraryAssets(orgId, [patch.thumbnailAssetId], "A download");
565
+ async updateDownload(id, patch, actor) {
566
+ this.require(actor, "downloads:write", id);
567
+ const orgId = actor.orgId;
568
+ await this.assertAttachable(actor, id, [patch.thumbnailAssetId], () => this.downloadAssetIds(orgId, id), "A download");
397
569
  const download = await this.uow.run(async ({ content, outbox }) => {
398
570
  const updated = await content.updateDownload(orgId, id, patch);
399
571
  if (!updated) {
@@ -412,11 +584,15 @@ var ContentService = class {
412
584
  this.logger.info("download updated", {
413
585
  orgId,
414
586
  downloadId: id,
415
- fields: Object.keys(patch)
587
+ fields: Object.keys(patch),
588
+ actorId: actor.id
416
589
  });
590
+ await this.syncThumbnail(download);
417
591
  return this.withThumbnail(download);
418
592
  }
419
- async removeDownload(orgId, id, actor) {
593
+ async removeDownload(id, actor) {
594
+ this.require(actor, "downloads:delete", id);
595
+ const orgId = actor.orgId;
420
596
  await this.uow.run(async ({ content, outbox }) => {
421
597
  const download = await content.getDownload(orgId, id);
422
598
  if (!download) {
@@ -435,14 +611,19 @@ var ContentService = class {
435
611
  });
436
612
  this.logger.info("download deleted", {
437
613
  orgId,
438
- downloadId: id
614
+ downloadId: id,
615
+ actorId: actor.id
439
616
  });
617
+ await this.withdrawThumbnail(orgId, id);
440
618
  }
441
- async listDownloadAssets(orgId, downloadId) {
442
- return this.downloadFiles(orgId, await this.repo.listDownloadAssets(orgId, downloadId));
619
+ async listDownloadAssets(downloadId, actor) {
620
+ this.require(actor, "downloads:read", downloadId);
621
+ return this.downloadFiles(actor.orgId, await this.repo.listDownloadAssets(actor.orgId, downloadId));
443
622
  }
444
- async addDownloadAsset(orgId, downloadId, input) {
445
- await this.assertLibraryAssets(orgId, [input.assetId], "A download");
623
+ async addDownloadAsset(downloadId, input, actor) {
624
+ this.require(actor, "downloads:write", downloadId);
625
+ const orgId = actor.orgId;
626
+ await this.assertAttachable(actor, downloadId, [input.assetId], () => this.downloadAssetIds(orgId, downloadId), "A download");
446
627
  const assets = await this.uow.run(async ({ content, outbox }) => {
447
628
  const assets = await content.addDownloadAsset(orgId, downloadId, input);
448
629
  await outbox.append([contentEvents.downloadAssetAdded.make({
@@ -457,11 +638,14 @@ var ContentService = class {
457
638
  this.logger.info("download asset added", {
458
639
  orgId,
459
640
  downloadId,
460
- assetId: input.assetId
641
+ assetId: input.assetId,
642
+ actorId: actor.id
461
643
  });
462
644
  return this.downloadFiles(orgId, assets);
463
645
  }
464
- async removeDownloadAsset(orgId, downloadId, assetId) {
646
+ async removeDownloadAsset(downloadId, assetId, actor) {
647
+ this.require(actor, "downloads:write", downloadId);
648
+ const orgId = actor.orgId;
465
649
  const assets = await this.uow.run(async ({ content, outbox }) => {
466
650
  const assets = await content.removeDownloadAsset(orgId, downloadId, assetId);
467
651
  await outbox.append([contentEvents.downloadAssetRemoved.make({
@@ -476,11 +660,14 @@ var ContentService = class {
476
660
  this.logger.info("download asset removed", {
477
661
  orgId,
478
662
  downloadId,
479
- assetId
663
+ assetId,
664
+ actorId: actor.id
480
665
  });
481
666
  return this.downloadFiles(orgId, assets);
482
667
  }
483
- async renameDownloadAsset(orgId, downloadId, assetId, displayName) {
668
+ async renameDownloadAsset(downloadId, assetId, displayName, actor) {
669
+ this.require(actor, "downloads:write", downloadId);
670
+ const orgId = actor.orgId;
484
671
  const assets = await this.uow.run(async ({ content, outbox }) => {
485
672
  const assets = await content.renameDownloadAsset(orgId, downloadId, assetId, displayName);
486
673
  await outbox.append([contentEvents.downloadAssetRenamed.make({
@@ -495,13 +682,16 @@ var ContentService = class {
495
682
  this.logger.info("download asset renamed", {
496
683
  orgId,
497
684
  downloadId,
498
- assetId
685
+ assetId,
686
+ actorId: actor.id
499
687
  });
500
688
  return this.downloadFiles(orgId, assets);
501
689
  }
502
690
  /** The caller must send the COMPLETE ordered set — a partial list would
503
691
  * silently unlink whatever it omitted. */
504
- async reorderDownloadAssets(orgId, downloadId, assetIds) {
692
+ async reorderDownloadAssets(downloadId, assetIds, actor) {
693
+ this.require(actor, "downloads:write", downloadId);
694
+ const orgId = actor.orgId;
505
695
  const assets = await this.uow.run(async ({ content, outbox }) => {
506
696
  const current = await content.listDownloadAssets(orgId, downloadId);
507
697
  const currentIds = new Set(current.map((a) => a.assetId));
@@ -531,13 +721,330 @@ var ContentService = class {
531
721
  });
532
722
  return this.downloadFiles(orgId, assets);
533
723
  }
534
- listBundles(orgId, query) {
535
- return this.repo.listBundles(orgId, query);
724
+ async listEvents(query, actor) {
725
+ const ids = this.visibleIds(actor, ["events:read"], "events");
726
+ return this.pageWithThumbnails(await this.repo.listEvents(actor.orgId, query, ids));
727
+ }
728
+ async getEvent(id, actor) {
729
+ const event = await this.findEvent(id, actor);
730
+ if (!event) throw new NotFoundError("Event", id);
731
+ return event;
732
+ }
733
+ async findEvent(id, actor) {
734
+ this.require(actor, "events:read", id);
735
+ const event = await this.repo.findEventById(actor.orgId, id);
736
+ return event ? this.withThumbnail(event) : null;
737
+ }
738
+ async findEvents(ids, actor) {
739
+ for (const id of ids) this.require(actor, "events:read", id);
740
+ if (ids.length === 0) return [];
741
+ return (await this.repo.findEventsByIds(actor.orgId, ids)).map((event) => this.withThumbnail(event));
742
+ }
743
+ async createEvent(input, actor) {
744
+ this.require(actor, "events:write");
745
+ assertEventTimes(input.startsAt, input.endsAt);
746
+ const orgId = actor.orgId;
747
+ const { settings, ...fields } = input;
748
+ const event = await this.uow.run(async ({ content, outbox }) => {
749
+ const created = await content.createEvent(orgId, fields);
750
+ const event = settings ? {
751
+ ...created,
752
+ settings: await content.patchEventSettings(orgId, created.id, settings)
753
+ } : created;
754
+ await outbox.append([contentEvents.eventCreated.make({
755
+ orgId,
756
+ data: event,
757
+ metadata: actorMetadata(actor)
758
+ })]);
759
+ return event;
760
+ });
761
+ this.logger.info("event created", {
762
+ orgId,
763
+ eventId: event.id,
764
+ title: event.title,
765
+ actorId: actor.id
766
+ });
767
+ return this.withThumbnail(event);
768
+ }
769
+ async updateEvent(id, patch, actor) {
770
+ this.require(actor, "events:write", id);
771
+ const orgId = actor.orgId;
772
+ await this.assertAttachable(actor, id, [patch.thumbnailAssetId], () => this.eventAssetIds(orgId, id), "An event");
773
+ const { settings, ...fields } = patch;
774
+ const { previous, event } = await this.uow.run(async ({ content, outbox }) => {
775
+ const previous = await content.findEventById(orgId, id);
776
+ if (!previous) {
777
+ this.logger.warn("event update rejected — not found", {
778
+ orgId,
779
+ eventId: id
780
+ });
781
+ throw new NotFoundError("Event", id);
782
+ }
783
+ if (previous.status === "canceled") {
784
+ this.logger.warn("event update rejected — canceled", {
785
+ orgId,
786
+ eventId: id
787
+ });
788
+ throw new ConflictError("The event is canceled", "canceled");
789
+ }
790
+ assertEventTimes(fields.startsAt ?? previous.startsAt, fields.endsAt ?? previous.endsAt);
791
+ const updated = await content.updateEvent(orgId, id, fields);
792
+ if (!updated) throw new NotFoundError("Event", id);
793
+ const event = settings ? {
794
+ ...updated,
795
+ settings: await content.patchEventSettings(orgId, id, settings)
796
+ } : updated;
797
+ await outbox.append([contentEvents.eventUpdated.make({
798
+ orgId,
799
+ data: event,
800
+ metadata: eventChange(actor, previous, event)
801
+ })]);
802
+ return {
803
+ previous,
804
+ event
805
+ };
806
+ });
807
+ this.logger.info("event updated", {
808
+ orgId,
809
+ eventId: id,
810
+ fields: Object.keys(patch),
811
+ actorId: actor.id
812
+ });
813
+ await this.scheduleStartingSoon(orgId, previous, event);
814
+ await this.syncThumbnail(event);
815
+ return this.withThumbnail(event);
816
+ }
817
+ async cancelEvent(id, actor) {
818
+ this.require(actor, "events:write", id);
819
+ const orgId = actor.orgId;
820
+ const event = await this.uow.run(async ({ content, outbox }) => {
821
+ const previous = await content.findEventById(orgId, id);
822
+ if (!previous) {
823
+ this.logger.warn("event cancel rejected — not found", {
824
+ orgId,
825
+ eventId: id
826
+ });
827
+ throw new NotFoundError("Event", id);
828
+ }
829
+ if (previous.status === "canceled") {
830
+ this.logger.warn("event cancel rejected — already canceled", {
831
+ orgId,
832
+ eventId: id
833
+ });
834
+ throw new ConflictError("The event is already canceled", "canceled");
835
+ }
836
+ const updated = await content.updateEvent(orgId, id, { status: "canceled" });
837
+ if (!updated) throw new NotFoundError("Event", id);
838
+ await outbox.append([contentEvents.eventUpdated.make({
839
+ orgId,
840
+ data: updated,
841
+ metadata: eventChange(actor, previous, updated)
842
+ })]);
843
+ return updated;
844
+ });
845
+ this.logger.info("event canceled", {
846
+ orgId,
847
+ eventId: id,
848
+ actorId: actor.id
849
+ });
850
+ await this.cancelStartingSoon(orgId, id);
851
+ await this.syncThumbnail(event);
852
+ return this.withThumbnail(event);
853
+ }
854
+ async deleteEvent(id, actor) {
855
+ this.require(actor, "events:delete", id);
856
+ const orgId = actor.orgId;
857
+ await this.uow.run(async ({ content, outbox }) => {
858
+ const event = await content.findEventById(orgId, id);
859
+ if (!event) {
860
+ this.logger.warn("event delete rejected — not found", {
861
+ orgId,
862
+ eventId: id
863
+ });
864
+ throw new NotFoundError("Event", id);
865
+ }
866
+ const linked = await content.spacesLinkingContent(orgId, id);
867
+ if (!await content.deleteEvent(orgId, id)) throw new NotFoundError("Event", id);
868
+ await content.deleteEventSettings(orgId, id);
869
+ await outbox.append([contentEvents.eventDeleted.make({
870
+ orgId,
871
+ data: event,
872
+ metadata: actorMetadata(actor)
873
+ }), ...await this.spaceUpdates(content, orgId, linked, actor)]);
874
+ });
875
+ this.logger.info("event deleted", {
876
+ orgId,
877
+ eventId: id,
878
+ actorId: actor.id
879
+ });
880
+ await this.withdrawThumbnail(orgId, id);
881
+ await this.cancelStartingSoon(orgId, id);
536
882
  }
537
- getBundle(orgId, id) {
538
- return this.repo.findBundleById(orgId, id);
883
+ async startingSoon(orgId, eventId, lead, now) {
884
+ const event = await this.repo.findEventById(orgId, eventId);
885
+ if (!event || event.status !== "published") {
886
+ this.logger.debug("event no longer published", {
887
+ orgId,
888
+ eventId,
889
+ lead
890
+ });
891
+ return;
892
+ }
893
+ const due = new Date(event.startsAt.getTime() - EVENT_LEADS[lead]);
894
+ if (due > now) {
895
+ await this.scheduler.at(EVENT_STARTING_SOON_JOB, due, startingSoonKey(eventId, lead), {
896
+ orgId,
897
+ eventId,
898
+ lead
899
+ });
900
+ this.logger.info("event starting soon timer moved", {
901
+ orgId,
902
+ eventId,
903
+ lead,
904
+ due: due.toISOString()
905
+ });
906
+ return;
907
+ }
908
+ await this.uow.run(({ outbox }) => outbox.append([contentEvents.eventStartingSoon.make({
909
+ orgId,
910
+ data: {
911
+ eventId,
912
+ lead,
913
+ startsAt: event.startsAt
914
+ }
915
+ })]));
916
+ this.logger.info("event starting soon", {
917
+ orgId,
918
+ eventId,
919
+ lead
920
+ });
539
921
  }
540
- async createBundle(orgId, input) {
922
+ async scheduleStartingSoon(orgId, previous, event) {
923
+ if (event.status !== "published") {
924
+ if (previous.status === "published") await this.cancelStartingSoon(orgId, event.id);
925
+ return;
926
+ }
927
+ if (previous.status === "published" && previous.startsAt.getTime() === event.startsAt.getTime()) return;
928
+ const now = Date.now();
929
+ for (const lead of EVENT_LEAD_KEYS) {
930
+ const key = startingSoonKey(event.id, lead);
931
+ const runAt = new Date(event.startsAt.getTime() - EVENT_LEADS[lead]);
932
+ try {
933
+ if (runAt.getTime() > now) await this.scheduler.at(EVENT_STARTING_SOON_JOB, runAt, key, {
934
+ orgId,
935
+ eventId: event.id,
936
+ lead
937
+ });
938
+ else await this.scheduler.cancel(EVENT_STARTING_SOON_JOB, key);
939
+ } catch (err) {
940
+ this.logger.error("event starting soon timer not set", {
941
+ orgId,
942
+ eventId: event.id,
943
+ lead,
944
+ err
945
+ });
946
+ }
947
+ }
948
+ }
949
+ async cancelStartingSoon(orgId, eventId) {
950
+ for (const lead of EVENT_LEAD_KEYS) try {
951
+ await this.scheduler.cancel(EVENT_STARTING_SOON_JOB, startingSoonKey(eventId, lead));
952
+ } catch (err) {
953
+ this.logger.warn("event starting soon timer not cancelled", {
954
+ orgId,
955
+ eventId,
956
+ lead,
957
+ err
958
+ });
959
+ }
960
+ }
961
+ async listEventAssets(eventId, actor) {
962
+ this.require(actor, "events:read", eventId);
963
+ const orgId = actor.orgId;
964
+ if (!await this.repo.findEventById(orgId, eventId)) throw new NotFoundError("Event", eventId);
965
+ return this.eventFiles(orgId, await this.repo.listEventAssets(orgId, eventId));
966
+ }
967
+ async eventFilesFor(eventIds, actor) {
968
+ for (const id of eventIds) this.require(actor, "events:read", id);
969
+ const byEvent = Object.fromEntries(eventIds.map((id) => [id, []]));
970
+ if (eventIds.length === 0) return byEvent;
971
+ const files = await this.eventFiles(actor.orgId, await this.repo.listEventAssetsFor(actor.orgId, eventIds));
972
+ for (const file of files) byEvent[file.eventId]?.push(file);
973
+ return byEvent;
974
+ }
975
+ async addEventAsset(eventId, input, actor) {
976
+ this.require(actor, "events:write", eventId);
977
+ const orgId = actor.orgId;
978
+ await this.assertAttachable(actor, eventId, [input.assetId], () => this.eventAssetIds(orgId, eventId), "An event");
979
+ const assets = await this.uow.run(({ content }) => content.addEventAsset(orgId, eventId, input));
980
+ this.logger.info("event asset added", {
981
+ orgId,
982
+ eventId,
983
+ assetId: input.assetId,
984
+ actorId: actor.id
985
+ });
986
+ return this.eventFiles(orgId, assets);
987
+ }
988
+ async removeEventAsset(eventId, assetId, actor) {
989
+ this.require(actor, "events:write", eventId);
990
+ const orgId = actor.orgId;
991
+ const assets = await this.uow.run(({ content }) => content.removeEventAsset(orgId, eventId, assetId));
992
+ this.logger.info("event asset removed", {
993
+ orgId,
994
+ eventId,
995
+ assetId,
996
+ actorId: actor.id
997
+ });
998
+ return this.eventFiles(orgId, assets);
999
+ }
1000
+ async renameEventAsset(eventId, assetId, displayName, actor) {
1001
+ this.require(actor, "events:write", eventId);
1002
+ const orgId = actor.orgId;
1003
+ const assets = await this.uow.run(({ content }) => content.renameEventAsset(orgId, eventId, assetId, displayName));
1004
+ this.logger.info("event asset renamed", {
1005
+ orgId,
1006
+ eventId,
1007
+ assetId,
1008
+ actorId: actor.id
1009
+ });
1010
+ return this.eventFiles(orgId, assets);
1011
+ }
1012
+ async reorderEventAssets(eventId, assetIds, actor) {
1013
+ this.require(actor, "events:write", eventId);
1014
+ const orgId = actor.orgId;
1015
+ const assets = await this.uow.run(async ({ content }) => {
1016
+ if (!sameIdSet((await content.listEventAssets(orgId, eventId)).map((link) => link.assetId), assetIds)) {
1017
+ this.logger.warn("event asset reorder rejected — id set mismatch", {
1018
+ orgId,
1019
+ eventId,
1020
+ assetIds
1021
+ });
1022
+ throw new ConflictError("Ordered asset ids does not match the event's current assets");
1023
+ }
1024
+ return content.reorderEventAssets(orgId, eventId, assetIds);
1025
+ });
1026
+ this.logger.debug("event assets reordered", {
1027
+ orgId,
1028
+ eventId
1029
+ });
1030
+ return this.eventFiles(orgId, assets);
1031
+ }
1032
+ async listBundles(query, actor) {
1033
+ this.require(actor, "bundles:read");
1034
+ return this.repo.listBundles(actor.orgId, query);
1035
+ }
1036
+ async getBundle(id, actor) {
1037
+ const bundle = await this.findBundle(id, actor);
1038
+ if (!bundle) throw new NotFoundError("Bundle", id);
1039
+ return bundle;
1040
+ }
1041
+ async findBundle(id, actor) {
1042
+ this.require(actor, "bundles:read");
1043
+ return this.repo.findBundleById(actor.orgId, id);
1044
+ }
1045
+ async createBundle(input, actor) {
1046
+ this.require(actor, "bundles:write");
1047
+ const orgId = actor.orgId;
541
1048
  const { bundle, items } = await this.uow.run(async ({ content, outbox }) => {
542
1049
  const created = await content.createBundle(orgId, input);
543
1050
  const initial = input.contentIds?.length ? await content.listBundleItems(orgId, created.id) : [];
@@ -560,11 +1067,14 @@ var ContentService = class {
560
1067
  orgId,
561
1068
  bundleId: bundle.id,
562
1069
  name: bundle.name,
563
- itemCount: items.length
1070
+ itemCount: items.length,
1071
+ actorId: actor.id
564
1072
  });
565
1073
  return bundle;
566
1074
  }
567
- async updateBundle(orgId, id, patch) {
1075
+ async updateBundle(id, patch, actor) {
1076
+ this.require(actor, "bundles:write");
1077
+ const orgId = actor.orgId;
568
1078
  const bundle = await this.uow.run(async ({ content, outbox }) => {
569
1079
  const updated = await content.updateBundle(orgId, id, patch);
570
1080
  if (!updated) {
@@ -583,11 +1093,14 @@ var ContentService = class {
583
1093
  this.logger.info("bundle updated", {
584
1094
  orgId,
585
1095
  bundleId: id,
586
- fields: Object.keys(patch)
1096
+ fields: Object.keys(patch),
1097
+ actorId: actor.id
587
1098
  });
588
1099
  return bundle;
589
1100
  }
590
- async deleteBundle(orgId, id) {
1101
+ async deleteBundle(id, actor) {
1102
+ this.require(actor, "bundles:delete");
1103
+ const orgId = actor.orgId;
591
1104
  await this.uow.run(async ({ content, outbox }) => {
592
1105
  const bundle = await content.findBundleById(orgId, id);
593
1106
  if (!bundle) {
@@ -605,13 +1118,17 @@ var ContentService = class {
605
1118
  });
606
1119
  this.logger.info("bundle deleted", {
607
1120
  orgId,
608
- bundleId: id
1121
+ bundleId: id,
1122
+ actorId: actor.id
609
1123
  });
610
1124
  }
611
- listBundleItems(orgId, bundleId) {
612
- return this.repo.listBundleItems(orgId, bundleId);
1125
+ async listBundleItems(bundleId, actor) {
1126
+ this.require(actor, "bundles:read");
1127
+ return this.repo.listBundleItems(actor.orgId, bundleId);
613
1128
  }
614
- async addBundleItem(orgId, bundleId, contentId) {
1129
+ async addBundleItem(bundleId, contentId, actor) {
1130
+ this.require(actor, "bundles:write");
1131
+ const orgId = actor.orgId;
615
1132
  const items = await this.uow.run(async ({ content, outbox }) => {
616
1133
  const all = await content.addBundleItem(orgId, bundleId, contentId);
617
1134
  await outbox.append([contentEvents.bundleItemAdded.make({
@@ -626,11 +1143,14 @@ var ContentService = class {
626
1143
  this.logger.info("bundle item added", {
627
1144
  orgId,
628
1145
  bundleId,
629
- contentId
1146
+ contentId,
1147
+ actorId: actor.id
630
1148
  });
631
1149
  return items;
632
1150
  }
633
- async removeBundleItem(orgId, bundleId, contentId) {
1151
+ async removeBundleItem(bundleId, contentId, actor) {
1152
+ this.require(actor, "bundles:write");
1153
+ const orgId = actor.orgId;
634
1154
  const items = await this.uow.run(async ({ content, outbox }) => {
635
1155
  const all = await content.removeBundleItem(orgId, bundleId, contentId);
636
1156
  await outbox.append([contentEvents.bundleItemRemoved.make({
@@ -645,7 +1165,8 @@ var ContentService = class {
645
1165
  this.logger.info("bundle item removed", {
646
1166
  orgId,
647
1167
  bundleId,
648
- contentId
1168
+ contentId,
1169
+ actorId: actor.id
649
1170
  });
650
1171
  return items;
651
1172
  }
@@ -678,35 +1199,58 @@ var ContentService = class {
678
1199
  settings
679
1200
  };
680
1201
  }
681
- async listCommunities(orgId, query) {
682
- return this.pageWithThumbnails(await this.repo.listCommunities(orgId, query));
1202
+ async listCommunities(query, actor) {
1203
+ const ids = this.visibleIds(actor, COMMUNITY_READERS, "communities");
1204
+ return this.pageWithThumbnails(await this.repo.listCommunities(actor.orgId, query, ids));
683
1205
  }
684
- async getCommunity(orgId, id) {
685
- const community = await this.repo.findCommunityById(orgId, id);
1206
+ async getCommunity(id, actor) {
1207
+ const community = await this.findCommunity(id, actor);
1208
+ if (!community) throw new NotFoundError("Community", id);
1209
+ return community;
1210
+ }
1211
+ async findCommunity(id, actor) {
1212
+ this.requireAny(actor, COMMUNITY_READERS, id);
1213
+ const community = await this.repo.findCommunityById(actor.orgId, id);
686
1214
  return community ? this.withThumbnail(community) : null;
687
1215
  }
688
- listPublishedCommunityIds(orgId) {
689
- return this.repo.listPublishedCommunityIds(orgId);
1216
+ communitiesWithDigest() {
1217
+ return this.repo.communitiesWithDigest();
690
1218
  }
691
- listCommunitiesById(orgId, ids) {
692
- return ids.length ? this.repo.findCommunitiesByIds(orgId, ids) : Promise.resolve([]);
1219
+ async listCommunitiesById(ids, actor) {
1220
+ if (!actor.permissions.anywhere("communities:read")) {
1221
+ this.logger.warn("content list refused", {
1222
+ orgId: actor.orgId,
1223
+ actorId: actor.id,
1224
+ permission: "communities:read"
1225
+ });
1226
+ throw new ForbiddenError("You cannot see communities");
1227
+ }
1228
+ const visible = ids.filter((id) => actor.permissions.has("communities:read", id));
1229
+ return visible.length ? this.repo.findCommunitiesByIds(actor.orgId, visible) : [];
693
1230
  }
694
- getSpace(orgId, spaceId) {
695
- return this.repo.findSpace(orgId, spaceId);
1231
+ async getSpace(spaceId, actor) {
1232
+ const space = await this.repo.findSpace(actor.orgId, spaceId);
1233
+ if (space) this.require(actor, "communities:read", space.communityId);
1234
+ return space;
696
1235
  }
697
- async getCommunitySpace(orgId, communityId, spaceId) {
698
- const space = await this.repo.findSpace(orgId, spaceId);
699
- return space?.communityId === communityId ? space : null;
1236
+ async getCommunitySpace(communityId, spaceId, actor) {
1237
+ this.require(actor, "communities:read", communityId);
1238
+ const space = await this.repo.findSpace(actor.orgId, spaceId);
1239
+ if (space?.communityId !== communityId) throw new NotFoundError("Space", spaceId);
1240
+ return space;
700
1241
  }
701
- async listSpaces(orgId, communityId) {
702
- if (!await this.getCommunity(orgId, communityId)) throw new NotFoundError("Community", communityId);
703
- return this.repo.listSpaces(orgId, communityId);
1242
+ async listSpaces(communityId, actor) {
1243
+ this.requireAny(actor, COMMUNITY_READERS, communityId);
1244
+ if (!await this.repo.findCommunityById(actor.orgId, communityId)) throw new NotFoundError("Community", communityId);
1245
+ return this.repo.listSpaces(actor.orgId, communityId);
704
1246
  }
705
- async getDmSettings(orgId, communityId) {
706
- return (await this.repo.findCommunityById(orgId, communityId))?.settings.dm ?? null;
1247
+ async getDmSettings(communityId, actor) {
1248
+ this.require(actor, "communities:read", communityId);
1249
+ return (await this.repo.findCommunityById(actor.orgId, communityId))?.settings.dm ?? null;
707
1250
  }
708
- async createCommunity(orgId, input, actor) {
709
- assertConfigurator(actor);
1251
+ async createCommunity(input, actor) {
1252
+ this.require(actor, "communities:write");
1253
+ const orgId = actor.orgId;
710
1254
  const community = await this.uow.run(async ({ content, outbox }) => {
711
1255
  const created = await content.createCommunity(orgId, input, slugify(input.title));
712
1256
  await outbox.append([contentEvents.communityCreated.make({
@@ -723,9 +1267,10 @@ var ContentService = class {
723
1267
  });
724
1268
  return this.withThumbnail(community);
725
1269
  }
726
- async updateCommunity(orgId, id, patch, actor) {
727
- assertConfigurator(actor);
728
- await this.assertLibraryAssets(orgId, [patch.thumbnailAssetId], "A community");
1270
+ async updateCommunity(id, patch, actor) {
1271
+ this.require(actor, "communities:write", id);
1272
+ const orgId = actor.orgId;
1273
+ await this.assertAttachable(actor, id, [patch.thumbnailAssetId], async () => [(await this.repo.findCommunityById(orgId, id))?.thumbnailAssetId ?? null], "A community");
729
1274
  const community = await this.uow.run(async ({ content, outbox }) => {
730
1275
  const updated = await content.updateCommunity(orgId, id, patch);
731
1276
  if (!updated) {
@@ -748,16 +1293,18 @@ var ContentService = class {
748
1293
  communityId: id,
749
1294
  actorId: actor.id
750
1295
  });
1296
+ await this.syncThumbnail(community);
751
1297
  return this.withThumbnail(community);
752
1298
  }
753
- publishCommunity(orgId, id, actor) {
754
- return this.updateCommunity(orgId, id, { status: "published" }, actor);
1299
+ publishCommunity(id, actor) {
1300
+ return this.updateCommunity(id, { status: "published" }, actor);
755
1301
  }
756
- unpublishCommunity(orgId, id, actor) {
757
- return this.updateCommunity(orgId, id, { status: "draft" }, actor);
1302
+ unpublishCommunity(id, actor) {
1303
+ return this.updateCommunity(id, { status: "draft" }, actor);
758
1304
  }
759
- async deleteCommunity(orgId, id, actor) {
760
- assertConfigurator(actor);
1305
+ async deleteCommunity(id, actor) {
1306
+ this.require(actor, "communities:delete", id);
1307
+ const orgId = actor.orgId;
761
1308
  await this.uow.run(async ({ content, outbox }) => {
762
1309
  const community = await content.findCommunityById(orgId, id);
763
1310
  if (!community) {
@@ -782,13 +1329,15 @@ var ContentService = class {
782
1329
  communityId: id,
783
1330
  actorId: actor.id
784
1331
  });
1332
+ await this.withdrawThumbnail(orgId, id);
785
1333
  }
786
- async patchDmSettings(orgId, id, patch, actor) {
787
- assertConfigurator(actor);
1334
+ async patchCommunitySettings(id, patch, actor) {
1335
+ this.require(actor, "communities:write", id);
1336
+ const orgId = actor.orgId;
788
1337
  const community = await this.uow.run(async ({ content, outbox }) => {
789
- const updated = await content.patchDmSettings(orgId, id, patch);
1338
+ const updated = await content.patchCommunitySettings(orgId, id, patch);
790
1339
  if (!updated) {
791
- this.logger.warn("community dm settings patch rejected — not found", {
1340
+ this.logger.warn("community settings patch rejected — not found", {
792
1341
  orgId,
793
1342
  communityId: id,
794
1343
  actorId: actor.id
@@ -802,15 +1351,16 @@ var ContentService = class {
802
1351
  })]);
803
1352
  return updated;
804
1353
  });
805
- this.logger.info("community dm settings patched", {
1354
+ this.logger.info("community settings patched", {
806
1355
  orgId,
807
1356
  communityId: id,
808
1357
  actorId: actor.id
809
1358
  });
810
1359
  return this.withThumbnail(community);
811
1360
  }
812
- async createSpace(orgId, communityId, input, actor) {
813
- assertConfigurator(actor);
1361
+ async createSpace(communityId, input, actor) {
1362
+ this.require(actor, "communities:write", communityId);
1363
+ const orgId = actor.orgId;
814
1364
  const resolved = this.resolvedSpace(input);
815
1365
  const space = await this.uow.run(async ({ content, outbox }) => {
816
1366
  if (!await content.findCommunityById(orgId, communityId)) {
@@ -842,8 +1392,9 @@ var ContentService = class {
842
1392
  });
843
1393
  return space;
844
1394
  }
845
- async updateSpace(orgId, communityId, spaceId, patch, actor) {
846
- assertConfigurator(actor);
1395
+ async updateSpace(communityId, spaceId, patch, actor) {
1396
+ this.require(actor, "communities:write", communityId);
1397
+ const orgId = actor.orgId;
847
1398
  const space = await this.uow.run(async ({ content, outbox }) => {
848
1399
  const current = await content.findSpace(orgId, spaceId);
849
1400
  if (!current || current.communityId !== communityId) {
@@ -878,8 +1429,9 @@ var ContentService = class {
878
1429
  });
879
1430
  return space;
880
1431
  }
881
- async deleteSpace(orgId, communityId, spaceId, actor) {
882
- assertConfigurator(actor);
1432
+ async deleteSpace(communityId, spaceId, actor) {
1433
+ this.require(actor, "communities:write", communityId);
1434
+ const orgId = actor.orgId;
883
1435
  await this.uow.run(async ({ content, outbox }) => {
884
1436
  const space = await content.findSpace(orgId, spaceId);
885
1437
  if (!space || space.communityId !== communityId) {
@@ -905,8 +1457,9 @@ var ContentService = class {
905
1457
  actorId: actor.id
906
1458
  });
907
1459
  }
908
- async reorderSpaces(orgId, communityId, orderedIds, actor) {
909
- assertConfigurator(actor);
1460
+ async reorderSpaces(communityId, orderedIds, actor) {
1461
+ this.require(actor, "communities:write", communityId);
1462
+ const orgId = actor.orgId;
910
1463
  const spaces = await this.uow.run(async ({ content, outbox }) => {
911
1464
  if (!await content.findCommunityById(orgId, communityId)) {
912
1465
  this.logger.warn("spaces reorder rejected — community not found", {
@@ -932,8 +1485,9 @@ var ContentService = class {
932
1485
  });
933
1486
  return spaces;
934
1487
  }
935
- async setSpaceTopics(orgId, communityId, spaceId, input, actor) {
936
- assertConfigurator(actor);
1488
+ async setSpaceTopics(communityId, spaceId, input, actor) {
1489
+ this.require(actor, "communities:write", communityId);
1490
+ const orgId = actor.orgId;
937
1491
  const names = input.topics.map((topic) => topic.name);
938
1492
  if (new Set(names).size !== names.length) throw new InvalidInputError("A space topic name is already used");
939
1493
  const space = await this.uow.run(async ({ content, outbox }) => {
@@ -968,8 +1522,9 @@ var ContentService = class {
968
1522
  });
969
1523
  return space;
970
1524
  }
971
- async addSpaceLink(orgId, communityId, spaceId, contentId, actor) {
972
- assertConfigurator(actor);
1525
+ async addSpaceLink(communityId, spaceId, contentId, actor) {
1526
+ this.require(actor, "communities:write", communityId);
1527
+ const orgId = actor.orgId;
973
1528
  const space = await this.uow.run(async ({ content, outbox }) => {
974
1529
  const current = await content.findSpace(orgId, spaceId);
975
1530
  if (!current || current.communityId !== communityId) {
@@ -999,8 +1554,9 @@ var ContentService = class {
999
1554
  });
1000
1555
  return space;
1001
1556
  }
1002
- async removeSpaceLink(orgId, communityId, spaceId, linkId, actor) {
1003
- assertConfigurator(actor);
1557
+ async removeSpaceLink(communityId, spaceId, linkId, actor) {
1558
+ this.require(actor, "communities:write", communityId);
1559
+ const orgId = actor.orgId;
1004
1560
  const space = await this.uow.run(async ({ content, outbox }) => {
1005
1561
  const current = await content.findSpace(orgId, spaceId);
1006
1562
  if (!current || current.communityId !== communityId) {
@@ -1029,8 +1585,9 @@ var ContentService = class {
1029
1585
  });
1030
1586
  return space;
1031
1587
  }
1032
- async reorderSpaceLinks(orgId, communityId, spaceId, orderedIds, actor) {
1033
- assertConfigurator(actor);
1588
+ async reorderSpaceLinks(communityId, spaceId, orderedIds, actor) {
1589
+ this.require(actor, "communities:write", communityId);
1590
+ const orgId = actor.orgId;
1034
1591
  const space = await this.uow.run(async ({ content, outbox }) => {
1035
1592
  const current = await content.findSpace(orgId, spaceId);
1036
1593
  if (!current || current.communityId !== communityId) {