@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,31 +1,59 @@
1
- import { ConflictError, ForbiddenError, NotFoundError } from "../shared/errors.js";
2
1
  import { noopLogger } from "../shared/logger.js";
3
- import { canModerate, isStaffRole } from "../organizations/roles.js";
2
+ import { ConflictError, ForbiddenError, NotFoundError } from "../shared/errors.js";
4
3
  import { entitlementEvents } from "./events.js";
5
- import { laterExpiry, opensSpace, seesSpace } from "./rules.js";
4
+ import { opensSpace, seesSpace } from "./rules.js";
6
5
  //#region src/entitlements/service.ts
7
6
  const ENDING_SOON_MS = 10080 * 60 * 1e3;
8
7
  const NOTICE_BATCH_SIZE = 100;
9
8
  var EntitlementsServiceImpl = class {
10
9
  repo;
11
10
  content;
11
+ permissions;
12
12
  uow;
13
13
  logger;
14
14
  constructor(params) {
15
15
  this.repo = params.repo;
16
16
  this.content = params.content;
17
+ this.permissions = params.permissions;
17
18
  this.uow = params.uow;
18
19
  this.logger = params.logger ?? noopLogger;
19
20
  }
20
- list(orgId, query) {
21
+ async list(query, actor) {
22
+ const orgId = actor.orgId;
23
+ if (!actor.permissions.has("access:read")) {
24
+ const contentId = query.contentId;
25
+ if (!(contentId !== void 0 && actor.permissions.has("progress:read", contentId) && await this.repo.contentTypeOf(orgId, contentId) === "course")) {
26
+ this.logger.warn("entitlements list refused", {
27
+ orgId,
28
+ actorId: actor.id,
29
+ contentId: contentId ?? null
30
+ });
31
+ throw new ForbiddenError(contentId === void 0 ? "You need access:read" : "You need access:read, or progress:read on this course, to see who holds it");
32
+ }
33
+ }
21
34
  return this.repo.list(orgId, query);
22
35
  }
23
- async grant(orgId, input) {
36
+ async grant(input, actor) {
37
+ const orgId = actor.orgId;
38
+ const { contentType, ...grant } = input;
39
+ const contentId = grant.contentId ?? null;
40
+ const type = contentId === null ? null : await this.repo.contentTypeOf(orgId, contentId);
41
+ if (contentType !== void 0 && type !== contentType) throw new NotFoundError(contentType === "event" ? "Event" : "Content", contentId ?? "");
42
+ if (contentId !== null && type === "event") return this.grantEvent({
43
+ ...grant,
44
+ contentId
45
+ }, actor);
46
+ actor.permissions.require("access:write");
47
+ if (contentId !== null && type === null) throw new NotFoundError("Content", contentId);
24
48
  const entitlement = await this.uow.run(async ({ entitlements, outbox }) => {
25
- const created = await entitlements.insert(orgId, input);
49
+ const created = await entitlements.insert(orgId, {
50
+ ...grant,
51
+ source: this.sourceOf(actor, grant.orgUserId)
52
+ });
26
53
  await outbox.append([entitlementEvents.entitlementCreated.make({
27
54
  orgId,
28
- data: created
55
+ data: created,
56
+ metadata: this.actorMetadata(actor)
29
57
  })]);
30
58
  return created;
31
59
  });
@@ -34,73 +62,144 @@ var EntitlementsServiceImpl = class {
34
62
  entitlementId: entitlement.id,
35
63
  orgUserId: entitlement.orgUserId,
36
64
  bundleId: entitlement.bundleId,
37
- contentId: entitlement.contentId
65
+ contentId: entitlement.contentId,
66
+ source: entitlement.source,
67
+ actorId: actor.id
38
68
  });
39
69
  return entitlement;
40
70
  }
41
- async ensureGrant(orgId, input) {
42
- const { entitlement, change } = await this.uow.run(async ({ entitlements, outbox }) => {
43
- const created = await entitlements.insertIfAbsent(orgId, input);
44
- if (created) {
45
- await outbox.append([entitlementEvents.entitlementCreated.make({
71
+ async revoke(input, actor) {
72
+ const orgId = actor.orgId;
73
+ const { orgUserId } = input;
74
+ const contentId = input.contentId ?? null;
75
+ const bundleId = input.bundleId ?? null;
76
+ const type = contentId === null ? null : await this.repo.contentTypeOf(orgId, contentId);
77
+ if (input.contentType !== void 0 && type !== input.contentType) throw new NotFoundError(input.contentType === "event" ? "Event" : "Content", contentId ?? "");
78
+ if (type === "event" && contentId !== null) {
79
+ if (orgUserId !== actor.id) this.requireAttendeeManager(actor, contentId);
80
+ } else {
81
+ actor.permissions.require("access:write");
82
+ if (contentId !== null && type === null) throw new NotFoundError("Content", contentId);
83
+ }
84
+ const target = contentId ?? bundleId;
85
+ const revoked = await this.uow.run(async ({ entitlements, outbox }) => {
86
+ const existing = await entitlements.lockGrant(orgId, {
87
+ orgUserId,
88
+ contentId,
89
+ bundleId
90
+ });
91
+ if (!existing || !activeNow(existing, /* @__PURE__ */ new Date())) {
92
+ this.logger.warn("entitlement revoke rejected — no direct active grant", {
46
93
  orgId,
47
- data: created
48
- })]);
94
+ orgUserId,
95
+ contentId,
96
+ bundleId,
97
+ actorId: actor.id
98
+ });
99
+ throw new NotFoundError("Entitlement", `${orgUserId}:${target}`);
100
+ }
101
+ const updated = await entitlements.setStatus(orgId, existing.id, "revoked");
102
+ if (!updated) throw new NotFoundError("Entitlement", existing.id);
103
+ await outbox.append([entitlementEvents.entitlementDeleted.make({
104
+ orgId,
105
+ data: updated,
106
+ metadata: this.actorMetadata(actor)
107
+ })]);
108
+ return updated;
109
+ });
110
+ this.logger.info("entitlement revoked", {
111
+ orgId,
112
+ entitlementId: revoked.id,
113
+ orgUserId,
114
+ contentId,
115
+ bundleId,
116
+ actorId: actor.id
117
+ });
118
+ return revoked;
119
+ }
120
+ async grantEvent(input, actor) {
121
+ const orgId = actor.orgId;
122
+ const eventId = input.contentId;
123
+ const orgUserId = input.orgUserId;
124
+ const self = orgUserId === actor.id;
125
+ if (!self) this.requireAttendeeManager(actor, eventId);
126
+ const event = await this.content.getEvent(eventId, this.system(orgId));
127
+ const now = /* @__PURE__ */ new Date();
128
+ if (self) await this.assertCanRegister(event, actor, now);
129
+ else assertOpenForAttendees(event, now);
130
+ const { entitlement, created } = await this.uow.run(async ({ entitlements, outbox }) => {
131
+ if (event.settings.capacity !== null) await entitlements.lockCapacity(orgId, eventId);
132
+ const existing = await entitlements.lockGrant(orgId, {
133
+ orgUserId,
134
+ contentId: eventId
135
+ });
136
+ if (existing && activeNow(existing, now)) {
137
+ if (self) throw alreadyRegistered();
49
138
  return {
50
- entitlement: created,
51
- change: "created"
139
+ entitlement: existing,
140
+ created: false
52
141
  };
53
142
  }
54
- const existing = await entitlements.lockGrant(orgId, input);
55
- if (!existing) throw new ConflictError("The grant changed while it was being given");
56
- if (existing.status === "revoked") return {
57
- entitlement: existing,
58
- change: null
59
- };
60
- const expiresAt = laterExpiry(existing.expiresAt, input.expiresAt ?? null);
61
- if (expiresAt?.getTime() === existing.expiresAt?.getTime()) return {
62
- entitlement: existing,
63
- change: null
64
- };
65
- const updated = await entitlements.setExpiresAt(orgId, existing.id, expiresAt);
66
- if (!updated) throw new NotFoundError("Entitlement", existing.id);
67
- await outbox.append([entitlementEvents.entitlementUpdated.make({
143
+ await this.assertPlace(entitlements, orgId, event, orgUserId);
144
+ const inserted = await entitlements.insert(orgId, {
145
+ orgUserId,
146
+ contentId: eventId,
147
+ expiresAt: self ? null : input.expiresAt ?? null,
148
+ source: this.sourceOf(actor, orgUserId)
149
+ });
150
+ await outbox.append([entitlementEvents.entitlementCreated.make({
68
151
  orgId,
69
- data: updated
152
+ data: inserted,
153
+ metadata: this.actorMetadata(actor)
70
154
  })]);
71
155
  return {
72
- entitlement: updated,
73
- change: "updated"
156
+ entitlement: inserted,
157
+ created: true
74
158
  };
75
159
  });
76
- if (change) this.logger.info(change === "created" ? "entitlement granted" : "entitlement extended", {
160
+ if (created) this.logger.info("entitlement granted", {
77
161
  orgId,
78
162
  entitlementId: entitlement.id,
79
- orgUserId: entitlement.orgUserId,
80
- bundleId: entitlement.bundleId,
81
- contentId: entitlement.contentId
163
+ orgUserId,
164
+ contentId: eventId,
165
+ source: entitlement.source,
166
+ actorId: actor.id
82
167
  });
83
168
  return entitlement;
84
169
  }
85
- async setStatus(orgId, id, status) {
86
- const entitlement = await this.uow.run(async ({ entitlements, outbox }) => {
87
- const updated = await entitlements.setStatus(orgId, id, status);
88
- if (!updated) return null;
89
- await outbox.append([status === "revoked" ? entitlementEvents.entitlementDeleted.make({
90
- orgId,
91
- data: updated
92
- }) : entitlementEvents.entitlementUpdated.make({
93
- orgId,
94
- data: updated
95
- })]);
96
- return updated;
97
- });
98
- if (entitlement) this.logger.info("entitlement status changed", {
99
- orgId,
100
- entitlementId: id,
101
- status
170
+ async assertCanRegister(event, actor, now) {
171
+ if (event.status !== "published") throw new NotFoundError("Event", event.id);
172
+ if (await this.repo.hasAccess(actor.orgId, actor.id, event.id)) throw alreadyRegistered();
173
+ if (event.settings.type !== "public") {
174
+ this.logger.warn("event registration refused — private", {
175
+ orgId: actor.orgId,
176
+ eventId: event.id,
177
+ actorId: actor.id
178
+ });
179
+ throw new ForbiddenError("This event is private: only its organizers can add attendees");
180
+ }
181
+ if (event.startsAt.getTime() <= now.getTime()) throw new ConflictError("This event has already started", "started");
182
+ }
183
+ requireAttendeeManager(actor, eventId) {
184
+ if (actor.permissions.has("access:write") || actor.permissions.has("events:write", eventId)) return;
185
+ this.logger.warn("event attendees refused — cannot manage", {
186
+ orgId: actor.orgId,
187
+ eventId,
188
+ actorId: actor.id
102
189
  });
103
- return entitlement;
190
+ throw new ForbiddenError("You need access:write, or events:write on this event");
191
+ }
192
+ async assertPlace(entitlements, orgId, event, orgUserId) {
193
+ const capacity = event.settings.capacity;
194
+ if (capacity === null) return;
195
+ if ((await entitlements.grantedOrgUserIds(orgId, event.id)).filter((id) => id !== orgUserId).length >= capacity) throw new ConflictError("This event is full", "full");
196
+ }
197
+ sourceOf(actor, orgUserId) {
198
+ if (actor.id === this.system(actor.orgId).id) return "automation";
199
+ return orgUserId === actor.id ? "self" : "manual";
200
+ }
201
+ actorMetadata(actor) {
202
+ return actor.id === this.system(actor.orgId).id ? {} : { actorId: actor.id };
104
203
  }
105
204
  hasAccess(orgId, orgUserId, contentId) {
106
205
  return this.repo.hasAccess(orgId, orgUserId, contentId);
@@ -108,66 +207,104 @@ var EntitlementsServiceImpl = class {
108
207
  listAccessibleContent(orgId, orgUserId, type) {
109
208
  return this.repo.listAccessibleContent(orgId, orgUserId, type);
110
209
  }
111
- async isCommunityMember(orgId, actor, communityId) {
112
- return (await this.accessOf(orgId, actor.id, communityId, null))?.member === true;
210
+ holdsAnyGrant(orgId, orgUserId) {
211
+ return this.repo.holdsAnyGrant(orgId, orgUserId);
212
+ }
213
+ async opensEverySpace(actor, communityId) {
214
+ return (await this.standingOf(actor, communityId, null)).everySpace;
113
215
  }
114
- async canOpenSpace(orgId, actor, spaceId) {
115
- const space = await this.content.getSpace(orgId, spaceId);
216
+ async isCommunityMember(communityId, actor) {
217
+ if (actor.permissions.has("communities:read", communityId)) return true;
218
+ return this.repo.hasCommunityAccess(actor.orgId, actor.id, communityId);
219
+ }
220
+ async canOpenSpace(spaceId, actor) {
221
+ const space = await this.content.getSpace(spaceId, this.system(actor.orgId));
116
222
  if (!space) return false;
117
- return this.canOpen(orgId, actor, space);
223
+ return this.canOpen(space, actor);
118
224
  }
119
- async canOpen(orgId, actor, space) {
120
- const facts = await this.accessOf(orgId, actor.id, space.communityId, space.id);
121
- if (!facts) return false;
122
- return opensSpace(facts.role, facts.member, space.accessRule, facts.activeInSpace);
225
+ async canOpen(space, actor) {
226
+ const standing = await this.standingOf(actor, space.communityId, space.id);
227
+ return opensSpace(standing.member, standing.everySpace, space.accessRule, standing.activeInSpace);
123
228
  }
124
- async spaceStandings(orgId, actor, communityId) {
125
- const rows = await this.repo.listSpaceMembersFor(orgId, actor.id, communityId);
229
+ async spaceStandings(communityId, actor) {
230
+ if ((await this.standingOf(actor, communityId, null)).everySpace) return {};
231
+ const rows = await this.repo.listSpaceMembersFor(actor.orgId, actor.id, communityId);
126
232
  return Object.fromEntries(rows.map((row) => [row.spaceId, row.status]));
127
233
  }
128
- async openSpacesFor(orgId, actor, communityId) {
129
- const facts = await this.accessOf(orgId, actor.id, communityId, null);
130
- if (!facts?.member) return [];
131
- const [spaces, activeIds] = await Promise.all([this.content.listSpaces(orgId, communityId), this.repo.activeSpaceIds(orgId, actor.id, communityId)]);
234
+ async openSpacesFor(communityId, actor) {
235
+ const orgId = actor.orgId;
236
+ const standing = await this.standingOf(actor, communityId, null);
237
+ if (!standing.member) return [];
238
+ const [spaces, activeIds] = await Promise.all([this.content.listSpaces(communityId, this.system(orgId)), standing.everySpace ? Promise.resolve([]) : this.repo.activeSpaceIds(orgId, actor.id, communityId)]);
132
239
  const active = new Set(activeIds);
133
- return spaces.filter((space) => opensSpace(facts.role, facts.member, space.accessRule, active.has(space.id)));
240
+ return spaces.filter((space) => opensSpace(true, standing.everySpace, space.accessRule, active.has(space.id)));
134
241
  }
135
- async openSpaceIds(orgId, actor, communityId) {
136
- return (await this.openSpacesFor(orgId, actor, communityId)).map((space) => space.id);
242
+ async openSpaceIds(communityId, actor) {
243
+ return (await this.openSpacesFor(communityId, actor)).map((space) => space.id);
137
244
  }
138
245
  async communityMembersAmong(orgId, communityId, orgUserIds) {
139
246
  if (orgUserIds.length === 0) return [];
140
- return (await this.repo.accessAmong(orgId, communityId, null, orgUserIds)).filter((fact) => fact.member).map((fact) => fact.orgUserId);
247
+ const [facts, team] = await Promise.all([this.repo.accessAmong(orgId, communityId, null, orgUserIds), this.teamOf(orgId, communityId)]);
248
+ return facts.filter((fact) => standingFrom(fact, team).member).map((fact) => fact.orgUserId);
141
249
  }
142
250
  async spaceOpenersAmong(orgId, spaceId, orgUserIds) {
143
251
  if (orgUserIds.length === 0) return [];
144
- const space = await this.content.getSpace(orgId, spaceId);
252
+ const space = await this.content.getSpace(spaceId, this.system(orgId));
145
253
  if (!space) return [];
146
- return (await this.repo.accessAmong(orgId, space.communityId, space.id, orgUserIds)).filter((fact) => opensSpace(fact.role, fact.member, space.accessRule, fact.activeInSpace)).map((fact) => fact.orgUserId);
254
+ const [facts, team] = await Promise.all([this.repo.accessAmong(orgId, space.communityId, space.id, orgUserIds), this.teamOf(orgId, space.communityId)]);
255
+ return facts.filter((fact) => {
256
+ const standing = standingFrom(fact, team);
257
+ return opensSpace(standing.member, standing.everySpace, space.accessRule, standing.activeInSpace);
258
+ }).map((fact) => fact.orgUserId);
147
259
  }
148
260
  async spaceOpeners(orgId, spaceId) {
149
- const space = await this.content.getSpace(orgId, spaceId);
261
+ const space = await this.content.getSpace(spaceId, this.system(orgId));
150
262
  if (!space) return [];
151
- const members = await this.repo.communityMemberIds(orgId, space.communityId);
263
+ const members = await this.communityMemberIds(orgId, space.communityId);
152
264
  return this.spaceOpenersAmong(orgId, spaceId, members);
153
265
  }
154
266
  communitiesOpenedBy(orgId, grant) {
155
267
  return this.repo.communitiesOpenedBy(orgId, grant);
156
268
  }
157
- communityModeratorIds(orgId, communityId) {
158
- return this.repo.communityModeratorIds(orgId, communityId);
269
+ async communityModeratorIds(orgId, communityId) {
270
+ const moderators = await this.permissions.holders(orgId, "communities:moderate", communityId);
271
+ return this.communityMembersAmong(orgId, communityId, moderators);
159
272
  }
160
273
  communityIdsOpenedFor(orgId, orgUserId) {
161
274
  return this.repo.communityIdsOpenedFor(orgId, orgUserId);
162
275
  }
163
- countActiveSpaceMembers(orgId, spaceId) {
164
- return this.repo.countActiveSpaceMembers(orgId, spaceId);
276
+ openCommunityIds(actor) {
277
+ const readable = actor.permissions.where("communities:read");
278
+ return this.repo.openCommunityIds(actor.orgId, actor.id, {
279
+ all: readable.org,
280
+ ids: readable.content
281
+ });
282
+ }
283
+ async communityMemberIds(orgId, communityId) {
284
+ const readers = await this.permissions.holders(orgId, "communities:read", communityId);
285
+ return this.repo.communityMemberIds(orgId, communityId, readers);
286
+ }
287
+ grantedOrgUserIds(orgId, contentId) {
288
+ return this.repo.grantedOrgUserIds(orgId, contentId);
289
+ }
290
+ async countActiveSpaceMembers(orgId, spaceId) {
291
+ const space = await this.content.getSpace(spaceId, this.system(orgId));
292
+ if (!space) return 0;
293
+ const excluding = await this.everySpaceOpeners(orgId, space.communityId);
294
+ return this.repo.countActiveSpaceMembers(orgId, spaceId, excluding);
295
+ }
296
+ async everySpaceOpeners(orgId, communityId) {
297
+ const team = await this.teamOf(orgId, communityId);
298
+ const moderators = [...team.moderators].filter((id) => !team.readers.has(id));
299
+ const granted = moderators.length === 0 ? [] : (await this.repo.accessAmong(orgId, communityId, null, moderators)).filter((fact) => fact.granted).map((fact) => fact.orgUserId);
300
+ return [...team.readers, ...granted];
165
301
  }
166
- async requestJoin(orgId, actor, communityId, spaceId) {
167
- const space = await this.requireVisibleSpaceIn(orgId, actor, communityId, spaceId);
302
+ async requestJoin(communityId, spaceId, actor) {
303
+ const orgId = actor.orgId;
304
+ const space = await this.requireVisibleSpaceIn(actor, communityId, spaceId);
168
305
  this.assertRule(space.accessRule, "by_request");
169
- if (canModerate(actor.role)) throw new ForbiddenError("Staff do not request a place in a space");
170
- await this.assertGrant(orgId, actor, actor.id, space.communityId);
306
+ await this.assertHolder(actor, space.communityId);
307
+ await this.assertMember(actor, actor.id, space.communityId);
171
308
  const created = await this.createMember(orgId, actor, space, actor.id, "requested", "request");
172
309
  this.logger.info("space member created", {
173
310
  orgId,
@@ -178,11 +315,12 @@ var EntitlementsServiceImpl = class {
178
315
  });
179
316
  return created;
180
317
  }
181
- async invite(orgId, actor, communityId, spaceId, orgUserId) {
182
- const space = await this.requireVisibleSpaceIn(orgId, actor, communityId, spaceId);
318
+ async invite(communityId, spaceId, orgUserId, actor) {
319
+ const orgId = actor.orgId;
320
+ const space = await this.requireVisibleSpaceIn(actor, communityId, spaceId);
321
+ await this.assertCanManage(actor, space.communityId);
183
322
  this.assertRule(space.accessRule, "by_invite");
184
- await this.assertCanManage(orgId, actor, space.communityId);
185
- await this.assertHolds(orgId, actor, orgUserId, space.communityId);
323
+ await this.assertHolds(actor, orgUserId, space.communityId);
186
324
  const created = await this.createMember(orgId, actor, space, orgUserId, "invited", "invite");
187
325
  this.logger.info("space member created", {
188
326
  orgId,
@@ -193,11 +331,13 @@ var EntitlementsServiceImpl = class {
193
331
  });
194
332
  return created;
195
333
  }
196
- async addSpaceMember(orgId, actor, communityId, spaceId, orgUserId) {
197
- const space = await this.requireVisibleSpaceIn(orgId, actor, communityId, spaceId);
334
+ async addSpaceMember(communityId, spaceId, orgUserId, actor) {
335
+ const orgId = actor.orgId;
336
+ const space = await this.requireVisibleSpaceIn(actor, communityId, spaceId);
337
+ await this.assertCanManage(actor, space.communityId);
198
338
  this.assertHasMembers(space.accessRule);
199
- await this.assertCanManage(orgId, actor, space.communityId);
200
- await this.assertHolds(orgId, actor, orgUserId, space.communityId);
339
+ await this.assertHolds(actor, orgUserId, space.communityId);
340
+ const excluding = await this.everySpaceOpeners(orgId, space.communityId);
201
341
  const created = await this.uow.run(async (scope) => {
202
342
  const locked = await this.lockSpace(scope.spaces, orgId, spaceId);
203
343
  this.assertHasMembers(locked.accessRule);
@@ -207,7 +347,10 @@ var EntitlementsServiceImpl = class {
207
347
  orgUserId,
208
348
  status: "active",
209
349
  source: "manual"
210
- }, locked);
350
+ }, {
351
+ space: locked,
352
+ excluding
353
+ });
211
354
  });
212
355
  this.logger.info("space member created", {
213
356
  orgId,
@@ -218,41 +361,46 @@ var EntitlementsServiceImpl = class {
218
361
  });
219
362
  return created;
220
363
  }
221
- async accept(orgId, actor, communityId, spaceId) {
222
- this.assertHolder(actor);
223
- const space = await this.requireVisibleSpaceIn(orgId, actor, communityId, spaceId);
364
+ async accept(communityId, spaceId, actor) {
365
+ const orgId = actor.orgId;
366
+ const space = await this.requireVisibleSpaceIn(actor, communityId, spaceId);
224
367
  this.assertRule(space.accessRule, "by_invite");
225
- await this.assertGrant(orgId, actor, actor.id, space.communityId);
226
- return this.activate(orgId, actor, spaceId, actor.id, "by_invite", "invited", () => ownRowMissing("invited"));
227
- }
228
- async approve(orgId, actor, communityId, spaceId, orgUserId) {
229
- const space = await this.requireVisibleSpaceIn(orgId, actor, communityId, spaceId);
368
+ await this.assertHolder(actor, space.communityId);
369
+ await this.assertMember(actor, actor.id, space.communityId);
370
+ const excluding = await this.everySpaceOpeners(orgId, space.communityId);
371
+ return this.activate(orgId, actor, spaceId, actor.id, "by_invite", "invited", excluding, () => ownRowMissing("invited"));
372
+ }
373
+ async approve(communityId, spaceId, orgUserId, actor) {
374
+ const orgId = actor.orgId;
375
+ const space = await this.requireVisibleSpaceIn(actor, communityId, spaceId);
376
+ await this.assertCanManage(actor, space.communityId);
230
377
  this.assertRule(space.accessRule, "by_request");
231
- await this.assertCanManage(orgId, actor, space.communityId);
232
- await this.assertGrant(orgId, actor, orgUserId, space.communityId);
233
- return this.activate(orgId, actor, spaceId, orgUserId, "by_request", "requested", () => new NotFoundError("Space member", orgUserId));
234
- }
235
- async decline(orgId, actor, communityId, spaceId, orgUserId) {
236
- const space = await this.requireVisibleSpaceIn(orgId, actor, communityId, spaceId);
237
- await this.assertCanManage(orgId, actor, space.communityId);
238
- return this.deleteMember(orgId, actor, spaceId, orgUserId, "requested", () => new NotFoundError("Space member", orgUserId));
239
- }
240
- async refuse(orgId, actor, communityId, spaceId) {
241
- const space = await this.requireVisibleSpaceIn(orgId, actor, communityId, spaceId);
242
- this.assertHolder(actor);
243
- await this.assertGrant(orgId, actor, actor.id, space.communityId);
244
- return this.deleteMember(orgId, actor, spaceId, actor.id, "invited", () => ownRowMissing("invited"));
245
- }
246
- async leave(orgId, actor, communityId, spaceId) {
247
- const space = await this.requireVisibleSpaceIn(orgId, actor, communityId, spaceId);
248
- this.assertHolder(actor);
249
- await this.assertGrant(orgId, actor, actor.id, space.communityId);
250
- return this.deleteMember(orgId, actor, spaceId, actor.id, "active", () => ownRowMissing("active"));
251
- }
252
- async remove(orgId, actor, communityId, spaceId, orgUserId) {
253
- const space = await this.requireVisibleSpaceIn(orgId, actor, communityId, spaceId);
378
+ await this.assertHolds(actor, orgUserId, space.communityId);
379
+ const excluding = await this.everySpaceOpeners(orgId, space.communityId);
380
+ return this.activate(orgId, actor, spaceId, orgUserId, "by_request", "requested", excluding, () => new NotFoundError("Space member", orgUserId));
381
+ }
382
+ async decline(communityId, spaceId, orgUserId, actor) {
383
+ const space = await this.requireVisibleSpaceIn(actor, communityId, spaceId);
384
+ await this.assertCanManage(actor, space.communityId);
385
+ return this.deleteMember(actor.orgId, actor, spaceId, orgUserId, "requested", () => new NotFoundError("Space member", orgUserId));
386
+ }
387
+ async refuse(communityId, spaceId, actor) {
388
+ const space = await this.requireVisibleSpaceIn(actor, communityId, spaceId);
389
+ await this.assertHolder(actor, space.communityId);
390
+ await this.assertMember(actor, actor.id, space.communityId);
391
+ return this.deleteMember(actor.orgId, actor, spaceId, actor.id, "invited", () => ownRowMissing("invited"));
392
+ }
393
+ async leave(communityId, spaceId, actor) {
394
+ const space = await this.requireVisibleSpaceIn(actor, communityId, spaceId);
395
+ await this.assertHolder(actor, space.communityId);
396
+ await this.assertMember(actor, actor.id, space.communityId);
397
+ return this.deleteMember(actor.orgId, actor, spaceId, actor.id, "active", () => ownRowMissing("active"));
398
+ }
399
+ async remove(communityId, spaceId, orgUserId, actor) {
400
+ const orgId = actor.orgId;
401
+ const space = await this.requireVisibleSpaceIn(actor, communityId, spaceId);
402
+ await this.assertCanManage(actor, space.communityId);
254
403
  this.assertHasMembers(space.accessRule);
255
- await this.assertCanManage(orgId, actor, space.communityId);
256
404
  const { updated, previousStatus } = await this.uow.run(async ({ entitlements, spaces, outbox }) => {
257
405
  const locked = await this.lockSpace(spaces, orgId, spaceId);
258
406
  this.assertHasMembers(locked.accessRule);
@@ -273,7 +421,7 @@ var EntitlementsServiceImpl = class {
273
421
  orgId,
274
422
  data: next,
275
423
  metadata: {
276
- actorOrgUserId: actor.id,
424
+ actorId: actor.id,
277
425
  previousStatus: row.status
278
426
  }
279
427
  })]);
@@ -291,18 +439,32 @@ var EntitlementsServiceImpl = class {
291
439
  });
292
440
  return updated;
293
441
  }
294
- async listSpaceMembers(orgId, actor, communityId, spaceId, query) {
295
- const space = await this.requireVisibleSpaceIn(orgId, actor, communityId, spaceId);
296
- if (!await this.canOpen(orgId, actor, space)) throw new NotFoundError("Space", spaceId);
442
+ async listSpaceMembers(communityId, spaceId, query, actor) {
443
+ const orgId = actor.orgId;
444
+ const space = await this.requireVisibleSpaceIn(actor, communityId, spaceId);
445
+ if (!await this.canOpen(space, actor)) throw new NotFoundError("Space", spaceId);
446
+ const excluding = await this.everySpaceOpeners(orgId, space.communityId);
297
447
  return this.repo.listSpaceMembers(orgId, spaceId, {
298
448
  ...query,
299
- ...canModerate(actor.role) ? {} : { statuses: ["active"] }
449
+ excluding,
450
+ ...actor.permissions.has("communities:moderate", space.communityId) ? {} : { statuses: ["active"] }
300
451
  });
301
452
  }
302
- hasDueNotices(orgId, now) {
303
- return this.repo.hasDueNotices(orgId, now, new Date(now.getTime() + ENDING_SOON_MS));
453
+ async expireDueGrants(now) {
454
+ const orgIds = await this.repo.orgIdsWithDueNotices(now, new Date(now.getTime() + ENDING_SOON_MS));
455
+ let failed = 0;
456
+ for (const orgId of orgIds) try {
457
+ await this.expireDueIn(orgId, now);
458
+ } catch (err) {
459
+ failed += 1;
460
+ this.logger.error("due grants not expired", {
461
+ orgId,
462
+ err
463
+ });
464
+ }
465
+ if (failed > 0) throw new Error(`due grants of ${failed} orgs could not be expired`);
304
466
  }
305
- async expireDue(orgId, now) {
467
+ async expireDueIn(orgId, now) {
306
468
  const until = new Date(now.getTime() + ENDING_SOON_MS);
307
469
  let appended = 0;
308
470
  let afterId;
@@ -372,8 +534,34 @@ var EntitlementsServiceImpl = class {
372
534
  events: appended
373
535
  });
374
536
  }
537
+ system(orgId) {
538
+ return this.permissions.systemActor(orgId);
539
+ }
540
+ async teamOf(orgId, communityId) {
541
+ const [readers, moderators] = await Promise.all([this.permissions.holders(orgId, "communities:read", communityId), this.permissions.holders(orgId, "communities:moderate", communityId)]);
542
+ return {
543
+ readers: new Set(readers),
544
+ moderators: new Set(moderators)
545
+ };
546
+ }
547
+ async standingOf(actor, communityId, spaceId) {
548
+ const reads = actor.permissions.has("communities:read", communityId);
549
+ const moderates = actor.permissions.has("communities:moderate", communityId);
550
+ if (reads) return {
551
+ member: true,
552
+ everySpace: true,
553
+ activeInSpace: false
554
+ };
555
+ const facts = await this.accessOf(actor.orgId, actor.id, communityId, spaceId);
556
+ const member = facts?.granted === true;
557
+ return {
558
+ member,
559
+ everySpace: member && moderates,
560
+ activeInSpace: facts?.activeInSpace === true
561
+ };
562
+ }
375
563
  async requireSpaceIn(orgId, communityId, spaceId) {
376
- const space = await this.content.getSpace(orgId, spaceId);
564
+ const space = await this.content.getSpace(spaceId, this.system(orgId));
377
565
  if (!space || space.communityId !== communityId) {
378
566
  this.logger.warn("space members rejected — space not found", {
379
567
  orgId,
@@ -384,11 +572,13 @@ var EntitlementsServiceImpl = class {
384
572
  }
385
573
  return space;
386
574
  }
387
- async requireVisibleSpaceIn(orgId, actor, communityId, spaceId) {
575
+ async requireVisibleSpaceIn(actor, communityId, spaceId) {
576
+ const orgId = actor.orgId;
388
577
  const space = await this.requireSpaceIn(orgId, communityId, spaceId);
389
- if (seesSpace(actor.role, space.accessRule, void 0)) return space;
578
+ if (seesSpace(false, space.accessRule, void 0)) return space;
579
+ if (await this.opensEverySpace(actor, communityId)) return space;
390
580
  const row = await this.repo.findSpaceMember(orgId, spaceId, actor.id);
391
- if (!seesSpace(actor.role, space.accessRule, row?.status)) throw new NotFoundError("Space", spaceId);
581
+ if (!seesSpace(false, space.accessRule, row?.status)) throw new NotFoundError("Space", spaceId);
392
582
  return space;
393
583
  }
394
584
  assertHasMembers(accessRule) {
@@ -398,21 +588,39 @@ var EntitlementsServiceImpl = class {
398
588
  this.assertHasMembers(accessRule);
399
589
  if (accessRule !== expected) throw new ConflictError(accessRule === "by_invite" ? "This space is by invitation" : "This space is by request");
400
590
  }
401
- assertHolder(actor) {
402
- if (isStaffRole(actor.role)) throw new ForbiddenError("Staff do not hold a place in a space");
591
+ async assertHolder(actor, communityId) {
592
+ if (await this.opensEverySpace(actor, communityId)) {
593
+ this.logger.warn("space place refused — opens every space", {
594
+ orgId: actor.orgId,
595
+ communityId,
596
+ actorId: actor.id
597
+ });
598
+ throw new ForbiddenError("You open every space in this community and hold no place in them");
599
+ }
403
600
  }
404
- async assertGrant(orgId, actor, orgUserId, communityId) {
405
- if (!await this.repo.hasCommunityAccess(orgId, orgUserId, communityId)) throw new ForbiddenError(orgUserId === actor.id ? "You are not a member of this community" : "That person is not a member of this community");
601
+ async assertMember(actor, orgUserId, communityId) {
602
+ if (!(orgUserId === actor.id ? await this.isCommunityMember(communityId, actor) : (await this.communityMembersAmong(actor.orgId, communityId, [orgUserId])).length > 0)) throw new ForbiddenError(orgUserId === actor.id ? "You are not a member of this community" : "That person is not a member of this community");
406
603
  }
407
- async assertHolds(orgId, actor, orgUserId, communityId) {
408
- const facts = await this.accessOf(orgId, orgUserId, communityId, null);
409
- if (facts && isStaffRole(facts.role)) throw new ForbiddenError("Staff do not hold a place in a space");
410
- await this.assertGrant(orgId, actor, orgUserId, communityId);
604
+ async assertHolds(actor, orgUserId, communityId) {
605
+ if ((await this.everySpaceOpeners(actor.orgId, communityId)).includes(orgUserId)) {
606
+ this.logger.warn("space place refused — person opens every space", {
607
+ orgId: actor.orgId,
608
+ communityId,
609
+ orgUserId,
610
+ actorId: actor.id
611
+ });
612
+ throw new ForbiddenError("That person opens every space in this community and holds no place in them");
613
+ }
614
+ await this.assertMember(actor, orgUserId, communityId);
411
615
  }
412
- async assertCanManage(orgId, actor, communityId) {
413
- const facts = await this.accessOf(orgId, actor.id, communityId, null);
414
- if (facts?.member && canModerate(facts.role)) return;
415
- throw new ForbiddenError("You cannot manage members of this space");
616
+ async assertCanManage(actor, communityId) {
617
+ if (actor.permissions.has("communities:moderate", communityId) && await this.isCommunityMember(communityId, actor)) return;
618
+ this.logger.warn("space members refused — cannot manage", {
619
+ orgId: actor.orgId,
620
+ communityId,
621
+ actorId: actor.id
622
+ });
623
+ throw new ForbiddenError("You need communities:moderate on this community, as its member");
416
624
  }
417
625
  async accessOf(orgId, orgUserId, communityId, spaceId) {
418
626
  const [facts] = await this.repo.accessAmong(orgId, communityId, spaceId, [orgUserId]);
@@ -430,17 +638,17 @@ var EntitlementsServiceImpl = class {
430
638
  async insertMember(entitlements, outbox, orgId, actor, input, room) {
431
639
  const existing = await entitlements.lockSpaceMember(orgId, input.spaceId, input.orgUserId);
432
640
  if (existing && existing.status !== "revoked") throw new ConflictError(input.orgUserId === actor.id ? "You already have a place in this space" : "That person already has a place in this space");
433
- if (room) await this.assertRoom(entitlements, orgId, room);
641
+ if (room) await this.assertRoom(entitlements, orgId, room.space, room.excluding);
434
642
  if (existing) await entitlements.deleteSpaceMember(orgId, existing.id);
435
643
  const created = await entitlements.insertSpaceMember(orgId, input);
436
644
  await outbox.append([entitlementEvents.spaceMemberCreated.make({
437
645
  orgId,
438
646
  data: created,
439
- metadata: { actorOrgUserId: actor.id }
647
+ metadata: { actorId: actor.id }
440
648
  })]);
441
649
  return created;
442
650
  }
443
- async activate(orgId, actor, spaceId, orgUserId, rule, from, missing) {
651
+ async activate(orgId, actor, spaceId, orgUserId, rule, from, excluding, missing) {
444
652
  const updated = await this.uow.run(async ({ entitlements, spaces, outbox }) => {
445
653
  const locked = await this.lockSpace(spaces, orgId, spaceId);
446
654
  this.assertRule(locked.accessRule, rule);
@@ -455,14 +663,14 @@ var EntitlementsServiceImpl = class {
455
663
  });
456
664
  throw missing();
457
665
  }
458
- await this.assertRoom(entitlements, orgId, locked);
666
+ await this.assertRoom(entitlements, orgId, locked, excluding);
459
667
  const next = await entitlements.updateSpaceMemberStatus(orgId, row.id, "active");
460
668
  if (!next) throw missing();
461
669
  await outbox.append([entitlementEvents.spaceMemberUpdated.make({
462
670
  orgId,
463
671
  data: next,
464
672
  metadata: {
465
- actorOrgUserId: actor.id,
673
+ actorId: actor.id,
466
674
  previousStatus: from
467
675
  }
468
676
  })]);
@@ -494,7 +702,7 @@ var EntitlementsServiceImpl = class {
494
702
  orgId,
495
703
  data: current,
496
704
  metadata: {
497
- actorOrgUserId: actor.id,
705
+ actorId: actor.id,
498
706
  previousStatus: from
499
707
  }
500
708
  })]);
@@ -514,17 +722,36 @@ var EntitlementsServiceImpl = class {
514
722
  if (!locked) throw new NotFoundError("Space", spaceId);
515
723
  return locked;
516
724
  }
517
- async assertRoom(entitlements, orgId, space) {
725
+ async assertRoom(entitlements, orgId, space, excluding) {
518
726
  if (space.capacity === null) return;
519
- if (await entitlements.countActiveSpaceMembers(orgId, space.id) >= space.capacity) throw new ConflictError("This space is full");
727
+ if (await entitlements.countActiveSpaceMembers(orgId, space.id, excluding) >= space.capacity) throw new ConflictError("This space is full");
520
728
  }
521
729
  };
730
+ function alreadyRegistered() {
731
+ return new ConflictError("You are already going to this event", "already_registered");
732
+ }
733
+ function activeNow(grant, now) {
734
+ return grant.status === "active" && (grant.expiresAt === null || grant.expiresAt.getTime() > now.getTime());
735
+ }
736
+ function assertOpenForAttendees(event, now) {
737
+ if (event.status !== "published") throw new ConflictError("This event is not published", "not_published");
738
+ if (event.endsAt.getTime() <= now.getTime()) throw new ConflictError("This event has ended", "ended");
739
+ }
522
740
  function alreadyPlaced(actor, orgUserId) {
523
741
  return new ConflictError(orgUserId === actor.id ? "You already have a place in this space" : "That person already has a place in this space");
524
742
  }
525
743
  function ownRowMissing(status) {
526
744
  return new ConflictError(status === "invited" ? "You do not have an invitation to this space" : "You are not a member of this space");
527
745
  }
746
+ function standingFrom(fact, team) {
747
+ const reads = team.readers.has(fact.orgUserId);
748
+ const member = reads || fact.granted;
749
+ return {
750
+ member,
751
+ everySpace: reads || member && team.moderators.has(fact.orgUserId),
752
+ activeInSpace: fact.activeInSpace
753
+ };
754
+ }
528
755
  //#endregion
529
756
  export { EntitlementsServiceImpl };
530
757