@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,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,75 +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
- openCommunityIds(orgId, actor) {
164
- return this.repo.openCommunityIds(orgId, actor.id);
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
+ });
165
282
  }
166
- communityMemberIds(orgId, communityId) {
167
- return this.repo.communityMemberIds(orgId, communityId);
283
+ async communityMemberIds(orgId, communityId) {
284
+ const readers = await this.permissions.holders(orgId, "communities:read", communityId);
285
+ return this.repo.communityMemberIds(orgId, communityId, readers);
168
286
  }
169
- contentHolderIds(orgId, contentId) {
170
- return this.repo.contentHolderIds(orgId, contentId);
287
+ grantedOrgUserIds(orgId, contentId) {
288
+ return this.repo.grantedOrgUserIds(orgId, contentId);
171
289
  }
172
- countActiveSpaceMembers(orgId, spaceId) {
173
- return this.repo.countActiveSpaceMembers(orgId, spaceId);
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);
174
295
  }
175
- async requestJoin(orgId, actor, communityId, spaceId) {
176
- const space = await this.requireVisibleSpaceIn(orgId, actor, communityId, spaceId);
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];
301
+ }
302
+ async requestJoin(communityId, spaceId, actor) {
303
+ const orgId = actor.orgId;
304
+ const space = await this.requireVisibleSpaceIn(actor, communityId, spaceId);
177
305
  this.assertRule(space.accessRule, "by_request");
178
- if (canModerate(actor.role)) throw new ForbiddenError("Staff do not request a place in a space");
179
- 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);
180
308
  const created = await this.createMember(orgId, actor, space, actor.id, "requested", "request");
181
309
  this.logger.info("space member created", {
182
310
  orgId,
@@ -187,11 +315,12 @@ var EntitlementsServiceImpl = class {
187
315
  });
188
316
  return created;
189
317
  }
190
- async invite(orgId, actor, communityId, spaceId, orgUserId) {
191
- 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);
192
322
  this.assertRule(space.accessRule, "by_invite");
193
- await this.assertCanManage(orgId, actor, space.communityId);
194
- await this.assertHolds(orgId, actor, orgUserId, space.communityId);
323
+ await this.assertHolds(actor, orgUserId, space.communityId);
195
324
  const created = await this.createMember(orgId, actor, space, orgUserId, "invited", "invite");
196
325
  this.logger.info("space member created", {
197
326
  orgId,
@@ -202,11 +331,13 @@ var EntitlementsServiceImpl = class {
202
331
  });
203
332
  return created;
204
333
  }
205
- async addSpaceMember(orgId, actor, communityId, spaceId, orgUserId) {
206
- 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);
207
338
  this.assertHasMembers(space.accessRule);
208
- await this.assertCanManage(orgId, actor, space.communityId);
209
- 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);
210
341
  const created = await this.uow.run(async (scope) => {
211
342
  const locked = await this.lockSpace(scope.spaces, orgId, spaceId);
212
343
  this.assertHasMembers(locked.accessRule);
@@ -216,7 +347,10 @@ var EntitlementsServiceImpl = class {
216
347
  orgUserId,
217
348
  status: "active",
218
349
  source: "manual"
219
- }, locked);
350
+ }, {
351
+ space: locked,
352
+ excluding
353
+ });
220
354
  });
221
355
  this.logger.info("space member created", {
222
356
  orgId,
@@ -227,41 +361,46 @@ var EntitlementsServiceImpl = class {
227
361
  });
228
362
  return created;
229
363
  }
230
- async accept(orgId, actor, communityId, spaceId) {
231
- this.assertHolder(actor);
232
- 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);
233
367
  this.assertRule(space.accessRule, "by_invite");
234
- await this.assertGrant(orgId, actor, actor.id, space.communityId);
235
- return this.activate(orgId, actor, spaceId, actor.id, "by_invite", "invited", () => ownRowMissing("invited"));
236
- }
237
- async approve(orgId, actor, communityId, spaceId, orgUserId) {
238
- 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);
239
377
  this.assertRule(space.accessRule, "by_request");
240
- await this.assertCanManage(orgId, actor, space.communityId);
241
- await this.assertGrant(orgId, actor, orgUserId, space.communityId);
242
- return this.activate(orgId, actor, spaceId, orgUserId, "by_request", "requested", () => new NotFoundError("Space member", orgUserId));
243
- }
244
- async decline(orgId, actor, communityId, spaceId, orgUserId) {
245
- const space = await this.requireVisibleSpaceIn(orgId, actor, communityId, spaceId);
246
- await this.assertCanManage(orgId, actor, space.communityId);
247
- return this.deleteMember(orgId, actor, spaceId, orgUserId, "requested", () => new NotFoundError("Space member", orgUserId));
248
- }
249
- async refuse(orgId, actor, communityId, spaceId) {
250
- const space = await this.requireVisibleSpaceIn(orgId, actor, communityId, spaceId);
251
- this.assertHolder(actor);
252
- await this.assertGrant(orgId, actor, actor.id, space.communityId);
253
- return this.deleteMember(orgId, actor, spaceId, actor.id, "invited", () => ownRowMissing("invited"));
254
- }
255
- async leave(orgId, actor, communityId, spaceId) {
256
- const space = await this.requireVisibleSpaceIn(orgId, actor, communityId, spaceId);
257
- this.assertHolder(actor);
258
- await this.assertGrant(orgId, actor, actor.id, space.communityId);
259
- return this.deleteMember(orgId, actor, spaceId, actor.id, "active", () => ownRowMissing("active"));
260
- }
261
- async remove(orgId, actor, communityId, spaceId, orgUserId) {
262
- 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);
263
403
  this.assertHasMembers(space.accessRule);
264
- await this.assertCanManage(orgId, actor, space.communityId);
265
404
  const { updated, previousStatus } = await this.uow.run(async ({ entitlements, spaces, outbox }) => {
266
405
  const locked = await this.lockSpace(spaces, orgId, spaceId);
267
406
  this.assertHasMembers(locked.accessRule);
@@ -282,7 +421,7 @@ var EntitlementsServiceImpl = class {
282
421
  orgId,
283
422
  data: next,
284
423
  metadata: {
285
- actorOrgUserId: actor.id,
424
+ actorId: actor.id,
286
425
  previousStatus: row.status
287
426
  }
288
427
  })]);
@@ -300,12 +439,15 @@ var EntitlementsServiceImpl = class {
300
439
  });
301
440
  return updated;
302
441
  }
303
- async listSpaceMembers(orgId, actor, communityId, spaceId, query) {
304
- const space = await this.requireVisibleSpaceIn(orgId, actor, communityId, spaceId);
305
- 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);
306
447
  return this.repo.listSpaceMembers(orgId, spaceId, {
307
448
  ...query,
308
- ...canModerate(actor.role) ? {} : { statuses: ["active"] }
449
+ excluding,
450
+ ...actor.permissions.has("communities:moderate", space.communityId) ? {} : { statuses: ["active"] }
309
451
  });
310
452
  }
311
453
  async expireDueGrants(now) {
@@ -392,8 +534,34 @@ var EntitlementsServiceImpl = class {
392
534
  events: appended
393
535
  });
394
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
+ }
395
563
  async requireSpaceIn(orgId, communityId, spaceId) {
396
- const space = await this.content.getSpace(orgId, spaceId);
564
+ const space = await this.content.getSpace(spaceId, this.system(orgId));
397
565
  if (!space || space.communityId !== communityId) {
398
566
  this.logger.warn("space members rejected — space not found", {
399
567
  orgId,
@@ -404,11 +572,13 @@ var EntitlementsServiceImpl = class {
404
572
  }
405
573
  return space;
406
574
  }
407
- async requireVisibleSpaceIn(orgId, actor, communityId, spaceId) {
575
+ async requireVisibleSpaceIn(actor, communityId, spaceId) {
576
+ const orgId = actor.orgId;
408
577
  const space = await this.requireSpaceIn(orgId, communityId, spaceId);
409
- 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;
410
580
  const row = await this.repo.findSpaceMember(orgId, spaceId, actor.id);
411
- 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);
412
582
  return space;
413
583
  }
414
584
  assertHasMembers(accessRule) {
@@ -418,21 +588,39 @@ var EntitlementsServiceImpl = class {
418
588
  this.assertHasMembers(accessRule);
419
589
  if (accessRule !== expected) throw new ConflictError(accessRule === "by_invite" ? "This space is by invitation" : "This space is by request");
420
590
  }
421
- assertHolder(actor) {
422
- 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
+ }
423
600
  }
424
- async assertGrant(orgId, actor, orgUserId, communityId) {
425
- 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");
426
603
  }
427
- async assertHolds(orgId, actor, orgUserId, communityId) {
428
- const facts = await this.accessOf(orgId, orgUserId, communityId, null);
429
- if (facts && isStaffRole(facts.role)) throw new ForbiddenError("Staff do not hold a place in a space");
430
- 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);
431
615
  }
432
- async assertCanManage(orgId, actor, communityId) {
433
- const facts = await this.accessOf(orgId, actor.id, communityId, null);
434
- if (facts?.member && canModerate(facts.role)) return;
435
- 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");
436
624
  }
437
625
  async accessOf(orgId, orgUserId, communityId, spaceId) {
438
626
  const [facts] = await this.repo.accessAmong(orgId, communityId, spaceId, [orgUserId]);
@@ -450,17 +638,17 @@ var EntitlementsServiceImpl = class {
450
638
  async insertMember(entitlements, outbox, orgId, actor, input, room) {
451
639
  const existing = await entitlements.lockSpaceMember(orgId, input.spaceId, input.orgUserId);
452
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");
453
- if (room) await this.assertRoom(entitlements, orgId, room);
641
+ if (room) await this.assertRoom(entitlements, orgId, room.space, room.excluding);
454
642
  if (existing) await entitlements.deleteSpaceMember(orgId, existing.id);
455
643
  const created = await entitlements.insertSpaceMember(orgId, input);
456
644
  await outbox.append([entitlementEvents.spaceMemberCreated.make({
457
645
  orgId,
458
646
  data: created,
459
- metadata: { actorOrgUserId: actor.id }
647
+ metadata: { actorId: actor.id }
460
648
  })]);
461
649
  return created;
462
650
  }
463
- async activate(orgId, actor, spaceId, orgUserId, rule, from, missing) {
651
+ async activate(orgId, actor, spaceId, orgUserId, rule, from, excluding, missing) {
464
652
  const updated = await this.uow.run(async ({ entitlements, spaces, outbox }) => {
465
653
  const locked = await this.lockSpace(spaces, orgId, spaceId);
466
654
  this.assertRule(locked.accessRule, rule);
@@ -475,14 +663,14 @@ var EntitlementsServiceImpl = class {
475
663
  });
476
664
  throw missing();
477
665
  }
478
- await this.assertRoom(entitlements, orgId, locked);
666
+ await this.assertRoom(entitlements, orgId, locked, excluding);
479
667
  const next = await entitlements.updateSpaceMemberStatus(orgId, row.id, "active");
480
668
  if (!next) throw missing();
481
669
  await outbox.append([entitlementEvents.spaceMemberUpdated.make({
482
670
  orgId,
483
671
  data: next,
484
672
  metadata: {
485
- actorOrgUserId: actor.id,
673
+ actorId: actor.id,
486
674
  previousStatus: from
487
675
  }
488
676
  })]);
@@ -514,7 +702,7 @@ var EntitlementsServiceImpl = class {
514
702
  orgId,
515
703
  data: current,
516
704
  metadata: {
517
- actorOrgUserId: actor.id,
705
+ actorId: actor.id,
518
706
  previousStatus: from
519
707
  }
520
708
  })]);
@@ -534,17 +722,36 @@ var EntitlementsServiceImpl = class {
534
722
  if (!locked) throw new NotFoundError("Space", spaceId);
535
723
  return locked;
536
724
  }
537
- async assertRoom(entitlements, orgId, space) {
725
+ async assertRoom(entitlements, orgId, space, excluding) {
538
726
  if (space.capacity === null) return;
539
- 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");
540
728
  }
541
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
+ }
542
740
  function alreadyPlaced(actor, orgUserId) {
543
741
  return new ConflictError(orgUserId === actor.id ? "You already have a place in this space" : "That person already has a place in this space");
544
742
  }
545
743
  function ownRowMissing(status) {
546
744
  return new ConflictError(status === "invited" ? "You do not have an invitation to this space" : "You are not a member of this space");
547
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
+ }
548
755
  //#endregion
549
756
  export { EntitlementsServiceImpl };
550
757