@headless-lms/server 0.3.0 → 0.4.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 (73) hide show
  1. package/dist/app/community-mail.d.ts +6 -8
  2. package/dist/app/community-mail.js +13 -17
  3. package/dist/app/community-mail.js.map +1 -1
  4. package/dist/app/container.d.ts +27 -10
  5. package/dist/app/container.js +178 -22
  6. package/dist/app/container.js.map +1 -1
  7. package/dist/app/notification-channels.d.ts +47 -0
  8. package/dist/app/notification-channels.js +174 -0
  9. package/dist/app/notification-channels.js.map +1 -0
  10. package/dist/app/notification-reads.d.ts +23 -0
  11. package/dist/app/notification-reads.js +95 -0
  12. package/dist/app/notification-reads.js.map +1 -0
  13. package/dist/http/errors.d.ts +1 -0
  14. package/dist/http/errors.js +16 -1
  15. package/dist/http/errors.js.map +1 -1
  16. package/dist/http/mcp/tools/communities.js +1 -1
  17. package/dist/http/mcp/tools/communities.js.map +1 -1
  18. package/dist/http/plugins/error-handler.js +3 -1
  19. package/dist/http/plugins/error-handler.js.map +1 -1
  20. package/dist/http/routes/automations.js +3 -3
  21. package/dist/http/routes/automations.js.map +1 -1
  22. package/dist/http/routes/communities.js +67 -7
  23. package/dist/http/routes/communities.js.map +1 -1
  24. package/dist/http/routes/member/actor.d.ts +1 -1
  25. package/dist/http/routes/member/chats.js +9 -30
  26. package/dist/http/routes/member/chats.js.map +1 -1
  27. package/dist/http/routes/member/community.js +25 -1
  28. package/dist/http/routes/member/community.js.map +1 -1
  29. package/dist/http/routes/member/devices.d.ts +7 -0
  30. package/dist/http/routes/member/devices.js +98 -0
  31. package/dist/http/routes/member/devices.js.map +1 -0
  32. package/dist/http/routes/member/index.js +4 -0
  33. package/dist/http/routes/member/index.js.map +1 -1
  34. package/dist/http/routes/member/notification-settings.d.ts +7 -0
  35. package/dist/http/routes/member/notification-settings.js +108 -0
  36. package/dist/http/routes/member/notification-settings.js.map +1 -0
  37. package/dist/http/routes/member/notifications.js +66 -42
  38. package/dist/http/routes/member/notifications.js.map +1 -1
  39. package/dist/http/routes/member/posts.js +33 -33
  40. package/dist/http/routes/member/posts.js.map +1 -1
  41. package/dist/http/routes/organizations.js +122 -1
  42. package/dist/http/routes/organizations.js.map +1 -1
  43. package/dist/http/schemas/assets.d.ts +6 -6
  44. package/dist/http/schemas/automations.d.ts +0 -4
  45. package/dist/http/schemas/chats.d.ts +34 -11
  46. package/dist/http/schemas/chats.js +4 -6
  47. package/dist/http/schemas/chats.js.map +1 -1
  48. package/dist/http/schemas/community.d.ts +77 -17
  49. package/dist/http/schemas/community.js +6 -3
  50. package/dist/http/schemas/community.js.map +1 -1
  51. package/dist/http/schemas/discussion.d.ts +23 -1
  52. package/dist/http/schemas/discussion.js +14 -2
  53. package/dist/http/schemas/discussion.js.map +1 -1
  54. package/dist/http/schemas/index.d.ts +6 -6
  55. package/dist/http/schemas/index.js +6 -6
  56. package/dist/http/schemas/integrations.d.ts +4 -4
  57. package/dist/http/schemas/members.d.ts +5 -5
  58. package/dist/http/schemas/notifications.d.ts +498 -175
  59. package/dist/http/schemas/notifications.js +148 -28
  60. package/dist/http/schemas/notifications.js.map +1 -1
  61. package/dist/http/schemas/posts.d.ts +37 -3
  62. package/dist/http/schemas/profile.d.ts +2 -2
  63. package/dist/http/schemas/shared.d.ts +10 -3
  64. package/dist/http/schemas/shared.js +3 -2
  65. package/dist/http/schemas/shared.js.map +1 -1
  66. package/dist/http/schemas/students.d.ts +3 -3
  67. package/dist/http/scope.d.ts +2 -1
  68. package/dist/http/scope.js +7 -1
  69. package/dist/http/scope.js.map +1 -1
  70. package/dist/index.d.ts +2 -1
  71. package/dist/index.js.map +1 -1
  72. package/dist/packages/server/package.js +1 -1
  73. package/package.json +5 -5
@@ -1,25 +1,23 @@
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";
8
7
  //#region src/app/community-mail.d.ts
9
8
  type CommunityMailDeps = {
10
9
  content: Pick<ContentService, 'getCommunity' | 'getSpace' | 'listCommunitiesById'>;
11
- entitlements: Pick<EntitlementsService, 'openSpacesFor' | 'communitiesOpenedBy' | 'communityMembersAmong' | 'spaceOpenersAmong' | 'spaceOpeners' | 'communityModeratorIds' | 'countActiveSpaceMembers' | 'communityIdsOpenedFor' | 'ensureGrant' | 'expireDue' | 'hasDueNotices'>;
10
+ entitlements: Pick<EntitlementsService, 'openSpacesFor' | 'communitiesOpenedBy' | 'communityMembersAmong' | 'spaceOpenersAmong' | 'spaceOpeners' | 'communityModeratorIds' | 'countActiveSpaceMembers' | 'communityIdsOpenedFor' | 'ensureGrant'>;
12
11
  organizations: Pick<OrganizationService, 'getOrgUserById' | 'staffOrgUserIds'>;
13
12
  identity: Pick<IdentityService, 'displayName'>;
14
- posts: Pick<PostService, 'followerIds' | 'find' | 'publishDue' | 'hasDue'>;
15
- discussion: Pick<DiscussionService, 'getComment' | 'sendDigest'>;
13
+ posts: Pick<PostService, 'find'>;
14
+ subscriptions: Pick<SubscriptionService, 'followerIds' | 'itemSubscriptions'>;
15
+ discussion: Pick<DiscussionService, 'getComment'>;
16
16
  chats: Pick<ChatService, 'chatSummary' | 'latestMessageBody' | 'isBlocked'>;
17
- assets: Pick<AssetsService, 'removeUnattached' | 'hasUnattached'>;
18
17
  };
19
18
  declare function communityMail(deps: CommunityMailDeps): {
20
19
  mail: Omit<CommunityMailReads, 'recentMessageRecipients'>;
21
20
  grants: GrantWriter;
22
- scheduledWork: ScheduledWork;
23
21
  };
24
22
  //#endregion
25
23
  export { CommunityMailDeps, communityMail };
@@ -31,12 +31,14 @@ function communityMail(deps) {
31
31
  const orgUser = await deps.organizations.getOrgUserById(orgId, orgUserId);
32
32
  return deps.identity.displayName(orgUser?.userId ?? null);
33
33
  },
34
- followerIds: (orgId, postId) => deps.posts.followerIds(orgId, postId),
34
+ followerIds: (orgId, postId) => deps.subscriptions.followerIds(orgId, postId),
35
35
  async comment(orgId, commentId) {
36
36
  const comment = await deps.discussion.getComment(orgId, commentId);
37
37
  return comment ? {
38
38
  orgUserId: comment.orgUserId,
39
39
  postId: comment.postId,
40
+ activityId: comment.activityId,
41
+ parentId: comment.parentId,
40
42
  body: comment.body
41
43
  } : null;
42
44
  },
@@ -52,18 +54,23 @@ function communityMail(deps) {
52
54
  },
53
55
  async chat(orgId, chatId) {
54
56
  const summary = await deps.chats.chatSummary(orgId, chatId);
55
- return summary ? {
57
+ if (!summary) return null;
58
+ const mutes = new Map((await deps.subscriptions.itemSubscriptions(orgId, {
59
+ type: "chat",
60
+ id: chatId
61
+ })).filter((subscription) => subscription.state === "muted").map((subscription) => [subscription.orgUserId, subscription.mutedUntil]));
62
+ return {
56
63
  communityId: summary.chat.communityId,
57
64
  kind: summary.chat.kind,
58
65
  status: summary.chat.status,
59
66
  createdBy: summary.chat.createdBy,
60
67
  participants: summary.participants.map((participant) => ({
61
68
  orgUserId: participant.orgUserId,
62
- muted: participant.muted,
63
- mutedUntil: participant.mutedUntil,
69
+ muted: mutes.has(participant.orgUserId),
70
+ mutedUntil: mutes.get(participant.orgUserId) ?? null,
64
71
  leftAt: participant.leftAt
65
72
  }))
66
- } : null;
73
+ };
67
74
  },
68
75
  latestMessageBody: (orgId, chatId) => deps.chats.latestMessageBody(orgId, chatId),
69
76
  pairBlocked: (orgId, communityId, a, b) => deps.chats.isBlocked(orgId, communityId, a, b),
@@ -76,18 +83,7 @@ function communityMail(deps) {
76
83
  }));
77
84
  }
78
85
  },
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
- }
86
+ grants: { grant: (orgId, input) => deps.entitlements.ensureGrant(orgId, input) }
91
87
  };
92
88
  }
93
89
  //#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';\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 >;\n organizations: Pick<OrganizationService, 'getOrgUserById' | 'staffOrgUserIds'>;\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};\n\nexport function communityMail(deps: CommunityMailDeps): {\n mail: Omit<CommunityMailReads, 'recentMessageRecipients'>;\n grants: GrantWriter;\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.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(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 };\n}\n"],"mappings":"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"}
@@ -2,21 +2,21 @@ import { McpEndpoints } from "./mcp-endpoints.js";
2
2
  import { PollingOutboxRelayConfig } from "@headless-lms/adapter-defaults/events/outbox-relay";
3
3
  import { LogLevel, PinoInstance, RequestLogContext } from "@headless-lms/adapter-defaults/logging";
4
4
  import { BetterAuth } from "@headless-lms/adapter-auth";
5
- import { ContentService } from "@headless-lms/core/content";
5
+ import { CommunityNotificationCategories, ContentService } from "@headless-lms/core/content";
6
6
  import { EntitlementsServiceImpl } from "@headless-lms/core/entitlements";
7
7
  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";
8
+ import { ChatServiceImpl, DiscussionServiceImpl, PostServiceImpl, SubscriptionServiceImpl } from "@headless-lms/core/discussion";
9
+ import { DevicesServiceImpl, IdentityServiceImpl, NotificationSettingsServiceImpl, SessionAdmin } from "@headless-lms/core/identity";
10
+ import { OrgAdmin, OrgNotificationCategories, OrganizationServiceImpl } from "@headless-lms/core/organizations";
11
11
  import { AssetsServiceImpl } from "@headless-lms/core/assets";
12
12
  import { IntegrationsServiceImpl } from "@headless-lms/core/integrations";
13
- import { AutomationsServiceImpl } from "@headless-lms/core/automations";
13
+ import { AutomationsServiceImpl, NotificationCategories, NotificationsServiceImpl } from "@headless-lms/core/automations";
14
+ import { Mailer } from "@headless-lms/core/shared/mailer";
14
15
  import { AdminReportServiceImpl } from "@headless-lms/core/reporting/admin";
15
16
  import { MemberReportServiceImpl } from "@headless-lms/core/reporting/member";
16
- import { Mailer } from "@headless-lms/core/shared/mailer";
17
17
  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";
18
+ import { AutomationEngine, PushConfig, WebhookSender } from "@headless-lms/core/types";
19
+ import { ChannelRegistry, CredentialStore, EmailSender, Logger, NotificationChannel, NotificationTypeRegistry, ObjectStorage, OutboxRelay, PushSender, Scheduler, TemplateContext, TemplateRenderer } from "@headless-lms/core/shared/ports";
20
20
  //#region src/app/container.d.ts
21
21
  /** Installation-supplied ports; an absent slot falls back to a fail-loudly stub. */
22
22
  interface AdapterOverrides {
@@ -25,9 +25,14 @@ interface AdapterOverrides {
25
25
  /** Resolves email templates to rendered content. Absent → fail-loudly stub. */
26
26
  templates?: TemplateRenderer;
27
27
  /** Durable automation engine, or a factory given the automations logger. Absent → InlineAutomationEngine (in-process, one attempt per action). */
28
- workflows?: AutomationEngine | ((logger: Logger) => AutomationEngine);
28
+ workflows?: (AutomationEngine & Scheduler) | ((logger: Logger) => AutomationEngine & Scheduler);
29
29
  /** Outbound HTTP for webhook deliveries. Absent → FetchWebhookSender (public addresses only, 10s timeout). */
30
30
  webhooks?: WebhookSender;
31
+ /** Notification channels beyond in-app, email and push; each key must be new. */
32
+ notificationChannels?: NotificationChannel[];
33
+ /** Push credentials per provider. Absent → no provider is configured and devices cannot register. */
34
+ push?: PushConfig;
35
+ pushSender?: PushSender;
31
36
  }
32
37
  interface BuildContainerOptions {
33
38
  /** Installation's plugins folder, scanned by loadIntegrations. Absent → no integrations. */
@@ -72,6 +77,7 @@ interface OutboxConfig {
72
77
  pollIntervalMs?: number;
73
78
  /** Max rows fetched/dispatched per tick. Default 100. */
74
79
  batchSize?: number;
80
+ concurrency?: number;
75
81
  }
76
82
  declare const OUTBOX_DEFAULTS: PollingOutboxRelayConfig;
77
83
  declare function resolveOutboxConfig(config?: OutboxConfig): PollingOutboxRelayConfig;
@@ -87,7 +93,6 @@ interface Container {
87
93
  mcp: McpEndpoints;
88
94
  orgProvider: OrgAdmin;
89
95
  identity: IdentityServiceImpl;
90
- notifications: NotificationServiceImpl;
91
96
  organizations: OrganizationServiceImpl;
92
97
  content: ContentService;
93
98
  entitlements: EntitlementsServiceImpl;
@@ -95,9 +100,20 @@ interface Container {
95
100
  discussion: DiscussionServiceImpl;
96
101
  posts: PostServiceImpl;
97
102
  chats: ChatServiceImpl;
103
+ subscriptions: SubscriptionServiceImpl;
98
104
  assets: AssetsServiceImpl;
99
105
  integrations: IntegrationsServiceImpl;
100
106
  automations: AutomationsServiceImpl;
107
+ notificationTypes: NotificationTypeRegistry;
108
+ notificationChannels: ChannelRegistry;
109
+ notificationCategories: {
110
+ community: CommunityNotificationCategories;
111
+ org: OrgNotificationCategories;
112
+ };
113
+ notificationCategoryLists: NotificationCategories;
114
+ notificationSettings: NotificationSettingsServiceImpl;
115
+ devices: DevicesServiceImpl;
116
+ notificationList: NotificationsServiceImpl;
101
117
  /** Cross-cutting settings store. Not a context — a leaf every context may use;
102
118
  * inject `settings.for('<namespace>')` into a service, never the service itself. */
103
119
  settings: SettingsService;
@@ -117,6 +133,7 @@ interface Container {
117
133
  * the installation's entry point starts it after listen (gen-openapi must
118
134
  * not run a worker). buildServer stops it onClose. */
119
135
  automationEngine: AutomationEngine;
136
+ scheduler: Scheduler;
120
137
  /** Root logger port — components receive children bound with { name }. */
121
138
  logger: Logger;
122
139
  /** The raw pino root; buildServer hands it to Fastify so HTTP shares the stream. */
@@ -1,7 +1,9 @@
1
+ import { buildNotificationRegistries, configuredPushSender } from "./notification-channels.js";
1
2
  import { loadIntegrations } from "./integrations.js";
2
3
  import { mcpEndpoints } from "./mcp-endpoints.js";
3
4
  import { communityMail } from "./community-mail.js";
4
- import { DrizzleAdminRepository, DrizzleAssetsRepository, DrizzleAutomationRunsRepository, DrizzleAutomationsRepository, DrizzleChatRepository, DrizzleConnectionsRepository, DrizzleContentRepository, DrizzleCredentialStore, DrizzleDiscussionRepository, DrizzleEntitlementsRepository, DrizzleIdentityRepository, DrizzleMemberRepository, DrizzleMembersRepository, DrizzleNotificationsRepository, DrizzleOrganizationsRepository, DrizzleOutboxAppender, DrizzleOutboxStore, DrizzlePostRepository, DrizzleProgressRepository, DrizzleSettingsRepository, DrizzleUnitOfWork, createDb } from "@headless-lms/adapter-db";
5
+ import { notificationReads } from "./notification-reads.js";
6
+ import { DrizzleAdminRepository, DrizzleAssetsRepository, DrizzleAutomationRunsRepository, DrizzleAutomationsRepository, DrizzleChatRepository, DrizzleConnectionsRepository, DrizzleContentRepository, DrizzleCredentialStore, DrizzleDevicesRepository, DrizzleDigestRunRepository, DrizzleDiscussionRepository, DrizzleEntitlementsRepository, DrizzleIdentityRepository, DrizzleMemberRepository, DrizzleMembersRepository, DrizzleMentionsRepository, DrizzleNotificationSettingsRepository, DrizzleNotificationsRepository, DrizzleOrganizationsRepository, DrizzleOutboxAppender, DrizzleOutboxStore, DrizzlePostRepository, DrizzleProgressRepository, DrizzleSettingsRepository, DrizzleSubscriptionsRepository, DrizzleUnitOfWork, createDb } from "@headless-lms/adapter-db";
5
7
  import { InMemoryEventBus } from "@headless-lms/adapter-defaults/events";
6
8
  import { PollingOutboxRelay } from "@headless-lms/adapter-defaults/events/outbox-relay";
7
9
  import { InlineAutomationEngine } from "@headless-lms/adapter-defaults/workflows";
@@ -10,24 +12,25 @@ import { EmailAdapter, StubTemplateRenderer } from "@headless-lms/adapter-defaul
10
12
  import { createRootLogger, requestLogContext } from "@headless-lms/adapter-defaults/logging";
11
13
  import { StorageAdapter } from "@headless-lms/adapter-defaults/storage";
12
14
  import { BetterAuth } from "@headless-lms/adapter-auth";
13
- import { ContentService } from "@headless-lms/core/content";
14
- import { EntitlementsServiceImpl } from "@headless-lms/core/entitlements";
15
+ import { CommunityNotificationCategories, ContentItemTitles, ContentService } from "@headless-lms/core/content";
16
+ import { EntitlementsServiceImpl, entitlementsJobs, entitlementsRecurringJobs } from "@headless-lms/core/entitlements";
15
17
  import { ProgressServiceImpl } from "@headless-lms/core/progress";
16
- import { ChatServiceImpl, DiscussionServiceImpl, PostServiceImpl } from "@headless-lms/core/discussion";
17
- import { IdentityServiceImpl, NotificationServiceImpl } from "@headless-lms/core/identity";
18
- import { OrganizationServiceImpl, parseRole } from "@headless-lms/core/organizations";
19
- import { AssetsServiceImpl, createFileUrls } from "@headless-lms/core/assets";
18
+ import { ChatServiceImpl, DigestServiceImpl, DiscussionItemTitles, DiscussionServiceImpl, PostServiceImpl, SubscriptionServiceImpl, discussionJobs, discussionRecurringJobs } from "@headless-lms/core/discussion";
19
+ import { DevicesServiceImpl, IdentityServiceImpl, NotificationSettingsServiceImpl } from "@headless-lms/core/identity";
20
+ import { OrgNotificationCategories, OrganizationServiceImpl, parseRole } from "@headless-lms/core/organizations";
21
+ import { AssetsServiceImpl, assetsJobs, assetsRecurringJobs, createFileUrls } from "@headless-lms/core/assets";
20
22
  import { IntegrationsServiceImpl } from "@headless-lms/core/integrations";
21
- import { AutomationsServiceImpl } from "@headless-lms/core/automations";
23
+ import { AutomationsServiceImpl, DEFAULT_COMMUNITY_CATEGORIES, DEFAULT_ORG_CATEGORIES, NOTIFICATION_TYPES, NotificationDelivery, NotificationProcessor, NotificationsServiceImpl, RECIPIENT_RULES, SchedulerChatDebounce, notificationJobs } from "@headless-lms/core/automations";
24
+ import { Mailer } from "@headless-lms/core/shared/mailer";
22
25
  import { AdminReportServiceImpl } from "@headless-lms/core/reporting/admin";
23
26
  import { MemberReportServiceImpl } from "@headless-lms/core/reporting/member";
24
- import { Mailer } from "@headless-lms/core/shared/mailer";
25
27
  import { SettingsService } from "@headless-lms/core/shared/settings";
26
28
  //#region src/app/container.ts
27
29
  const OUTBOX_DEFAULTS = {
28
30
  enabled: true,
29
31
  pollIntervalMs: 1e3,
30
- batchSize: 100
32
+ batchSize: 100,
33
+ concurrency: 16
31
34
  };
32
35
  function resolveOutboxConfig(config = {}) {
33
36
  const overrides = Object.fromEntries(Object.entries(config).filter(([, value]) => value !== void 0));
@@ -89,10 +92,6 @@ async function buildContainer(config, options) {
89
92
  images: assets,
90
93
  fileUrls
91
94
  });
92
- const notifications = new NotificationServiceImpl({
93
- repo: new DrizzleNotificationsRepository(db),
94
- logger: logger.child({ name: "notifications" })
95
- });
96
95
  const organizationsRepo = new DrizzleOrganizationsRepository(db, logger.child({ name: "org" }));
97
96
  const organizationsUow = new DrizzleUnitOfWork(db, (tx) => ({
98
97
  organizations: new DrizzleOrganizationsRepository(tx, logger.child({ name: "org" })),
@@ -128,6 +127,38 @@ async function buildContainer(config, options) {
128
127
  fileUrls,
129
128
  logger: contentLogger
130
129
  });
130
+ const pushConfig = options?.adapters?.push ?? {};
131
+ const pushSender = configuredPushSender(pushConfig, options?.adapters?.pushSender);
132
+ const notificationRegistries = buildNotificationRegistries({
133
+ declarations: NOTIFICATION_TYPES,
134
+ defaults: {
135
+ community: DEFAULT_COMMUNITY_CATEGORIES,
136
+ org: DEFAULT_ORG_CATEGORIES
137
+ },
138
+ mailer: options?.adapters?.email ? mailer : null,
139
+ emailOf: async (_orgId, userId) => (await identity.getUserById(userId))?.email ?? null,
140
+ push: pushSender ? {
141
+ sender: pushSender,
142
+ activeDevices: (orgId, userId, since) => devices.activeDevices(orgId, userId, since)
143
+ } : null,
144
+ channels: options?.adapters?.notificationChannels ?? []
145
+ });
146
+ const notificationCategories = {
147
+ community: new CommunityNotificationCategories({
148
+ communities: new DrizzleContentRepository(db, contentLogger),
149
+ settings,
150
+ types: notificationRegistries.types,
151
+ channels: notificationRegistries.channels,
152
+ logger: contentLogger
153
+ }),
154
+ org: new OrgNotificationCategories({
155
+ settings,
156
+ types: notificationRegistries.types,
157
+ channels: notificationRegistries.channels,
158
+ logger: logger.child({ name: "org" })
159
+ })
160
+ };
161
+ const notificationCategoryLists = { listFor: async (orgId, communityId) => (communityId ? await notificationCategories.community.get(orgId, communityId) : await notificationCategories.org.get(orgId, orgId)).categories };
131
162
  const entitlementsUow = new DrizzleUnitOfWork(db, (tx) => ({
132
163
  entitlements: new DrizzleEntitlementsRepository(tx, entitlementsLogger),
133
164
  spaces: new DrizzleContentRepository(tx, contentLogger),
@@ -148,6 +179,22 @@ async function buildContainer(config, options) {
148
179
  uow: entitlementsUow,
149
180
  logger: entitlementsLogger
150
181
  });
182
+ const identityLogger = logger.child({ name: "identity" });
183
+ const notificationSettings = new NotificationSettingsServiceImpl({
184
+ repo: new DrizzleNotificationSettingsRepository(db, identityLogger),
185
+ categories: {
186
+ communityList: async (orgId, communityId) => (await notificationCategories.community.get(orgId, communityId)).categories,
187
+ orgList: async (orgId) => (await notificationCategories.org.get(orgId, orgId)).categories
188
+ },
189
+ access: { canOpen: (orgId, actor, communityId) => entitlements.isCommunityMember(orgId, actor, communityId) },
190
+ channels: notificationRegistries.channels,
191
+ logger: identityLogger
192
+ });
193
+ const devices = new DevicesServiceImpl({
194
+ repo: new DrizzleDevicesRepository(db, identityLogger),
195
+ push: pushConfig,
196
+ logger: identityLogger
197
+ });
151
198
  const progressUow = new DrizzleUnitOfWork(db, (tx) => ({
152
199
  progress: new DrizzleProgressRepository(tx, progressLogger),
153
200
  outbox: new DrizzleOutboxAppender(tx, outboxLogger)
@@ -162,6 +209,8 @@ async function buildContainer(config, options) {
162
209
  const postReads = new DrizzlePostRepository(db, discussionLogger);
163
210
  const discussionReads = new DrizzleDiscussionRepository(db, discussionLogger);
164
211
  const chatReads = new DrizzleChatRepository(db, discussionLogger);
212
+ const subscriptionReads = new DrizzleSubscriptionsRepository(db, discussionLogger);
213
+ const mentionReads = new DrizzleMentionsRepository(db, discussionLogger);
165
214
  const posts = new PostServiceImpl({
166
215
  repo: postReads,
167
216
  access: entitlements,
@@ -169,12 +218,16 @@ async function buildContainer(config, options) {
169
218
  authors,
170
219
  profiles: discussionReads,
171
220
  comments: discussionReads,
221
+ subscriptions: subscriptionReads,
222
+ mentions: mentionReads,
172
223
  images: assets,
173
224
  fileUrls,
174
225
  uow: new DrizzleUnitOfWork(db, (tx) => ({
175
226
  posts: new DrizzlePostRepository(tx, discussionLogger),
227
+ mentions: new DrizzleMentionsRepository(tx, discussionLogger),
176
228
  outbox: new DrizzleOutboxAppender(tx, outboxLogger)
177
229
  })),
230
+ scheduler: automationEngine,
178
231
  logger: discussionLogger
179
232
  });
180
233
  const chats = new ChatServiceImpl({
@@ -183,10 +236,13 @@ async function buildContainer(config, options) {
183
236
  content,
184
237
  authors,
185
238
  profiles: discussionReads,
239
+ subscriptions: subscriptionReads,
240
+ mentions: mentionReads,
186
241
  images: assets,
187
242
  fileUrls,
188
243
  uow: new DrizzleUnitOfWork(db, (tx) => ({
189
244
  chats: new DrizzleChatRepository(tx, discussionLogger),
245
+ mentions: new DrizzleMentionsRepository(tx, discussionLogger),
190
246
  outbox: new DrizzleOutboxAppender(tx, outboxLogger)
191
247
  })),
192
248
  logger: discussionLogger
@@ -196,15 +252,53 @@ async function buildContainer(config, options) {
196
252
  access: entitlements,
197
253
  content,
198
254
  posts,
199
- communities: { listIds: (orgId) => content.listPublishedCommunityIds(orgId) },
255
+ spaces: entitlements,
256
+ mentions: mentionReads,
200
257
  fileUrls,
201
258
  uow: new DrizzleUnitOfWork(db, (tx) => ({
202
259
  discussion: new DrizzleDiscussionRepository(tx, discussionLogger),
260
+ mentions: new DrizzleMentionsRepository(tx, discussionLogger),
203
261
  outbox: new DrizzleOutboxAppender(tx, outboxLogger)
204
262
  })),
205
263
  settings,
206
264
  logger: discussionLogger
207
265
  });
266
+ const subscriptions = new SubscriptionServiceImpl({
267
+ repo: subscriptionReads,
268
+ access: entitlements,
269
+ posts,
270
+ chats,
271
+ logger: discussionLogger
272
+ });
273
+ const digests = new DigestServiceImpl({
274
+ schedules: { communitiesWithDigest: () => content.communitiesWithDigest() },
275
+ runs: new DrizzleDigestRunRepository(db, discussionLogger),
276
+ uow: new DrizzleUnitOfWork(db, (tx) => ({
277
+ digests: new DrizzleDigestRunRepository(tx, discussionLogger),
278
+ outbox: new DrizzleOutboxAppender(tx, outboxLogger)
279
+ })),
280
+ logger: discussionLogger
281
+ });
282
+ const notificationsRepo = new DrizzleNotificationsRepository(db);
283
+ const notificationList = new NotificationsServiceImpl({
284
+ repo: notificationsRepo,
285
+ logger: automationsLogger
286
+ });
287
+ const itemTitles = {
288
+ discussion: new DiscussionItemTitles({
289
+ posts,
290
+ chats,
291
+ comments: discussionReads,
292
+ messages: chatReads,
293
+ profiles: discussionReads,
294
+ activities: content,
295
+ access: entitlements
296
+ }),
297
+ content: new ContentItemTitles({
298
+ content,
299
+ access: entitlements
300
+ })
301
+ };
208
302
  const reporting = {
209
303
  admin: new AdminReportServiceImpl({ repo: new DrizzleAdminRepository(db, reportingLogger) }),
210
304
  member: new MemberReportServiceImpl({
@@ -214,9 +308,12 @@ async function buildContainer(config, options) {
214
308
  files: assets,
215
309
  fileUrls,
216
310
  access: entitlements,
217
- notifications,
218
- posts,
219
- chats
311
+ notifications: notificationList,
312
+ titles: itemTitles,
313
+ subscriptions,
314
+ notificationCategories,
315
+ notificationSettings,
316
+ channels: notificationRegistries.channels
220
317
  })
221
318
  };
222
319
  const credentialStore = new DrizzleCredentialStore(db, config.credentialStoreKey, integrationsLogger);
@@ -276,9 +373,33 @@ async function buildContainer(config, options) {
276
373
  organizations,
277
374
  identity,
278
375
  posts,
376
+ subscriptions,
377
+ discussion,
378
+ chats
379
+ });
380
+ const recipientReads = notificationReads({
381
+ content,
382
+ entitlements,
383
+ organizations,
384
+ identity,
385
+ posts,
386
+ subscriptions,
279
387
  discussion,
280
388
  chats,
281
- assets
389
+ settings: notificationSettings
390
+ });
391
+ const notificationProcessor = new NotificationProcessor({
392
+ rules: RECIPIENT_RULES,
393
+ reads: recipientReads,
394
+ types: notificationRegistries.types,
395
+ channels: notificationRegistries.channels,
396
+ categories: notificationCategoryLists,
397
+ settings: notificationSettings,
398
+ devices,
399
+ repo: notificationsRepo,
400
+ engine: automationEngine,
401
+ debounce: new SchedulerChatDebounce(automationEngine),
402
+ logger: automationsLogger
282
403
  });
283
404
  const automations = new AutomationsServiceImpl({
284
405
  repo: new DrizzleAutomationsRepository(db, automationsLogger),
@@ -292,11 +413,38 @@ async function buildContainer(config, options) {
292
413
  webhooks: options?.adapters?.webhooks ?? new FetchWebhookSender({ allowPrivateNetwork: config.webhookAllowPrivateNetwork }),
293
414
  mail: wired.mail,
294
415
  grants: wired.grants,
295
- desk: notifications,
296
- scheduledWork: wired.scheduledWork,
416
+ delivery: new NotificationDelivery({
417
+ repo: notificationsRepo,
418
+ channels: notificationRegistries.channels,
419
+ devices,
420
+ logger: automationsLogger
421
+ }),
422
+ processor: notificationProcessor,
297
423
  logger: automationsLogger
298
424
  });
299
425
  automationEngine.register(automations);
426
+ automationEngine.registerJobs([
427
+ ...discussionJobs({
428
+ posts,
429
+ digests,
430
+ logger: discussionLogger
431
+ }),
432
+ ...entitlementsJobs({ grants: entitlements }),
433
+ ...assetsJobs({
434
+ assets,
435
+ orgs: organizations,
436
+ logger: assetsLogger
437
+ }),
438
+ ...notificationJobs({
439
+ processor: notificationProcessor,
440
+ reads: recipientReads,
441
+ logger: automationsLogger
442
+ })
443
+ ], [
444
+ ...discussionRecurringJobs,
445
+ ...entitlementsRecurringJobs,
446
+ ...assetsRecurringJobs
447
+ ]);
300
448
  const eventBus = new InMemoryEventBus();
301
449
  eventBus.subscribeAll((event) => automations.handle(event));
302
450
  const outboxConfig = resolveOutboxConfig(config.outbox);
@@ -405,7 +553,6 @@ async function buildContainer(config, options) {
405
553
  auth,
406
554
  mcp,
407
555
  identity,
408
- notifications,
409
556
  organizations,
410
557
  orgProvider: auth,
411
558
  content,
@@ -414,9 +561,17 @@ async function buildContainer(config, options) {
414
561
  discussion,
415
562
  posts,
416
563
  chats,
564
+ subscriptions,
417
565
  assets,
418
566
  integrations,
419
567
  automations,
568
+ notificationTypes: notificationRegistries.types,
569
+ notificationChannels: notificationRegistries.channels,
570
+ notificationCategories,
571
+ notificationCategoryLists,
572
+ notificationSettings,
573
+ devices,
574
+ notificationList,
420
575
  settings,
421
576
  reporting,
422
577
  storage,
@@ -424,6 +579,7 @@ async function buildContainer(config, options) {
424
579
  credentials: credentialStore,
425
580
  outboxRelay,
426
581
  automationEngine,
582
+ scheduler: automationEngine,
427
583
  logger,
428
584
  loggerInstance,
429
585
  requestContext: requestLogContext,