@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
@@ -1 +1 @@
1
- {"version":3,"file":"container.js","names":[],"sources":["../../src/app/container.ts"],"sourcesContent":["// Wires adapters + services in dependency order. Starts nothing.\nimport {\n createDb,\n DrizzleAssetsRepository,\n DrizzleAutomationRunsRepository,\n DrizzleAutomationsRepository,\n DrizzleConnectionsRepository,\n DrizzleContentRepository,\n DrizzleCredentialStore,\n DrizzleAdminRepository,\n DrizzleDiscussionRepository,\n DrizzlePostRepository,\n DrizzleChatRepository,\n DrizzleEntitlementsRepository,\n DrizzleIdentityRepository,\n DrizzleMemberRepository,\n DrizzleMembersRepository,\n DrizzleNotificationsRepository,\n DrizzleOrganizationsRepository,\n DrizzleOutboxAppender,\n DrizzleOutboxStore,\n DrizzleProgressRepository,\n DrizzleSettingsRepository,\n DrizzleUnitOfWork,\n} from '@headless-lms/adapter-db';\nimport { InMemoryEventBus } from '@headless-lms/adapter-defaults/events';\nimport {\n PollingOutboxRelay,\n type PollingOutboxRelayConfig,\n} from '@headless-lms/adapter-defaults/events/outbox-relay';\nimport { InlineAutomationEngine } from '@headless-lms/adapter-defaults/workflows';\nimport { FetchWebhookSender } from '@headless-lms/adapter-defaults/webhooks';\nimport { EmailAdapter, StubTemplateRenderer } from '@headless-lms/adapter-defaults/email';\nimport {\n createRootLogger,\n type LogLevel,\n type PinoInstance,\n requestLogContext,\n type RequestLogContext,\n} from '@headless-lms/adapter-defaults/logging';\nimport { StorageAdapter } from '@headless-lms/adapter-defaults/storage';\nimport { BetterAuth } from '@headless-lms/adapter-auth';\n\nimport { ContentService } from '@headless-lms/core/content';\nimport { EntitlementsServiceImpl } from '@headless-lms/core/entitlements';\nimport { ProgressServiceImpl } from '@headless-lms/core/progress';\nimport {\n ChatServiceImpl,\n DiscussionServiceImpl,\n PostServiceImpl,\n} from '@headless-lms/core/discussion';\nimport {\n IdentityServiceImpl,\n NotificationServiceImpl,\n type SessionAdmin,\n} from '@headless-lms/core/identity';\nimport {\n type OrgAdmin,\n OrganizationServiceImpl,\n parseRole,\n} from '@headless-lms/core/organizations';\nimport { AssetsServiceImpl, createFileUrls } from '@headless-lms/core/assets';\nimport { IntegrationsServiceImpl } from '@headless-lms/core/integrations';\nimport { AutomationsServiceImpl } from '@headless-lms/core/automations';\nimport { loadIntegrations } from './integrations.js';\nimport { mcpEndpoints, type McpEndpoints } from './mcp-endpoints.js';\nimport { communityMail } from './community-mail.js';\nimport { AdminReportServiceImpl } from '@headless-lms/core/reporting/admin';\nimport { MemberReportServiceImpl } from '@headless-lms/core/reporting/member';\nimport { Mailer, type MailerLookups } from '@headless-lms/core/shared/mailer';\nimport { SettingsService } from '@headless-lms/core/shared/settings';\n\nimport type {\n CredentialStore,\n EmailSender,\n Logger,\n ObjectStorage,\n OutboxRelay,\n TemplateContext,\n TemplateRenderer,\n} from '@headless-lms/core/shared/ports';\nimport type { AutomationEngine, WebhookSender } from '@headless-lms/core/types';\n\n/** Installation-supplied ports; an absent slot falls back to a fail-loudly stub. */\nexport interface AdapterOverrides {\n email?: EmailSender;\n storage?: ObjectStorage;\n /** Resolves email templates to rendered content. Absent → fail-loudly stub. */\n templates?: TemplateRenderer;\n /** Durable automation engine, or a factory given the automations logger. Absent → InlineAutomationEngine (in-process, one attempt per action). */\n workflows?: AutomationEngine | ((logger: Logger) => AutomationEngine);\n /** Outbound HTTP for webhook deliveries. Absent → FetchWebhookSender (public addresses only, 10s timeout). */\n webhooks?: WebhookSender;\n}\n\nexport interface BuildContainerOptions {\n /** Installation's plugins folder, scanned by loadIntegrations. Absent → no integrations. */\n pluginsDir?: string;\n adapters?: AdapterOverrides;\n}\n\nexport interface Config {\n databaseUrl: string;\n authBaseURL: string;\n authSecret: string;\n trustedOrigins: string[];\n /** Branding threaded into every email template. Default: brandName \"Headless LMS\", baseUrl = adminAppUrl.\n * (studentPortalUrl is composed in from the top-level config field.) */\n emailBranding?: Omit<TemplateContext, 'studentPortalUrl'>;\n /** base64-encoded 32-byte key for the credential store (CREDENTIAL_STORE_KEY). */\n credentialStoreKey: string;\n /** Parent domain for cross-subdomain session cookies (e.g. \".example.com\"); undefined → host-only cookie. */\n cookieDomain?: string;\n /** Mark session cookies Secure (set behind HTTPS / in production). */\n secureCookies?: boolean;\n /** Student portal origin — invite links for students, and the origin whose signups are invite-gated. */\n studentPortalUrl: string;\n /** Admin app origin — invite links for staff. */\n adminAppUrl: string;\n /** Transactional-outbox relay tuning. All optional — see OUTBOX_DEFAULTS. */\n outbox?: OutboxConfig;\n /** Log level for the process-wide logger (HTTP + domain + relay). Default \"info\". */\n logging?: LoggingConfig;\n /** Lifetime of every signed file URL a response carries, seconds. Mirrors\n * ServerConfig.deliveryExpirySeconds — createContainer threads it through. */\n deliveryExpirySeconds: number;\n memberImageMaxBytes: number;\n /** Let the default webhook sender reach loopback and private network addresses. Local development only. */\n webhookAllowPrivateNetwork?: boolean;\n}\n\n/** Tuning for the transactional-outbox relay. Every field is optional; the\n * container resolves against OUTBOX_DEFAULTS. */\nexport interface OutboxConfig {\n /** Master switch for the same-process relay. Default true. */\n enabled?: boolean;\n /** Idle delay between polls. Default 1000. */\n pollIntervalMs?: number;\n /** Max rows fetched/dispatched per tick. Default 100. */\n batchSize?: number;\n}\n\nexport const OUTBOX_DEFAULTS: PollingOutboxRelayConfig = {\n enabled: true,\n pollIntervalMs: 1000,\n batchSize: 100,\n};\n\nexport function resolveOutboxConfig(config: OutboxConfig = {}): PollingOutboxRelayConfig {\n const overrides = Object.fromEntries(\n Object.entries(config).filter(([, value]) => value !== undefined),\n );\n return { ...OUTBOX_DEFAULTS, ...overrides };\n}\n\n/** Logging tuning. Optional; resolved against LOGGING_DEFAULTS. */\nexport interface LoggingConfig {\n /** Minimum level emitted. Default \"info\". */\n level?: LogLevel;\n}\n\nexport const LOGGING_DEFAULTS: Required<LoggingConfig> = { level: 'info' };\n\nexport function resolveLoggingConfig(config: LoggingConfig = {}): Required<LoggingConfig> {\n return { level: config.level ?? LOGGING_DEFAULTS.level };\n}\n\nexport interface Container {\n auth: BetterAuth;\n mcp: McpEndpoints;\n // Org Provider\n orgProvider: OrgAdmin;\n\n // Domains\n identity: IdentityServiceImpl;\n notifications: NotificationServiceImpl;\n organizations: OrganizationServiceImpl;\n content: ContentService;\n entitlements: EntitlementsServiceImpl;\n progress: ProgressServiceImpl;\n discussion: DiscussionServiceImpl;\n posts: PostServiceImpl;\n chats: ChatServiceImpl;\n assets: AssetsServiceImpl;\n integrations: IntegrationsServiceImpl;\n automations: AutomationsServiceImpl;\n /** Cross-cutting settings store. Not a context — a leaf every context may use;\n * inject `settings.for('<namespace>')` into a service, never the service itself. */\n settings: SettingsService;\n // Reporting read layer (composed cross-context reads; owns no domain rules).\n reporting: {\n admin: AdminReportServiceImpl;\n member: MemberReportServiceImpl;\n };\n storage: ObjectStorage;\n mailer: Mailer;\n /** Shared secure credential store — encrypted at rest, org-scoped, decrypt at point of use. */\n credentials: CredentialStore;\n /** The outbox relay — constructed but NEVER started by the container; the\n * installation's entry point starts it after listen (gen-openapi must not\n * poll). buildServer stops it onClose. */\n outboxRelay: OutboxRelay;\n /** The automation engine — constructed but NEVER started by the container;\n * the installation's entry point starts it after listen (gen-openapi must\n * not run a worker). buildServer stops it onClose. */\n automationEngine: AutomationEngine;\n /** Root logger port — components receive children bound with { name }. */\n logger: Logger;\n /** The raw pino root; buildServer hands it to Fastify so HTTP shares the stream. */\n loggerInstance: PinoInstance;\n /** Request-scoped log correlation; the HTTP layer enters it per request. */\n requestContext: RequestLogContext;\n /** Identity's session-write port; the auth adapter fulfils it. */\n sessions: SessionAdmin;\n}\n\nexport async function buildContainer(\n config: Config,\n options?: BuildContainerOptions,\n): Promise<Container> {\n const { instance: loggerInstance, logger } = createRootLogger(\n resolveLoggingConfig(config.logging).level,\n );\n // One child per domain — a context's service and repositories share it.\n\n const contentLogger = logger.child({ name: 'content' });\n const entitlementsLogger = logger.child({ name: 'entitlements' });\n const progressLogger = logger.child({ name: 'progress' });\n const discussionLogger = logger.child({ name: 'discussion' });\n const assetsLogger = logger.child({ name: 'assets' });\n const integrationsLogger = logger.child({ name: 'integrations' });\n const automationsLogger = logger.child({ name: 'automations' });\n const reportingLogger = logger.child({ name: 'reporting' });\n const outboxLogger = logger.child({ name: 'outbox' });\n\n // Outbound adapters\n const db = createDb(config.databaseUrl);\n const email = options?.adapters?.email ?? new EmailAdapter(logger.child({ name: 'email' }));\n const storage: ObjectStorage =\n options?.adapters?.storage ?? new StorageAdapter(logger.child({ name: 'storage' }));\n const templates =\n options?.adapters?.templates ?? new StubTemplateRenderer(logger.child({ name: 'email' }));\n const workflows = options?.adapters?.workflows;\n const automationEngine: AutomationEngine =\n typeof workflows === 'function'\n ? workflows(automationsLogger)\n : (workflows ?? new InlineAutomationEngine(automationsLogger));\n const mailer = new Mailer(templates, email, {\n ...(config.emailBranding ?? { brandName: 'Headless LMS', baseUrl: config.adminAppUrl }),\n studentPortalUrl: config.studentPortalUrl,\n });\n\n const fileUrls = createFileUrls(config.authBaseURL);\n const assets = new AssetsServiceImpl({\n storage,\n repo: new DrizzleAssetsRepository(db, assetsLogger),\n uow: new DrizzleUnitOfWork(db, (tx) => ({\n assets: new DrizzleAssetsRepository(tx, assetsLogger),\n outbox: new DrizzleOutboxAppender(tx, outboxLogger),\n })),\n memberImageMaxBytes: config.memberImageMaxBytes,\n downloadLinkExpirySeconds: config.deliveryExpirySeconds,\n fileUrls,\n logger: assetsLogger,\n });\n\n /*\n * Identity\n */\n const identityUoW = new DrizzleUnitOfWork(db, (tx) => ({\n identity: new DrizzleIdentityRepository(tx, logger.child({ name: 'identity' })),\n outbox: new DrizzleOutboxAppender(tx, logger.child({ name: 'identity' })),\n }));\n const identity = new IdentityServiceImpl({\n repo: new DrizzleIdentityRepository(db, logger.child({ name: 'identity_repo' })),\n logger: logger.child({ name: 'identity' }),\n uow: identityUoW,\n mailer,\n images: assets,\n fileUrls,\n });\n const notifications = new NotificationServiceImpl({\n repo: new DrizzleNotificationsRepository(db),\n logger: logger.child({ name: 'notifications' }),\n });\n\n /*\n * Organization\n */\n const organizationsRepo = new DrizzleOrganizationsRepository(db, logger.child({ name: 'org' }));\n const organizationsUow = new DrizzleUnitOfWork(db, (tx) => ({\n organizations: new DrizzleOrganizationsRepository(tx, logger.child({ name: 'org' })),\n outbox: new DrizzleOutboxAppender(tx, logger.child({ name: 'org' })),\n }));\n const orgAdminRef: { current: OrgAdmin | undefined } = { current: undefined };\n const organizations = new OrganizationServiceImpl({\n repo: organizationsRepo,\n membersRepo: new DrizzleMembersRepository(db, logger.child({ name: 'org' })),\n people: identity,\n uow: organizationsUow,\n orgAdmin: () => {\n if (!orgAdminRef.current) {\n throw new Error('orgAdmin not initialised');\n }\n return orgAdminRef.current;\n },\n logger: logger.child({ name: 'org' }),\n mailer,\n inviteUrls: { studentPortalUrl: config.studentPortalUrl, adminAppUrl: config.adminAppUrl },\n });\n\n /*\n * Settings\n */\n const settings = new SettingsService(\n new DrizzleSettingsRepository(db, logger.child({ name: 'settings' })),\n );\n // Content: reads on the root db; course writes + outbox append in one tx.\n const contentUow = new DrizzleUnitOfWork(db, (tx) => ({\n content: new DrizzleContentRepository(tx, contentLogger),\n outbox: new DrizzleOutboxAppender(tx, outboxLogger),\n }));\n const content = new ContentService({\n repo: new DrizzleContentRepository(db, contentLogger),\n uow: contentUow,\n libraryAssets: assets,\n files: assets,\n fileUrls,\n logger: contentLogger,\n });\n // Entitlements: reads on the root db; writes + outbox append in one tx.\n const entitlementsUow = new DrizzleUnitOfWork(db, (tx) => ({\n entitlements: new DrizzleEntitlementsRepository(tx, entitlementsLogger),\n spaces: new DrizzleContentRepository(tx, contentLogger),\n outbox: new DrizzleOutboxAppender(tx, outboxLogger),\n }));\n const authors = {\n roleOf: async (orgId: string, orgUserId: string) => {\n const row = await organizationsRepo.findOrgUserById(orgId, orgUserId);\n return row?.role ?? null;\n },\n rolesOf: async (orgId: string, orgUserIds: string[]) => {\n const rows = await organizationsRepo.findOrgUsersByIds(orgId, orgUserIds);\n return Object.fromEntries(rows.map((row) => [row.id, row.role]));\n },\n };\n const entitlementsRepo = new DrizzleEntitlementsRepository(db, entitlementsLogger);\n const entitlements = new EntitlementsServiceImpl({\n repo: entitlementsRepo,\n content,\n uow: entitlementsUow,\n logger: entitlementsLogger,\n });\n // Progress: report writes + outbox append in one tx; content supplies the\n // structure and completion rules the service evaluates against.\n const progressUow = new DrizzleUnitOfWork(db, (tx) => ({\n progress: new DrizzleProgressRepository(tx, progressLogger),\n outbox: new DrizzleOutboxAppender(tx, outboxLogger),\n }));\n const progress = new ProgressServiceImpl({\n repo: new DrizzleProgressRepository(db, progressLogger),\n content,\n access: entitlements,\n uow: progressUow,\n logger: progressLogger,\n });\n\n const postReads = new DrizzlePostRepository(db, discussionLogger);\n const discussionReads = new DrizzleDiscussionRepository(db, discussionLogger);\n const chatReads = new DrizzleChatRepository(db, discussionLogger);\n const posts = new PostServiceImpl({\n repo: postReads,\n access: entitlements,\n content,\n authors,\n profiles: discussionReads,\n comments: discussionReads,\n images: assets,\n fileUrls,\n uow: new DrizzleUnitOfWork(db, (tx) => ({\n posts: new DrizzlePostRepository(tx, discussionLogger),\n outbox: new DrizzleOutboxAppender(tx, outboxLogger),\n })),\n logger: discussionLogger,\n });\n const chats = new ChatServiceImpl({\n repo: chatReads,\n access: entitlements,\n content,\n authors,\n profiles: discussionReads,\n images: assets,\n fileUrls,\n uow: new DrizzleUnitOfWork(db, (tx) => ({\n chats: new DrizzleChatRepository(tx, discussionLogger),\n outbox: new DrizzleOutboxAppender(tx, outboxLogger),\n })),\n logger: discussionLogger,\n });\n const discussion = new DiscussionServiceImpl({\n repo: discussionReads,\n access: entitlements,\n content: content,\n posts,\n communities: { listIds: (orgId) => content.listPublishedCommunityIds(orgId) },\n fileUrls,\n uow: new DrizzleUnitOfWork(db, (tx) => ({\n discussion: new DrizzleDiscussionRepository(tx, discussionLogger),\n outbox: new DrizzleOutboxAppender(tx, outboxLogger),\n })),\n settings,\n logger: discussionLogger,\n });\n\n const reporting = {\n admin: new AdminReportServiceImpl({\n repo: new DrizzleAdminRepository(db, reportingLogger),\n }),\n member: new MemberReportServiceImpl({\n repo: new DrizzleMemberRepository(db, reportingLogger),\n content,\n progress,\n files: assets,\n fileUrls,\n access: entitlements,\n notifications,\n posts,\n chats,\n }),\n };\n\n const credentialStore = new DrizzleCredentialStore(\n db,\n config.credentialStoreKey,\n integrationsLogger,\n );\n // The integrations this deployment supports: the installation's declared\n // plugins folder (directory name = integration id), loaded at startup.\n // Connect/configure reject undeclared ids and validate config with the\n // integration's own schema. No folder → no integrations.\n const integrationsRegistry = await loadIntegrations(options?.pluginsDir, integrationsLogger);\n // Integrations: credential + connection writes + outbox append in one tx\n // (a tx-bound credential store instance shares the scope's transaction).\n const integrationsUow = new DrizzleUnitOfWork(db, (tx) => ({\n connections: new DrizzleConnectionsRepository(tx, integrationsLogger),\n credentials: new DrizzleCredentialStore(tx, config.credentialStoreKey, integrationsLogger),\n outbox: new DrizzleOutboxAppender(tx, outboxLogger),\n }));\n const integrations = new IntegrationsServiceImpl({\n registry: integrationsRegistry,\n repo: new DrizzleConnectionsRepository(db, integrationsLogger),\n uow: integrationsUow,\n credentials: credentialStore,\n logger: integrationsLogger,\n });\n\n // Automations: run writes + outbox append in one tx; the engine drives\n // execution (register below), integrations supplies the loaded\n // integrations' actions for `available`.\n const automationsUow = new DrizzleUnitOfWork(db, (tx) => ({\n automations: new DrizzleAutomationsRepository(tx, automationsLogger),\n runs: new DrizzleAutomationRunsRepository(tx, automationsLogger),\n credentials: new DrizzleCredentialStore(tx, config.credentialStoreKey, automationsLogger),\n outbox: new DrizzleOutboxAppender(tx, automationsLogger),\n }));\n // Emails triggered by row-shaped events resolve their recipient and content\n // details at send time — the event carries ids only.\n const mailerLookups: MailerLookups = {\n orgUserPerson: async (orgId, orgUserId) => {\n const orgUser = await organizationsRepo.findOrgUserById(orgId, orgUserId);\n const user = orgUser ? await identity.getUserById(orgUser.userId) : null;\n return user ? { userId: user.id, email: user.email } : null;\n },\n contentInfo: async (orgId, contentId) => {\n const course = await content.getCourse(orgId, contentId);\n if (course) {\n return { id: course.id, title: course.title, type: course.type };\n }\n const download = await content.getDownload(orgId, contentId);\n if (download) {\n return { id: download.id, title: download.title, type: download.type };\n }\n const community = await content.getCommunity(orgId, contentId);\n return community ? { id: community.id, title: community.title, type: community.type } : null;\n },\n };\n\n const runsRepo = new DrizzleAutomationRunsRepository(db, automationsLogger);\n const wired = communityMail({\n content,\n entitlements,\n organizations,\n identity,\n posts,\n discussion,\n chats,\n assets,\n });\n const automations = new AutomationsServiceImpl({\n repo: new DrizzleAutomationsRepository(db, automationsLogger),\n runsRepo,\n uow: automationsUow,\n engine: automationEngine,\n mailer,\n lookups: mailerLookups,\n integrations,\n credentials: credentialStore,\n webhooks:\n options?.adapters?.webhooks ??\n new FetchWebhookSender({ allowPrivateNetwork: config.webhookAllowPrivateNetwork }),\n mail: wired.mail,\n grants: wired.grants,\n desk: notifications,\n scheduledWork: wired.scheduledWork,\n logger: automationsLogger,\n });\n automationEngine.register(automations);\n\n const eventBus = new InMemoryEventBus();\n eventBus.subscribeAll((event) => automations.handle(event));\n const outboxConfig = resolveOutboxConfig(config.outbox);\n const outboxRelay = new PollingOutboxRelay(\n new DrizzleOutboxStore(db, outboxLogger),\n eventBus,\n outboxConfig,\n outboxLogger,\n );\n\n /*\n * Auth Adapter\n */\n const authLogger = logger.child({ name: 'auth' });\n // Hook failures otherwise surface through Better Auth's route as an opaque\n // 500 — log which hook died before rethrowing.\n const authHook =\n <A extends unknown[], R>(name: string, fn: (...args: A) => Promise<R>) =>\n async (...args: A): Promise<R> => {\n try {\n return await fn(...args);\n } catch (err) {\n authLogger.error(`auth hook ${name} failed`, { err });\n throw err;\n }\n };\n const mcp = mcpEndpoints(config);\n if (!mcp.oauthEnabled) {\n authLogger.warn(\n 'MCP OAuth is off: BETTER_AUTH_URL must be https or a loopback host; /mcp accepts API keys only',\n { authBaseURL: config.authBaseURL },\n );\n }\n const auth = new BetterAuth({\n db,\n baseUrl: config.authBaseURL,\n mcp: {\n oauthEnabled: mcp.oauthEnabled,\n resource: mcp.resource,\n loginPage: mcp.loginPage,\n consentPage: mcp.consentPage,\n orgPickerPage: mcp.orgPickerPage,\n },\n secret: config.authSecret,\n trustedOrigins: config.trustedOrigins,\n hooks: {\n sendResetPassword: async ({ user, url }) => {\n await identity.sendPasswordReset({ email: user.email, url });\n },\n onPasswordReset: async ({ user }) => {\n await identity.sendPasswordChanged({ email: user.email });\n },\n sendMagicLink: authHook('sendMagicLink', async ({ email, url }) => {\n await identity.sendMagicLink({\n email,\n url,\n });\n }),\n beforeUserCreate: authHook('beforeUserCreate', async (user) => {\n const { email, name } = user;\n const [firstName, lastName] = name.split(' ');\n // Better Auth has not minted an id yet — the id returned below becomes\n // its account id. A person provisioned at invite time is linked instead of re-created.\n const domainUser = await identity.linkOrCreateUser({\n email,\n ...(firstName !== undefined && { firstName }),\n ...(lastName !== undefined && { lastName }),\n });\n\n return {\n data: {\n ...user,\n id: domainUser.id,\n },\n };\n }),\n beforeCreateSession: authHook('beforeCreateSession', async (session) => {\n const person = await identity.getUserByExternalId(session.userId);\n if (!person) {\n return;\n }\n const orgs = await organizations.getOrgUsersForUser(person.id);\n if (orgs.length !== 1) {\n return;\n }\n // TODO - fix, why default to the first?\n const org = await organizations.getById(orgs[0]!.orgId);\n if (!org) {\n return;\n }\n\n return { data: { ...session, activeOrganizationId: org.id } };\n }),\n /*\n * We're making sure the domain org creates and using the same ID for better-auth org ID.\n */\n beforeCreateOrganization: authHook(\n 'beforeCreateOrganization',\n async ({ organization: org, user: baUser }) => {\n const domainOrg = await organizations.createOrganization({\n ownerId: baUser.id,\n ...org,\n logo: org.logo ?? undefined,\n });\n return { data: { ...org, id: domainOrg.id } };\n },\n ),\n beforeUpdateOrganization: authHook('beforeUpdateOrganization', async ({ organization }) => {\n await organizations.updateOrganization(organization.id, organization);\n }),\n beforeDeleteOrganization: authHook('beforeDeleteOrganization', async ({ organization }) => {\n await organizations.deleteOrganization(organization.id);\n }),\n beforeAddMember: authHook('beforeAddMember', async ({ member }) => {\n await organizations.addOrgUser({\n orgId: member.organizationId,\n userId: member.userId,\n role: parseRole(member.role),\n });\n }),\n beforeUpdateMemberRole: authHook('beforeUpdateMemberRole', async ({ member, newRole }) => {\n await organizations.updateOrgUser(member.organizationId, member.userId, {\n role: parseRole(newRole),\n });\n }),\n beforeRemoveMember: authHook('beforeRemoveMember', async ({ member }) => {\n await organizations.removeOrgUser(member.organizationId, member.userId);\n }),\n },\n logger: authLogger,\n cookieDomain: config.cookieDomain,\n secureCookies: config.secureCookies,\n });\n orgAdminRef.current = auth;\n\n return {\n auth,\n mcp,\n identity,\n notifications,\n organizations,\n // Better Auth owns org writes; the same instance fulfils the OrgAdmin port.\n orgProvider: auth,\n content,\n entitlements,\n progress,\n discussion,\n posts,\n chats,\n assets,\n integrations,\n automations,\n settings,\n reporting,\n storage,\n mailer,\n credentials: credentialStore,\n outboxRelay,\n automationEngine,\n logger,\n loggerInstance,\n requestContext: requestLogContext,\n // Better Auth owns the session store; the same instance fulfils SessionAdmin.\n sessions: auth,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"container.js","names":[],"sources":["../../src/app/container.ts"],"sourcesContent":["// Wires adapters + services in dependency order. Starts nothing.\nimport {\n createDb,\n type DatabasePoolOptions,\n DrizzleAssetsRepository,\n DrizzleAutomationRunsRepository,\n DrizzleAutomationsRepository,\n DrizzleConnectionsRepository,\n DrizzleContentRepository,\n DrizzleCredentialStore,\n DrizzleAdminRepository,\n DrizzleDigestRunRepository,\n DrizzleDevicesRepository,\n DrizzleDiscussionRepository,\n DrizzlePostRepository,\n DrizzleChatRepository,\n DrizzleEntitlementsRepository,\n DrizzleIdentityRepository,\n DrizzleMemberRepository,\n DrizzleMembersRepository,\n DrizzleMentionsRepository,\n DrizzleNotificationsRepository,\n DrizzleNotificationSettingsRepository,\n DrizzleOrganizationsRepository,\n DrizzleRolesRepository,\n DrizzleOutboxAppender,\n DrizzleOutboxStore,\n DrizzleProgressRepository,\n DrizzleSettingsRepository,\n DrizzleSubscriptionsRepository,\n DrizzleUnitOfWork,\n} from '@headless-lms/adapter-db';\nimport { InMemoryEventBus } from '@headless-lms/adapter-defaults/events';\nimport {\n PollingOutboxRelay,\n type PollingOutboxRelayConfig,\n} from '@headless-lms/adapter-defaults/events/outbox-relay';\nimport { InlineAutomationEngine } from '@headless-lms/adapter-defaults/workflows';\nimport { FetchWebhookSender } from '@headless-lms/adapter-defaults/webhooks';\nimport { EmailAdapter, StubTemplateRenderer } from '@headless-lms/adapter-defaults/email';\nimport {\n createRootLogger,\n type LogLevel,\n type PinoInstance,\n requestLogContext,\n type RequestLogContext,\n} from '@headless-lms/adapter-defaults/logging';\nimport { StorageAdapter } from '@headless-lms/adapter-defaults/storage';\nimport { PlainBodyParser } from '@headless-lms/adapter-defaults/body';\nimport { BetterAuth } from '@headless-lms/adapter-auth';\n\nimport {\n CommunityNotificationCategories,\n ContentItemTitles,\n ContentService,\n contentJobs,\n} from '@headless-lms/core/content';\nimport {\n EntitlementsServiceImpl,\n entitlementsJobs,\n entitlementsRecurringJobs,\n} from '@headless-lms/core/entitlements';\nimport { ProgressServiceImpl } from '@headless-lms/core/progress';\nimport {\n type ChatHub,\n type ChatHubOptions,\n ChatLiveForwarder,\n ChatServiceImpl,\n DigestServiceImpl,\n DiscussionItemTitles,\n DiscussionServiceImpl,\n type AuthorPermissionReader,\n PostServiceImpl,\n SubscriptionServiceImpl,\n discussionJobs,\n discussionRecurringJobs,\n} from '@headless-lms/core/discussion';\nimport {\n DevicesServiceImpl,\n IdentityServiceImpl,\n NotificationSettingsServiceImpl,\n type SessionAdmin,\n} from '@headless-lms/core/identity';\nimport {\n type ApiKeys,\n type GrantReader,\n type OrgAdmin,\n NotificationCatalog,\n OrganizationServiceImpl,\n OrgNotificationCategories,\n type RoleDefinition,\n} from '@headless-lms/core/organizations';\nimport {\n AssetsServiceImpl,\n assetsJobs,\n assetsRecurringJobs,\n createFileUrls,\n} from '@headless-lms/core/assets';\nimport { IntegrationsServiceImpl } from '@headless-lms/core/integrations';\nimport {\n AutomationsServiceImpl,\n DEFAULT_COMMUNITY_CATEGORIES,\n DEFAULT_ORG_CATEGORIES,\n NOTIFICATION_TYPES,\n NotificationDelivery,\n NotificationProcessor,\n NotificationsServiceImpl,\n RECIPIENT_RULES,\n SchedulerChatDebounce,\n notificationJobs,\n type NotificationCategories,\n} from '@headless-lms/core/automations';\nimport { buildNotificationRegistries, configuredPushSender } from './notification-channels.js';\nimport { loadIntegrations } from './integrations.js';\nimport { mcpEndpoints, type McpEndpoints } from './mcp-endpoints.js';\nimport { communityMail } from './community-mail.js';\nimport { notificationReads } from './notification-reads.js';\nimport { AdminReportServiceImpl } from '@headless-lms/core/reporting/admin';\nimport { MemberReportServiceImpl } from '@headless-lms/core/reporting/member';\nimport { Mailer, type MailerLookups } from '@headless-lms/core/shared/mailer';\nimport { SettingsService } from '@headless-lms/core/shared/settings';\nimport { platformKeyCheck, type PlatformConfig, type PlatformKeyCheck } from './platform.js';\n\nimport type {\n BodyParser,\n ChannelRegistry,\n CredentialStore,\n EmailSender,\n Logger,\n NotificationChannel,\n NotificationTypeRegistry,\n ObjectStorage,\n OutboxRelay,\n PubSub,\n PushSender,\n Scheduler,\n TemplateContext,\n TemplateRenderer,\n} from '@headless-lms/core/shared/ports';\nimport type { AutomationEngine, PushConfig, WebhookSender } from '@headless-lms/core/types';\n\n/** Installation-supplied ports; an absent slot falls back to a fail-loudly stub. */\nexport interface AdapterOverrides {\n email?: EmailSender;\n storage?: ObjectStorage;\n /** Resolves email templates to rendered content. Absent → fail-loudly stub. */\n templates?: TemplateRenderer;\n /** Durable automation engine, or a factory given the automations logger. Absent → InlineAutomationEngine (in-process, one attempt per action). */\n workflows?: (AutomationEngine & Scheduler) | ((logger: Logger) => AutomationEngine & Scheduler);\n /** Outbound HTTP for webhook deliveries. Absent → FetchWebhookSender (public addresses only, 10s timeout). */\n webhooks?: WebhookSender;\n /** Notification channels beyond in-app, email and push; each key must be new. */\n notificationChannels?: NotificationChannel[];\n /** Push credentials per provider. Absent → no provider is configured and devices cannot register. */\n push?: PushConfig;\n pushSender?: PushSender;\n /** Reads post bodies: their text, mentions and images. Absent → PlainBodyParser (plain text, no images). */\n bodyParser?: BodyParser;\n /** Delivery to every API instance, or a factory given its logger. Absent → no live chat; the chat stream answers 503. */\n pubsub?: PubSub | ((logger: Logger) => PubSub);\n}\n\nexport interface BuildContainerOptions {\n /** Installation's plugins folder, scanned by loadIntegrations. Absent → no integrations. */\n pluginsDir?: string;\n adapters?: AdapterOverrides;\n roles?: readonly RoleDefinition[];\n /** Live chat hub timings. Absent → the defaults (1 s state flush, 30 s session check). */\n live?: ChatHubOptions;\n}\n\nexport interface Config {\n databaseUrl: string;\n databasePool?: DatabasePoolOptions;\n authBaseURL: string;\n authSecret: string;\n trustedOrigins: string[];\n /** Branding threaded into every email template. Default: brandName \"Headless LMS\", baseUrl = adminAppUrl.\n * (studentPortalUrl is composed in from the top-level config field.) */\n emailBranding?: Omit<TemplateContext, 'studentPortalUrl'>;\n /** base64-encoded 32-byte key for the credential store (CREDENTIAL_STORE_KEY). */\n credentialStoreKey: string;\n /** Parent domain for cross-subdomain session cookies (e.g. \".example.com\"); undefined → host-only cookie. */\n cookieDomain?: string;\n /** Mark session cookies Secure (set behind HTTPS / in production). */\n secureCookies?: boolean;\n /** Student portal origin — invite links for students, and the origin whose signups are invite-gated. */\n studentPortalUrl: string;\n /** Admin app origin — invite links for staff. */\n adminAppUrl: string;\n /** Transactional-outbox relay tuning. All optional — see OUTBOX_DEFAULTS. */\n outbox?: OutboxConfig;\n /** Log level for the process-wide logger (HTTP + domain + relay). Default \"info\". */\n logging?: LoggingConfig;\n /** Lifetime of every signed file URL a response carries, seconds. Mirrors\n * ServerConfig.deliveryExpirySeconds — createContainer threads it through. */\n deliveryExpirySeconds: number;\n memberImageMaxBytes: number;\n memberFileMaxBytes: number;\n /** Let the default webhook sender reach loopback and private network addresses. Local development only. */\n webhookAllowPrivateNetwork?: boolean;\n platform?: PlatformConfig;\n}\n\n/** Tuning for the transactional-outbox relay. Every field is optional; the\n * container resolves against OUTBOX_DEFAULTS. */\nexport interface OutboxConfig {\n /** Master switch for the same-process relay. Default true. */\n enabled?: boolean;\n /** Idle delay between polls. Default 1000. */\n pollIntervalMs?: number;\n /** Max rows fetched/dispatched per tick. Default 100. */\n batchSize?: number;\n concurrency?: number;\n}\n\nexport const OUTBOX_DEFAULTS: PollingOutboxRelayConfig = {\n enabled: true,\n pollIntervalMs: 1000,\n batchSize: 100,\n concurrency: 16,\n};\n\nexport function resolveOutboxConfig(config: OutboxConfig = {}): PollingOutboxRelayConfig {\n const overrides = Object.fromEntries(\n Object.entries(config).filter(([, value]) => value !== undefined),\n );\n return { ...OUTBOX_DEFAULTS, ...overrides };\n}\n\n/** Logging tuning. Optional; resolved against LOGGING_DEFAULTS. */\nexport interface LoggingConfig {\n /** Minimum level emitted. Default \"info\". */\n level?: LogLevel;\n}\n\nexport const LOGGING_DEFAULTS: Required<LoggingConfig> = { level: 'info' };\n\nexport function resolveLoggingConfig(config: LoggingConfig = {}): Required<LoggingConfig> {\n return { level: config.level ?? LOGGING_DEFAULTS.level };\n}\n\nexport interface Container {\n auth: BetterAuth;\n mcp: McpEndpoints;\n platform: PlatformKeyCheck | null;\n // Org Provider\n orgProvider: OrgAdmin;\n\n // Domains\n identity: IdentityServiceImpl;\n organizations: OrganizationServiceImpl;\n content: ContentService;\n entitlements: EntitlementsServiceImpl;\n progress: ProgressServiceImpl;\n discussion: DiscussionServiceImpl;\n posts: PostServiceImpl;\n chats: ChatServiceImpl;\n subscriptions: SubscriptionServiceImpl;\n assets: AssetsServiceImpl;\n integrations: IntegrationsServiceImpl;\n automations: AutomationsServiceImpl;\n notificationTypes: NotificationTypeRegistry;\n notificationChannels: ChannelRegistry;\n notificationCategories: {\n community: CommunityNotificationCategories;\n org: OrgNotificationCategories;\n };\n notificationCatalog: NotificationCatalog;\n notificationCategoryLists: NotificationCategories;\n notificationSettings: NotificationSettingsServiceImpl;\n devices: DevicesServiceImpl;\n notificationList: NotificationsServiceImpl;\n /** Cross-cutting settings store. Not a context — a leaf every context may use;\n * inject `settings.for('<namespace>')` into a service, never the service itself. */\n settings: SettingsService;\n // Reporting read layer (composed cross-context reads; owns no domain rules).\n reporting: {\n admin: AdminReportServiceImpl;\n member: MemberReportServiceImpl;\n };\n storage: ObjectStorage;\n mailer: Mailer;\n /** Shared secure credential store — encrypted at rest, org-scoped, decrypt at point of use. */\n credentials: CredentialStore;\n /** The outbox relay — constructed but NEVER started by the container; the\n * installation's entry point starts it after listen (gen-openapi must not\n * poll). buildServer stops it onClose. */\n outboxRelay: OutboxRelay;\n /** The automation engine — constructed but NEVER started by the container;\n * the installation's entry point starts it after listen (gen-openapi must\n * not run a worker). buildServer stops it onClose. */\n automationEngine: AutomationEngine;\n scheduler: Scheduler;\n /** The PubSub adapter — constructed but NEVER started by the container; the\n * installation's entry point starts it after listen. buildServer stops it\n * onClose. Null without one. */\n pubsub: PubSub | null;\n /** This process's live chat hub; started by the entry point after `pubsub`,\n * closed by buildServer preClose. Null without a PubSub. */\n chatHub: ChatHub | null;\n /** Root logger port — components receive children bound with { name }. */\n logger: Logger;\n /** The raw pino root; buildServer hands it to Fastify so HTTP shares the stream. */\n loggerInstance: PinoInstance;\n /** Request-scoped log correlation; the HTTP layer enters it per request. */\n requestContext: RequestLogContext;\n /** Identity's session-write port; the auth adapter fulfils it. */\n sessions: SessionAdmin;\n}\n\nexport async function buildContainer(\n config: Config,\n options?: BuildContainerOptions,\n): Promise<Container> {\n const { instance: loggerInstance, logger } = createRootLogger(\n resolveLoggingConfig(config.logging).level,\n );\n // One child per domain — a context's service and repositories share it.\n\n const contentLogger = logger.child({ name: 'content' });\n const entitlementsLogger = logger.child({ name: 'entitlements' });\n const progressLogger = logger.child({ name: 'progress' });\n const discussionLogger = logger.child({ name: 'discussion' });\n const assetsLogger = logger.child({ name: 'assets' });\n const integrationsLogger = logger.child({ name: 'integrations' });\n const automationsLogger = logger.child({ name: 'automations' });\n const reportingLogger = logger.child({ name: 'reporting' });\n const outboxLogger = logger.child({ name: 'outbox' });\n\n // Outbound adapters\n const db = createDb(config.databaseUrl, logger.child({ name: 'db' }), config.databasePool);\n const email = options?.adapters?.email ?? new EmailAdapter(logger.child({ name: 'email' }));\n const storage: ObjectStorage =\n options?.adapters?.storage ?? new StorageAdapter(logger.child({ name: 'storage' }));\n const templates =\n options?.adapters?.templates ?? new StubTemplateRenderer(logger.child({ name: 'email' }));\n const bodies = options?.adapters?.bodyParser ?? new PlainBodyParser();\n const workflows = options?.adapters?.workflows;\n const automationEngine: AutomationEngine & Scheduler =\n typeof workflows === 'function'\n ? workflows(automationsLogger)\n : (workflows ?? new InlineAutomationEngine(automationsLogger));\n const pubsubOption = options?.adapters?.pubsub;\n const pubsub: PubSub | null =\n typeof pubsubOption === 'function'\n ? pubsubOption(logger.child({ name: 'pubsub' }))\n : (pubsubOption ?? null);\n const mailer = new Mailer(templates, email, {\n ...(config.emailBranding ?? { brandName: 'Headless LMS', baseUrl: config.adminAppUrl }),\n studentPortalUrl: config.studentPortalUrl,\n });\n\n const fileUrls = createFileUrls(config.authBaseURL, (key) => storage.publicUrl(key));\n const assets = new AssetsServiceImpl({\n storage,\n repo: new DrizzleAssetsRepository(db, assetsLogger),\n uow: new DrizzleUnitOfWork(db, (tx) => ({\n assets: new DrizzleAssetsRepository(tx, assetsLogger),\n outbox: new DrizzleOutboxAppender(tx, outboxLogger),\n })),\n memberImageMaxBytes: config.memberImageMaxBytes,\n memberFileMaxBytes: config.memberFileMaxBytes,\n downloadLinkExpirySeconds: config.deliveryExpirySeconds,\n fileUrls,\n logger: assetsLogger,\n });\n\n /*\n * Identity\n */\n const identityUoW = new DrizzleUnitOfWork(db, (tx) => ({\n identity: new DrizzleIdentityRepository(tx, logger.child({ name: 'identity' })),\n outbox: new DrizzleOutboxAppender(tx, logger.child({ name: 'identity' })),\n }));\n const identity = new IdentityServiceImpl({\n repo: new DrizzleIdentityRepository(db, logger.child({ name: 'identity_repo' })),\n logger: logger.child({ name: 'identity' }),\n uow: identityUoW,\n mailer,\n images: assets,\n fileUrls,\n });\n\n /*\n * Organization\n */\n const organizationsRepo = new DrizzleOrganizationsRepository(db, logger.child({ name: 'org' }));\n const organizationsUow = new DrizzleUnitOfWork(db, (tx) => ({\n organizations: new DrizzleOrganizationsRepository(tx, logger.child({ name: 'org' })),\n roles: new DrizzleRolesRepository(tx),\n outbox: new DrizzleOutboxAppender(tx, logger.child({ name: 'org' })),\n }));\n const orgAdminRef: { current: BetterAuth | undefined } = { current: undefined };\n const platform = config.platform ? platformKeyCheck(config.platform) : null;\n const organizations: OrganizationServiceImpl = new OrganizationServiceImpl({\n repo: organizationsRepo,\n roles: new DrizzleRolesRepository(db),\n ...(options?.roles && { defaultRoles: options.roles }),\n apiKeys: (): ApiKeys => {\n if (!orgAdminRef.current) {\n throw new Error('api keys not initialised');\n }\n return orgAdminRef.current;\n },\n accounts: () => {\n if (!orgAdminRef.current) {\n throw new Error('accounts not initialised');\n }\n return orgAdminRef.current;\n },\n ...(config.platform && {\n platform: {\n email: config.platform.accountEmail,\n name: config.platform.accountName ?? 'Platform',\n },\n }),\n membersRepo: new DrizzleMembersRepository(db, logger.child({ name: 'org' })),\n people: identity,\n images: assets,\n uow: organizationsUow,\n orgAdmin: () => {\n if (!orgAdminRef.current) {\n throw new Error('orgAdmin not initialised');\n }\n return orgAdminRef.current;\n },\n sessions: () => {\n if (!orgAdminRef.current) {\n throw new Error('sessions not initialised');\n }\n return orgAdminRef.current;\n },\n logger: logger.child({ name: 'org' }),\n mailer,\n inviteUrls: { studentPortalUrl: config.studentPortalUrl, adminAppUrl: config.adminAppUrl },\n grants: (): GrantReader => entitlements,\n });\n\n /*\n * Settings\n */\n const settings = new SettingsService(\n new DrizzleSettingsRepository(db, logger.child({ name: 'settings' })),\n );\n // Content: reads on the root db; course writes + outbox append in one tx.\n const contentUow = new DrizzleUnitOfWork(db, (tx) => ({\n content: new DrizzleContentRepository(tx, contentLogger),\n outbox: new DrizzleOutboxAppender(tx, outboxLogger),\n }));\n const content = new ContentService({\n repo: new DrizzleContentRepository(db, contentLogger),\n uow: contentUow,\n libraryAssets: assets,\n files: assets,\n fileUrls,\n scheduler: automationEngine,\n logger: contentLogger,\n });\n const pushConfig = options?.adapters?.push ?? {};\n const pushSender = configuredPushSender(pushConfig, options?.adapters?.pushSender);\n const notificationRegistries = buildNotificationRegistries({\n declarations: NOTIFICATION_TYPES,\n defaults: { community: DEFAULT_COMMUNITY_CATEGORIES, org: DEFAULT_ORG_CATEGORIES },\n mailer: options?.adapters?.email ? mailer : null,\n emailOf: async (_orgId, userId) => (await identity.getUserById(userId))?.email ?? null,\n push: pushSender\n ? {\n sender: pushSender,\n activeDevices: (orgId, userId, since) => devices.activeDevices(orgId, userId, since),\n }\n : null,\n channels: options?.adapters?.notificationChannels ?? [],\n });\n const notificationCategories = {\n community: new CommunityNotificationCategories({\n communities: new DrizzleContentRepository(db, contentLogger),\n settings,\n types: notificationRegistries.types,\n channels: notificationRegistries.channels,\n logger: contentLogger,\n }),\n org: new OrgNotificationCategories({\n settings,\n types: notificationRegistries.types,\n channels: notificationRegistries.channels,\n logger: logger.child({ name: 'org' }),\n }),\n };\n const notificationCatalog = new NotificationCatalog({\n types: notificationRegistries.types,\n channels: notificationRegistries.channels,\n });\n const notificationCategoryLists: NotificationCategories = {\n listFor: async (orgId, communityId) =>\n (communityId\n ? await notificationCategories.community.get(communityId, organizations.systemActor(orgId))\n : await notificationCategories.org.get(orgId, organizations.systemActor(orgId))\n ).categories,\n };\n // Entitlements: reads on the root db; writes + outbox append in one tx.\n const entitlementsUow = new DrizzleUnitOfWork(db, (tx) => ({\n entitlements: new DrizzleEntitlementsRepository(tx, entitlementsLogger),\n spaces: new DrizzleContentRepository(tx, contentLogger),\n outbox: new DrizzleOutboxAppender(tx, outboxLogger),\n }));\n const authors: AuthorPermissionReader = organizations;\n const entitlementsRepo = new DrizzleEntitlementsRepository(db, entitlementsLogger);\n const entitlements = new EntitlementsServiceImpl({\n repo: entitlementsRepo,\n content,\n permissions: organizations,\n uow: entitlementsUow,\n logger: entitlementsLogger,\n });\n const identityLogger = logger.child({ name: 'identity' });\n const notificationSettings = new NotificationSettingsServiceImpl({\n repo: new DrizzleNotificationSettingsRepository(db, identityLogger),\n categories: {\n communityList: async (orgId, communityId) =>\n (await notificationCategories.community.get(communityId, organizations.systemActor(orgId)))\n .categories,\n orgList: async (orgId) =>\n (await notificationCategories.org.get(orgId, organizations.systemActor(orgId))).categories,\n },\n access: {\n canOpen: (orgId, actor, communityId) => entitlements.isCommunityMember(communityId, actor),\n },\n channels: notificationRegistries.channels,\n logger: identityLogger,\n });\n const devices = new DevicesServiceImpl({\n repo: new DrizzleDevicesRepository(db, identityLogger),\n push: pushConfig,\n logger: identityLogger,\n });\n // Progress: report writes + outbox append in one tx; content supplies the\n // structure and completion rules the service evaluates against.\n const progressUow = new DrizzleUnitOfWork(db, (tx) => ({\n progress: new DrizzleProgressRepository(tx, progressLogger),\n outbox: new DrizzleOutboxAppender(tx, outboxLogger),\n }));\n const progress = new ProgressServiceImpl({\n repo: new DrizzleProgressRepository(db, progressLogger),\n content,\n access: entitlements,\n system: organizations,\n uow: progressUow,\n logger: progressLogger,\n });\n\n const postReads = new DrizzlePostRepository(db, discussionLogger);\n const discussionReads = new DrizzleDiscussionRepository(db, discussionLogger);\n const chatReads = new DrizzleChatRepository(db, discussionLogger);\n const subscriptionReads = new DrizzleSubscriptionsRepository(db, discussionLogger);\n const mentionReads = new DrizzleMentionsRepository(db, discussionLogger);\n const posts = new PostServiceImpl({\n repo: postReads,\n access: entitlements,\n content,\n authors,\n profiles: discussionReads,\n comments: discussionReads,\n subscriptions: subscriptionReads,\n mentions: mentionReads,\n images: assets,\n fileUrls,\n bodies,\n uow: new DrizzleUnitOfWork(db, (tx) => ({\n posts: new DrizzlePostRepository(tx, discussionLogger),\n mentions: new DrizzleMentionsRepository(tx, discussionLogger),\n outbox: new DrizzleOutboxAppender(tx, outboxLogger),\n })),\n scheduler: automationEngine,\n logger: discussionLogger,\n });\n const chats = new ChatServiceImpl({\n repo: chatReads,\n access: entitlements,\n content,\n authors,\n profiles: discussionReads,\n subscriptions: subscriptionReads,\n mentions: mentionReads,\n images: assets,\n fileUrls,\n uow: new DrizzleUnitOfWork(db, (tx) => ({\n chats: new DrizzleChatRepository(tx, discussionLogger),\n mentions: new DrizzleMentionsRepository(tx, discussionLogger),\n outbox: new DrizzleOutboxAppender(tx, outboxLogger),\n })),\n ...(pubsub && { pubsub }),\n ...(options?.live && { live: options.live }),\n logger: discussionLogger,\n });\n const discussion = new DiscussionServiceImpl({\n repo: discussionReads,\n access: entitlements,\n content: content,\n posts,\n spaces: entitlements,\n authors,\n mentions: mentionReads,\n fileUrls,\n uow: new DrizzleUnitOfWork(db, (tx) => ({\n discussion: new DrizzleDiscussionRepository(tx, discussionLogger),\n mentions: new DrizzleMentionsRepository(tx, discussionLogger),\n outbox: new DrizzleOutboxAppender(tx, outboxLogger),\n })),\n settings,\n logger: discussionLogger,\n });\n\n const subscriptions = new SubscriptionServiceImpl({\n repo: subscriptionReads,\n access: entitlements,\n posts,\n chats,\n uow: new DrizzleUnitOfWork(db, (tx) => ({\n subscriptions: new DrizzleSubscriptionsRepository(tx, discussionLogger),\n outbox: new DrizzleOutboxAppender(tx, outboxLogger),\n })),\n logger: discussionLogger,\n });\n\n const digests = new DigestServiceImpl({\n schedules: { communitiesWithDigest: () => content.communitiesWithDigest() },\n runs: new DrizzleDigestRunRepository(db, discussionLogger),\n uow: new DrizzleUnitOfWork(db, (tx) => ({\n digests: new DrizzleDigestRunRepository(tx, discussionLogger),\n outbox: new DrizzleOutboxAppender(tx, outboxLogger),\n })),\n logger: discussionLogger,\n });\n\n const notificationsRepo = new DrizzleNotificationsRepository(db);\n const notificationList = new NotificationsServiceImpl({\n repo: notificationsRepo,\n logger: automationsLogger,\n });\n const itemTitles = {\n discussion: new DiscussionItemTitles({\n posts,\n chats,\n comments: discussionReads,\n messages: chatReads,\n profiles: discussionReads,\n activities: content,\n access: entitlements,\n actors: organizations,\n }),\n content: new ContentItemTitles({ content, access: entitlements, organizations }),\n };\n\n const reporting = {\n admin: new AdminReportServiceImpl({\n repo: new DrizzleAdminRepository(db, reportingLogger),\n permissions: organizations,\n spaces: entitlements,\n events: content,\n fileUrls,\n }),\n member: new MemberReportServiceImpl({\n repo: new DrizzleMemberRepository(db, reportingLogger),\n content,\n progress,\n files: assets,\n fileUrls,\n access: entitlements,\n people: organizations,\n notifications: notificationList,\n titles: itemTitles,\n subscriptions,\n notificationCategories,\n notificationSettings,\n channels: notificationRegistries.channels,\n studentPortalUrl: config.studentPortalUrl,\n }),\n };\n\n const credentialStore = new DrizzleCredentialStore(\n db,\n config.credentialStoreKey,\n integrationsLogger,\n );\n // The integrations this deployment supports: the installation's declared\n // plugins folder (directory name = integration id), loaded at startup.\n // Connect/configure reject undeclared ids and validate config with the\n // integration's own schema. No folder → no integrations.\n const integrationsRegistry = await loadIntegrations(options?.pluginsDir, integrationsLogger);\n // Integrations: credential + connection writes + outbox append in one tx\n // (a tx-bound credential store instance shares the scope's transaction).\n const integrationsUow = new DrizzleUnitOfWork(db, (tx) => ({\n connections: new DrizzleConnectionsRepository(tx, integrationsLogger),\n credentials: new DrizzleCredentialStore(tx, config.credentialStoreKey, integrationsLogger),\n outbox: new DrizzleOutboxAppender(tx, outboxLogger),\n }));\n const integrations = new IntegrationsServiceImpl({\n registry: integrationsRegistry,\n repo: new DrizzleConnectionsRepository(db, integrationsLogger),\n uow: integrationsUow,\n credentials: credentialStore,\n logger: integrationsLogger,\n });\n\n // Automations: run writes + outbox append in one tx; the engine drives\n // execution (register below), integrations supplies the loaded\n // integrations' actions for `available`.\n const automationsUow = new DrizzleUnitOfWork(db, (tx) => ({\n automations: new DrizzleAutomationsRepository(tx, automationsLogger),\n runs: new DrizzleAutomationRunsRepository(tx, automationsLogger),\n credentials: new DrizzleCredentialStore(tx, config.credentialStoreKey, automationsLogger),\n outbox: new DrizzleOutboxAppender(tx, automationsLogger),\n }));\n // Emails triggered by row-shaped events resolve their recipient and content\n // details at send time — the event carries ids only.\n const mailerLookups: MailerLookups = {\n orgUserPerson: async (orgId, orgUserId) => {\n const orgUser = await organizationsRepo.findOrgUserById(orgId, orgUserId);\n const user = orgUser ? await identity.getUserById(orgUser.userId) : null;\n return user ? { userId: user.id, email: user.email } : null;\n },\n contentInfo: async (orgId, contentId) => {\n const system = organizations.systemActor(orgId);\n const course = await content.findCourse(contentId, system);\n if (course) {\n return { id: course.id, title: course.title, type: course.type };\n }\n const download = await content.findDownload(contentId, system);\n if (download) {\n return { id: download.id, title: download.title, type: download.type };\n }\n const community = await content.findCommunity(contentId, system);\n if (community) {\n return { id: community.id, title: community.title, type: community.type };\n }\n const event = await content.findEvent(contentId, system);\n return event ? { id: event.id, title: event.title, type: event.type } : null;\n },\n };\n\n const runsRepo = new DrizzleAutomationRunsRepository(db, automationsLogger);\n const wired = communityMail({\n content,\n entitlements,\n organizations,\n identity,\n posts,\n subscriptions,\n discussion,\n chats,\n bodies,\n });\n const recipientReads = notificationReads({\n content,\n entitlements,\n organizations,\n identity,\n posts,\n subscriptions,\n discussion,\n chats,\n bodies,\n settings: notificationSettings,\n });\n const notificationProcessor = new NotificationProcessor({\n rules: RECIPIENT_RULES,\n reads: recipientReads,\n types: notificationRegistries.types,\n channels: notificationRegistries.channels,\n categories: notificationCategoryLists,\n settings: notificationSettings,\n devices,\n repo: notificationsRepo,\n engine: automationEngine,\n debounce: new SchedulerChatDebounce(automationEngine),\n logger: automationsLogger,\n });\n const automations = new AutomationsServiceImpl({\n repo: new DrizzleAutomationsRepository(db, automationsLogger),\n runsRepo,\n uow: automationsUow,\n engine: automationEngine,\n mailer,\n lookups: mailerLookups,\n integrations,\n systemActor: (orgId) => organizations.systemActor(orgId),\n credentials: credentialStore,\n webhooks:\n options?.adapters?.webhooks ??\n new FetchWebhookSender({ allowPrivateNetwork: config.webhookAllowPrivateNetwork }),\n mail: wired.mail,\n grants: wired.grants,\n delivery: new NotificationDelivery({\n repo: notificationsRepo,\n channels: notificationRegistries.channels,\n devices,\n logger: automationsLogger,\n }),\n processor: notificationProcessor,\n logger: automationsLogger,\n });\n automationEngine.register(automations);\n automationEngine.registerJobs(\n [\n ...discussionJobs({ posts, digests, logger: discussionLogger }),\n ...contentJobs({ events: content, logger: contentLogger }),\n ...entitlementsJobs({ grants: entitlements }),\n ...assetsJobs({ assets, orgs: organizations, logger: assetsLogger }),\n ...notificationJobs({\n processor: notificationProcessor,\n reads: recipientReads,\n logger: automationsLogger,\n }),\n ],\n [...discussionRecurringJobs, ...entitlementsRecurringJobs, ...assetsRecurringJobs],\n );\n\n const eventBus = new InMemoryEventBus();\n if (pubsub) {\n new ChatLiveForwarder({ pubsub, logger: discussionLogger }).attach(eventBus);\n }\n eventBus.subscribeAll((event) => automations.handle(event));\n const outboxConfig = resolveOutboxConfig(config.outbox);\n const outboxRelay = new PollingOutboxRelay(\n new DrizzleOutboxStore(db, outboxLogger),\n eventBus,\n outboxConfig,\n outboxLogger,\n );\n\n /*\n * Auth Adapter\n */\n const authLogger = logger.child({ name: 'auth' });\n // Hook failures otherwise surface through Better Auth's route as an opaque\n // 500 — log which hook died before rethrowing.\n const authHook =\n <A extends unknown[], R>(name: string, fn: (...args: A) => Promise<R>) =>\n async (...args: A): Promise<R> => {\n try {\n return await fn(...args);\n } catch (err) {\n authLogger.error(`auth hook ${name} failed`, { err });\n throw err;\n }\n };\n const mcp = mcpEndpoints(config);\n if (!mcp.oauthEnabled) {\n authLogger.warn(\n 'MCP OAuth is off: BETTER_AUTH_URL must be https or a loopback host; /mcp accepts API keys only',\n { authBaseURL: config.authBaseURL },\n );\n }\n const auth: BetterAuth = new BetterAuth({\n db,\n baseUrl: config.authBaseURL,\n mcp: {\n oauthEnabled: mcp.oauthEnabled,\n resource: mcp.resource,\n loginPage: mcp.loginPage,\n consentPage: mcp.consentPage,\n orgPickerPage: mcp.orgPickerPage,\n },\n secret: config.authSecret,\n trustedOrigins: config.trustedOrigins,\n hooks: {\n sendResetPassword: async ({ user, url }) => {\n await identity.sendPasswordReset({ email: user.email, url });\n },\n onPasswordReset: async ({ user }) => {\n await identity.sendPasswordChanged({ email: user.email });\n },\n sendMagicLink: authHook('sendMagicLink', async ({ email, url }) => {\n await identity.sendMagicLink({\n email,\n url,\n });\n }),\n beforeUserCreate: authHook('beforeUserCreate', async (user) => {\n const { email, name } = user;\n const [firstName, lastName] = name.split(' ');\n // Better Auth has not minted an id yet — the id returned below becomes\n // its account id. A person provisioned at invite time is linked instead of re-created.\n const domainUser = await identity.linkOrCreateUser({\n email,\n ...(firstName !== undefined && { firstName }),\n ...(lastName !== undefined && { lastName }),\n });\n\n return {\n data: {\n ...user,\n id: domainUser.id,\n },\n };\n }),\n beforeCreateSession: authHook('beforeCreateSession', async (session) => {\n const person = await identity.getUserByExternalId(session.userId);\n if (!person) {\n return;\n }\n if (organizations.isPlatformAccount(person.email)) {\n authLogger.warn('sign-in refused for the platform account', { userId: person.id });\n return false;\n }\n const orgs = await organizations.getOrgUsersForUser(person.id);\n if (orgs.length !== 1) {\n return;\n }\n // TODO - fix, why default to the first?\n const org = await organizations.getById(orgs[0]!.orgId);\n if (!org) {\n return;\n }\n\n return { data: { ...session, activeOrganizationId: org.id } };\n }),\n /*\n * We're making sure the domain org creates and using the same ID for better-auth org ID.\n */\n beforeCreateOrganization: authHook(\n 'beforeCreateOrganization',\n async ({ organization: org, user: baUser }) => {\n const domainOrg = await organizations.provisionOrganization({\n ownerId: baUser.id,\n ...org,\n logo: org.logo ?? undefined,\n });\n return { data: { ...org, id: domainOrg.id } };\n },\n ),\n beforeDeleteOrganization: authHook('beforeDeleteOrganization', async ({ organization }) => {\n await organizations.deleteOrganization(organization.id);\n }),\n beforeAddMember: authHook('beforeAddMember', async ({ member }) => {\n await organizations.addOrgUser({\n orgId: member.organizationId,\n userId: member.userId,\n });\n }),\n beforeRemoveMember: authHook('beforeRemoveMember', async ({ member }) => {\n await organizations.removeOrgUser(member.organizationId, member.userId);\n }),\n },\n logger: authLogger,\n cookieDomain: config.cookieDomain,\n secureCookies: config.secureCookies,\n });\n orgAdminRef.current = auth;\n\n return {\n auth,\n mcp,\n platform,\n identity,\n organizations,\n // Better Auth owns org writes; the same instance fulfils the OrgAdmin port.\n orgProvider: auth,\n content,\n entitlements,\n progress,\n discussion,\n posts,\n chats,\n subscriptions,\n assets,\n integrations,\n automations,\n notificationTypes: notificationRegistries.types,\n notificationChannels: notificationRegistries.channels,\n notificationCategories,\n notificationCatalog,\n notificationCategoryLists,\n notificationSettings,\n devices,\n notificationList,\n settings,\n reporting,\n storage,\n mailer,\n credentials: credentialStore,\n outboxRelay,\n automationEngine,\n scheduler: automationEngine,\n pubsub,\n chatHub: chats.hub,\n logger,\n loggerInstance,\n requestContext: requestLogContext,\n // Better Auth owns the session store; the same instance fulfils SessionAdmin.\n sessions: auth,\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,47 @@
1
+ import { Mailer } from "@headless-lms/core/shared/mailer";
2
+ import { PushConfig } from "@headless-lms/core/types";
3
+ import { ChannelRegistry, EmailTemplateId, JsonValue, NotificationCategory, NotificationChannel, NotificationTypeDeclaration, NotificationTypeRegistry, PushDevice, PushSender, StoredNotification } from "@headless-lms/core/shared/ports";
4
+ //#region src/app/notification-channels.d.ts
5
+ declare const IN_APP = "inApp";
6
+ declare const EMAIL = "email";
7
+ declare function buildNotificationTypeRegistry(declarations: NotificationTypeDeclaration[], defaults: {
8
+ community: NotificationCategory[];
9
+ org: NotificationCategory[];
10
+ }, configured: string[]): NotificationTypeRegistry;
11
+ declare function buildChannelRegistry(channels: NotificationChannel[]): ChannelRegistry;
12
+ declare function emailTemplateFor(declaration: NotificationTypeDeclaration, notification: Pick<StoredNotification, 'scope' | 'matchedReason'>): EmailTemplateId | null;
13
+ declare function emailParams(notification: StoredNotification): Record<string, JsonValue>;
14
+ declare function emailChannel(deps: {
15
+ mailer: Pick<Mailer, 'send'>;
16
+ types: NotificationTypeRegistry;
17
+ emailOf: (orgId: string, userId: string) => Promise<string | null>;
18
+ }): NotificationChannel;
19
+ declare function renderPushText(template: string, display: Record<string, JsonValue>): string;
20
+ declare function configuredPushSender(config: PushConfig, sender: PushSender | undefined): PushSender | null;
21
+ type ActiveDevices = (orgId: string, userId: string, since: Date) => Promise<PushDevice[]>;
22
+ declare function pushChannel(deps: {
23
+ sender: PushSender;
24
+ types: NotificationTypeRegistry;
25
+ activeDevices: ActiveDevices;
26
+ now?: () => Date;
27
+ }): NotificationChannel;
28
+ declare function buildNotificationRegistries(deps: {
29
+ declarations: NotificationTypeDeclaration[];
30
+ defaults: {
31
+ community: NotificationCategory[];
32
+ org: NotificationCategory[];
33
+ };
34
+ mailer: Pick<Mailer, 'send'> | null;
35
+ emailOf: (orgId: string, userId: string) => Promise<string | null>;
36
+ push: {
37
+ sender: PushSender;
38
+ activeDevices: ActiveDevices;
39
+ } | null;
40
+ channels: NotificationChannel[];
41
+ }): {
42
+ types: NotificationTypeRegistry;
43
+ channels: ChannelRegistry;
44
+ };
45
+ //#endregion
46
+ export { ActiveDevices, EMAIL, IN_APP, buildChannelRegistry, buildNotificationRegistries, buildNotificationTypeRegistry, configuredPushSender, emailChannel, emailParams, emailTemplateFor, pushChannel, renderPushText };
47
+ //# sourceMappingURL=notification-channels.d.ts.map
@@ -0,0 +1,175 @@
1
+ import { DEVICE_WINDOW_MS, PUSH } from "@headless-lms/core/automations";
2
+ import { offeringConfigured } from "@headless-lms/core/shared/notification-categories";
3
+ import { isEmailRejected, isEmailRenderError } from "@headless-lms/core/shared/mailer";
4
+ //#region src/app/notification-channels.ts
5
+ const IN_APP = "inApp";
6
+ const EMAIL = "email";
7
+ const CONTENT_SUBJECTS = /* @__PURE__ */ new Set([
8
+ "course",
9
+ "download",
10
+ "community",
11
+ "event",
12
+ "bundle"
13
+ ]);
14
+ function buildNotificationTypeRegistry(declarations, defaults, configured) {
15
+ const byType = /* @__PURE__ */ new Map();
16
+ for (const declaration of declarations) {
17
+ if (byType.has(declaration.type)) throw new Error(`notification type ${declaration.type} is declared twice`);
18
+ byType.set(declaration.type, declaration);
19
+ }
20
+ const lists = {
21
+ community: offeringConfigured(defaults.community, configured),
22
+ org: offeringConfigured(defaults.org, configured)
23
+ };
24
+ return {
25
+ list: () => declarations.map(({ type, scopes, reasons }) => ({
26
+ type,
27
+ scopes: [...scopes],
28
+ reasons: [...reasons]
29
+ })),
30
+ get: (type) => byType.get(type),
31
+ defaults: (scope) => structuredClone(lists[scope])
32
+ };
33
+ }
34
+ function buildChannelRegistry(channels) {
35
+ const byKey = /* @__PURE__ */ new Map();
36
+ for (const channel of channels) {
37
+ if (channel.key === "inApp" || byKey.has(channel.key)) throw new Error(`notification channel ${channel.key} is registered twice`);
38
+ byKey.set(channel.key, channel);
39
+ }
40
+ const keys = [IN_APP, ...byKey.keys()];
41
+ return {
42
+ list: () => [...keys],
43
+ get: (key) => byKey.get(key)
44
+ };
45
+ }
46
+ function emailTemplateFor(declaration, notification) {
47
+ const email = declaration.email?.[notification.scope];
48
+ if (!email) return null;
49
+ return email.byReason?.[notification.matchedReason] ?? email.template;
50
+ }
51
+ function emailParams(notification) {
52
+ const ids = {};
53
+ for (const ref of [notification.parent, notification.subject]) if (ref) ids[`${ref.type}Id`] = ref.id;
54
+ if (notification.subject && CONTENT_SUBJECTS.has(notification.subject.type)) ids.contentId = notification.subject.id;
55
+ if (notification.communityId) ids.communityId = notification.communityId;
56
+ if (notification.spaceId) ids.spaceId = notification.spaceId;
57
+ return {
58
+ ...ids,
59
+ ...notification.display
60
+ };
61
+ }
62
+ function emailChannel(deps) {
63
+ return {
64
+ key: EMAIL,
65
+ recipient: async (orgId, userId) => {
66
+ const email = await deps.emailOf(orgId, userId);
67
+ return email ? { email } : null;
68
+ },
69
+ deliver: async (notification, to) => {
70
+ const declaration = deps.types.get(notification.type);
71
+ const template = declaration ? emailTemplateFor(declaration, notification) : null;
72
+ if (!to.email) return {
73
+ accepted: false,
74
+ deadDeviceIds: [],
75
+ error: "no address"
76
+ };
77
+ if (!template) return {
78
+ accepted: false,
79
+ deadDeviceIds: [],
80
+ error: `no email template for ${notification.type} in ${notification.scope}`
81
+ };
82
+ try {
83
+ await deps.mailer.send(to.email, template, emailParams(notification));
84
+ } catch (err) {
85
+ if (isEmailRejected(err) || isEmailRenderError(err)) return {
86
+ accepted: false,
87
+ deadDeviceIds: [],
88
+ error: err.message
89
+ };
90
+ throw err;
91
+ }
92
+ return {
93
+ accepted: true,
94
+ deadDeviceIds: []
95
+ };
96
+ }
97
+ };
98
+ }
99
+ function renderPushText(template, display) {
100
+ return template.replace(/\{\{(\w+)\}\}/g, (_placeholder, key) => {
101
+ const value = display[key];
102
+ if (typeof value === "string") return value;
103
+ if (Array.isArray(value) && value.every((item) => typeof item === "string")) return value.join(", ");
104
+ return "";
105
+ });
106
+ }
107
+ function configuredPushSender(config, sender) {
108
+ return Object.values(config).some((provider) => provider !== void 0) && sender ? sender : null;
109
+ }
110
+ function pushChannel(deps) {
111
+ const now = deps.now ?? (() => /* @__PURE__ */ new Date());
112
+ return {
113
+ key: PUSH,
114
+ recipient: async (orgId, userId) => {
115
+ const since = new Date(now().getTime() - DEVICE_WINDOW_MS);
116
+ const devices = await deps.activeDevices(orgId, userId, since);
117
+ return devices.length > 0 ? { devices } : null;
118
+ },
119
+ deliver: async (notification, to) => {
120
+ const text = deps.types.get(notification.type)?.push?.[notification.scope];
121
+ if (!to.devices) return {
122
+ accepted: false,
123
+ deadDeviceIds: [],
124
+ error: "no address"
125
+ };
126
+ if (!text) return {
127
+ accepted: false,
128
+ deadDeviceIds: [],
129
+ error: `no push text for ${notification.type} in ${notification.scope}`
130
+ };
131
+ const courseId = notification.display.courseId;
132
+ return deps.sender.send(to.devices, {
133
+ title: renderPushText(text.title, notification.display),
134
+ body: renderPushText(text.body, notification.display),
135
+ data: {
136
+ notificationId: notification.id,
137
+ type: notification.type,
138
+ subject: notification.subject,
139
+ parent: notification.parent,
140
+ communityId: notification.communityId,
141
+ spaceId: notification.spaceId,
142
+ courseId: typeof courseId === "string" ? courseId : null
143
+ }
144
+ });
145
+ }
146
+ };
147
+ }
148
+ function buildNotificationRegistries(deps) {
149
+ const configured = [
150
+ IN_APP,
151
+ ...deps.mailer ? [EMAIL] : [],
152
+ ...deps.push ? [PUSH] : [],
153
+ ...deps.channels.map((channel) => channel.key)
154
+ ];
155
+ const types = buildNotificationTypeRegistry(deps.declarations, deps.defaults, configured);
156
+ return {
157
+ types,
158
+ channels: buildChannelRegistry([
159
+ ...deps.mailer ? [emailChannel({
160
+ mailer: deps.mailer,
161
+ types,
162
+ emailOf: deps.emailOf
163
+ })] : [],
164
+ ...deps.push ? [pushChannel({
165
+ ...deps.push,
166
+ types
167
+ })] : [],
168
+ ...deps.channels
169
+ ])
170
+ };
171
+ }
172
+ //#endregion
173
+ export { EMAIL, IN_APP, buildChannelRegistry, buildNotificationRegistries, buildNotificationTypeRegistry, configuredPushSender, emailChannel, emailParams, emailTemplateFor, pushChannel, renderPushText };
174
+
175
+ //# sourceMappingURL=notification-channels.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notification-channels.js","names":[],"sources":["../../src/app/notification-channels.ts"],"sourcesContent":["import type {\n ChannelRegistry,\n EmailTemplateId,\n EmailTemplateParams,\n JsonValue,\n NotificationCategory,\n NotificationChannel,\n NotificationTypeDeclaration,\n NotificationTypeRegistry,\n PushDevice,\n PushSender,\n StoredNotification,\n} from '@headless-lms/core/shared/ports';\nimport { DEVICE_WINDOW_MS, PUSH } from '@headless-lms/core/automations';\nimport type { PushConfig } from '@headless-lms/core/types';\nimport { offeringConfigured } from '@headless-lms/core/shared/notification-categories';\nimport { isEmailRejected, isEmailRenderError, type Mailer } from '@headless-lms/core/shared/mailer';\n\nexport const IN_APP = 'inApp';\nexport const EMAIL = 'email';\n\nconst CONTENT_SUBJECTS = new Set(['course', 'download', 'community', 'event', 'bundle']);\n\nexport function buildNotificationTypeRegistry(\n declarations: NotificationTypeDeclaration[],\n defaults: { community: NotificationCategory[]; org: NotificationCategory[] },\n configured: string[],\n): NotificationTypeRegistry {\n const byType = new Map<string, NotificationTypeDeclaration>();\n for (const declaration of declarations) {\n if (byType.has(declaration.type)) {\n throw new Error(`notification type ${declaration.type} is declared twice`);\n }\n byType.set(declaration.type, declaration);\n }\n const lists = {\n community: offeringConfigured(defaults.community, configured),\n org: offeringConfigured(defaults.org, configured),\n };\n return {\n list: () =>\n declarations.map(({ type, scopes, reasons }) => ({\n type,\n scopes: [...scopes],\n reasons: [...reasons],\n })),\n get: (type) => byType.get(type),\n defaults: (scope) => structuredClone(lists[scope]),\n };\n}\n\nexport function buildChannelRegistry(channels: NotificationChannel[]): ChannelRegistry {\n const byKey = new Map<string, NotificationChannel>();\n for (const channel of channels) {\n if (channel.key === IN_APP || byKey.has(channel.key)) {\n throw new Error(`notification channel ${channel.key} is registered twice`);\n }\n byKey.set(channel.key, channel);\n }\n const keys = [IN_APP, ...byKey.keys()];\n return {\n list: () => [...keys],\n get: (key) => byKey.get(key),\n };\n}\n\nexport function emailTemplateFor(\n declaration: NotificationTypeDeclaration,\n notification: Pick<StoredNotification, 'scope' | 'matchedReason'>,\n): EmailTemplateId | null {\n const email = declaration.email?.[notification.scope];\n if (!email) {\n return null;\n }\n return email.byReason?.[notification.matchedReason] ?? email.template;\n}\n\nexport function emailParams(notification: StoredNotification): Record<string, JsonValue> {\n const ids: Record<string, JsonValue> = {};\n for (const ref of [notification.parent, notification.subject]) {\n if (ref) {\n ids[`${ref.type}Id`] = ref.id;\n }\n }\n if (notification.subject && CONTENT_SUBJECTS.has(notification.subject.type)) {\n ids.contentId = notification.subject.id;\n }\n if (notification.communityId) {\n ids.communityId = notification.communityId;\n }\n if (notification.spaceId) {\n ids.spaceId = notification.spaceId;\n }\n return { ...ids, ...notification.display };\n}\n\nexport function emailChannel(deps: {\n mailer: Pick<Mailer, 'send'>;\n types: NotificationTypeRegistry;\n emailOf: (orgId: string, userId: string) => Promise<string | null>;\n}): NotificationChannel {\n return {\n key: EMAIL,\n recipient: async (orgId, userId) => {\n const email = await deps.emailOf(orgId, userId);\n return email ? { email } : null;\n },\n deliver: async (notification, to) => {\n const declaration = deps.types.get(notification.type);\n const template = declaration ? emailTemplateFor(declaration, notification) : null;\n if (!to.email) {\n return { accepted: false, deadDeviceIds: [], error: 'no address' };\n }\n if (!template) {\n return {\n accepted: false,\n deadDeviceIds: [],\n error: `no email template for ${notification.type} in ${notification.scope}`,\n };\n }\n try {\n await deps.mailer.send(\n to.email,\n template,\n emailParams(notification) as unknown as EmailTemplateParams[typeof template],\n );\n } catch (err) {\n if (isEmailRejected(err) || isEmailRenderError(err)) {\n return { accepted: false, deadDeviceIds: [], error: err.message };\n }\n throw err;\n }\n return { accepted: true, deadDeviceIds: [] };\n },\n };\n}\n\nexport function renderPushText(template: string, display: Record<string, JsonValue>): string {\n return template.replace(/\\{\\{(\\w+)\\}\\}/g, (_placeholder, key: string) => {\n const value = display[key];\n if (typeof value === 'string') {\n return value;\n }\n if (Array.isArray(value) && value.every((item) => typeof item === 'string')) {\n return value.join(', ');\n }\n return '';\n });\n}\n\nexport function configuredPushSender(\n config: PushConfig,\n sender: PushSender | undefined,\n): PushSender | null {\n const configured = Object.values(config).some((provider) => provider !== undefined);\n return configured && sender ? sender : null;\n}\n\nexport type ActiveDevices = (orgId: string, userId: string, since: Date) => Promise<PushDevice[]>;\n\nexport function pushChannel(deps: {\n sender: PushSender;\n types: NotificationTypeRegistry;\n activeDevices: ActiveDevices;\n now?: () => Date;\n}): NotificationChannel {\n const now = deps.now ?? (() => new Date());\n return {\n key: PUSH,\n recipient: async (orgId, userId) => {\n const since = new Date(now().getTime() - DEVICE_WINDOW_MS);\n const devices = await deps.activeDevices(orgId, userId, since);\n return devices.length > 0 ? { devices } : null;\n },\n deliver: async (notification, to) => {\n const text = deps.types.get(notification.type)?.push?.[notification.scope];\n if (!to.devices) {\n return { accepted: false, deadDeviceIds: [], error: 'no address' };\n }\n if (!text) {\n return {\n accepted: false,\n deadDeviceIds: [],\n error: `no push text for ${notification.type} in ${notification.scope}`,\n };\n }\n const courseId = notification.display.courseId;\n return deps.sender.send(to.devices, {\n title: renderPushText(text.title, notification.display),\n body: renderPushText(text.body, notification.display),\n data: {\n notificationId: notification.id,\n type: notification.type,\n subject: notification.subject,\n parent: notification.parent,\n communityId: notification.communityId,\n spaceId: notification.spaceId,\n courseId: typeof courseId === 'string' ? courseId : null,\n },\n });\n },\n };\n}\n\nexport function buildNotificationRegistries(deps: {\n declarations: NotificationTypeDeclaration[];\n defaults: { community: NotificationCategory[]; org: NotificationCategory[] };\n mailer: Pick<Mailer, 'send'> | null;\n emailOf: (orgId: string, userId: string) => Promise<string | null>;\n push: { sender: PushSender; activeDevices: ActiveDevices } | null;\n channels: NotificationChannel[];\n}): { types: NotificationTypeRegistry; channels: ChannelRegistry } {\n const configured = [\n IN_APP,\n ...(deps.mailer ? [EMAIL] : []),\n ...(deps.push ? [PUSH] : []),\n ...deps.channels.map((channel) => channel.key),\n ];\n const types = buildNotificationTypeRegistry(deps.declarations, deps.defaults, configured);\n const channels = buildChannelRegistry([\n ...(deps.mailer ? [emailChannel({ mailer: deps.mailer, types, emailOf: deps.emailOf })] : []),\n ...(deps.push ? [pushChannel({ ...deps.push, types })] : []),\n ...deps.channels,\n ]);\n return { types, channels };\n}\n"],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ import { CommunityMailDeps } from "./community-mail.js";
2
+ import { ContentService } from "@headless-lms/core/content";
3
+ import { EntitlementsService } from "@headless-lms/core/entitlements";
4
+ import { ChatService, DiscussionService, PostService, SubscriptionService } from "@headless-lms/core/discussion";
5
+ import { IdentityService, NotificationSettingsService } from "@headless-lms/core/identity";
6
+ import { OrganizationService } from "@headless-lms/core/organizations";
7
+ import { NotificationReads } from "@headless-lms/core/automations";
8
+ //#region src/app/notification-reads.d.ts
9
+ type NotificationReadsDeps = CommunityMailDeps & {
10
+ content: Pick<ContentService, 'getActivity' | 'getModule' | 'findCourse' | 'findEvent' | 'listBundleItems'>;
11
+ entitlements: Pick<EntitlementsService, 'communityMemberIds' | 'grantedOrgUserIds' | 'hasAccess'>;
12
+ organizations: Pick<OrganizationService, 'getOrgUsersByIds' | 'systemActor'>;
13
+ identity: Pick<IdentityService, 'getUserById'>;
14
+ posts: Pick<PostService, 'publishedBetween'>;
15
+ subscriptions: Pick<SubscriptionService, 'mutedAmong'>;
16
+ discussion: Pick<DiscussionService, 'commenterIds'>;
17
+ chats: Pick<ChatService, 'unreadInChat' | 'findMessage'>;
18
+ settings: Pick<NotificationSettingsService, 'switchedOn'>;
19
+ };
20
+ declare function notificationReads(deps: NotificationReadsDeps): NotificationReads;
21
+ //#endregion
22
+ export { NotificationReadsDeps, notificationReads };
23
+ //# sourceMappingURL=notification-reads.d.ts.map
@@ -0,0 +1,74 @@
1
+ import { communityMail } from "./community-mail.js";
2
+ import { activityTitle } from "@headless-lms/core/content";
3
+ //#region src/app/notification-reads.ts
4
+ function notificationReads(deps) {
5
+ const { mail } = communityMail(deps);
6
+ const nameOf = (orgId, orgUserId) => mail.displayName(orgId, orgUserId);
7
+ const system = (orgId) => deps.organizations.systemActor(orgId);
8
+ return {
9
+ ...mail,
10
+ async threadParticipants(orgId, item) {
11
+ if (item.type === "post") return deps.discussion.commenterIds(orgId, { postId: item.id });
12
+ if (item.type === "activity") return deps.discussion.commenterIds(orgId, { activityId: item.id });
13
+ return [];
14
+ },
15
+ mutedAmong: (orgId, item, orgUserIds, at) => deps.subscriptions.mutedAmong(orgId, item, orgUserIds, at),
16
+ communityMembers: (orgId, communityId) => deps.entitlements.communityMemberIds(orgId, communityId),
17
+ switchedOn: (orgId, communityId, category, spaceId) => deps.settings.switchedOn(orgId, communityId, category, spaceId),
18
+ courseMembers: (orgId, courseId) => deps.entitlements.grantedOrgUserIds(orgId, courseId),
19
+ event: (orgId, eventId) => deps.content.findEvent(eventId, system(orgId)),
20
+ eventAttendees: (orgId, eventId) => deps.entitlements.grantedOrgUserIds(orgId, eventId),
21
+ async contentLostBy(orgId, grant) {
22
+ const opened = grant.bundleId ? (await deps.content.listBundleItems(grant.bundleId, system(orgId))).map((item) => item.contentId) : grant.contentId ? [grant.contentId] : [];
23
+ const held = await Promise.all(opened.map((contentId) => deps.entitlements.hasAccess(orgId, grant.orgUserId, contentId)));
24
+ return opened.filter((_, index) => !held[index]);
25
+ },
26
+ async activity(orgId, activityId) {
27
+ const activity = await deps.content.getActivity(activityId, system(orgId));
28
+ if (!activity) return null;
29
+ const [module, course] = await Promise.all([deps.content.getModule(activity.moduleId, system(orgId)), deps.content.findCourse(activity.courseId, system(orgId))]);
30
+ if (!course) return null;
31
+ return {
32
+ title: activityTitle(activity),
33
+ moduleTitle: module?.title ?? "",
34
+ courseId: course.id,
35
+ courseTitle: course.title
36
+ };
37
+ },
38
+ async userIdsOf(orgId, orgUserIds) {
39
+ const orgUsers = await deps.organizations.getOrgUsersByIds(orgId, orgUserIds);
40
+ return Object.fromEntries(orgUsers.map((orgUser) => [orgUser.id, orgUser.userId]));
41
+ },
42
+ async emailOf(_orgId, userId) {
43
+ return (await deps.identity.getUserById(userId))?.email ?? null;
44
+ },
45
+ async message(orgId, messageId) {
46
+ const message = await deps.chats.findMessage(orgId, messageId);
47
+ return message && message.deletedAt === null ? {
48
+ orgUserId: message.orgUserId,
49
+ chatId: message.chatId,
50
+ body: message.body
51
+ } : null;
52
+ },
53
+ async postsPublished(orgId, communityId, from, to) {
54
+ const posts = await deps.posts.publishedBetween(orgId, communityId, from, to);
55
+ const spaceNames = /* @__PURE__ */ new Map();
56
+ for (const spaceId of new Set(posts.map((post) => post.spaceId))) spaceNames.set(spaceId, (await mail.space(orgId, spaceId))?.name ?? "");
57
+ const authorNames = /* @__PURE__ */ new Map();
58
+ for (const orgUserId of new Set(posts.map((post) => post.orgUserId))) authorNames.set(orgUserId, await nameOf(orgId, orgUserId));
59
+ return posts.map((post) => ({
60
+ postId: post.postId,
61
+ spaceId: post.spaceId,
62
+ title: post.title,
63
+ authorName: authorNames.get(post.orgUserId) ?? "",
64
+ spaceName: spaceNames.get(post.spaceId) ?? "",
65
+ commentCount: post.commentCount
66
+ }));
67
+ },
68
+ unreadInChat: (orgId, chatId, orgUserId) => deps.chats.unreadInChat(orgId, chatId, orgUserId)
69
+ };
70
+ }
71
+ //#endregion
72
+ export { notificationReads };
73
+
74
+ //# sourceMappingURL=notification-reads.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notification-reads.js","names":[],"sources":["../../src/app/notification-reads.ts"],"sourcesContent":["import type { NotificationReads } from '@headless-lms/core/automations';\nimport { activityTitle, 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, NotificationSettingsService } from '@headless-lms/core/identity';\nimport type { OrganizationService } from '@headless-lms/core/organizations';\nimport { communityMail, type CommunityMailDeps } from './community-mail.js';\n\nexport type NotificationReadsDeps = CommunityMailDeps & {\n content: Pick<\n ContentService,\n 'getActivity' | 'getModule' | 'findCourse' | 'findEvent' | 'listBundleItems'\n >;\n entitlements: Pick<EntitlementsService, 'communityMemberIds' | 'grantedOrgUserIds' | 'hasAccess'>;\n organizations: Pick<OrganizationService, 'getOrgUsersByIds' | 'systemActor'>;\n identity: Pick<IdentityService, 'getUserById'>;\n posts: Pick<PostService, 'publishedBetween'>;\n subscriptions: Pick<SubscriptionService, 'mutedAmong'>;\n discussion: Pick<DiscussionService, 'commenterIds'>;\n chats: Pick<ChatService, 'unreadInChat' | 'findMessage'>;\n settings: Pick<NotificationSettingsService, 'switchedOn'>;\n};\n\nexport function notificationReads(deps: NotificationReadsDeps): NotificationReads {\n const { mail } = communityMail(deps);\n const nameOf = (orgId: string, orgUserId: string) => mail.displayName(orgId, orgUserId);\n const system = (orgId: string) => deps.organizations.systemActor(orgId);\n return {\n ...mail,\n async threadParticipants(orgId, item) {\n if (item.type === 'post') {\n return deps.discussion.commenterIds(orgId, { postId: item.id });\n }\n if (item.type === 'activity') {\n return deps.discussion.commenterIds(orgId, { activityId: item.id });\n }\n return [];\n },\n mutedAmong: (orgId, item, orgUserIds, at) =>\n deps.subscriptions.mutedAmong(orgId, item, orgUserIds, at),\n communityMembers: (orgId, communityId) =>\n deps.entitlements.communityMemberIds(orgId, communityId),\n switchedOn: (orgId, communityId, category, spaceId) =>\n deps.settings.switchedOn(orgId, communityId, category, spaceId),\n courseMembers: (orgId, courseId) => deps.entitlements.grantedOrgUserIds(orgId, courseId),\n event: (orgId, eventId) => deps.content.findEvent(eventId, system(orgId)),\n eventAttendees: (orgId, eventId) => deps.entitlements.grantedOrgUserIds(orgId, eventId),\n async contentLostBy(orgId, grant) {\n const opened = grant.bundleId\n ? (await deps.content.listBundleItems(grant.bundleId, system(orgId))).map(\n (item) => item.contentId,\n )\n : grant.contentId\n ? [grant.contentId]\n : [];\n const held = await Promise.all(\n opened.map((contentId) => deps.entitlements.hasAccess(orgId, grant.orgUserId, contentId)),\n );\n return opened.filter((_, index) => !held[index]);\n },\n async activity(orgId, activityId) {\n const activity = await deps.content.getActivity(activityId, system(orgId));\n if (!activity) {\n return null;\n }\n const [module, course] = await Promise.all([\n deps.content.getModule(activity.moduleId, system(orgId)),\n deps.content.findCourse(activity.courseId, system(orgId)),\n ]);\n if (!course) {\n return null;\n }\n return {\n title: activityTitle(activity),\n moduleTitle: module?.title ?? '',\n courseId: course.id,\n courseTitle: course.title,\n };\n },\n async userIdsOf(orgId, orgUserIds) {\n const orgUsers = await deps.organizations.getOrgUsersByIds(orgId, orgUserIds);\n return Object.fromEntries(orgUsers.map((orgUser) => [orgUser.id, orgUser.userId]));\n },\n async emailOf(_orgId, userId) {\n return (await deps.identity.getUserById(userId))?.email ?? null;\n },\n async message(orgId, messageId) {\n const message = await deps.chats.findMessage(orgId, messageId);\n return message && message.deletedAt === null\n ? { orgUserId: message.orgUserId, chatId: message.chatId, body: message.body }\n : null;\n },\n async postsPublished(orgId, communityId, from, to) {\n const posts = await deps.posts.publishedBetween(orgId, communityId, from, to);\n const spaceNames = new Map<string, string>();\n for (const spaceId of new Set(posts.map((post) => post.spaceId))) {\n spaceNames.set(spaceId, (await mail.space(orgId, spaceId))?.name ?? '');\n }\n const authorNames = new Map<string, string>();\n for (const orgUserId of new Set(posts.map((post) => post.orgUserId))) {\n authorNames.set(orgUserId, await nameOf(orgId, orgUserId));\n }\n return posts.map((post) => ({\n postId: post.postId,\n spaceId: post.spaceId,\n title: post.title,\n authorName: authorNames.get(post.orgUserId) ?? '',\n spaceName: spaceNames.get(post.spaceId) ?? '',\n commentCount: post.commentCount,\n }));\n },\n unreadInChat: (orgId, chatId, orgUserId) => deps.chats.unreadInChat(orgId, chatId, orgUserId),\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ //#region src/app/platform.d.ts
2
+ declare const PLATFORM_KEY_MIN_LENGTH = 32;
3
+ interface PlatformConfig {
4
+ key: string;
5
+ accountEmail: string;
6
+ accountName?: string;
7
+ }
8
+ interface PlatformKeyCheck {
9
+ matches(candidate: string): boolean;
10
+ }
11
+ declare function platformKeyCheck(config: PlatformConfig): PlatformKeyCheck;
12
+ //#endregion
13
+ export { PLATFORM_KEY_MIN_LENGTH, PlatformConfig, PlatformKeyCheck, platformKeyCheck };
14
+ //# sourceMappingURL=platform.d.ts.map
@@ -0,0 +1,14 @@
1
+ import { createHash, timingSafeEqual } from "node:crypto";
2
+ //#region src/app/platform.ts
3
+ const PLATFORM_KEY_MIN_LENGTH = 32;
4
+ const digest = (value) => createHash("sha256").update(value).digest();
5
+ function platformKeyCheck(config) {
6
+ if (config.key.length < 32) throw new Error(`the platform key must be at least 32 characters`);
7
+ if (!config.accountEmail.includes("@")) throw new Error("the platform account needs an email address");
8
+ const expected = digest(config.key);
9
+ return { matches: (candidate) => timingSafeEqual(digest(candidate), expected) };
10
+ }
11
+ //#endregion
12
+ export { PLATFORM_KEY_MIN_LENGTH, platformKeyCheck };
13
+
14
+ //# sourceMappingURL=platform.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"platform.js","names":[],"sources":["../../src/app/platform.ts"],"sourcesContent":["import { createHash, timingSafeEqual } from 'node:crypto';\n\nexport const PLATFORM_KEY_MIN_LENGTH = 32;\n\nexport interface PlatformConfig {\n key: string;\n accountEmail: string;\n accountName?: string;\n}\n\nexport interface PlatformKeyCheck {\n matches(candidate: string): boolean;\n}\n\nconst digest = (value: string) => createHash('sha256').update(value).digest();\n\nexport function platformKeyCheck(config: PlatformConfig): PlatformKeyCheck {\n if (config.key.length < PLATFORM_KEY_MIN_LENGTH) {\n throw new Error(`the platform key must be at least ${PLATFORM_KEY_MIN_LENGTH} characters`);\n }\n if (!config.accountEmail.includes('@')) {\n throw new Error('the platform account needs an email address');\n }\n const expected = digest(config.key);\n return { matches: (candidate) => timingSafeEqual(digest(candidate), expected) };\n}\n"],"mappings":";;AAEA,MAAa,0BAA0B;AAYvC,MAAM,UAAU,UAAkB,WAAW,QAAQ,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,OAAO;AAE5E,SAAgB,iBAAiB,QAA0C;CACzE,IAAI,OAAO,IAAI,SAAA,IACb,MAAM,IAAI,MAAM,iDAAyE;CAE3F,IAAI,CAAC,OAAO,aAAa,SAAS,GAAG,GACnC,MAAM,IAAI,MAAM,6CAA6C;CAE/D,MAAM,WAAW,OAAO,OAAO,GAAG;CAClC,OAAO,EAAE,UAAU,cAAc,gBAAgB,OAAO,SAAS,GAAG,QAAQ,EAAE;AAChF"}
@@ -18,6 +18,10 @@ interface ServerConfig {
18
18
  * can't silently extend paywalled link lifetimes. */
19
19
  deliveryExpirySeconds: number;
20
20
  memberImageMaxBytes: number;
21
+ memberFileMaxBytes: number;
22
+ /** Postgres URL for the PubSub adapter's session connection. It must reach
23
+ * Postgres directly, not through a transaction-mode pooler. */
24
+ pubsubDatabaseUrl: string;
21
25
  /** Everything the composition container needs to wire adapters + services. */
22
26
  container: Config;
23
27
  }