@headless-lms/core 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (249) hide show
  1. package/dist/assets/events.d.ts +27 -21
  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 +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 +230 -56
  10. package/dist/assets/service.js.map +1 -1
  11. package/dist/automations/actions.js.map +1 -1
  12. package/dist/automations/catalog.js +69 -4
  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 +3 -3
  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 +13 -13
  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 +91 -62
  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 +413 -72
  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 +99 -67
  72. package/dist/content/service.js +680 -139
  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 +393 -161
  83. package/dist/discussion/chats.js.map +1 -1
  84. package/dist/discussion/events.d.ts +77 -39
  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 +268 -154
  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 +252 -188
  104. package/dist/discussion/service.js.map +1 -1
  105. package/dist/discussion/subscriptions.d.ts +8 -8
  106. package/dist/discussion/subscriptions.js +29 -13
  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 +20 -20
  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 +370 -163
  121. package/dist/entitlements/service.js.map +1 -1
  122. package/dist/identity/devices.d.ts +3 -3
  123. package/dist/identity/devices.js +7 -5
  124. package/dist/identity/devices.js.map +1 -1
  125. package/dist/identity/events.d.ts +2 -2
  126. package/dist/identity/index.d.ts +2 -2
  127. package/dist/identity/index.js +2 -2
  128. package/dist/identity/notification-settings.d.ts +1 -1
  129. package/dist/identity/notification-settings.js +3 -2
  130. package/dist/identity/notification-settings.js.map +1 -1
  131. package/dist/identity/ports.d.ts +7 -7
  132. package/dist/identity/service.d.ts +8 -7
  133. package/dist/identity/service.js +17 -13
  134. package/dist/identity/service.js.map +1 -1
  135. package/dist/integrations/events.d.ts +3 -3
  136. package/dist/integrations/ports.d.ts +9 -8
  137. package/dist/integrations/service.d.ts +9 -8
  138. package/dist/integrations/service.js +33 -16
  139. package/dist/integrations/service.js.map +1 -1
  140. package/dist/organizations/default-roles.d.ts +22 -0
  141. package/dist/organizations/default-roles.js +89 -0
  142. package/dist/organizations/default-roles.js.map +1 -0
  143. package/dist/organizations/effective-permissions.d.ts +45 -0
  144. package/dist/organizations/effective-permissions.js +113 -0
  145. package/dist/organizations/effective-permissions.js.map +1 -0
  146. package/dist/organizations/events.d.ts +320 -181
  147. package/dist/organizations/events.js +90 -18
  148. package/dist/organizations/events.js.map +1 -1
  149. package/dist/organizations/index.d.ts +14 -10
  150. package/dist/organizations/index.js +7 -3
  151. package/dist/organizations/members.d.ts +66 -8
  152. package/dist/organizations/members.js.map +1 -1
  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 +184 -65
  166. package/dist/organizations/service.d.ts +102 -35
  167. package/dist/organizations/service.js +1245 -328
  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 +23 -8
  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 +26 -5
  193. package/dist/reporting/member/ports.d.ts +48 -29
  194. package/dist/reporting/member/service.d.ts +56 -29
  195. package/dist/reporting/member/service.js +328 -113
  196. package/dist/reporting/member/service.js.map +1 -1
  197. package/dist/shared/actor.d.ts +22 -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 +537 -13
  224. package/dist/types/schemas/content.js +201 -7
  225. package/dist/types/schemas/content.js.map +1 -1
  226. package/dist/types/schemas/discussion.d.ts +282 -59
  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 +14 -1
  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 +13 -12
  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 +96 -60
  238. package/dist/types/schemas/organizations.js +78 -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
@@ -1,7 +1,9 @@
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) {
@@ -11,10 +13,28 @@ 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 };
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
+ };
15
28
  }
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");
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";
18
38
  }
19
39
  function sameIdSet(current, requested) {
20
40
  if (current.length !== requested.length) return false;
@@ -27,6 +47,7 @@ var ContentService = class {
27
47
  libraryAssets;
28
48
  files;
29
49
  fileUrls;
50
+ scheduler;
30
51
  logger;
31
52
  constructor(params) {
32
53
  this.repo = params.repo;
@@ -34,14 +55,49 @@ var ContentService = class {
34
55
  this.libraryAssets = params.libraryAssets;
35
56
  this.files = params.files;
36
57
  this.fileUrls = params.fileUrls;
58
+ this.scheduler = params.scheduler;
37
59
  this.logger = params.logger ?? noopLogger;
38
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
+ }
39
89
  withThumbnail(item) {
40
90
  return {
41
91
  ...item,
42
- 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)
43
93
  };
44
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
+ }
45
101
  pageWithThumbnails(page) {
46
102
  return {
47
103
  ...page,
@@ -59,14 +115,76 @@ var ContentService = class {
59
115
  }] : [];
60
116
  });
61
117
  }
62
- async listCourses(orgId, query) {
63
- 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
+ });
64
128
  }
65
- async getCourse(orgId, id) {
66
- const course = await this.repo.findCourseById(orgId, id);
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)];
161
+ }
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);
67
183
  return course ? this.withThumbnail(course) : null;
68
184
  }
69
- async createCourse(orgId, input) {
185
+ async createCourse(input, actor) {
186
+ this.require(actor, "courses:write");
187
+ const orgId = actor.orgId;
70
188
  const course = await this.uow.run(async ({ content, outbox }) => {
71
189
  const created = await content.createCourse(orgId, input, slugify(input.title));
72
190
  await outbox.append([contentEvents.courseCreated.make({
@@ -78,12 +196,15 @@ var ContentService = class {
78
196
  this.logger.info("course created", {
79
197
  orgId,
80
198
  courseId: course.id,
81
- title: course.title
199
+ title: course.title,
200
+ actorId: actor.id
82
201
  });
83
202
  return this.withThumbnail(course);
84
203
  }
85
- async updateCourse(orgId, id, patch) {
86
- 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");
87
208
  const course = await this.uow.run(async ({ content, outbox }) => {
88
209
  const updated = await content.updateCourse(orgId, id, patch);
89
210
  if (!updated) {
@@ -102,11 +223,15 @@ var ContentService = class {
102
223
  this.logger.info("course updated", {
103
224
  orgId,
104
225
  courseId: id,
105
- fields: Object.keys(patch)
226
+ fields: Object.keys(patch),
227
+ actorId: actor.id
106
228
  });
229
+ await this.syncThumbnail(course);
107
230
  return this.withThumbnail(course);
108
231
  }
109
- async patchCourseSettings(orgId, id, value) {
232
+ async patchCourseSettings(id, value, actor) {
233
+ this.require(actor, "courses:write", id);
234
+ const orgId = actor.orgId;
110
235
  const merged = await this.uow.run(async ({ content, outbox }) => {
111
236
  const course = await content.findCourseById(orgId, id);
112
237
  if (!course) {
@@ -129,11 +254,14 @@ var ContentService = class {
129
254
  this.logger.info("course settings patched", {
130
255
  orgId,
131
256
  courseId: id,
132
- value
257
+ value,
258
+ actorId: actor.id
133
259
  });
134
260
  return merged;
135
261
  }
136
- async deleteCourse(orgId, id, actor) {
262
+ async deleteCourse(id, actor) {
263
+ this.require(actor, "courses:delete", id);
264
+ const orgId = actor.orgId;
137
265
  await this.uow.run(async ({ content, outbox }) => {
138
266
  const course = await content.findCourseById(orgId, id);
139
267
  if (!course) {
@@ -152,32 +280,44 @@ var ContentService = class {
152
280
  });
153
281
  this.logger.info("course deleted", {
154
282
  orgId,
155
- courseId: id
283
+ courseId: id,
284
+ actorId: actor.id
156
285
  });
286
+ await this.withdrawThumbnail(orgId, id);
157
287
  }
158
- listCourseModules(orgId, courseId) {
159
- 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);
160
291
  }
161
- listCourseActivities(orgId, courseId) {
162
- 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);
163
295
  }
164
- async listCourseActivityViews(orgId, courseId) {
165
- 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)]);
166
299
  return activities.map((activity) => ({
167
300
  ...activity,
168
301
  assetUrls: Object.fromEntries(links.filter((link) => link.activityId === activity.id).map((link) => [link.assetId, this.fileUrls.activityAsset(activity.id, link.assetId)]))
169
302
  }));
170
303
  }
171
- listCourseActivityAssets(orgId, courseId) {
172
- 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);
173
307
  }
174
- getActivity(orgId, activityId) {
175
- 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;
176
312
  }
177
- getModule(orgId, moduleId) {
178
- 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;
179
317
  }
180
- async reorderModules(orgId, courseId, orderedIds) {
318
+ async reorderModules(courseId, orderedIds, actor) {
319
+ this.require(actor, "courses:write", courseId);
320
+ const orgId = actor.orgId;
181
321
  const modules = await this.uow.run(async ({ content, outbox }) => {
182
322
  const reordered = await content.reorderModules(orgId, courseId, orderedIds);
183
323
  await outbox.append([contentEvents.modulesReordered.make({
@@ -193,7 +333,9 @@ var ContentService = class {
193
333
  });
194
334
  return modules;
195
335
  }
196
- async createModule(orgId, courseId, title) {
336
+ async createModule(courseId, title, actor) {
337
+ this.require(actor, "courses:write", courseId);
338
+ const orgId = actor.orgId;
197
339
  const { modules, created } = await this.uow.run(async ({ content, outbox }) => {
198
340
  const all = await content.createModule(orgId, courseId, title);
199
341
  const module = all.at(-1);
@@ -211,11 +353,14 @@ var ContentService = class {
211
353
  orgId,
212
354
  courseId,
213
355
  moduleId: created.id,
214
- title
356
+ title,
357
+ actorId: actor.id
215
358
  });
216
359
  return modules;
217
360
  }
218
- 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;
219
364
  const modules = await this.uow.run(async ({ content, outbox }) => {
220
365
  const existing = await content.findModule(orgId, moduleId);
221
366
  if (!existing || existing.courseId !== courseId) {
@@ -239,11 +384,14 @@ var ContentService = class {
239
384
  orgId,
240
385
  courseId,
241
386
  moduleId,
242
- title
387
+ title,
388
+ actorId: actor.id
243
389
  });
244
390
  return modules;
245
391
  }
246
- async deleteModule(orgId, courseId, moduleId) {
392
+ async deleteModule(courseId, moduleId, actor) {
393
+ this.require(actor, "courses:write", courseId);
394
+ const orgId = actor.orgId;
247
395
  const modules = await this.uow.run(async ({ content, outbox }) => {
248
396
  const module = await content.findModule(orgId, moduleId);
249
397
  if (!module || module.courseId !== courseId) {
@@ -264,11 +412,14 @@ var ContentService = class {
264
412
  this.logger.info("module deleted", {
265
413
  orgId,
266
414
  courseId,
267
- moduleId
415
+ moduleId,
416
+ actorId: actor.id
268
417
  });
269
418
  return modules;
270
419
  }
271
- 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;
272
423
  const modules = await this.uow.run(async ({ content, outbox }) => {
273
424
  const reordered = await content.reorderActivities(orgId, courseId, moduleId, orderedIds);
274
425
  const module = reordered.find((m) => m.id === moduleId);
@@ -286,21 +437,10 @@ var ContentService = class {
286
437
  });
287
438
  return modules;
288
439
  }
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 ?? []);
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");
304
444
  const { modules, activity } = await this.uow.run(async ({ content, outbox }) => {
305
445
  let wasPublished = false;
306
446
  if (activityId) {
@@ -326,7 +466,7 @@ var ContentService = class {
326
466
  throw new NotFoundError("Module", moduleId);
327
467
  }
328
468
  }
329
- const { modules: all, activity: saved } = await content.saveActivity(orgId, courseId, moduleId, input, activityId);
469
+ const { modules: all, activity: saved } = await content.saveActivity(orgId, courseId, moduleId, input, activityId ?? void 0);
330
470
  const events = [activityId ? contentEvents.activityUpdated.make({
331
471
  orgId,
332
472
  data: saved
@@ -354,11 +494,14 @@ var ContentService = class {
354
494
  orgId,
355
495
  courseId,
356
496
  moduleId,
357
- activityId: activity.id
497
+ activityId: activity.id,
498
+ actorId: actor.id
358
499
  });
359
500
  return modules;
360
501
  }
361
- 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;
362
505
  const modules = await this.uow.run(async ({ content, outbox }) => {
363
506
  const activity = await content.findActivity(orgId, activityId);
364
507
  if (!activity || activity.moduleId !== moduleId || activity.courseId !== courseId) {
@@ -381,18 +524,28 @@ var ContentService = class {
381
524
  orgId,
382
525
  courseId,
383
526
  moduleId,
384
- activityId
527
+ activityId,
528
+ actorId: actor.id
385
529
  });
386
530
  return modules;
387
531
  }
388
- async listDownloads(orgId, query) {
389
- 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));
390
535
  }
391
- async getDownload(orgId, id) {
392
- const download = await this.repo.getDownload(orgId, id);
536
+ async getDownload(id, actor) {
537
+ const download = await this.findDownload(id, actor);
538
+ if (!download) throw new NotFoundError("Download", id);
539
+ return download;
540
+ }
541
+ async findDownload(id, actor) {
542
+ this.require(actor, "downloads:read", id);
543
+ const download = await this.repo.getDownload(actor.orgId, id);
393
544
  return download ? this.withThumbnail(download) : null;
394
545
  }
395
- async createDownload(orgId, input) {
546
+ async createDownload(input, actor) {
547
+ this.require(actor, "downloads:write");
548
+ const orgId = actor.orgId;
396
549
  const download = await this.uow.run(async ({ content, outbox }) => {
397
550
  const created = await content.createDownload(orgId, input, slugify(input.title));
398
551
  await outbox.append([contentEvents.downloadCreated.make({
@@ -404,12 +557,15 @@ var ContentService = class {
404
557
  this.logger.info("download created", {
405
558
  orgId,
406
559
  downloadId: download.id,
407
- title: download.title
560
+ title: download.title,
561
+ actorId: actor.id
408
562
  });
409
563
  return this.withThumbnail(download);
410
564
  }
411
- async updateDownload(orgId, id, patch) {
412
- 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");
413
569
  const download = await this.uow.run(async ({ content, outbox }) => {
414
570
  const updated = await content.updateDownload(orgId, id, patch);
415
571
  if (!updated) {
@@ -428,11 +584,15 @@ var ContentService = class {
428
584
  this.logger.info("download updated", {
429
585
  orgId,
430
586
  downloadId: id,
431
- fields: Object.keys(patch)
587
+ fields: Object.keys(patch),
588
+ actorId: actor.id
432
589
  });
590
+ await this.syncThumbnail(download);
433
591
  return this.withThumbnail(download);
434
592
  }
435
- async removeDownload(orgId, id, actor) {
593
+ async removeDownload(id, actor) {
594
+ this.require(actor, "downloads:delete", id);
595
+ const orgId = actor.orgId;
436
596
  await this.uow.run(async ({ content, outbox }) => {
437
597
  const download = await content.getDownload(orgId, id);
438
598
  if (!download) {
@@ -451,14 +611,19 @@ var ContentService = class {
451
611
  });
452
612
  this.logger.info("download deleted", {
453
613
  orgId,
454
- downloadId: id
614
+ downloadId: id,
615
+ actorId: actor.id
455
616
  });
617
+ await this.withdrawThumbnail(orgId, id);
456
618
  }
457
- async listDownloadAssets(orgId, downloadId) {
458
- 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));
459
622
  }
460
- async addDownloadAsset(orgId, downloadId, input) {
461
- 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");
462
627
  const assets = await this.uow.run(async ({ content, outbox }) => {
463
628
  const assets = await content.addDownloadAsset(orgId, downloadId, input);
464
629
  await outbox.append([contentEvents.downloadAssetAdded.make({
@@ -473,11 +638,14 @@ var ContentService = class {
473
638
  this.logger.info("download asset added", {
474
639
  orgId,
475
640
  downloadId,
476
- assetId: input.assetId
641
+ assetId: input.assetId,
642
+ actorId: actor.id
477
643
  });
478
644
  return this.downloadFiles(orgId, assets);
479
645
  }
480
- async removeDownloadAsset(orgId, downloadId, assetId) {
646
+ async removeDownloadAsset(downloadId, assetId, actor) {
647
+ this.require(actor, "downloads:write", downloadId);
648
+ const orgId = actor.orgId;
481
649
  const assets = await this.uow.run(async ({ content, outbox }) => {
482
650
  const assets = await content.removeDownloadAsset(orgId, downloadId, assetId);
483
651
  await outbox.append([contentEvents.downloadAssetRemoved.make({
@@ -492,11 +660,14 @@ var ContentService = class {
492
660
  this.logger.info("download asset removed", {
493
661
  orgId,
494
662
  downloadId,
495
- assetId
663
+ assetId,
664
+ actorId: actor.id
496
665
  });
497
666
  return this.downloadFiles(orgId, assets);
498
667
  }
499
- 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;
500
671
  const assets = await this.uow.run(async ({ content, outbox }) => {
501
672
  const assets = await content.renameDownloadAsset(orgId, downloadId, assetId, displayName);
502
673
  await outbox.append([contentEvents.downloadAssetRenamed.make({
@@ -511,13 +682,16 @@ var ContentService = class {
511
682
  this.logger.info("download asset renamed", {
512
683
  orgId,
513
684
  downloadId,
514
- assetId
685
+ assetId,
686
+ actorId: actor.id
515
687
  });
516
688
  return this.downloadFiles(orgId, assets);
517
689
  }
518
690
  /** The caller must send the COMPLETE ordered set — a partial list would
519
691
  * silently unlink whatever it omitted. */
520
- async reorderDownloadAssets(orgId, downloadId, assetIds) {
692
+ async reorderDownloadAssets(downloadId, assetIds, actor) {
693
+ this.require(actor, "downloads:write", downloadId);
694
+ const orgId = actor.orgId;
521
695
  const assets = await this.uow.run(async ({ content, outbox }) => {
522
696
  const current = await content.listDownloadAssets(orgId, downloadId);
523
697
  const currentIds = new Set(current.map((a) => a.assetId));
@@ -547,13 +721,330 @@ var ContentService = class {
547
721
  });
548
722
  return this.downloadFiles(orgId, assets);
549
723
  }
550
- listBundles(orgId, query) {
551
- 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);
552
882
  }
553
- getBundle(orgId, id) {
554
- 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
+ });
555
921
  }
556
- 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;
557
1048
  const { bundle, items } = await this.uow.run(async ({ content, outbox }) => {
558
1049
  const created = await content.createBundle(orgId, input);
559
1050
  const initial = input.contentIds?.length ? await content.listBundleItems(orgId, created.id) : [];
@@ -576,11 +1067,14 @@ var ContentService = class {
576
1067
  orgId,
577
1068
  bundleId: bundle.id,
578
1069
  name: bundle.name,
579
- itemCount: items.length
1070
+ itemCount: items.length,
1071
+ actorId: actor.id
580
1072
  });
581
1073
  return bundle;
582
1074
  }
583
- async updateBundle(orgId, id, patch) {
1075
+ async updateBundle(id, patch, actor) {
1076
+ this.require(actor, "bundles:write");
1077
+ const orgId = actor.orgId;
584
1078
  const bundle = await this.uow.run(async ({ content, outbox }) => {
585
1079
  const updated = await content.updateBundle(orgId, id, patch);
586
1080
  if (!updated) {
@@ -599,11 +1093,14 @@ var ContentService = class {
599
1093
  this.logger.info("bundle updated", {
600
1094
  orgId,
601
1095
  bundleId: id,
602
- fields: Object.keys(patch)
1096
+ fields: Object.keys(patch),
1097
+ actorId: actor.id
603
1098
  });
604
1099
  return bundle;
605
1100
  }
606
- async deleteBundle(orgId, id) {
1101
+ async deleteBundle(id, actor) {
1102
+ this.require(actor, "bundles:delete");
1103
+ const orgId = actor.orgId;
607
1104
  await this.uow.run(async ({ content, outbox }) => {
608
1105
  const bundle = await content.findBundleById(orgId, id);
609
1106
  if (!bundle) {
@@ -621,13 +1118,17 @@ var ContentService = class {
621
1118
  });
622
1119
  this.logger.info("bundle deleted", {
623
1120
  orgId,
624
- bundleId: id
1121
+ bundleId: id,
1122
+ actorId: actor.id
625
1123
  });
626
1124
  }
627
- listBundleItems(orgId, bundleId) {
628
- 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);
629
1128
  }
630
- async addBundleItem(orgId, bundleId, contentId) {
1129
+ async addBundleItem(bundleId, contentId, actor) {
1130
+ this.require(actor, "bundles:write");
1131
+ const orgId = actor.orgId;
631
1132
  const items = await this.uow.run(async ({ content, outbox }) => {
632
1133
  const all = await content.addBundleItem(orgId, bundleId, contentId);
633
1134
  await outbox.append([contentEvents.bundleItemAdded.make({
@@ -642,11 +1143,14 @@ var ContentService = class {
642
1143
  this.logger.info("bundle item added", {
643
1144
  orgId,
644
1145
  bundleId,
645
- contentId
1146
+ contentId,
1147
+ actorId: actor.id
646
1148
  });
647
1149
  return items;
648
1150
  }
649
- async removeBundleItem(orgId, bundleId, contentId) {
1151
+ async removeBundleItem(bundleId, contentId, actor) {
1152
+ this.require(actor, "bundles:write");
1153
+ const orgId = actor.orgId;
650
1154
  const items = await this.uow.run(async ({ content, outbox }) => {
651
1155
  const all = await content.removeBundleItem(orgId, bundleId, contentId);
652
1156
  await outbox.append([contentEvents.bundleItemRemoved.make({
@@ -661,7 +1165,8 @@ var ContentService = class {
661
1165
  this.logger.info("bundle item removed", {
662
1166
  orgId,
663
1167
  bundleId,
664
- contentId
1168
+ contentId,
1169
+ actorId: actor.id
665
1170
  });
666
1171
  return items;
667
1172
  }
@@ -694,35 +1199,58 @@ var ContentService = class {
694
1199
  settings
695
1200
  };
696
1201
  }
697
- async listCommunities(orgId, query) {
698
- 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));
699
1205
  }
700
- async getCommunity(orgId, id) {
701
- 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);
702
1214
  return community ? this.withThumbnail(community) : null;
703
1215
  }
704
1216
  communitiesWithDigest() {
705
1217
  return this.repo.communitiesWithDigest();
706
1218
  }
707
- listCommunitiesById(orgId, ids) {
708
- 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) : [];
709
1230
  }
710
- getSpace(orgId, spaceId) {
711
- 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;
712
1235
  }
713
- async getCommunitySpace(orgId, communityId, spaceId) {
714
- const space = await this.repo.findSpace(orgId, spaceId);
715
- 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;
716
1241
  }
717
- async listSpaces(orgId, communityId) {
718
- if (!await this.getCommunity(orgId, communityId)) throw new NotFoundError("Community", communityId);
719
- 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);
720
1246
  }
721
- async getDmSettings(orgId, communityId) {
722
- 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;
723
1250
  }
724
- async createCommunity(orgId, input, actor) {
725
- assertConfigurator(actor);
1251
+ async createCommunity(input, actor) {
1252
+ this.require(actor, "communities:write");
1253
+ const orgId = actor.orgId;
726
1254
  const community = await this.uow.run(async ({ content, outbox }) => {
727
1255
  const created = await content.createCommunity(orgId, input, slugify(input.title));
728
1256
  await outbox.append([contentEvents.communityCreated.make({
@@ -739,9 +1267,10 @@ var ContentService = class {
739
1267
  });
740
1268
  return this.withThumbnail(community);
741
1269
  }
742
- async updateCommunity(orgId, id, patch, actor) {
743
- assertConfigurator(actor);
744
- 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");
745
1274
  const community = await this.uow.run(async ({ content, outbox }) => {
746
1275
  const updated = await content.updateCommunity(orgId, id, patch);
747
1276
  if (!updated) {
@@ -764,16 +1293,18 @@ var ContentService = class {
764
1293
  communityId: id,
765
1294
  actorId: actor.id
766
1295
  });
1296
+ await this.syncThumbnail(community);
767
1297
  return this.withThumbnail(community);
768
1298
  }
769
- publishCommunity(orgId, id, actor) {
770
- return this.updateCommunity(orgId, id, { status: "published" }, actor);
1299
+ publishCommunity(id, actor) {
1300
+ return this.updateCommunity(id, { status: "published" }, actor);
771
1301
  }
772
- unpublishCommunity(orgId, id, actor) {
773
- return this.updateCommunity(orgId, id, { status: "draft" }, actor);
1302
+ unpublishCommunity(id, actor) {
1303
+ return this.updateCommunity(id, { status: "draft" }, actor);
774
1304
  }
775
- async deleteCommunity(orgId, id, actor) {
776
- assertConfigurator(actor);
1305
+ async deleteCommunity(id, actor) {
1306
+ this.require(actor, "communities:delete", id);
1307
+ const orgId = actor.orgId;
777
1308
  await this.uow.run(async ({ content, outbox }) => {
778
1309
  const community = await content.findCommunityById(orgId, id);
779
1310
  if (!community) {
@@ -798,9 +1329,11 @@ var ContentService = class {
798
1329
  communityId: id,
799
1330
  actorId: actor.id
800
1331
  });
1332
+ await this.withdrawThumbnail(orgId, id);
801
1333
  }
802
- async patchCommunitySettings(orgId, id, patch, actor) {
803
- assertConfigurator(actor);
1334
+ async patchCommunitySettings(id, patch, actor) {
1335
+ this.require(actor, "communities:write", id);
1336
+ const orgId = actor.orgId;
804
1337
  const community = await this.uow.run(async ({ content, outbox }) => {
805
1338
  const updated = await content.patchCommunitySettings(orgId, id, patch);
806
1339
  if (!updated) {
@@ -825,8 +1358,9 @@ var ContentService = class {
825
1358
  });
826
1359
  return this.withThumbnail(community);
827
1360
  }
828
- async createSpace(orgId, communityId, input, actor) {
829
- assertConfigurator(actor);
1361
+ async createSpace(communityId, input, actor) {
1362
+ this.require(actor, "communities:write", communityId);
1363
+ const orgId = actor.orgId;
830
1364
  const resolved = this.resolvedSpace(input);
831
1365
  const space = await this.uow.run(async ({ content, outbox }) => {
832
1366
  if (!await content.findCommunityById(orgId, communityId)) {
@@ -858,8 +1392,9 @@ var ContentService = class {
858
1392
  });
859
1393
  return space;
860
1394
  }
861
- async updateSpace(orgId, communityId, spaceId, patch, actor) {
862
- assertConfigurator(actor);
1395
+ async updateSpace(communityId, spaceId, patch, actor) {
1396
+ this.require(actor, "communities:write", communityId);
1397
+ const orgId = actor.orgId;
863
1398
  const space = await this.uow.run(async ({ content, outbox }) => {
864
1399
  const current = await content.findSpace(orgId, spaceId);
865
1400
  if (!current || current.communityId !== communityId) {
@@ -894,8 +1429,9 @@ var ContentService = class {
894
1429
  });
895
1430
  return space;
896
1431
  }
897
- async deleteSpace(orgId, communityId, spaceId, actor) {
898
- assertConfigurator(actor);
1432
+ async deleteSpace(communityId, spaceId, actor) {
1433
+ this.require(actor, "communities:write", communityId);
1434
+ const orgId = actor.orgId;
899
1435
  await this.uow.run(async ({ content, outbox }) => {
900
1436
  const space = await content.findSpace(orgId, spaceId);
901
1437
  if (!space || space.communityId !== communityId) {
@@ -921,8 +1457,9 @@ var ContentService = class {
921
1457
  actorId: actor.id
922
1458
  });
923
1459
  }
924
- async reorderSpaces(orgId, communityId, orderedIds, actor) {
925
- assertConfigurator(actor);
1460
+ async reorderSpaces(communityId, orderedIds, actor) {
1461
+ this.require(actor, "communities:write", communityId);
1462
+ const orgId = actor.orgId;
926
1463
  const spaces = await this.uow.run(async ({ content, outbox }) => {
927
1464
  if (!await content.findCommunityById(orgId, communityId)) {
928
1465
  this.logger.warn("spaces reorder rejected — community not found", {
@@ -948,8 +1485,9 @@ var ContentService = class {
948
1485
  });
949
1486
  return spaces;
950
1487
  }
951
- async setSpaceTopics(orgId, communityId, spaceId, input, actor) {
952
- assertConfigurator(actor);
1488
+ async setSpaceTopics(communityId, spaceId, input, actor) {
1489
+ this.require(actor, "communities:write", communityId);
1490
+ const orgId = actor.orgId;
953
1491
  const names = input.topics.map((topic) => topic.name);
954
1492
  if (new Set(names).size !== names.length) throw new InvalidInputError("A space topic name is already used");
955
1493
  const space = await this.uow.run(async ({ content, outbox }) => {
@@ -984,8 +1522,9 @@ var ContentService = class {
984
1522
  });
985
1523
  return space;
986
1524
  }
987
- async addSpaceLink(orgId, communityId, spaceId, contentId, actor) {
988
- assertConfigurator(actor);
1525
+ async addSpaceLink(communityId, spaceId, contentId, actor) {
1526
+ this.require(actor, "communities:write", communityId);
1527
+ const orgId = actor.orgId;
989
1528
  const space = await this.uow.run(async ({ content, outbox }) => {
990
1529
  const current = await content.findSpace(orgId, spaceId);
991
1530
  if (!current || current.communityId !== communityId) {
@@ -1015,8 +1554,9 @@ var ContentService = class {
1015
1554
  });
1016
1555
  return space;
1017
1556
  }
1018
- async removeSpaceLink(orgId, communityId, spaceId, linkId, actor) {
1019
- assertConfigurator(actor);
1557
+ async removeSpaceLink(communityId, spaceId, linkId, actor) {
1558
+ this.require(actor, "communities:write", communityId);
1559
+ const orgId = actor.orgId;
1020
1560
  const space = await this.uow.run(async ({ content, outbox }) => {
1021
1561
  const current = await content.findSpace(orgId, spaceId);
1022
1562
  if (!current || current.communityId !== communityId) {
@@ -1045,8 +1585,9 @@ var ContentService = class {
1045
1585
  });
1046
1586
  return space;
1047
1587
  }
1048
- async reorderSpaceLinks(orgId, communityId, spaceId, orderedIds, actor) {
1049
- assertConfigurator(actor);
1588
+ async reorderSpaceLinks(communityId, spaceId, orderedIds, actor) {
1589
+ this.require(actor, "communities:write", communityId);
1590
+ const orgId = actor.orgId;
1050
1591
  const space = await this.uow.run(async ({ content, outbox }) => {
1051
1592
  const current = await content.findSpace(orgId, spaceId);
1052
1593
  if (!current || current.communityId !== communityId) {