@headless-lms/server 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 (228) hide show
  1. package/README.md +14 -0
  2. package/dist/app/community-mail.d.ts +10 -10
  3. package/dist/app/community-mail.js +72 -28
  4. package/dist/app/community-mail.js.map +1 -1
  5. package/dist/app/container.d.ts +48 -10
  6. package/dist/app/container.js +268 -48
  7. package/dist/app/container.js.map +1 -1
  8. package/dist/app/notification-channels.d.ts +47 -0
  9. package/dist/app/notification-channels.js +175 -0
  10. package/dist/app/notification-channels.js.map +1 -0
  11. package/dist/app/notification-reads.d.ts +23 -0
  12. package/dist/app/notification-reads.js +74 -0
  13. package/dist/app/notification-reads.js.map +1 -0
  14. package/dist/app/platform.d.ts +14 -0
  15. package/dist/app/platform.js +14 -0
  16. package/dist/app/platform.js.map +1 -0
  17. package/dist/http/config.d.ts +4 -0
  18. package/dist/http/errors.d.ts +1 -0
  19. package/dist/http/errors.js +24 -4
  20. package/dist/http/errors.js.map +1 -1
  21. package/dist/http/fastify.d.ts +8 -0
  22. package/dist/http/mcp/catalogue.d.ts +11 -9
  23. package/dist/http/mcp/catalogue.js +15 -21
  24. package/dist/http/mcp/catalogue.js.map +1 -1
  25. package/dist/http/mcp/challenge.js +3 -3
  26. package/dist/http/mcp/challenge.js.map +1 -1
  27. package/dist/http/mcp/route.js +7 -7
  28. package/dist/http/mcp/route.js.map +1 -1
  29. package/dist/http/mcp/test-support.d.ts +6 -5
  30. package/dist/http/mcp/test-support.js +32 -11
  31. package/dist/http/mcp/test-support.js.map +1 -1
  32. package/dist/http/mcp/tool.d.ts +3 -9
  33. package/dist/http/mcp/tool.js +14 -6
  34. package/dist/http/mcp/tool.js.map +1 -1
  35. package/dist/http/mcp/tools/automations.js +10 -20
  36. package/dist/http/mcp/tools/automations.js.map +1 -1
  37. package/dist/http/mcp/tools/catalog.js +25 -42
  38. package/dist/http/mcp/tools/catalog.js.map +1 -1
  39. package/dist/http/mcp/tools/comments.js +9 -19
  40. package/dist/http/mcp/tools/comments.js.map +1 -1
  41. package/dist/http/mcp/tools/communities.js +16 -31
  42. package/dist/http/mcp/tools/communities.js.map +1 -1
  43. package/dist/http/mcp/tools/community-members.js +9 -16
  44. package/dist/http/mcp/tools/community-members.js.map +1 -1
  45. package/dist/http/mcp/tools/community-moderation.js +13 -25
  46. package/dist/http/mcp/tools/community-moderation.js.map +1 -1
  47. package/dist/http/mcp/tools/courses.js +11 -21
  48. package/dist/http/mcp/tools/courses.js.map +1 -1
  49. package/dist/http/mcp/tools/events.d.ts +6 -0
  50. package/dist/http/mcp/tools/events.js +170 -0
  51. package/dist/http/mcp/tools/events.js.map +1 -0
  52. package/dist/http/mcp/tools/index.d.ts +5 -1
  53. package/dist/http/mcp/tools/index.js +45 -10
  54. package/dist/http/mcp/tools/index.js.map +1 -1
  55. package/dist/http/mcp/tools/organization.d.ts +6 -0
  56. package/dist/http/mcp/tools/organization.js +214 -0
  57. package/dist/http/mcp/tools/organization.js.map +1 -0
  58. package/dist/http/mcp/tools/people.js +30 -49
  59. package/dist/http/mcp/tools/people.js.map +1 -1
  60. package/dist/http/plugins/auth.js +37 -8
  61. package/dist/http/plugins/auth.js.map +1 -1
  62. package/dist/http/plugins/error-handler.d.ts +5 -1
  63. package/dist/http/plugins/error-handler.js +13 -3
  64. package/dist/http/plugins/error-handler.js.map +1 -1
  65. package/dist/http/plugins/openapi.js +18 -2
  66. package/dist/http/plugins/openapi.js.map +1 -1
  67. package/dist/http/plugins/platform.d.ts +7 -0
  68. package/dist/http/plugins/platform.js +13 -0
  69. package/dist/http/plugins/platform.js.map +1 -0
  70. package/dist/http/routes/access.js +54 -36
  71. package/dist/http/routes/access.js.map +1 -1
  72. package/dist/http/routes/activities.js +45 -45
  73. package/dist/http/routes/activities.js.map +1 -1
  74. package/dist/http/routes/assets.js +30 -65
  75. package/dist/http/routes/assets.js.map +1 -1
  76. package/dist/http/routes/automations.js +37 -42
  77. package/dist/http/routes/automations.js.map +1 -1
  78. package/dist/http/routes/bundles.js +33 -36
  79. package/dist/http/routes/bundles.js.map +1 -1
  80. package/dist/http/routes/communities.js +144 -136
  81. package/dist/http/routes/communities.js.map +1 -1
  82. package/dist/http/routes/community-moderation.js +83 -137
  83. package/dist/http/routes/community-moderation.js.map +1 -1
  84. package/dist/http/routes/course-analytics.js +32 -15
  85. package/dist/http/routes/course-analytics.js.map +1 -1
  86. package/dist/http/routes/courses.js +25 -32
  87. package/dist/http/routes/courses.js.map +1 -1
  88. package/dist/http/routes/dashboard.js +9 -9
  89. package/dist/http/routes/dashboard.js.map +1 -1
  90. package/dist/http/routes/discussion.js +21 -39
  91. package/dist/http/routes/discussion.js.map +1 -1
  92. package/dist/http/routes/downloads.js +41 -47
  93. package/dist/http/routes/downloads.js.map +1 -1
  94. package/dist/http/routes/entitlements.js +19 -23
  95. package/dist/http/routes/entitlements.js.map +1 -1
  96. package/dist/http/routes/events.d.ts +7 -0
  97. package/dist/http/routes/events.js +333 -0
  98. package/dist/http/routes/events.js.map +1 -0
  99. package/dist/http/routes/integrations.js +40 -73
  100. package/dist/http/routes/integrations.js.map +1 -1
  101. package/dist/http/routes/member/assets.js +10 -13
  102. package/dist/http/routes/member/assets.js.map +1 -1
  103. package/dist/http/routes/member/chats.js +177 -75
  104. package/dist/http/routes/member/chats.js.map +1 -1
  105. package/dist/http/routes/member/comments.js +65 -26
  106. package/dist/http/routes/member/comments.js.map +1 -1
  107. package/dist/http/routes/member/community.js +68 -51
  108. package/dist/http/routes/member/community.js.map +1 -1
  109. package/dist/http/routes/member/courses.js +12 -19
  110. package/dist/http/routes/member/courses.js.map +1 -1
  111. package/dist/http/routes/member/devices.d.ts +7 -0
  112. package/dist/http/routes/member/devices.js +97 -0
  113. package/dist/http/routes/member/devices.js.map +1 -0
  114. package/dist/http/routes/member/downloads.js +6 -9
  115. package/dist/http/routes/member/downloads.js.map +1 -1
  116. package/dist/http/routes/member/events.d.ts +7 -0
  117. package/dist/http/routes/member/events.js +386 -0
  118. package/dist/http/routes/member/events.js.map +1 -0
  119. package/dist/http/routes/member/files.js +71 -46
  120. package/dist/http/routes/member/files.js.map +1 -1
  121. package/dist/http/routes/member/index.js +6 -0
  122. package/dist/http/routes/member/index.js.map +1 -1
  123. package/dist/http/routes/member/notification-settings.d.ts +7 -0
  124. package/dist/http/routes/member/notification-settings.js +100 -0
  125. package/dist/http/routes/member/notification-settings.js.map +1 -0
  126. package/dist/http/routes/member/notifications.js +69 -45
  127. package/dist/http/routes/member/notifications.js.map +1 -1
  128. package/dist/http/routes/member/org.js +5 -15
  129. package/dist/http/routes/member/org.js.map +1 -1
  130. package/dist/http/routes/member/posts.js +94 -95
  131. package/dist/http/routes/member/posts.js.map +1 -1
  132. package/dist/http/routes/member/profile.js +15 -6
  133. package/dist/http/routes/member/profile.js.map +1 -1
  134. package/dist/http/routes/member/progress.js +7 -11
  135. package/dist/http/routes/member/progress.js.map +1 -1
  136. package/dist/http/routes/organizations.js +415 -110
  137. package/dist/http/routes/organizations.js.map +1 -1
  138. package/dist/http/routes/platform.d.ts +7 -0
  139. package/dist/http/routes/platform.js +61 -0
  140. package/dist/http/routes/platform.js.map +1 -0
  141. package/dist/http/routes/students.js +12 -75
  142. package/dist/http/routes/students.js.map +1 -1
  143. package/dist/http/routes/webhooks.js +30 -39
  144. package/dist/http/routes/webhooks.js.map +1 -1
  145. package/dist/http/routes.js +9 -0
  146. package/dist/http/routes.js.map +1 -1
  147. package/dist/http/schemas/access.d.ts +346 -64
  148. package/dist/http/schemas/access.js +39 -12
  149. package/dist/http/schemas/access.js.map +1 -1
  150. package/dist/http/schemas/assets.d.ts +13 -8
  151. package/dist/http/schemas/assets.js +6 -5
  152. package/dist/http/schemas/assets.js.map +1 -1
  153. package/dist/http/schemas/automations.d.ts +4 -8
  154. package/dist/http/schemas/chats.d.ts +238 -36
  155. package/dist/http/schemas/chats.js +14 -7
  156. package/dist/http/schemas/chats.js.map +1 -1
  157. package/dist/http/schemas/community.d.ts +140 -33
  158. package/dist/http/schemas/community.js +25 -9
  159. package/dist/http/schemas/community.js.map +1 -1
  160. package/dist/http/schemas/course-analytics.d.ts +51 -1
  161. package/dist/http/schemas/course-analytics.js +19 -1
  162. package/dist/http/schemas/course-analytics.js.map +1 -1
  163. package/dist/http/schemas/dashboard.d.ts +1 -0
  164. package/dist/http/schemas/dashboard.js +2 -1
  165. package/dist/http/schemas/dashboard.js.map +1 -1
  166. package/dist/http/schemas/discussion.d.ts +45 -73
  167. package/dist/http/schemas/discussion.js +16 -4
  168. package/dist/http/schemas/discussion.js.map +1 -1
  169. package/dist/http/schemas/entitlements.d.ts +9 -12
  170. package/dist/http/schemas/entitlements.js +11 -5
  171. package/dist/http/schemas/entitlements.js.map +1 -1
  172. package/dist/http/schemas/events.d.ts +527 -0
  173. package/dist/http/schemas/events.js +64 -0
  174. package/dist/http/schemas/events.js.map +1 -0
  175. package/dist/http/schemas/files.d.ts +15 -5
  176. package/dist/http/schemas/files.js +10 -2
  177. package/dist/http/schemas/files.js.map +1 -1
  178. package/dist/http/schemas/index.d.ts +14 -12
  179. package/dist/http/schemas/index.js +14 -12
  180. package/dist/http/schemas/invites.d.ts +3 -47
  181. package/dist/http/schemas/invites.js +4 -25
  182. package/dist/http/schemas/invites.js.map +1 -1
  183. package/dist/http/schemas/member.d.ts +94 -7
  184. package/dist/http/schemas/member.js +7 -3
  185. package/dist/http/schemas/member.js.map +1 -1
  186. package/dist/http/schemas/members.d.ts +329 -65
  187. package/dist/http/schemas/members.js +67 -25
  188. package/dist/http/schemas/members.js.map +1 -1
  189. package/dist/http/schemas/notifications.d.ts +502 -175
  190. package/dist/http/schemas/notifications.js +148 -28
  191. package/dist/http/schemas/notifications.js.map +1 -1
  192. package/dist/http/schemas/platform.d.ts +37 -0
  193. package/dist/http/schemas/platform.js +19 -0
  194. package/dist/http/schemas/platform.js.map +1 -0
  195. package/dist/http/schemas/posts.d.ts +92 -38
  196. package/dist/http/schemas/posts.js +4 -4
  197. package/dist/http/schemas/posts.js.map +1 -1
  198. package/dist/http/schemas/profile.d.ts +1 -1
  199. package/dist/http/schemas/shared.d.ts +10 -13
  200. package/dist/http/schemas/shared.js +3 -11
  201. package/dist/http/schemas/shared.js.map +1 -1
  202. package/dist/http/schemas/students.d.ts +22 -0
  203. package/dist/http/schemas/students.js +3 -1
  204. package/dist/http/schemas/students.js.map +1 -1
  205. package/dist/http/scope.d.ts +9 -12
  206. package/dist/http/scope.js +30 -35
  207. package/dist/http/scope.js.map +1 -1
  208. package/dist/http/server.js +9 -0
  209. package/dist/http/server.js.map +1 -1
  210. package/dist/http/shutdown.d.ts +11 -0
  211. package/dist/http/shutdown.js +40 -0
  212. package/dist/http/shutdown.js.map +1 -0
  213. package/dist/http/sse.d.ts +19 -0
  214. package/dist/http/sse.js +104 -0
  215. package/dist/http/sse.js.map +1 -0
  216. package/dist/http/test-caller.d.ts +13 -0
  217. package/dist/http/test-caller.js +14 -0
  218. package/dist/http/test-caller.js.map +1 -0
  219. package/dist/http/well-known.js +2 -2
  220. package/dist/http/well-known.js.map +1 -1
  221. package/dist/index.d.ts +5 -2
  222. package/dist/index.js +4 -2
  223. package/dist/index.js.map +1 -1
  224. package/dist/packages/server/package.js +1 -1
  225. package/package.json +7 -6
  226. package/dist/http/routes/member/actor.d.ts +0 -14
  227. package/dist/http/routes/member/actor.js +0 -11
  228. package/dist/http/routes/member/actor.js.map +0 -1
package/README.md CHANGED
@@ -10,14 +10,28 @@ integration plugins. The `headless-lms` bin lives in `@headless-lms/cli`.
10
10
 
11
11
  ```ts
12
12
  import { createContainer, buildServer } from '@headless-lms/server';
13
+ import { PostgresPubSub } from '@headless-lms/adapter-pubsub-postgres';
13
14
 
14
15
  const container = await createContainer(config, {
15
16
  pluginsDir, // one folder per integration (directory name = integration id)
17
+ adapters: {
18
+ pubsub: (logger) => new PostgresPubSub({ connectionString: config.pubsubDatabaseUrl, logger }),
19
+ },
16
20
  });
17
21
  const app = await buildServer(config, container);
18
22
  await app.listen({ port: config.port, host: config.host });
23
+ await container.pubsub?.start();
24
+ await container.chatHub?.start();
25
+ container.outboxRelay.start();
26
+ await container.automationEngine.start();
19
27
  ```
20
28
 
29
+ The container constructs the outbox relay, the automation engine, `PubSub` and the
30
+ chat hub but starts none of them; the entry point starts them after `listen`, `PubSub`
31
+ and the hub before the relay. `buildServer` closes the hub in `preClose`, so open chat
32
+ streams receive their closing frame, and on close stops the relay, the engine and then
33
+ `PubSub`.
34
+
21
35
  Public surface (`src/index.ts`): `createContainer`, `buildServer`,
22
36
  `loadIntegrations`, the operational function `runMigrations` (re-exported from
23
37
  `@headless-lms/adapter-db`, which ships the `drizzle/` migration assets;
@@ -1,25 +1,25 @@
1
1
  import { ContentService } from "@headless-lms/core/content";
2
2
  import { EntitlementsService } from "@headless-lms/core/entitlements";
3
- import { ChatService, DiscussionService, PostService } from "@headless-lms/core/discussion";
3
+ import { ChatService, DiscussionService, PostService, SubscriptionService } from "@headless-lms/core/discussion";
4
4
  import { IdentityService } from "@headless-lms/core/identity";
5
5
  import { OrganizationService } from "@headless-lms/core/organizations";
6
- import { AssetsService } from "@headless-lms/core/assets";
7
- import { CommunityMailReads, GrantWriter, ScheduledWork } from "@headless-lms/core/automations";
6
+ import { CommunityMailReads, GrantWriter } from "@headless-lms/core/automations";
7
+ import { BodyParser } from "@headless-lms/core/shared/ports";
8
8
  //#region src/app/community-mail.d.ts
9
9
  type CommunityMailDeps = {
10
- content: Pick<ContentService, 'getCommunity' | 'getSpace' | 'listCommunitiesById'>;
11
- entitlements: Pick<EntitlementsService, 'openSpacesFor' | 'communitiesOpenedBy' | 'communityMembersAmong' | 'spaceOpenersAmong' | 'spaceOpeners' | 'communityModeratorIds' | 'countActiveSpaceMembers' | 'communityIdsOpenedFor' | 'ensureGrant' | 'expireDue' | 'hasDueNotices'>;
12
- organizations: Pick<OrganizationService, 'getOrgUserById' | 'staffOrgUserIds'>;
10
+ content: Pick<ContentService, 'findCommunity' | 'getSpace' | 'listCommunitiesById' | 'findCourse' | 'findDownload' | 'findEvent' | 'findBundle'>;
11
+ entitlements: Pick<EntitlementsService, 'openSpacesFor' | 'communitiesOpenedBy' | 'communityMembersAmong' | 'spaceOpenersAmong' | 'spaceOpeners' | 'countActiveSpaceMembers' | 'communityIdsOpenedFor' | 'grant'>;
12
+ organizations: Pick<OrganizationService, 'getById' | 'getOrgUserById' | 'actorFor' | 'systemActor' | 'holders' | 'getRole' | 'permissionsOfMany'>;
13
13
  identity: Pick<IdentityService, 'displayName'>;
14
- posts: Pick<PostService, 'followerIds' | 'find' | 'publishDue' | 'hasDue'>;
15
- discussion: Pick<DiscussionService, 'getComment' | 'sendDigest'>;
14
+ posts: Pick<PostService, 'find'>;
15
+ subscriptions: Pick<SubscriptionService, 'followerIds' | 'itemSubscriptions'>;
16
+ discussion: Pick<DiscussionService, 'getComment'>;
16
17
  chats: Pick<ChatService, 'chatSummary' | 'latestMessageBody' | 'isBlocked'>;
17
- assets: Pick<AssetsService, 'removeUnattached' | 'hasUnattached'>;
18
+ bodies: BodyParser;
18
19
  };
19
20
  declare function communityMail(deps: CommunityMailDeps): {
20
21
  mail: Omit<CommunityMailReads, 'recentMessageRecipients'>;
21
22
  grants: GrantWriter;
22
- scheduledWork: ScheduledWork;
23
23
  };
24
24
  //#endregion
25
25
  export { CommunityMailDeps, communityMail };
@@ -1,12 +1,14 @@
1
+ import { NotFoundError } from "@headless-lms/core/shared/errors";
1
2
  //#region src/app/community-mail.ts
2
3
  function communityMail(deps) {
4
+ const system = (orgId) => deps.organizations.systemActor(orgId);
3
5
  return {
4
6
  mail: {
5
7
  async communityTitle(orgId, communityId) {
6
- return (await deps.content.getCommunity(orgId, communityId))?.title ?? null;
8
+ return (await deps.content.findCommunity(communityId, system(orgId)))?.title ?? null;
7
9
  },
8
10
  async space(orgId, spaceId) {
9
- const space = await deps.content.getSpace(orgId, spaceId);
11
+ const space = await deps.content.getSpace(spaceId, system(orgId));
10
12
  return space ? {
11
13
  name: space.name,
12
14
  communityId: space.communityId,
@@ -14,29 +16,77 @@ function communityMail(deps) {
14
16
  } : null;
15
17
  },
16
18
  async openSpaceNames(orgId, orgUserId, communityId) {
17
- const orgUser = await deps.organizations.getOrgUserById(orgId, orgUserId);
18
- if (!orgUser) return [];
19
- return (await deps.entitlements.openSpacesFor(orgId, {
20
- id: orgUser.id,
21
- role: orgUser.role
22
- }, communityId)).map((space) => space.name);
19
+ const person = await deps.organizations.actorFor(orgId, orgUserId);
20
+ if (!person) return [];
21
+ return (await deps.entitlements.openSpacesFor(communityId, person)).map((space) => space.name);
23
22
  },
24
23
  communitiesOpenedBy: (orgId, grant) => deps.entitlements.communitiesOpenedBy(orgId, grant),
25
24
  communityMembersAmong: (orgId, communityId, ids) => deps.entitlements.communityMembersAmong(orgId, communityId, ids),
26
25
  spaceOpenersAmong: (orgId, spaceId, ids) => deps.entitlements.spaceOpenersAmong(orgId, spaceId, ids),
27
26
  spaceOpeners: (orgId, spaceId) => deps.entitlements.spaceOpeners(orgId, spaceId),
28
- moderationAudience: (orgId, communityId) => deps.entitlements.communityModeratorIds(orgId, communityId),
29
- staffIds: (orgId) => deps.organizations.staffOrgUserIds(orgId),
27
+ holders: (orgId, permission, on) => deps.organizations.holders(orgId, permission, on),
28
+ async heldOnOrg(orgId, orgUserId, permissions) {
29
+ const held = (await deps.organizations.permissionsOfMany(orgId, [orgUserId])).get(orgUserId);
30
+ return held ? permissions.filter((permission) => held.has(permission)) : [];
31
+ },
32
+ async role(orgId, roleKey) {
33
+ try {
34
+ const role = await deps.organizations.getRole(roleKey, system(orgId));
35
+ return {
36
+ name: role.name,
37
+ permissions: role.permissions
38
+ };
39
+ } catch (err) {
40
+ if (err instanceof NotFoundError) return null;
41
+ throw err;
42
+ }
43
+ },
44
+ async orgName(orgId) {
45
+ return (await deps.organizations.getById(orgId))?.name ?? null;
46
+ },
47
+ async contentInfo(orgId, item) {
48
+ const actor = system(orgId);
49
+ if ("bundleId" in item) {
50
+ const bundle = await deps.content.findBundle(item.bundleId, actor);
51
+ return bundle ? {
52
+ title: bundle.name,
53
+ type: "bundle"
54
+ } : null;
55
+ }
56
+ const course = await deps.content.findCourse(item.contentId, actor);
57
+ if (course) return {
58
+ title: course.title,
59
+ type: course.type
60
+ };
61
+ const download = await deps.content.findDownload(item.contentId, actor);
62
+ if (download) return {
63
+ title: download.title,
64
+ type: download.type
65
+ };
66
+ const community = await deps.content.findCommunity(item.contentId, actor);
67
+ if (community) return {
68
+ title: community.title,
69
+ type: community.type
70
+ };
71
+ const event = await deps.content.findEvent(item.contentId, actor);
72
+ return event ? {
73
+ title: event.title,
74
+ type: event.type
75
+ } : null;
76
+ },
30
77
  async displayName(orgId, orgUserId) {
31
78
  const orgUser = await deps.organizations.getOrgUserById(orgId, orgUserId);
32
79
  return deps.identity.displayName(orgUser?.userId ?? null);
33
80
  },
34
- followerIds: (orgId, postId) => deps.posts.followerIds(orgId, postId),
81
+ postText: (body) => deps.bodies.parse(body).text,
82
+ followerIds: (orgId, postId) => deps.subscriptions.followerIds(orgId, postId),
35
83
  async comment(orgId, commentId) {
36
84
  const comment = await deps.discussion.getComment(orgId, commentId);
37
85
  return comment ? {
38
86
  orgUserId: comment.orgUserId,
39
87
  postId: comment.postId,
88
+ activityId: comment.activityId,
89
+ parentId: comment.parentId,
40
90
  body: comment.body
41
91
  } : null;
42
92
  },
@@ -52,42 +102,36 @@ function communityMail(deps) {
52
102
  },
53
103
  async chat(orgId, chatId) {
54
104
  const summary = await deps.chats.chatSummary(orgId, chatId);
55
- return summary ? {
105
+ if (!summary) return null;
106
+ const mutes = new Map((await deps.subscriptions.itemSubscriptions(orgId, {
107
+ type: "chat",
108
+ id: chatId
109
+ })).filter((subscription) => subscription.state === "muted").map((subscription) => [subscription.orgUserId, subscription.mutedUntil]));
110
+ return {
56
111
  communityId: summary.chat.communityId,
57
112
  kind: summary.chat.kind,
58
113
  status: summary.chat.status,
59
114
  createdBy: summary.chat.createdBy,
60
115
  participants: summary.participants.map((participant) => ({
61
116
  orgUserId: participant.orgUserId,
62
- muted: participant.muted,
63
- mutedUntil: participant.mutedUntil,
117
+ muted: mutes.has(participant.orgUserId),
118
+ mutedUntil: mutes.get(participant.orgUserId) ?? null,
64
119
  leftAt: participant.leftAt
65
120
  }))
66
- } : null;
121
+ };
67
122
  },
68
123
  latestMessageBody: (orgId, chatId) => deps.chats.latestMessageBody(orgId, chatId),
69
124
  pairBlocked: (orgId, communityId, a, b) => deps.chats.isBlocked(orgId, communityId, a, b),
70
125
  activeMemberCount: (orgId, spaceId) => deps.entitlements.countActiveSpaceMembers(orgId, spaceId),
71
126
  async memberCommunityTitles(orgId, orgUserId) {
72
127
  const ids = await deps.entitlements.communityIdsOpenedFor(orgId, orgUserId);
73
- return (await deps.content.listCommunitiesById(orgId, ids)).map((community) => ({
128
+ return (await deps.content.listCommunitiesById(ids, system(orgId))).map((community) => ({
74
129
  id: community.id,
75
130
  title: community.title
76
131
  }));
77
132
  }
78
133
  },
79
- grants: { grant: (orgId, input) => deps.entitlements.ensureGrant(orgId, input) },
80
- scheduledWork: {
81
- publishDuePosts: (orgId, now) => deps.posts.publishDue(orgId, now),
82
- hasDuePosts: (orgId, now) => deps.posts.hasDue(orgId, now),
83
- expireDueGrants: (orgId, now) => deps.entitlements.expireDue(orgId, now),
84
- hasDueGrants: (orgId, now) => deps.entitlements.hasDueNotices(orgId, now),
85
- sendDigest: (orgId, now) => deps.discussion.sendDigest(orgId, now),
86
- removeUnattachedUploads: async (orgId, now) => {
87
- await deps.assets.removeUnattached(orgId, now);
88
- },
89
- hasUnattachedUploads: (orgId, now) => deps.assets.hasUnattached(orgId, now)
90
- }
134
+ grants: { grant: (orgId, input) => deps.entitlements.grant(input, system(orgId)) }
91
135
  };
92
136
  }
93
137
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"community-mail.js","names":[],"sources":["../../src/app/community-mail.ts"],"sourcesContent":["import type { AssetsService } from '@headless-lms/core/assets';\nimport type {\n CommunityMailReads,\n GrantWriter,\n ScheduledWork,\n} from '@headless-lms/core/automations';\nimport type { ContentService } from '@headless-lms/core/content';\nimport type { ChatService, DiscussionService, PostService } from '@headless-lms/core/discussion';\nimport type { EntitlementsService } from '@headless-lms/core/entitlements';\nimport type { IdentityService } from '@headless-lms/core/identity';\nimport type { OrganizationService } from '@headless-lms/core/organizations';\n\nexport type CommunityMailDeps = {\n content: Pick<ContentService, 'getCommunity' | 'getSpace' | 'listCommunitiesById'>;\n entitlements: Pick<\n EntitlementsService,\n | 'openSpacesFor'\n | 'communitiesOpenedBy'\n | 'communityMembersAmong'\n | 'spaceOpenersAmong'\n | 'spaceOpeners'\n | 'communityModeratorIds'\n | 'countActiveSpaceMembers'\n | 'communityIdsOpenedFor'\n | 'ensureGrant'\n | 'expireDue'\n | 'hasDueNotices'\n >;\n organizations: Pick<OrganizationService, 'getOrgUserById' | 'staffOrgUserIds'>;\n identity: Pick<IdentityService, 'displayName'>;\n posts: Pick<PostService, 'followerIds' | 'find' | 'publishDue' | 'hasDue'>;\n discussion: Pick<DiscussionService, 'getComment' | 'sendDigest'>;\n chats: Pick<ChatService, 'chatSummary' | 'latestMessageBody' | 'isBlocked'>;\n assets: Pick<AssetsService, 'removeUnattached' | 'hasUnattached'>;\n};\n\nexport function communityMail(deps: CommunityMailDeps): {\n mail: Omit<CommunityMailReads, 'recentMessageRecipients'>;\n grants: GrantWriter;\n scheduledWork: ScheduledWork;\n} {\n const mail: Omit<CommunityMailReads, 'recentMessageRecipients'> = {\n async communityTitle(orgId, communityId) {\n return (await deps.content.getCommunity(orgId, communityId))?.title ?? null;\n },\n async space(orgId, spaceId) {\n const space = await deps.content.getSpace(orgId, spaceId);\n return space\n ? { name: space.name, communityId: space.communityId, capacity: space.capacity }\n : null;\n },\n async openSpaceNames(orgId, orgUserId, communityId) {\n const orgUser = await deps.organizations.getOrgUserById(orgId, orgUserId);\n if (!orgUser) {\n return [];\n }\n const spaces = await deps.entitlements.openSpacesFor(\n orgId,\n { id: orgUser.id, role: orgUser.role },\n communityId,\n );\n return spaces.map((space) => space.name);\n },\n communitiesOpenedBy: (orgId, grant) => deps.entitlements.communitiesOpenedBy(orgId, grant),\n communityMembersAmong: (orgId, communityId, ids) =>\n deps.entitlements.communityMembersAmong(orgId, communityId, ids),\n spaceOpenersAmong: (orgId, spaceId, ids) =>\n deps.entitlements.spaceOpenersAmong(orgId, spaceId, ids),\n spaceOpeners: (orgId, spaceId) => deps.entitlements.spaceOpeners(orgId, spaceId),\n moderationAudience: (orgId, communityId) =>\n deps.entitlements.communityModeratorIds(orgId, communityId),\n staffIds: (orgId) => deps.organizations.staffOrgUserIds(orgId),\n async displayName(orgId, orgUserId) {\n const orgUser = await deps.organizations.getOrgUserById(orgId, orgUserId);\n return deps.identity.displayName(orgUser?.userId ?? null);\n },\n followerIds: (orgId, postId) => deps.posts.followerIds(orgId, postId),\n async comment(orgId, commentId) {\n const comment = await deps.discussion.getComment(orgId, commentId);\n return comment\n ? { orgUserId: comment.orgUserId, postId: comment.postId, body: comment.body }\n : null;\n },\n async post(orgId, postId) {\n const post = await deps.posts.find(orgId, postId);\n return post\n ? {\n title: post.title,\n body: post.body,\n spaceId: post.spaceId,\n orgUserId: post.orgUserId,\n communityId: post.communityId,\n }\n : null;\n },\n async chat(orgId, chatId) {\n const summary = await deps.chats.chatSummary(orgId, chatId);\n return summary\n ? {\n communityId: summary.chat.communityId,\n kind: summary.chat.kind,\n status: summary.chat.status,\n createdBy: summary.chat.createdBy,\n participants: summary.participants.map((participant) => ({\n orgUserId: participant.orgUserId,\n muted: participant.muted,\n mutedUntil: participant.mutedUntil,\n leftAt: participant.leftAt,\n })),\n }\n : null;\n },\n latestMessageBody: (orgId, chatId) => deps.chats.latestMessageBody(orgId, chatId),\n pairBlocked: (orgId, communityId, a, b) => deps.chats.isBlocked(orgId, communityId, a, b),\n activeMemberCount: (orgId, spaceId) =>\n deps.entitlements.countActiveSpaceMembers(orgId, spaceId),\n async memberCommunityTitles(orgId, orgUserId) {\n const ids = await deps.entitlements.communityIdsOpenedFor(orgId, orgUserId);\n const communities = await deps.content.listCommunitiesById(orgId, ids);\n return communities.map((community) => ({ id: community.id, title: community.title }));\n },\n };\n return {\n mail,\n grants: {\n grant: (orgId, input) => deps.entitlements.ensureGrant(orgId, input),\n },\n scheduledWork: {\n publishDuePosts: (orgId, now) => deps.posts.publishDue(orgId, now),\n hasDuePosts: (orgId, now) => deps.posts.hasDue(orgId, now),\n expireDueGrants: (orgId, now) => deps.entitlements.expireDue(orgId, now),\n hasDueGrants: (orgId, now) => deps.entitlements.hasDueNotices(orgId, now),\n sendDigest: (orgId, now) => deps.discussion.sendDigest(orgId, now),\n removeUnattachedUploads: async (orgId, now) => {\n await deps.assets.removeUnattached(orgId, now);\n },\n hasUnattachedUploads: (orgId, now) => deps.assets.hasUnattached(orgId, now),\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"community-mail.js","names":[],"sources":["../../src/app/community-mail.ts"],"sourcesContent":["import type { CommunityMailReads, GrantWriter } from '@headless-lms/core/automations';\nimport type { ContentService } from '@headless-lms/core/content';\nimport type {\n ChatService,\n DiscussionService,\n PostService,\n SubscriptionService,\n} from '@headless-lms/core/discussion';\nimport type { EntitlementsService } from '@headless-lms/core/entitlements';\nimport type { IdentityService } from '@headless-lms/core/identity';\nimport type { OrganizationService } from '@headless-lms/core/organizations';\nimport { NotFoundError } from '@headless-lms/core/shared/errors';\nimport type { BodyParser } from '@headless-lms/core/shared/ports';\n\nexport type CommunityMailDeps = {\n content: Pick<\n ContentService,\n | 'findCommunity'\n | 'getSpace'\n | 'listCommunitiesById'\n | 'findCourse'\n | 'findDownload'\n | 'findEvent'\n | 'findBundle'\n >;\n entitlements: Pick<\n EntitlementsService,\n | 'openSpacesFor'\n | 'communitiesOpenedBy'\n | 'communityMembersAmong'\n | 'spaceOpenersAmong'\n | 'spaceOpeners'\n | 'countActiveSpaceMembers'\n | 'communityIdsOpenedFor'\n | 'grant'\n >;\n organizations: Pick<\n OrganizationService,\n | 'getById'\n | 'getOrgUserById'\n | 'actorFor'\n | 'systemActor'\n | 'holders'\n | 'getRole'\n | 'permissionsOfMany'\n >;\n identity: Pick<IdentityService, 'displayName'>;\n posts: Pick<PostService, 'find'>;\n subscriptions: Pick<SubscriptionService, 'followerIds' | 'itemSubscriptions'>;\n discussion: Pick<DiscussionService, 'getComment'>;\n chats: Pick<ChatService, 'chatSummary' | 'latestMessageBody' | 'isBlocked'>;\n bodies: BodyParser;\n};\n\nexport function communityMail(deps: CommunityMailDeps): {\n mail: Omit<CommunityMailReads, 'recentMessageRecipients'>;\n grants: GrantWriter;\n} {\n const system = (orgId: string) => deps.organizations.systemActor(orgId);\n const mail: Omit<CommunityMailReads, 'recentMessageRecipients'> = {\n async communityTitle(orgId, communityId) {\n return (await deps.content.findCommunity(communityId, system(orgId)))?.title ?? null;\n },\n async space(orgId, spaceId) {\n const space = await deps.content.getSpace(spaceId, system(orgId));\n return space\n ? { name: space.name, communityId: space.communityId, capacity: space.capacity }\n : null;\n },\n async openSpaceNames(orgId, orgUserId, communityId) {\n const person = await deps.organizations.actorFor(orgId, orgUserId);\n if (!person) {\n return [];\n }\n const spaces = await deps.entitlements.openSpacesFor(communityId, person);\n return spaces.map((space) => space.name);\n },\n communitiesOpenedBy: (orgId, grant) => deps.entitlements.communitiesOpenedBy(orgId, grant),\n communityMembersAmong: (orgId, communityId, ids) =>\n deps.entitlements.communityMembersAmong(orgId, communityId, ids),\n spaceOpenersAmong: (orgId, spaceId, ids) =>\n deps.entitlements.spaceOpenersAmong(orgId, spaceId, ids),\n spaceOpeners: (orgId, spaceId) => deps.entitlements.spaceOpeners(orgId, spaceId),\n holders: (orgId, permission, on) => deps.organizations.holders(orgId, permission, on),\n async heldOnOrg(orgId, orgUserId, permissions) {\n const held = (await deps.organizations.permissionsOfMany(orgId, [orgUserId])).get(orgUserId);\n return held ? permissions.filter((permission) => held.has(permission)) : [];\n },\n async role(orgId, roleKey) {\n try {\n const role = await deps.organizations.getRole(roleKey, system(orgId));\n return { name: role.name, permissions: role.permissions };\n } catch (err) {\n if (err instanceof NotFoundError) {\n return null;\n }\n throw err;\n }\n },\n async orgName(orgId) {\n return (await deps.organizations.getById(orgId))?.name ?? null;\n },\n async contentInfo(orgId, item) {\n const actor = system(orgId);\n if ('bundleId' in item) {\n const bundle = await deps.content.findBundle(item.bundleId, actor);\n return bundle ? { title: bundle.name, type: 'bundle' } : null;\n }\n const course = await deps.content.findCourse(item.contentId, actor);\n if (course) {\n return { title: course.title, type: course.type };\n }\n const download = await deps.content.findDownload(item.contentId, actor);\n if (download) {\n return { title: download.title, type: download.type };\n }\n const community = await deps.content.findCommunity(item.contentId, actor);\n if (community) {\n return { title: community.title, type: community.type };\n }\n const event = await deps.content.findEvent(item.contentId, actor);\n return event ? { title: event.title, type: event.type } : null;\n },\n async displayName(orgId, orgUserId) {\n const orgUser = await deps.organizations.getOrgUserById(orgId, orgUserId);\n return deps.identity.displayName(orgUser?.userId ?? null);\n },\n postText: (body) => deps.bodies.parse(body).text,\n followerIds: (orgId, postId) => deps.subscriptions.followerIds(orgId, postId),\n async comment(orgId, commentId) {\n const comment = await deps.discussion.getComment(orgId, commentId);\n return comment\n ? {\n orgUserId: comment.orgUserId,\n postId: comment.postId,\n activityId: comment.activityId,\n parentId: comment.parentId,\n body: comment.body,\n }\n : null;\n },\n async post(orgId, postId) {\n const post = await deps.posts.find(orgId, postId);\n return post\n ? {\n title: post.title,\n body: post.body,\n spaceId: post.spaceId,\n orgUserId: post.orgUserId,\n communityId: post.communityId,\n }\n : null;\n },\n async chat(orgId, chatId) {\n const summary = await deps.chats.chatSummary(orgId, chatId);\n if (!summary) {\n return null;\n }\n const mutes = new Map(\n (await deps.subscriptions.itemSubscriptions(orgId, { type: 'chat', id: chatId }))\n .filter((subscription) => subscription.state === 'muted')\n .map((subscription) => [subscription.orgUserId, subscription.mutedUntil]),\n );\n return {\n communityId: summary.chat.communityId,\n kind: summary.chat.kind,\n status: summary.chat.status,\n createdBy: summary.chat.createdBy,\n participants: summary.participants.map((participant) => ({\n orgUserId: participant.orgUserId,\n muted: mutes.has(participant.orgUserId),\n mutedUntil: mutes.get(participant.orgUserId) ?? null,\n leftAt: participant.leftAt,\n })),\n };\n },\n latestMessageBody: (orgId, chatId) => deps.chats.latestMessageBody(orgId, chatId),\n pairBlocked: (orgId, communityId, a, b) => deps.chats.isBlocked(orgId, communityId, a, b),\n activeMemberCount: (orgId, spaceId) =>\n deps.entitlements.countActiveSpaceMembers(orgId, spaceId),\n async memberCommunityTitles(orgId, orgUserId) {\n const ids = await deps.entitlements.communityIdsOpenedFor(orgId, orgUserId);\n const communities = await deps.content.listCommunitiesById(ids, system(orgId));\n return communities.map((community) => ({ id: community.id, title: community.title }));\n },\n };\n return {\n mail,\n grants: {\n grant: (orgId, input) => deps.entitlements.grant(input, system(orgId)),\n },\n };\n}\n"],"mappings":"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"}
@@ -1,22 +1,24 @@
1
1
  import { McpEndpoints } from "./mcp-endpoints.js";
2
+ import { PlatformConfig, PlatformKeyCheck } from "./platform.js";
3
+ import { DatabasePoolOptions } from "@headless-lms/adapter-db";
2
4
  import { PollingOutboxRelayConfig } from "@headless-lms/adapter-defaults/events/outbox-relay";
3
5
  import { LogLevel, PinoInstance, RequestLogContext } from "@headless-lms/adapter-defaults/logging";
4
6
  import { BetterAuth } from "@headless-lms/adapter-auth";
5
- import { ContentService } from "@headless-lms/core/content";
7
+ import { CommunityNotificationCategories, ContentService } from "@headless-lms/core/content";
6
8
  import { EntitlementsServiceImpl } from "@headless-lms/core/entitlements";
7
9
  import { ProgressServiceImpl } from "@headless-lms/core/progress";
8
- import { ChatServiceImpl, DiscussionServiceImpl, PostServiceImpl } from "@headless-lms/core/discussion";
9
- import { IdentityServiceImpl, NotificationServiceImpl, SessionAdmin } from "@headless-lms/core/identity";
10
- import { OrgAdmin, OrganizationServiceImpl } from "@headless-lms/core/organizations";
10
+ import { ChatHub, ChatHubOptions, ChatServiceImpl, DiscussionServiceImpl, PostServiceImpl, SubscriptionServiceImpl } from "@headless-lms/core/discussion";
11
+ import { DevicesServiceImpl, IdentityServiceImpl, NotificationSettingsServiceImpl, SessionAdmin } from "@headless-lms/core/identity";
12
+ import { NotificationCatalog, OrgAdmin, OrgNotificationCategories, OrganizationServiceImpl, RoleDefinition } from "@headless-lms/core/organizations";
11
13
  import { AssetsServiceImpl } from "@headless-lms/core/assets";
12
14
  import { IntegrationsServiceImpl } from "@headless-lms/core/integrations";
13
- import { AutomationsServiceImpl } from "@headless-lms/core/automations";
15
+ import { AutomationsServiceImpl, NotificationCategories, NotificationsServiceImpl } from "@headless-lms/core/automations";
16
+ import { Mailer } from "@headless-lms/core/shared/mailer";
14
17
  import { AdminReportServiceImpl } from "@headless-lms/core/reporting/admin";
15
18
  import { MemberReportServiceImpl } from "@headless-lms/core/reporting/member";
16
- import { Mailer } from "@headless-lms/core/shared/mailer";
17
19
  import { SettingsService } from "@headless-lms/core/shared/settings";
18
- import { AutomationEngine, WebhookSender } from "@headless-lms/core/types";
19
- import { CredentialStore, EmailSender, Logger, ObjectStorage, OutboxRelay, TemplateContext, TemplateRenderer } from "@headless-lms/core/shared/ports";
20
+ import { AutomationEngine, PushConfig, WebhookSender } from "@headless-lms/core/types";
21
+ import { BodyParser, ChannelRegistry, CredentialStore, EmailSender, Logger, NotificationChannel, NotificationTypeRegistry, ObjectStorage, OutboxRelay, PubSub, PushSender, Scheduler, TemplateContext, TemplateRenderer } from "@headless-lms/core/shared/ports";
20
22
  //#region src/app/container.d.ts
21
23
  /** Installation-supplied ports; an absent slot falls back to a fail-loudly stub. */
22
24
  interface AdapterOverrides {
@@ -25,17 +27,30 @@ interface AdapterOverrides {
25
27
  /** Resolves email templates to rendered content. Absent → fail-loudly stub. */
26
28
  templates?: TemplateRenderer;
27
29
  /** Durable automation engine, or a factory given the automations logger. Absent → InlineAutomationEngine (in-process, one attempt per action). */
28
- workflows?: AutomationEngine | ((logger: Logger) => AutomationEngine);
30
+ workflows?: (AutomationEngine & Scheduler) | ((logger: Logger) => AutomationEngine & Scheduler);
29
31
  /** Outbound HTTP for webhook deliveries. Absent → FetchWebhookSender (public addresses only, 10s timeout). */
30
32
  webhooks?: WebhookSender;
33
+ /** Notification channels beyond in-app, email and push; each key must be new. */
34
+ notificationChannels?: NotificationChannel[];
35
+ /** Push credentials per provider. Absent → no provider is configured and devices cannot register. */
36
+ push?: PushConfig;
37
+ pushSender?: PushSender;
38
+ /** Reads post bodies: their text, mentions and images. Absent → PlainBodyParser (plain text, no images). */
39
+ bodyParser?: BodyParser;
40
+ /** Delivery to every API instance, or a factory given its logger. Absent → no live chat; the chat stream answers 503. */
41
+ pubsub?: PubSub | ((logger: Logger) => PubSub);
31
42
  }
32
43
  interface BuildContainerOptions {
33
44
  /** Installation's plugins folder, scanned by loadIntegrations. Absent → no integrations. */
34
45
  pluginsDir?: string;
35
46
  adapters?: AdapterOverrides;
47
+ roles?: readonly RoleDefinition[];
48
+ /** Live chat hub timings. Absent → the defaults (1 s state flush, 30 s session check). */
49
+ live?: ChatHubOptions;
36
50
  }
37
51
  interface Config {
38
52
  databaseUrl: string;
53
+ databasePool?: DatabasePoolOptions;
39
54
  authBaseURL: string;
40
55
  authSecret: string;
41
56
  trustedOrigins: string[];
@@ -60,8 +75,10 @@ interface Config {
60
75
  * ServerConfig.deliveryExpirySeconds — createContainer threads it through. */
61
76
  deliveryExpirySeconds: number;
62
77
  memberImageMaxBytes: number;
78
+ memberFileMaxBytes: number;
63
79
  /** Let the default webhook sender reach loopback and private network addresses. Local development only. */
64
80
  webhookAllowPrivateNetwork?: boolean;
81
+ platform?: PlatformConfig;
65
82
  }
66
83
  /** Tuning for the transactional-outbox relay. Every field is optional; the
67
84
  * container resolves against OUTBOX_DEFAULTS. */
@@ -72,6 +89,7 @@ interface OutboxConfig {
72
89
  pollIntervalMs?: number;
73
90
  /** Max rows fetched/dispatched per tick. Default 100. */
74
91
  batchSize?: number;
92
+ concurrency?: number;
75
93
  }
76
94
  declare const OUTBOX_DEFAULTS: PollingOutboxRelayConfig;
77
95
  declare function resolveOutboxConfig(config?: OutboxConfig): PollingOutboxRelayConfig;
@@ -85,9 +103,9 @@ declare function resolveLoggingConfig(config?: LoggingConfig): Required<LoggingC
85
103
  interface Container {
86
104
  auth: BetterAuth;
87
105
  mcp: McpEndpoints;
106
+ platform: PlatformKeyCheck | null;
88
107
  orgProvider: OrgAdmin;
89
108
  identity: IdentityServiceImpl;
90
- notifications: NotificationServiceImpl;
91
109
  organizations: OrganizationServiceImpl;
92
110
  content: ContentService;
93
111
  entitlements: EntitlementsServiceImpl;
@@ -95,9 +113,21 @@ interface Container {
95
113
  discussion: DiscussionServiceImpl;
96
114
  posts: PostServiceImpl;
97
115
  chats: ChatServiceImpl;
116
+ subscriptions: SubscriptionServiceImpl;
98
117
  assets: AssetsServiceImpl;
99
118
  integrations: IntegrationsServiceImpl;
100
119
  automations: AutomationsServiceImpl;
120
+ notificationTypes: NotificationTypeRegistry;
121
+ notificationChannels: ChannelRegistry;
122
+ notificationCategories: {
123
+ community: CommunityNotificationCategories;
124
+ org: OrgNotificationCategories;
125
+ };
126
+ notificationCatalog: NotificationCatalog;
127
+ notificationCategoryLists: NotificationCategories;
128
+ notificationSettings: NotificationSettingsServiceImpl;
129
+ devices: DevicesServiceImpl;
130
+ notificationList: NotificationsServiceImpl;
101
131
  /** Cross-cutting settings store. Not a context — a leaf every context may use;
102
132
  * inject `settings.for('<namespace>')` into a service, never the service itself. */
103
133
  settings: SettingsService;
@@ -117,6 +147,14 @@ interface Container {
117
147
  * the installation's entry point starts it after listen (gen-openapi must
118
148
  * not run a worker). buildServer stops it onClose. */
119
149
  automationEngine: AutomationEngine;
150
+ scheduler: Scheduler;
151
+ /** The PubSub adapter — constructed but NEVER started by the container; the
152
+ * installation's entry point starts it after listen. buildServer stops it
153
+ * onClose. Null without one. */
154
+ pubsub: PubSub | null;
155
+ /** This process's live chat hub; started by the entry point after `pubsub`,
156
+ * closed by buildServer preClose. Null without a PubSub. */
157
+ chatHub: ChatHub | null;
120
158
  /** Root logger port — components receive children bound with { name }. */
121
159
  logger: Logger;
122
160
  /** The raw pino root; buildServer hands it to Fastify so HTTP shares the stream. */