@headless-lms/server 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (223) hide show
  1. package/README.md +14 -0
  2. package/dist/app/community-mail.d.ts +5 -3
  3. package/dist/app/community-mail.js +60 -12
  4. package/dist/app/community-mail.js.map +1 -1
  5. package/dist/app/container.d.ts +24 -3
  6. package/dist/app/container.js +102 -38
  7. package/dist/app/container.js.map +1 -1
  8. package/dist/app/notification-channels.js +1 -0
  9. package/dist/app/notification-channels.js.map +1 -1
  10. package/dist/app/notification-reads.d.ts +3 -3
  11. package/dist/app/notification-reads.js +7 -28
  12. package/dist/app/notification-reads.js.map +1 -1
  13. package/dist/app/platform.d.ts +14 -0
  14. package/dist/app/platform.js +14 -0
  15. package/dist/app/platform.js.map +1 -0
  16. package/dist/http/config.d.ts +4 -0
  17. package/dist/http/errors.js +9 -4
  18. package/dist/http/errors.js.map +1 -1
  19. package/dist/http/fastify.d.ts +8 -0
  20. package/dist/http/mcp/catalogue.d.ts +11 -9
  21. package/dist/http/mcp/catalogue.js +15 -21
  22. package/dist/http/mcp/catalogue.js.map +1 -1
  23. package/dist/http/mcp/challenge.js +3 -3
  24. package/dist/http/mcp/challenge.js.map +1 -1
  25. package/dist/http/mcp/route.js +7 -7
  26. package/dist/http/mcp/route.js.map +1 -1
  27. package/dist/http/mcp/test-support.d.ts +6 -5
  28. package/dist/http/mcp/test-support.js +32 -11
  29. package/dist/http/mcp/test-support.js.map +1 -1
  30. package/dist/http/mcp/tool.d.ts +3 -9
  31. package/dist/http/mcp/tool.js +14 -6
  32. package/dist/http/mcp/tool.js.map +1 -1
  33. package/dist/http/mcp/tools/automations.js +10 -20
  34. package/dist/http/mcp/tools/automations.js.map +1 -1
  35. package/dist/http/mcp/tools/catalog.js +25 -42
  36. package/dist/http/mcp/tools/catalog.js.map +1 -1
  37. package/dist/http/mcp/tools/comments.js +9 -19
  38. package/dist/http/mcp/tools/comments.js.map +1 -1
  39. package/dist/http/mcp/tools/communities.js +16 -31
  40. package/dist/http/mcp/tools/communities.js.map +1 -1
  41. package/dist/http/mcp/tools/community-members.js +9 -16
  42. package/dist/http/mcp/tools/community-members.js.map +1 -1
  43. package/dist/http/mcp/tools/community-moderation.js +13 -25
  44. package/dist/http/mcp/tools/community-moderation.js.map +1 -1
  45. package/dist/http/mcp/tools/courses.js +11 -21
  46. package/dist/http/mcp/tools/courses.js.map +1 -1
  47. package/dist/http/mcp/tools/events.d.ts +6 -0
  48. package/dist/http/mcp/tools/events.js +170 -0
  49. package/dist/http/mcp/tools/events.js.map +1 -0
  50. package/dist/http/mcp/tools/index.d.ts +5 -1
  51. package/dist/http/mcp/tools/index.js +45 -10
  52. package/dist/http/mcp/tools/index.js.map +1 -1
  53. package/dist/http/mcp/tools/organization.d.ts +6 -0
  54. package/dist/http/mcp/tools/organization.js +214 -0
  55. package/dist/http/mcp/tools/organization.js.map +1 -0
  56. package/dist/http/mcp/tools/people.js +30 -49
  57. package/dist/http/mcp/tools/people.js.map +1 -1
  58. package/dist/http/plugins/auth.js +37 -8
  59. package/dist/http/plugins/auth.js.map +1 -1
  60. package/dist/http/plugins/error-handler.d.ts +5 -1
  61. package/dist/http/plugins/error-handler.js +10 -2
  62. package/dist/http/plugins/error-handler.js.map +1 -1
  63. package/dist/http/plugins/openapi.js +18 -2
  64. package/dist/http/plugins/openapi.js.map +1 -1
  65. package/dist/http/plugins/platform.d.ts +7 -0
  66. package/dist/http/plugins/platform.js +13 -0
  67. package/dist/http/plugins/platform.js.map +1 -0
  68. package/dist/http/routes/access.js +54 -36
  69. package/dist/http/routes/access.js.map +1 -1
  70. package/dist/http/routes/activities.js +45 -45
  71. package/dist/http/routes/activities.js.map +1 -1
  72. package/dist/http/routes/assets.js +30 -65
  73. package/dist/http/routes/assets.js.map +1 -1
  74. package/dist/http/routes/automations.js +37 -42
  75. package/dist/http/routes/automations.js.map +1 -1
  76. package/dist/http/routes/bundles.js +33 -36
  77. package/dist/http/routes/bundles.js.map +1 -1
  78. package/dist/http/routes/communities.js +93 -145
  79. package/dist/http/routes/communities.js.map +1 -1
  80. package/dist/http/routes/community-moderation.js +83 -137
  81. package/dist/http/routes/community-moderation.js.map +1 -1
  82. package/dist/http/routes/course-analytics.js +32 -15
  83. package/dist/http/routes/course-analytics.js.map +1 -1
  84. package/dist/http/routes/courses.js +25 -32
  85. package/dist/http/routes/courses.js.map +1 -1
  86. package/dist/http/routes/dashboard.js +9 -9
  87. package/dist/http/routes/dashboard.js.map +1 -1
  88. package/dist/http/routes/discussion.js +21 -39
  89. package/dist/http/routes/discussion.js.map +1 -1
  90. package/dist/http/routes/downloads.js +41 -47
  91. package/dist/http/routes/downloads.js.map +1 -1
  92. package/dist/http/routes/entitlements.js +19 -23
  93. package/dist/http/routes/entitlements.js.map +1 -1
  94. package/dist/http/routes/events.d.ts +7 -0
  95. package/dist/http/routes/events.js +333 -0
  96. package/dist/http/routes/events.js.map +1 -0
  97. package/dist/http/routes/integrations.js +40 -73
  98. package/dist/http/routes/integrations.js.map +1 -1
  99. package/dist/http/routes/member/assets.js +10 -13
  100. package/dist/http/routes/member/assets.js.map +1 -1
  101. package/dist/http/routes/member/chats.js +170 -47
  102. package/dist/http/routes/member/chats.js.map +1 -1
  103. package/dist/http/routes/member/comments.js +65 -26
  104. package/dist/http/routes/member/comments.js.map +1 -1
  105. package/dist/http/routes/member/community.js +45 -52
  106. package/dist/http/routes/member/community.js.map +1 -1
  107. package/dist/http/routes/member/courses.js +12 -19
  108. package/dist/http/routes/member/courses.js.map +1 -1
  109. package/dist/http/routes/member/devices.js +9 -10
  110. package/dist/http/routes/member/devices.js.map +1 -1
  111. package/dist/http/routes/member/downloads.js +6 -9
  112. package/dist/http/routes/member/downloads.js.map +1 -1
  113. package/dist/http/routes/member/events.d.ts +7 -0
  114. package/dist/http/routes/member/events.js +386 -0
  115. package/dist/http/routes/member/events.js.map +1 -0
  116. package/dist/http/routes/member/files.js +71 -46
  117. package/dist/http/routes/member/files.js.map +1 -1
  118. package/dist/http/routes/member/index.js +2 -0
  119. package/dist/http/routes/member/index.js.map +1 -1
  120. package/dist/http/routes/member/notification-settings.js +15 -23
  121. package/dist/http/routes/member/notification-settings.js.map +1 -1
  122. package/dist/http/routes/member/notifications.js +16 -16
  123. package/dist/http/routes/member/notifications.js.map +1 -1
  124. package/dist/http/routes/member/org.js +5 -15
  125. package/dist/http/routes/member/org.js.map +1 -1
  126. package/dist/http/routes/member/posts.js +67 -68
  127. package/dist/http/routes/member/posts.js.map +1 -1
  128. package/dist/http/routes/member/profile.js +15 -6
  129. package/dist/http/routes/member/profile.js.map +1 -1
  130. package/dist/http/routes/member/progress.js +7 -11
  131. package/dist/http/routes/member/progress.js.map +1 -1
  132. package/dist/http/routes/organizations.js +319 -135
  133. package/dist/http/routes/organizations.js.map +1 -1
  134. package/dist/http/routes/platform.d.ts +7 -0
  135. package/dist/http/routes/platform.js +61 -0
  136. package/dist/http/routes/platform.js.map +1 -0
  137. package/dist/http/routes/students.js +12 -75
  138. package/dist/http/routes/students.js.map +1 -1
  139. package/dist/http/routes/webhooks.js +30 -39
  140. package/dist/http/routes/webhooks.js.map +1 -1
  141. package/dist/http/routes.js +9 -0
  142. package/dist/http/routes.js.map +1 -1
  143. package/dist/http/schemas/access.d.ts +346 -64
  144. package/dist/http/schemas/access.js +39 -12
  145. package/dist/http/schemas/access.js.map +1 -1
  146. package/dist/http/schemas/assets.d.ts +19 -14
  147. package/dist/http/schemas/assets.js +6 -5
  148. package/dist/http/schemas/assets.js.map +1 -1
  149. package/dist/http/schemas/automations.d.ts +4 -4
  150. package/dist/http/schemas/chats.d.ts +211 -32
  151. package/dist/http/schemas/chats.js +13 -4
  152. package/dist/http/schemas/chats.js.map +1 -1
  153. package/dist/http/schemas/community.d.ts +91 -44
  154. package/dist/http/schemas/community.js +21 -8
  155. package/dist/http/schemas/community.js.map +1 -1
  156. package/dist/http/schemas/course-analytics.d.ts +51 -1
  157. package/dist/http/schemas/course-analytics.js +19 -1
  158. package/dist/http/schemas/course-analytics.js.map +1 -1
  159. package/dist/http/schemas/dashboard.d.ts +1 -0
  160. package/dist/http/schemas/dashboard.js +2 -1
  161. package/dist/http/schemas/dashboard.js.map +1 -1
  162. package/dist/http/schemas/discussion.d.ts +22 -72
  163. package/dist/http/schemas/discussion.js +2 -2
  164. package/dist/http/schemas/discussion.js.map +1 -1
  165. package/dist/http/schemas/entitlements.d.ts +9 -12
  166. package/dist/http/schemas/entitlements.js +11 -5
  167. package/dist/http/schemas/entitlements.js.map +1 -1
  168. package/dist/http/schemas/events.d.ts +527 -0
  169. package/dist/http/schemas/events.js +64 -0
  170. package/dist/http/schemas/events.js.map +1 -0
  171. package/dist/http/schemas/files.d.ts +15 -5
  172. package/dist/http/schemas/files.js +10 -2
  173. package/dist/http/schemas/files.js.map +1 -1
  174. package/dist/http/schemas/index.d.ts +11 -9
  175. package/dist/http/schemas/index.js +11 -9
  176. package/dist/http/schemas/integrations.d.ts +4 -4
  177. package/dist/http/schemas/invites.d.ts +3 -47
  178. package/dist/http/schemas/invites.js +4 -25
  179. package/dist/http/schemas/invites.js.map +1 -1
  180. package/dist/http/schemas/member.d.ts +94 -7
  181. package/dist/http/schemas/member.js +7 -3
  182. package/dist/http/schemas/member.js.map +1 -1
  183. package/dist/http/schemas/members.d.ts +333 -69
  184. package/dist/http/schemas/members.js +67 -25
  185. package/dist/http/schemas/members.js.map +1 -1
  186. package/dist/http/schemas/notifications.d.ts +57 -53
  187. package/dist/http/schemas/platform.d.ts +37 -0
  188. package/dist/http/schemas/platform.js +19 -0
  189. package/dist/http/schemas/platform.js.map +1 -0
  190. package/dist/http/schemas/posts.d.ts +55 -35
  191. package/dist/http/schemas/posts.js +4 -4
  192. package/dist/http/schemas/posts.js.map +1 -1
  193. package/dist/http/schemas/profile.d.ts +1 -1
  194. package/dist/http/schemas/shared.d.ts +1 -11
  195. package/dist/http/schemas/shared.js +1 -10
  196. package/dist/http/schemas/shared.js.map +1 -1
  197. package/dist/http/schemas/students.d.ts +25 -3
  198. package/dist/http/schemas/students.js +3 -1
  199. package/dist/http/schemas/students.js.map +1 -1
  200. package/dist/http/scope.d.ts +9 -13
  201. package/dist/http/scope.js +29 -40
  202. package/dist/http/scope.js.map +1 -1
  203. package/dist/http/server.js +9 -0
  204. package/dist/http/server.js.map +1 -1
  205. package/dist/http/shutdown.d.ts +11 -0
  206. package/dist/http/shutdown.js +40 -0
  207. package/dist/http/shutdown.js.map +1 -0
  208. package/dist/http/sse.d.ts +19 -0
  209. package/dist/http/sse.js +104 -0
  210. package/dist/http/sse.js.map +1 -0
  211. package/dist/http/test-caller.d.ts +13 -0
  212. package/dist/http/test-caller.js +14 -0
  213. package/dist/http/test-caller.js.map +1 -0
  214. package/dist/http/well-known.js +2 -2
  215. package/dist/http/well-known.js.map +1 -1
  216. package/dist/index.d.ts +4 -2
  217. package/dist/index.js +4 -2
  218. package/dist/index.js.map +1 -1
  219. package/dist/packages/server/package.js +1 -1
  220. package/package.json +7 -6
  221. package/dist/http/routes/member/actor.d.ts +0 -14
  222. package/dist/http/routes/member/actor.js +0 -11
  223. package/dist/http/routes/member/actor.js.map +0 -1
@@ -1 +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', '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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"}
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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"}
@@ -7,9 +7,9 @@ import { OrganizationService } from "@headless-lms/core/organizations";
7
7
  import { NotificationReads } from "@headless-lms/core/automations";
8
8
  //#region src/app/notification-reads.d.ts
9
9
  type NotificationReadsDeps = CommunityMailDeps & {
10
- content: Pick<ContentService, 'getActivity' | 'getModule' | 'getCourse' | 'getDownload' | 'getBundle' | 'listBundleItems'>;
11
- entitlements: Pick<EntitlementsService, 'communityMemberIds' | 'contentHolderIds' | 'hasAccess'>;
12
- organizations: Pick<OrganizationService, 'getOrgUsersByIds'>;
10
+ content: Pick<ContentService, 'getActivity' | 'getModule' | 'findCourse' | 'findEvent' | 'listBundleItems'>;
11
+ entitlements: Pick<EntitlementsService, 'communityMemberIds' | 'grantedOrgUserIds' | 'hasAccess'>;
12
+ organizations: Pick<OrganizationService, 'getOrgUsersByIds' | 'systemActor'>;
13
13
  identity: Pick<IdentityService, 'getUserById'>;
14
14
  posts: Pick<PostService, 'publishedBetween'>;
15
15
  subscriptions: Pick<SubscriptionService, 'mutedAmong'>;
@@ -4,6 +4,7 @@ import { activityTitle } from "@headless-lms/core/content";
4
4
  function notificationReads(deps) {
5
5
  const { mail } = communityMail(deps);
6
6
  const nameOf = (orgId, orgUserId) => mail.displayName(orgId, orgUserId);
7
+ const system = (orgId) => deps.organizations.systemActor(orgId);
7
8
  return {
8
9
  ...mail,
9
10
  async threadParticipants(orgId, item) {
@@ -14,16 +15,18 @@ function notificationReads(deps) {
14
15
  mutedAmong: (orgId, item, orgUserIds, at) => deps.subscriptions.mutedAmong(orgId, item, orgUserIds, at),
15
16
  communityMembers: (orgId, communityId) => deps.entitlements.communityMemberIds(orgId, communityId),
16
17
  switchedOn: (orgId, communityId, category, spaceId) => deps.settings.switchedOn(orgId, communityId, category, spaceId),
17
- courseHolders: (orgId, courseId) => deps.entitlements.contentHolderIds(orgId, courseId),
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),
18
21
  async contentLostBy(orgId, grant) {
19
- const opened = grant.bundleId ? (await deps.content.listBundleItems(orgId, grant.bundleId)).map((item) => item.contentId) : grant.contentId ? [grant.contentId] : [];
22
+ const opened = grant.bundleId ? (await deps.content.listBundleItems(grant.bundleId, system(orgId))).map((item) => item.contentId) : grant.contentId ? [grant.contentId] : [];
20
23
  const held = await Promise.all(opened.map((contentId) => deps.entitlements.hasAccess(orgId, grant.orgUserId, contentId)));
21
24
  return opened.filter((_, index) => !held[index]);
22
25
  },
23
26
  async activity(orgId, activityId) {
24
- const activity = await deps.content.getActivity(orgId, activityId);
27
+ const activity = await deps.content.getActivity(activityId, system(orgId));
25
28
  if (!activity) return null;
26
- const [module, course] = await Promise.all([deps.content.getModule(orgId, activity.moduleId), deps.content.getCourse(orgId, activity.courseId)]);
29
+ const [module, course] = await Promise.all([deps.content.getModule(activity.moduleId, system(orgId)), deps.content.findCourse(activity.courseId, system(orgId))]);
27
30
  if (!course) return null;
28
31
  return {
29
32
  title: activityTitle(activity),
@@ -39,30 +42,6 @@ function notificationReads(deps) {
39
42
  async emailOf(_orgId, userId) {
40
43
  return (await deps.identity.getUserById(userId))?.email ?? null;
41
44
  },
42
- async contentInfo(orgId, item) {
43
- if ("bundleId" in item) {
44
- const bundle = await deps.content.getBundle(orgId, item.bundleId);
45
- return bundle ? {
46
- title: bundle.name,
47
- type: "bundle"
48
- } : null;
49
- }
50
- const course = await deps.content.getCourse(orgId, item.contentId);
51
- if (course) return {
52
- title: course.title,
53
- type: course.type
54
- };
55
- const download = await deps.content.getDownload(orgId, item.contentId);
56
- if (download) return {
57
- title: download.title,
58
- type: download.type
59
- };
60
- const community = await deps.content.getCommunity(orgId, item.contentId);
61
- return community ? {
62
- title: community.title,
63
- type: community.type
64
- } : null;
65
- },
66
45
  async message(orgId, messageId) {
67
46
  const message = await deps.chats.findMessage(orgId, messageId);
68
47
  return message && message.deletedAt === null ? {
@@ -1 +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' | 'getCourse' | 'getDownload' | 'getBundle' | 'listBundleItems'\n >;\n entitlements: Pick<EntitlementsService, 'communityMemberIds' | 'contentHolderIds' | 'hasAccess'>;\n organizations: Pick<OrganizationService, 'getOrgUsersByIds'>;\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 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 courseHolders: (orgId, courseId) => deps.entitlements.contentHolderIds(orgId, courseId),\n async contentLostBy(orgId, grant) {\n const opened = grant.bundleId\n ? (await deps.content.listBundleItems(orgId, grant.bundleId)).map((item) => item.contentId)\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(orgId, activityId);\n if (!activity) {\n return null;\n }\n const [module, course] = await Promise.all([\n deps.content.getModule(orgId, activity.moduleId),\n deps.content.getCourse(orgId, activity.courseId),\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 contentInfo(orgId, item) {\n if ('bundleId' in item) {\n const bundle = await deps.content.getBundle(orgId, item.bundleId);\n return bundle ? { title: bundle.name, type: 'bundle' } : null;\n }\n const course = await deps.content.getCourse(orgId, item.contentId);\n if (course) {\n return { title: course.title, type: course.type };\n }\n const download = await deps.content.getDownload(orgId, item.contentId);\n if (download) {\n return { title: download.title, type: download.type };\n }\n const community = await deps.content.getCommunity(orgId, item.contentId);\n return community ? { title: community.title, type: community.type } : 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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"}
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
  }
@@ -3,8 +3,8 @@ import { NoActiveOrgError } from "./scope.js";
3
3
  import { InviteError, OrganizationRuleError } from "@headless-lms/core/organizations";
4
4
  import { ActionInvocationError, AlreadyConnectedError, InvalidConfigError, UnknownIntegrationError } from "@headless-lms/core/integrations";
5
5
  import { AutomationKindMismatchError, InvalidTriggerError, ReservedActionError } from "@headless-lms/core/automations";
6
- import { hasZodFastifySchemaValidationErrors } from "fastify-type-provider-zod";
7
6
  import { ConflictError, ForbiddenError, InvalidInputError, NotFoundError, UnavailableError, ValidationError } from "@headless-lms/core/shared/errors";
7
+ import { hasZodFastifySchemaValidationErrors } from "fastify-type-provider-zod";
8
8
  //#region src/http/errors.ts
9
9
  const BODY_ERRORS = {
10
10
  FST_ERR_CTP_INVALID_JSON_BODY: {
@@ -39,13 +39,13 @@ function issuePath(context, instancePath) {
39
39
  function describeIssue(keyword, params, fallback) {
40
40
  switch (keyword) {
41
41
  case "invalid_type":
42
- if (params.received === "undefined" || params.received === "null") return "is required";
42
+ if (params.received === "undefined" || params.received === "null" || /received (undefined|null)$/.test(fallback)) return "is required";
43
43
  return params.expected ? `must be ${withArticle(params.expected)}` : fallback;
44
44
  case "too_small": return params.origin === "string" ? Number(params.minimum) === 1 ? "must not be empty" : `must be at least ${params.minimum} characters` : params.origin === "array" ? `must have at least ${params.minimum} ${Number(params.minimum) === 1 ? "item" : "items"}` : `must be ${params.inclusive === false ? "greater than" : "at least"} ${params.minimum}`;
45
45
  case "too_big": return params.origin === "string" ? `must be at most ${params.maximum} characters` : params.origin === "array" ? `must have at most ${params.maximum} items` : `must be ${params.inclusive === false ? "less than" : "at most"} ${params.maximum}`;
46
46
  case "unrecognized_keys": return `has ${params.keys && params.keys.length > 1 ? "unknown fields" : "an unknown field"} ${(params.keys ?? []).map((key) => `"${key}"`).join(", ")}`;
47
47
  case "invalid_value": return `must be one of ${(params.values ?? []).map((value) => JSON.stringify(value)).join(", ")}`;
48
- case "invalid_format": return params.format ? `must be a valid ${params.format.replace(/_/g, " ")}` : fallback;
48
+ case "invalid_format": return params.format && params.format !== "regex" ? `must be a valid ${params.format.replace(/_/g, " ")}` : fallback;
49
49
  case "invalid_union": return "is not in an accepted shape";
50
50
  default: return fallback;
51
51
  }
@@ -73,7 +73,12 @@ function classifyError(error) {
73
73
  message: problem.message
74
74
  }))
75
75
  };
76
- if (error instanceof ConflictError || error instanceof OrganizationRuleError) return {
76
+ if (error instanceof ConflictError) return {
77
+ status: 409,
78
+ error: error.code ?? "conflict",
79
+ message: error.message
80
+ };
81
+ if (error instanceof OrganizationRuleError) return {
77
82
  status: 409,
78
83
  error: "conflict",
79
84
  message: error.message
@@ -1 +1 @@
1
- {"version":3,"file":"errors.js","names":[],"sources":["../../src/http/errors.ts"],"sourcesContent":["import {\n ConflictError,\n ForbiddenError,\n InvalidInputError,\n NotFoundError,\n UnavailableError,\n ValidationError,\n} from '@headless-lms/core/shared/errors';\nimport { InviteError, OrganizationRuleError } from '@headless-lms/core/organizations';\nimport {\n ActionInvocationError,\n AlreadyConnectedError,\n InvalidConfigError,\n UnknownIntegrationError,\n} from '@headless-lms/core/integrations';\nimport {\n AutomationKindMismatchError,\n InvalidTriggerError,\n ReservedActionError,\n} from '@headless-lms/core/automations';\nimport { hasZodFastifySchemaValidationErrors } from 'fastify-type-provider-zod';\nimport { NoActiveOrgError } from './scope.js';\nimport { UnauthorizedError } from './plugins/auth.js';\n\nexport type Refusal = {\n status: number;\n error: string;\n message: string;\n issues?: { path: string; message: string }[];\n details?: Record<string, string>;\n};\n\nconst BODY_ERRORS: Record<string, Refusal> = {\n FST_ERR_CTP_INVALID_JSON_BODY: {\n status: 400,\n error: 'invalid_body',\n message: 'The request body must be valid JSON',\n },\n FST_ERR_CTP_EMPTY_JSON_BODY: {\n status: 400,\n error: 'invalid_body',\n message: 'The request body must be valid JSON',\n },\n FST_ERR_CTP_INVALID_MEDIA_TYPE: {\n status: 415,\n error: 'unsupported_media_type',\n message: 'Send the request body as application/json',\n },\n FST_ERR_CTP_BODY_TOO_LARGE: {\n status: 413,\n error: 'payload_too_large',\n message: 'The request body is too large',\n },\n FST_ERR_NOT_FOUND: { status: 404, error: 'not_found', message: 'No such route' },\n};\n\nfunction issuePath(context: string, instancePath: string): string {\n const inner = instancePath\n .split('/')\n .filter((part) => part.length > 0)\n .map((part) => (/^\\d+$/.test(part) ? `[${part}]` : `.${part}`))\n .join('');\n return `${context}${inner}`;\n}\n\ntype IssueParams = {\n expected?: string;\n received?: string;\n minimum?: number | bigint;\n maximum?: number | bigint;\n inclusive?: boolean;\n origin?: string;\n keys?: string[];\n values?: unknown[];\n format?: string;\n};\n\nfunction describeIssue(keyword: string | undefined, params: IssueParams, fallback: string): string {\n switch (keyword) {\n case 'invalid_type':\n if (params.received === 'undefined' || params.received === 'null') {\n return 'is required';\n }\n return params.expected ? `must be ${withArticle(params.expected)}` : fallback;\n case 'too_small':\n return params.origin === 'string'\n ? Number(params.minimum) === 1\n ? 'must not be empty'\n : `must be at least ${params.minimum} characters`\n : params.origin === 'array'\n ? `must have at least ${params.minimum} ${Number(params.minimum) === 1 ? 'item' : 'items'}`\n : `must be ${params.inclusive === false ? 'greater than' : 'at least'} ${params.minimum}`;\n case 'too_big':\n return params.origin === 'string'\n ? `must be at most ${params.maximum} characters`\n : params.origin === 'array'\n ? `must have at most ${params.maximum} items`\n : `must be ${params.inclusive === false ? 'less than' : 'at most'} ${params.maximum}`;\n case 'unrecognized_keys':\n return `has ${params.keys && params.keys.length > 1 ? 'unknown fields' : 'an unknown field'} ${(params.keys ?? []).map((key) => `\"${key}\"`).join(', ')}`;\n case 'invalid_value':\n return `must be one of ${(params.values ?? []).map((value) => JSON.stringify(value)).join(', ')}`;\n case 'invalid_format':\n return params.format ? `must be a valid ${params.format.replace(/_/g, ' ')}` : fallback;\n case 'invalid_union':\n return 'is not in an accepted shape';\n default:\n return fallback;\n }\n}\n\nfunction withArticle(noun: string): string {\n return /^[aeiou]/i.test(noun) ? `an ${noun}` : `a ${noun}`;\n}\n\nexport function classifyError(error: unknown): Refusal | undefined {\n if (error instanceof NotFoundError) {\n return { status: 404, error: 'not_found', message: error.message };\n }\n if (error instanceof InvalidInputError) {\n return { status: 400, error: 'invalid_input', message: error.message };\n }\n if (error instanceof ValidationError) {\n return {\n status: 422,\n error: 'unprocessable',\n message: error.message,\n issues: error.problems.map((problem) => ({\n path: problem.path ? `body.${problem.path}` : 'body',\n message: problem.message,\n })),\n };\n }\n if (error instanceof ConflictError || error instanceof OrganizationRuleError) {\n return { status: 409, error: 'conflict', message: error.message };\n }\n if (error instanceof ForbiddenError || error instanceof NoActiveOrgError) {\n return { status: 403, error: 'forbidden', message: error.message };\n }\n if (error instanceof UnavailableError) {\n return {\n status: 503,\n error: 'unavailable',\n message: error.message,\n ...(error.details && { details: error.details }),\n };\n }\n if (error instanceof UnauthorizedError) {\n return { status: 401, error: 'unauthorized', message: error.message };\n }\n if (error instanceof InviteError) {\n return { status: 400, error: 'invalid_invite', message: error.message };\n }\n if (error instanceof AlreadyConnectedError) {\n return { status: 409, error: 'already_connected', message: error.message };\n }\n if (error instanceof UnknownIntegrationError) {\n return { status: 400, error: 'unknown_integration', message: error.message };\n }\n if (error instanceof InvalidConfigError) {\n return { status: 400, error: 'invalid_config', message: error.message };\n }\n if (error instanceof InvalidTriggerError) {\n return { status: 400, error: 'invalid_trigger', message: error.message };\n }\n if (error instanceof ReservedActionError) {\n return { status: 400, error: 'reserved_action', message: error.message };\n }\n if (error instanceof ActionInvocationError) {\n return {\n status: 502,\n error: 'action_failed',\n message: 'The integration did not respond as expected',\n };\n }\n if (error instanceof AutomationKindMismatchError) {\n return { status: 409, error: 'kind_mismatch', message: error.message };\n }\n if (hasZodFastifySchemaValidationErrors(error)) {\n const context = (error as { validationContext?: string }).validationContext ?? 'request';\n const issues = error.validation.map((issue) => ({\n path: issuePath(context, issue.instancePath),\n message: describeIssue(\n issue.keyword,\n (issue.params ?? {}) as IssueParams,\n issue.message ?? 'is invalid',\n ),\n }));\n const first = issues[0];\n return {\n status: 400,\n error: 'validation',\n message: first ? `${first.path}: ${first.message}` : 'The request is invalid',\n issues,\n };\n }\n const err = error as { statusCode?: number; code?: string };\n if (err.code && BODY_ERRORS[err.code]) {\n return BODY_ERRORS[err.code];\n }\n if (typeof err.statusCode === 'number' && err.statusCode >= 400 && err.statusCode < 500) {\n return {\n status: err.statusCode,\n error: 'bad_request',\n message: 'The request could not be processed',\n };\n }\n return undefined;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"errors.js","names":[],"sources":["../../src/http/errors.ts"],"sourcesContent":["import {\n ConflictError,\n ForbiddenError,\n InvalidInputError,\n NotFoundError,\n UnavailableError,\n ValidationError,\n} from '@headless-lms/core/shared/errors';\nimport { InviteError, OrganizationRuleError } from '@headless-lms/core/organizations';\nimport {\n ActionInvocationError,\n AlreadyConnectedError,\n InvalidConfigError,\n UnknownIntegrationError,\n} from '@headless-lms/core/integrations';\nimport {\n AutomationKindMismatchError,\n InvalidTriggerError,\n ReservedActionError,\n} from '@headless-lms/core/automations';\nimport { hasZodFastifySchemaValidationErrors } from 'fastify-type-provider-zod';\nimport { NoActiveOrgError } from './scope.js';\nimport { UnauthorizedError } from './plugins/auth.js';\n\nexport type Refusal = {\n status: number;\n error: string;\n message: string;\n issues?: { path: string; message: string }[];\n details?: Record<string, string>;\n};\n\nconst BODY_ERRORS: Record<string, Refusal> = {\n FST_ERR_CTP_INVALID_JSON_BODY: {\n status: 400,\n error: 'invalid_body',\n message: 'The request body must be valid JSON',\n },\n FST_ERR_CTP_EMPTY_JSON_BODY: {\n status: 400,\n error: 'invalid_body',\n message: 'The request body must be valid JSON',\n },\n FST_ERR_CTP_INVALID_MEDIA_TYPE: {\n status: 415,\n error: 'unsupported_media_type',\n message: 'Send the request body as application/json',\n },\n FST_ERR_CTP_BODY_TOO_LARGE: {\n status: 413,\n error: 'payload_too_large',\n message: 'The request body is too large',\n },\n FST_ERR_NOT_FOUND: { status: 404, error: 'not_found', message: 'No such route' },\n};\n\nfunction issuePath(context: string, instancePath: string): string {\n const inner = instancePath\n .split('/')\n .filter((part) => part.length > 0)\n .map((part) => (/^\\d+$/.test(part) ? `[${part}]` : `.${part}`))\n .join('');\n return `${context}${inner}`;\n}\n\ntype IssueParams = {\n expected?: string;\n received?: string;\n minimum?: number | bigint;\n maximum?: number | bigint;\n inclusive?: boolean;\n origin?: string;\n keys?: string[];\n values?: unknown[];\n format?: string;\n};\n\nfunction describeIssue(keyword: string | undefined, params: IssueParams, fallback: string): string {\n switch (keyword) {\n case 'invalid_type':\n if (\n params.received === 'undefined' ||\n params.received === 'null' ||\n /received (undefined|null)$/.test(fallback)\n ) {\n return 'is required';\n }\n return params.expected ? `must be ${withArticle(params.expected)}` : fallback;\n case 'too_small':\n return params.origin === 'string'\n ? Number(params.minimum) === 1\n ? 'must not be empty'\n : `must be at least ${params.minimum} characters`\n : params.origin === 'array'\n ? `must have at least ${params.minimum} ${Number(params.minimum) === 1 ? 'item' : 'items'}`\n : `must be ${params.inclusive === false ? 'greater than' : 'at least'} ${params.minimum}`;\n case 'too_big':\n return params.origin === 'string'\n ? `must be at most ${params.maximum} characters`\n : params.origin === 'array'\n ? `must have at most ${params.maximum} items`\n : `must be ${params.inclusive === false ? 'less than' : 'at most'} ${params.maximum}`;\n case 'unrecognized_keys':\n return `has ${params.keys && params.keys.length > 1 ? 'unknown fields' : 'an unknown field'} ${(params.keys ?? []).map((key) => `\"${key}\"`).join(', ')}`;\n case 'invalid_value':\n return `must be one of ${(params.values ?? []).map((value) => JSON.stringify(value)).join(', ')}`;\n case 'invalid_format':\n return params.format && params.format !== 'regex'\n ? `must be a valid ${params.format.replace(/_/g, ' ')}`\n : fallback;\n case 'invalid_union':\n return 'is not in an accepted shape';\n default:\n return fallback;\n }\n}\n\nfunction withArticle(noun: string): string {\n return /^[aeiou]/i.test(noun) ? `an ${noun}` : `a ${noun}`;\n}\n\nexport function classifyError(error: unknown): Refusal | undefined {\n if (error instanceof NotFoundError) {\n return { status: 404, error: 'not_found', message: error.message };\n }\n if (error instanceof InvalidInputError) {\n return { status: 400, error: 'invalid_input', message: error.message };\n }\n if (error instanceof ValidationError) {\n return {\n status: 422,\n error: 'unprocessable',\n message: error.message,\n issues: error.problems.map((problem) => ({\n path: problem.path ? `body.${problem.path}` : 'body',\n message: problem.message,\n })),\n };\n }\n if (error instanceof ConflictError) {\n return { status: 409, error: error.code ?? 'conflict', message: error.message };\n }\n if (error instanceof OrganizationRuleError) {\n return { status: 409, error: 'conflict', message: error.message };\n }\n if (error instanceof ForbiddenError || error instanceof NoActiveOrgError) {\n return { status: 403, error: 'forbidden', message: error.message };\n }\n if (error instanceof UnavailableError) {\n return {\n status: 503,\n error: 'unavailable',\n message: error.message,\n ...(error.details && { details: error.details }),\n };\n }\n if (error instanceof UnauthorizedError) {\n return { status: 401, error: 'unauthorized', message: error.message };\n }\n if (error instanceof InviteError) {\n return { status: 400, error: 'invalid_invite', message: error.message };\n }\n if (error instanceof AlreadyConnectedError) {\n return { status: 409, error: 'already_connected', message: error.message };\n }\n if (error instanceof UnknownIntegrationError) {\n return { status: 400, error: 'unknown_integration', message: error.message };\n }\n if (error instanceof InvalidConfigError) {\n return { status: 400, error: 'invalid_config', message: error.message };\n }\n if (error instanceof InvalidTriggerError) {\n return { status: 400, error: 'invalid_trigger', message: error.message };\n }\n if (error instanceof ReservedActionError) {\n return { status: 400, error: 'reserved_action', message: error.message };\n }\n if (error instanceof ActionInvocationError) {\n return {\n status: 502,\n error: 'action_failed',\n message: 'The integration did not respond as expected',\n };\n }\n if (error instanceof AutomationKindMismatchError) {\n return { status: 409, error: 'kind_mismatch', message: error.message };\n }\n if (hasZodFastifySchemaValidationErrors(error)) {\n const context = (error as { validationContext?: string }).validationContext ?? 'request';\n const issues = error.validation.map((issue) => ({\n path: issuePath(context, issue.instancePath),\n message: describeIssue(\n issue.keyword,\n (issue.params ?? {}) as IssueParams,\n issue.message ?? 'is invalid',\n ),\n }));\n const first = issues[0];\n return {\n status: 400,\n error: 'validation',\n message: first ? `${first.path}: ${first.message}` : 'The request is invalid',\n issues,\n };\n }\n const err = error as { statusCode?: number; code?: string };\n if (err.code && BODY_ERRORS[err.code]) {\n return BODY_ERRORS[err.code];\n }\n if (typeof err.statusCode === 'number' && err.statusCode >= 400 && err.statusCode < 500) {\n return {\n status: err.statusCode,\n error: 'bad_request',\n message: 'The request could not be processed',\n };\n }\n return undefined;\n}\n"],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import { ResolvedCaller } from "@headless-lms/core/organizations";
1
2
  import "fastify";
2
3
  import { User } from "@headless-lms/core/types";
3
4
  //#region src/http/fastify.d.ts
@@ -5,6 +6,10 @@ import { User } from "@headless-lms/core/types";
5
6
  * package entry point so this module — and its augmentation — gets loaded. */
6
7
  type AuthUser = User;
7
8
  declare module 'fastify' {
9
+ interface FastifyContextConfig {
10
+ /** A server-sent event stream; shutdown waits for it to finish. */
11
+ eventStream?: boolean;
12
+ }
8
13
  interface FastifyInstance {
9
14
  /** Session guard. Populates `authUser` (and `orgId` when the session has an
10
15
  * active org), or throws `UnauthorizedError`. Use for the routes reached
@@ -12,9 +17,11 @@ declare module 'fastify' {
12
17
  requireSession(request: FastifyRequest): Promise<void>;
13
18
  /** As `requireSession`, but also requires an active org on the session. */
14
19
  requireOrgSession(request: FastifyRequest): Promise<void>;
20
+ requirePlatform(request: FastifyRequest): Promise<void>;
15
21
  }
16
22
  interface FastifyRequest {
17
23
  authUser: User;
24
+ authUserId: string;
18
25
  /** Domain `organizations.id` for the session's active org — what every
19
26
  * domain read keys off. Guaranteed set by `requireOrgSession`; absent under
20
27
  * `requireSession` when the session has no active org. */
@@ -28,6 +35,7 @@ declare module 'fastify' {
28
35
  * and every other domain row references. Not `authUser.id`: the two match
29
36
  * only for a self-signup, never for a person an admin provisioned first. */
30
37
  userId: string;
38
+ resolvedCaller?: ResolvedCaller;
31
39
  }
32
40
  }
33
41
  //#endregion
@@ -1,5 +1,5 @@
1
1
  import { ToolKind } from "./tool.js";
2
- import { AccessScope, StaffRole } from "@headless-lms/core/organizations";
2
+ import { Level, Permission } from "@headless-lms/core/types";
3
3
  //#region src/http/mcp/catalogue.d.ts
4
4
  interface CatalogueField {
5
5
  name: string;
@@ -11,27 +11,29 @@ interface CatalogueTool {
11
11
  name: string;
12
12
  title: string;
13
13
  description: string;
14
- resource: string;
15
- scope: AccessScope;
14
+ group: string;
16
15
  kind: ToolKind;
17
16
  idempotent: boolean;
18
17
  openWorld: boolean;
19
18
  fields: CatalogueField[];
20
19
  }
21
- interface CatalogueScope {
22
- scope: AccessScope;
20
+ interface CataloguePermission {
21
+ permission: Permission;
23
22
  resource: string;
24
23
  action: string;
25
- permission: string | null;
26
- roles: StaffRole[];
24
+ label: string;
25
+ description: string;
26
+ about: Level;
27
+ implies: Permission[];
28
+ roles: string[];
27
29
  removes: boolean;
28
30
  }
29
31
  interface McpCatalogue {
30
32
  instructions: string;
31
- scopes: CatalogueScope[];
33
+ permissions: CataloguePermission[];
32
34
  tools: CatalogueTool[];
33
35
  }
34
36
  declare function mcpCatalogue(): McpCatalogue;
35
37
  //#endregion
36
- export { CatalogueField, CatalogueScope, CatalogueTool, McpCatalogue, mcpCatalogue };
38
+ export { CatalogueField, CataloguePermission, CatalogueTool, McpCatalogue, mcpCatalogue };
37
39
  //# sourceMappingURL=catalogue.d.ts.map
@@ -1,7 +1,7 @@
1
1
  import { toolInputSchema } from "./input-schema.js";
2
2
  import { MCP_INSTRUCTIONS } from "./server.js";
3
- import { TOOLS } from "./tools/index.js";
4
- import { ACCESS_SCOPES, SCOPE_PERMISSION, STAFF_ROLES, isDeleteScope, roleHoldsScope } from "@headless-lms/core/organizations";
3
+ import { TOOL_GROUPS } from "./tools/index.js";
4
+ import { ALL_PERMISSIONS, PERMISSIONS, defaultHolders, withImplied } from "@headless-lms/core/organizations";
5
5
  //#region src/http/mcp/catalogue.ts
6
6
  function typeOf(schema) {
7
7
  const variants = schema.anyOf ?? schema.oneOf;
@@ -27,40 +27,34 @@ function fieldsOf(tool) {
27
27
  }));
28
28
  return [...fields.filter((field) => field.required), ...fields.filter((field) => !field.required)];
29
29
  }
30
- const REMOVING_WRITE_SCOPES = [
31
- "downloads:write",
32
- "bundles:write",
33
- "access:write",
34
- "comments:moderate",
35
- "communities:write",
36
- "community_members:write",
37
- "community_moderation:write"
38
- ];
39
30
  function mcpCatalogue() {
40
31
  return {
41
32
  instructions: MCP_INSTRUCTIONS,
42
- scopes: ACCESS_SCOPES.map((scope) => {
43
- const [resource = scope, action = ""] = scope.split(":");
33
+ permissions: ALL_PERMISSIONS.map((permission) => {
34
+ const [resource = permission, action = ""] = permission.split(":");
35
+ const definition = PERMISSIONS[permission];
44
36
  return {
45
- scope,
37
+ permission,
46
38
  resource,
47
39
  action,
48
- permission: SCOPE_PERMISSION[scope],
49
- roles: STAFF_ROLES.filter((role) => roleHoldsScope(role, scope)),
50
- removes: isDeleteScope(scope) || REMOVING_WRITE_SCOPES.includes(scope)
40
+ label: definition.label,
41
+ description: definition.description,
42
+ about: definition.about,
43
+ implies: [...withImplied([permission])].filter((p) => p !== permission),
44
+ roles: defaultHolders(permission),
45
+ removes: action === "delete"
51
46
  };
52
47
  }),
53
- tools: TOOLS.map((tool) => ({
48
+ tools: TOOL_GROUPS.flatMap(({ title: group, tools }) => tools.map((tool) => ({
54
49
  name: tool.name,
55
50
  title: tool.title,
56
51
  description: tool.description,
57
- resource: tool.scope.split(":")[0] ?? tool.scope,
58
- scope: tool.scope,
52
+ group,
59
53
  kind: tool.kind,
60
54
  idempotent: tool.idempotent,
61
55
  openWorld: tool.openWorld,
62
56
  fields: fieldsOf(tool)
63
- }))
57
+ })))
64
58
  };
65
59
  }
66
60
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"catalogue.js","names":[],"sources":["../../../src/http/mcp/catalogue.ts"],"sourcesContent":["import {\n ACCESS_SCOPES,\n SCOPE_PERMISSION,\n STAFF_ROLES,\n isDeleteScope,\n roleHoldsScope,\n type AccessScope,\n type StaffRole,\n} from '@headless-lms/core/organizations';\nimport { toolInputSchema } from './input-schema.js';\nimport { MCP_INSTRUCTIONS } from './server.js';\nimport type { ToolKind } from './tool.js';\nimport { TOOLS } from './tools/index.js';\n\nexport interface CatalogueField {\n name: string;\n type: string;\n required: boolean;\n description: string | null;\n}\n\nexport interface CatalogueTool {\n name: string;\n title: string;\n description: string;\n resource: string;\n scope: AccessScope;\n kind: ToolKind;\n idempotent: boolean;\n openWorld: boolean;\n fields: CatalogueField[];\n}\n\nexport interface CatalogueScope {\n scope: AccessScope;\n resource: string;\n action: string;\n permission: string | null;\n roles: StaffRole[];\n removes: boolean;\n}\n\nexport interface McpCatalogue {\n instructions: string;\n scopes: CatalogueScope[];\n tools: CatalogueTool[];\n}\n\ntype JsonSchema = {\n type?: string | string[];\n format?: string;\n enum?: unknown[];\n const?: unknown;\n items?: JsonSchema;\n anyOf?: JsonSchema[];\n oneOf?: JsonSchema[];\n description?: string;\n properties?: Record<string, JsonSchema>;\n required?: string[];\n};\n\nfunction typeOf(schema: JsonSchema): string {\n const variants = schema.anyOf ?? schema.oneOf;\n if (variants) {\n return [...new Set(variants.map(typeOf))].join(' | ');\n }\n if (schema.enum) {\n return schema.enum.map((value) => JSON.stringify(value)).join(' | ');\n }\n if (schema.const !== undefined) {\n return JSON.stringify(schema.const);\n }\n if (schema.type === 'array') {\n return `${schema.items ? typeOf(schema.items) : 'unknown'}[]`;\n }\n if (schema.format === 'date-time') {\n return 'date-time';\n }\n if (Array.isArray(schema.type)) {\n return schema.type.join(' | ');\n }\n return schema.type ?? 'unknown';\n}\n\nfunction descriptionOf(schema: JsonSchema): string | null {\n return (\n schema.description ??\n (schema.anyOf ?? schema.oneOf)?.find((variant) => variant.description)?.description ??\n null\n );\n}\n\nfunction fieldsOf(tool: (typeof TOOLS)[number]): CatalogueField[] {\n const json = toolInputSchema(tool.input)['~standard'].jsonSchema.input({\n target: 'draft-2020-12',\n }) as JsonSchema;\n const required = new Set(json.required ?? []);\n const fields = Object.entries(json.properties ?? {}).map(([name, schema]) => ({\n name,\n type: typeOf(schema),\n required: required.has(name),\n description: descriptionOf(schema),\n }));\n return [\n ...fields.filter((field) => field.required),\n ...fields.filter((field) => !field.required),\n ];\n}\n\nconst REMOVING_WRITE_SCOPES: readonly AccessScope[] = [\n 'downloads:write',\n 'bundles:write',\n 'access:write',\n 'comments:moderate',\n 'communities:write',\n 'community_members:write',\n 'community_moderation:write',\n];\n\nexport function mcpCatalogue(): McpCatalogue {\n return {\n instructions: MCP_INSTRUCTIONS,\n scopes: ACCESS_SCOPES.map((scope) => {\n const [resource = scope, action = ''] = scope.split(':');\n return {\n scope,\n resource,\n action,\n permission: SCOPE_PERMISSION[scope],\n roles: STAFF_ROLES.filter((role) => roleHoldsScope(role, scope)),\n removes: isDeleteScope(scope) || REMOVING_WRITE_SCOPES.includes(scope),\n };\n }),\n tools: TOOLS.map((tool) => ({\n name: tool.name,\n title: tool.title,\n description: tool.description,\n resource: tool.scope.split(':')[0] ?? tool.scope,\n scope: tool.scope,\n kind: tool.kind,\n idempotent: tool.idempotent,\n openWorld: tool.openWorld,\n fields: fieldsOf(tool),\n })),\n };\n}\n"],"mappings":";;;;;AA6DA,SAAS,OAAO,QAA4B;CAC1C,MAAM,WAAW,OAAO,SAAS,OAAO;CACxC,IAAI,UACF,OAAO,CAAC,GAAG,IAAI,IAAI,SAAS,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK,KAAK;CAEtD,IAAI,OAAO,MACT,OAAO,OAAO,KAAK,KAAK,UAAU,KAAK,UAAU,KAAK,CAAC,CAAC,CAAC,KAAK,KAAK;CAErE,IAAI,OAAO,UAAU,KAAA,GACnB,OAAO,KAAK,UAAU,OAAO,KAAK;CAEpC,IAAI,OAAO,SAAS,SAClB,OAAO,GAAG,OAAO,QAAQ,OAAO,OAAO,KAAK,IAAI,UAAU;CAE5D,IAAI,OAAO,WAAW,aACpB,OAAO;CAET,IAAI,MAAM,QAAQ,OAAO,IAAI,GAC3B,OAAO,OAAO,KAAK,KAAK,KAAK;CAE/B,OAAO,OAAO,QAAQ;AACxB;AAEA,SAAS,cAAc,QAAmC;CACxD,OACE,OAAO,gBACN,OAAO,SAAS,OAAO,MAAA,EAAQ,MAAM,YAAY,QAAQ,WAAW,CAAC,EAAE,eACxE;AAEJ;AAEA,SAAS,SAAS,MAAgD;CAChE,MAAM,OAAO,gBAAgB,KAAK,KAAK,CAAC,CAAC,YAAY,CAAC,WAAW,MAAM,EACrE,QAAQ,gBACV,CAAC;CACD,MAAM,WAAW,IAAI,IAAI,KAAK,YAAY,CAAC,CAAC;CAC5C,MAAM,SAAS,OAAO,QAAQ,KAAK,cAAc,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,aAAa;EAC5E;EACA,MAAM,OAAO,MAAM;EACnB,UAAU,SAAS,IAAI,IAAI;EAC3B,aAAa,cAAc,MAAM;CACnC,EAAE;CACF,OAAO,CACL,GAAG,OAAO,QAAQ,UAAU,MAAM,QAAQ,GAC1C,GAAG,OAAO,QAAQ,UAAU,CAAC,MAAM,QAAQ,CAC7C;AACF;AAEA,MAAM,wBAAgD;CACpD;CACA;CACA;CACA;CACA;CACA;CACA;AACF;AAEA,SAAgB,eAA6B;CAC3C,OAAO;EACL,cAAc;EACd,QAAQ,cAAc,KAAK,UAAU;GACnC,MAAM,CAAC,WAAW,OAAO,SAAS,MAAM,MAAM,MAAM,GAAG;GACvD,OAAO;IACL;IACA;IACA;IACA,YAAY,iBAAiB;IAC7B,OAAO,YAAY,QAAQ,SAAS,eAAe,MAAM,KAAK,CAAC;IAC/D,SAAS,cAAc,KAAK,KAAK,sBAAsB,SAAS,KAAK;GACvE;EACF,CAAC;EACD,OAAO,MAAM,KAAK,UAAU;GAC1B,MAAM,KAAK;GACX,OAAO,KAAK;GACZ,aAAa,KAAK;GAClB,UAAU,KAAK,MAAM,MAAM,GAAG,CAAC,CAAC,MAAM,KAAK;GAC3C,OAAO,KAAK;GACZ,MAAM,KAAK;GACX,YAAY,KAAK;GACjB,WAAW,KAAK;GAChB,QAAQ,SAAS,IAAI;EACvB,EAAE;CACJ;AACF"}
1
+ {"version":3,"file":"catalogue.js","names":[],"sources":["../../../src/http/mcp/catalogue.ts"],"sourcesContent":["import {\n ALL_PERMISSIONS,\n defaultHolders,\n PERMISSIONS,\n withImplied,\n} from '@headless-lms/core/organizations';\nimport type { Level, Permission } from '@headless-lms/core/types';\nimport { toolInputSchema } from './input-schema.js';\nimport { MCP_INSTRUCTIONS } from './server.js';\nimport type { Tool, ToolKind } from './tool.js';\nimport { TOOL_GROUPS } from './tools/index.js';\n\nexport interface CatalogueField {\n name: string;\n type: string;\n required: boolean;\n description: string | null;\n}\n\nexport interface CatalogueTool {\n name: string;\n title: string;\n description: string;\n group: string;\n kind: ToolKind;\n idempotent: boolean;\n openWorld: boolean;\n fields: CatalogueField[];\n}\n\nexport interface CataloguePermission {\n permission: Permission;\n resource: string;\n action: string;\n label: string;\n description: string;\n about: Level;\n implies: Permission[];\n roles: string[];\n removes: boolean;\n}\n\nexport interface McpCatalogue {\n instructions: string;\n permissions: CataloguePermission[];\n tools: CatalogueTool[];\n}\n\ntype JsonSchema = {\n type?: string | string[];\n format?: string;\n enum?: unknown[];\n const?: unknown;\n items?: JsonSchema;\n anyOf?: JsonSchema[];\n oneOf?: JsonSchema[];\n description?: string;\n properties?: Record<string, JsonSchema>;\n required?: string[];\n};\n\nfunction typeOf(schema: JsonSchema): string {\n const variants = schema.anyOf ?? schema.oneOf;\n if (variants) {\n return [...new Set(variants.map(typeOf))].join(' | ');\n }\n if (schema.enum) {\n return schema.enum.map((value) => JSON.stringify(value)).join(' | ');\n }\n if (schema.const !== undefined) {\n return JSON.stringify(schema.const);\n }\n if (schema.type === 'array') {\n return `${schema.items ? typeOf(schema.items) : 'unknown'}[]`;\n }\n if (schema.format === 'date-time') {\n return 'date-time';\n }\n if (Array.isArray(schema.type)) {\n return schema.type.join(' | ');\n }\n return schema.type ?? 'unknown';\n}\n\nfunction descriptionOf(schema: JsonSchema): string | null {\n return (\n schema.description ??\n (schema.anyOf ?? schema.oneOf)?.find((variant) => variant.description)?.description ??\n null\n );\n}\n\nfunction fieldsOf(tool: Tool): CatalogueField[] {\n const json = toolInputSchema(tool.input)['~standard'].jsonSchema.input({\n target: 'draft-2020-12',\n }) as JsonSchema;\n const required = new Set(json.required ?? []);\n const fields = Object.entries(json.properties ?? {}).map(([name, schema]) => ({\n name,\n type: typeOf(schema),\n required: required.has(name),\n description: descriptionOf(schema),\n }));\n return [\n ...fields.filter((field) => field.required),\n ...fields.filter((field) => !field.required),\n ];\n}\n\nexport function mcpCatalogue(): McpCatalogue {\n return {\n instructions: MCP_INSTRUCTIONS,\n permissions: ALL_PERMISSIONS.map((permission) => {\n const [resource = permission, action = ''] = permission.split(':');\n const definition = PERMISSIONS[permission];\n return {\n permission,\n resource,\n action,\n label: definition.label,\n description: definition.description,\n about: definition.about,\n implies: [...withImplied([permission])].filter((p) => p !== permission),\n roles: defaultHolders(permission),\n removes: action === 'delete',\n };\n }),\n tools: TOOL_GROUPS.flatMap(({ title: group, tools }) =>\n tools.map((tool) => ({\n name: tool.name,\n title: tool.title,\n description: tool.description,\n group,\n kind: tool.kind,\n idempotent: tool.idempotent,\n openWorld: tool.openWorld,\n fields: fieldsOf(tool),\n })),\n ),\n };\n}\n"],"mappings":";;;;;AA6DA,SAAS,OAAO,QAA4B;CAC1C,MAAM,WAAW,OAAO,SAAS,OAAO;CACxC,IAAI,UACF,OAAO,CAAC,GAAG,IAAI,IAAI,SAAS,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK,KAAK;CAEtD,IAAI,OAAO,MACT,OAAO,OAAO,KAAK,KAAK,UAAU,KAAK,UAAU,KAAK,CAAC,CAAC,CAAC,KAAK,KAAK;CAErE,IAAI,OAAO,UAAU,KAAA,GACnB,OAAO,KAAK,UAAU,OAAO,KAAK;CAEpC,IAAI,OAAO,SAAS,SAClB,OAAO,GAAG,OAAO,QAAQ,OAAO,OAAO,KAAK,IAAI,UAAU;CAE5D,IAAI,OAAO,WAAW,aACpB,OAAO;CAET,IAAI,MAAM,QAAQ,OAAO,IAAI,GAC3B,OAAO,OAAO,KAAK,KAAK,KAAK;CAE/B,OAAO,OAAO,QAAQ;AACxB;AAEA,SAAS,cAAc,QAAmC;CACxD,OACE,OAAO,gBACN,OAAO,SAAS,OAAO,MAAA,EAAQ,MAAM,YAAY,QAAQ,WAAW,CAAC,EAAE,eACxE;AAEJ;AAEA,SAAS,SAAS,MAA8B;CAC9C,MAAM,OAAO,gBAAgB,KAAK,KAAK,CAAC,CAAC,YAAY,CAAC,WAAW,MAAM,EACrE,QAAQ,gBACV,CAAC;CACD,MAAM,WAAW,IAAI,IAAI,KAAK,YAAY,CAAC,CAAC;CAC5C,MAAM,SAAS,OAAO,QAAQ,KAAK,cAAc,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,aAAa;EAC5E;EACA,MAAM,OAAO,MAAM;EACnB,UAAU,SAAS,IAAI,IAAI;EAC3B,aAAa,cAAc,MAAM;CACnC,EAAE;CACF,OAAO,CACL,GAAG,OAAO,QAAQ,UAAU,MAAM,QAAQ,GAC1C,GAAG,OAAO,QAAQ,UAAU,CAAC,MAAM,QAAQ,CAC7C;AACF;AAEA,SAAgB,eAA6B;CAC3C,OAAO;EACL,cAAc;EACd,aAAa,gBAAgB,KAAK,eAAe;GAC/C,MAAM,CAAC,WAAW,YAAY,SAAS,MAAM,WAAW,MAAM,GAAG;GACjE,MAAM,aAAa,YAAY;GAC/B,OAAO;IACL;IACA;IACA;IACA,OAAO,WAAW;IAClB,aAAa,WAAW;IACxB,OAAO,WAAW;IAClB,SAAS,CAAC,GAAG,YAAY,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,QAAQ,MAAM,MAAM,UAAU;IACtE,OAAO,eAAe,UAAU;IAChC,SAAS,WAAW;GACtB;EACF,CAAC;EACD,OAAO,YAAY,SAAS,EAAE,OAAO,OAAO,YAC1C,MAAM,KAAK,UAAU;GACnB,MAAM,KAAK;GACX,OAAO,KAAK;GACZ,aAAa,KAAK;GAClB;GACA,MAAM,KAAK;GACX,YAAY,KAAK;GACjB,WAAW,KAAK;GAChB,QAAQ,SAAS,IAAI;EACvB,EAAE,CACJ;CACF;AACF"}
@@ -1,4 +1,4 @@
1
- import { ACCESS_SCOPES } from "@headless-lms/core/organizations";
1
+ import { ALL_PERMISSIONS } from "@headless-lms/core/organizations";
2
2
  //#region src/http/mcp/challenge.ts
3
3
  function unauthorized(mcp) {
4
4
  return new Response(JSON.stringify({
@@ -12,7 +12,7 @@ function unauthorized(mcp) {
12
12
  status: 401,
13
13
  headers: {
14
14
  "content-type": "application/json",
15
- "www-authenticate": `Bearer resource_metadata="${mcp.resourceMetadataUrl}", scope="${ACCESS_SCOPES.join(" ")}"`
15
+ "www-authenticate": `Bearer resource_metadata="${mcp.resourceMetadataUrl}", scope="${ALL_PERMISSIONS.join(" ")}"`
16
16
  }
17
17
  });
18
18
  }
@@ -21,7 +21,7 @@ function forbidden() {
21
21
  jsonrpc: "2.0",
22
22
  error: {
23
23
  code: -32e3,
24
- message: "Only staff of this organization can use this server"
24
+ message: "Only active members of this organization can use this server"
25
25
  },
26
26
  id: null
27
27
  }), {
@@ -1 +1 @@
1
- {"version":3,"file":"challenge.js","names":[],"sources":["../../../src/http/mcp/challenge.ts"],"sourcesContent":["import { ACCESS_SCOPES } from '@headless-lms/core/organizations';\nimport type { McpEndpoints } from '../../app/mcp-endpoints.js';\n\nexport function unauthorized(mcp: McpEndpoints): Response {\n const body = { jsonrpc: '2.0', error: { code: -32000, message: 'Unauthorized' }, id: null };\n return new Response(JSON.stringify(body), {\n status: 401,\n headers: {\n 'content-type': 'application/json',\n 'www-authenticate': `Bearer resource_metadata=\"${mcp.resourceMetadataUrl}\", scope=\"${ACCESS_SCOPES.join(' ')}\"`,\n },\n });\n}\n\nexport function forbidden(): Response {\n const body = {\n jsonrpc: '2.0',\n error: { code: -32000, message: 'Only staff of this organization can use this server' },\n id: null,\n };\n return new Response(JSON.stringify(body), {\n status: 403,\n headers: { 'content-type': 'application/json' },\n });\n}\n"],"mappings":";;AAGA,SAAgB,aAAa,KAA6B;CAExD,OAAO,IAAI,SAAS,KAAK,UAAU;EADpB,SAAS;EAAO,OAAO;GAAE,MAAM;GAAQ,SAAS;EAAe;EAAG,IAAI;CAC/C,CAAC,GAAG;EACxC,QAAQ;EACR,SAAS;GACP,gBAAgB;GAChB,oBAAoB,6BAA6B,IAAI,oBAAoB,YAAY,cAAc,KAAK,GAAG,EAAE;EAC/G;CACF,CAAC;AACH;AAEA,SAAgB,YAAsB;CAMpC,OAAO,IAAI,SAAS,KAAK,UAAU;EAJjC,SAAS;EACT,OAAO;GAAE,MAAM;GAAQ,SAAS;EAAsD;EACtF,IAAI;CAEgC,CAAC,GAAG;EACxC,QAAQ;EACR,SAAS,EAAE,gBAAgB,mBAAmB;CAChD,CAAC;AACH"}
1
+ {"version":3,"file":"challenge.js","names":[],"sources":["../../../src/http/mcp/challenge.ts"],"sourcesContent":["import { ALL_PERMISSIONS } from '@headless-lms/core/organizations';\nimport type { McpEndpoints } from '../../app/mcp-endpoints.js';\n\nexport function unauthorized(mcp: McpEndpoints): Response {\n const body = { jsonrpc: '2.0', error: { code: -32000, message: 'Unauthorized' }, id: null };\n return new Response(JSON.stringify(body), {\n status: 401,\n headers: {\n 'content-type': 'application/json',\n 'www-authenticate': `Bearer resource_metadata=\"${mcp.resourceMetadataUrl}\", scope=\"${ALL_PERMISSIONS.join(' ')}\"`,\n },\n });\n}\n\nexport function forbidden(): Response {\n const body = {\n jsonrpc: '2.0',\n error: {\n code: -32000,\n message: 'Only active members of this organization can use this server',\n },\n id: null,\n };\n return new Response(JSON.stringify(body), {\n status: 403,\n headers: { 'content-type': 'application/json' },\n });\n}\n"],"mappings":";;AAGA,SAAgB,aAAa,KAA6B;CAExD,OAAO,IAAI,SAAS,KAAK,UAAU;EADpB,SAAS;EAAO,OAAO;GAAE,MAAM;GAAQ,SAAS;EAAe;EAAG,IAAI;CAC/C,CAAC,GAAG;EACxC,QAAQ;EACR,SAAS;GACP,gBAAgB;GAChB,oBAAoB,6BAA6B,IAAI,oBAAoB,YAAY,gBAAgB,KAAK,GAAG,EAAE;EACjH;CACF,CAAC;AACH;AAEA,SAAgB,YAAsB;CASpC,OAAO,IAAI,SAAS,KAAK,UAAU;EAPjC,SAAS;EACT,OAAO;GACL,MAAM;GACN,SAAS;EACX;EACA,IAAI;CAEgC,CAAC,GAAG;EACxC,QAAQ;EACR,SAAS,EAAE,gBAAgB,mBAAmB;CAChD,CAAC;AACH"}
@@ -1,8 +1,8 @@
1
- import { NoActiveOrgError, resolveScopeFor } from "../scope.js";
1
+ import { NoActiveOrgError, resolveCallerFor } from "../scope.js";
2
2
  import { forbidden, unauthorized } from "./challenge.js";
3
3
  import { createToolServer } from "./server.js";
4
4
  import { TOOLS } from "./tools/index.js";
5
- import { roleHoldsScope } from "@headless-lms/core/organizations";
5
+ import { isPermission } from "@headless-lms/core/organizations";
6
6
  import { createMcpHandler } from "@modelcontextprotocol/server";
7
7
  import { toWebRequest } from "@modelcontextprotocol/node";
8
8
  //#region src/http/mcp/route.ts
@@ -29,9 +29,9 @@ function registerMcp(app, container, tools = TOOLS) {
29
29
  headers: request.headers
30
30
  });
31
31
  if (!grant) return reply.send(unauthorized(container.mcp));
32
- let scope;
32
+ let caller;
33
33
  try {
34
- scope = await resolveScopeFor(container, grant.userId, grant.orgId);
34
+ caller = await resolveCallerFor(container, grant.userId, grant.orgId, "roleKey" in grant.limit ? grant.limit : { scopes: grant.limit.scopes.filter(isPermission) });
35
35
  } catch (error) {
36
36
  if (error instanceof NoActiveOrgError) return reply.send(forbidden());
37
37
  throw error;
@@ -39,16 +39,16 @@ function registerMcp(app, container, tools = TOOLS) {
39
39
  const connection = {
40
40
  context: {
41
41
  container,
42
- scope,
42
+ caller,
43
43
  requestId: request.id
44
44
  },
45
- tools: tools.filter((tool) => grant.scopes.includes(tool.scope) && roleHoldsScope(scope.role, tool.scope))
45
+ tools
46
46
  };
47
47
  const response = await handler.fetch(await webRequest(request, reply), {
48
48
  authInfo: {
49
49
  token: "",
50
50
  clientId: grant.clientId,
51
- scopes: grant.scopes,
51
+ scopes: "scopes" in grant.limit ? grant.limit.scopes : [],
52
52
  extra: { connection }
53
53
  },
54
54
  parsedBody: request.body
@@ -1 +1 @@
1
- {"version":3,"file":"route.js","names":[],"sources":["../../../src/http/mcp/route.ts"],"sourcesContent":["import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';\nimport { createMcpHandler } from '@modelcontextprotocol/server';\nimport { toWebRequest } from '@modelcontextprotocol/node';\nimport { roleHoldsScope } from '@headless-lms/core/organizations';\nimport type { Container } from '../../app/container.js';\nimport { NoActiveOrgError, resolveScopeFor, type OrgScope } from '../scope.js';\nimport { forbidden, unauthorized } from './challenge.js';\nimport { createToolServer } from './server.js';\nimport type { Tool, ToolContext } from './tool.js';\nimport { TOOLS } from './tools/index.js';\n\ntype Connection = { context: ToolContext; tools: Tool[] };\n\nexport function registerMcp(\n app: FastifyInstance,\n container: Container,\n tools: readonly Tool[] = TOOLS,\n): void {\n const handler = createMcpHandler(\n (ctx) => {\n const connection = ctx.authInfo?.extra?.connection as Connection | undefined;\n if (!connection) {\n throw new Error('mcp connection missing');\n }\n return createToolServer(connection.context, connection.tools);\n },\n {\n legacy: 'stateless',\n onerror: (err) => container.logger.error('mcp handler error', { err }),\n },\n );\n\n const webRequest = async (request: FastifyRequest, reply: FastifyReply) => {\n const abort = new AbortController();\n reply.raw.on('close', () => {\n if (!reply.raw.writableFinished) {\n abort.abort();\n }\n });\n return toWebRequest(request.raw, request.body, { signal: abort.signal });\n };\n\n app.post('/mcp', { schema: { hide: true } }, async (request, reply) => {\n const grant = await container.auth.verifyAccess({\n method: request.method,\n url: container.mcp.resource,\n headers: request.headers,\n });\n if (!grant) {\n return reply.send(unauthorized(container.mcp));\n }\n let scope: OrgScope;\n try {\n scope = await resolveScopeFor(container, grant.userId, grant.orgId);\n } catch (error) {\n if (error instanceof NoActiveOrgError) {\n return reply.send(forbidden());\n }\n throw error;\n }\n const connection: Connection = {\n context: { container, scope, requestId: request.id },\n tools: tools.filter(\n (tool) => grant.scopes.includes(tool.scope) && roleHoldsScope(scope.role, tool.scope),\n ),\n };\n const response = await handler.fetch(await webRequest(request, reply), {\n authInfo: {\n token: '',\n clientId: grant.clientId,\n scopes: grant.scopes,\n extra: { connection },\n },\n parsedBody: request.body,\n });\n return reply.send(response);\n });\n\n app.route({\n method: ['GET', 'DELETE'],\n url: '/mcp',\n schema: { hide: true },\n handler: async (_request, reply) =>\n reply\n .code(405)\n .header('allow', 'POST')\n .header('content-type', 'application/json')\n .send({\n jsonrpc: '2.0',\n error: { code: -32000, message: 'Method not allowed.' },\n id: null,\n }),\n });\n}\n"],"mappings":";;;;;;;;AAaA,SAAgB,YACd,KACA,WACA,QAAyB,OACnB;CACN,MAAM,UAAU,kBACb,QAAQ;EACP,MAAM,aAAa,IAAI,UAAU,OAAO;EACxC,IAAI,CAAC,YACH,MAAM,IAAI,MAAM,wBAAwB;EAE1C,OAAO,iBAAiB,WAAW,SAAS,WAAW,KAAK;CAC9D,GACA;EACE,QAAQ;EACR,UAAU,QAAQ,UAAU,OAAO,MAAM,qBAAqB,EAAE,IAAI,CAAC;CACvE,CACF;CAEA,MAAM,aAAa,OAAO,SAAyB,UAAwB;EACzE,MAAM,QAAQ,IAAI,gBAAgB;EAClC,MAAM,IAAI,GAAG,eAAe;GAC1B,IAAI,CAAC,MAAM,IAAI,kBACb,MAAM,MAAM;EAEhB,CAAC;EACD,OAAO,aAAa,QAAQ,KAAK,QAAQ,MAAM,EAAE,QAAQ,MAAM,OAAO,CAAC;CACzE;CAEA,IAAI,KAAK,QAAQ,EAAE,QAAQ,EAAE,MAAM,KAAK,EAAE,GAAG,OAAO,SAAS,UAAU;EACrE,MAAM,QAAQ,MAAM,UAAU,KAAK,aAAa;GAC9C,QAAQ,QAAQ;GAChB,KAAK,UAAU,IAAI;GACnB,SAAS,QAAQ;EACnB,CAAC;EACD,IAAI,CAAC,OACH,OAAO,MAAM,KAAK,aAAa,UAAU,GAAG,CAAC;EAE/C,IAAI;EACJ,IAAI;GACF,QAAQ,MAAM,gBAAgB,WAAW,MAAM,QAAQ,MAAM,KAAK;EACpE,SAAS,OAAO;GACd,IAAI,iBAAiB,kBACnB,OAAO,MAAM,KAAK,UAAU,CAAC;GAE/B,MAAM;EACR;EACA,MAAM,aAAyB;GAC7B,SAAS;IAAE;IAAW;IAAO,WAAW,QAAQ;GAAG;GACnD,OAAO,MAAM,QACV,SAAS,MAAM,OAAO,SAAS,KAAK,KAAK,KAAK,eAAe,MAAM,MAAM,KAAK,KAAK,CACtF;EACF;EACA,MAAM,WAAW,MAAM,QAAQ,MAAM,MAAM,WAAW,SAAS,KAAK,GAAG;GACrE,UAAU;IACR,OAAO;IACP,UAAU,MAAM;IAChB,QAAQ,MAAM;IACd,OAAO,EAAE,WAAW;GACtB;GACA,YAAY,QAAQ;EACtB,CAAC;EACD,OAAO,MAAM,KAAK,QAAQ;CAC5B,CAAC;CAED,IAAI,MAAM;EACR,QAAQ,CAAC,OAAO,QAAQ;EACxB,KAAK;EACL,QAAQ,EAAE,MAAM,KAAK;EACrB,SAAS,OAAO,UAAU,UACxB,MACG,KAAK,GAAG,CAAC,CACT,OAAO,SAAS,MAAM,CAAC,CACvB,OAAO,gBAAgB,kBAAkB,CAAC,CAC1C,KAAK;GACJ,SAAS;GACT,OAAO;IAAE,MAAM;IAAQ,SAAS;GAAsB;GACtD,IAAI;EACN,CAAC;CACP,CAAC;AACH"}
1
+ {"version":3,"file":"route.js","names":[],"sources":["../../../src/http/mcp/route.ts"],"sourcesContent":["import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';\nimport { createMcpHandler } from '@modelcontextprotocol/server';\nimport { toWebRequest } from '@modelcontextprotocol/node';\nimport { isPermission } from '@headless-lms/core/organizations';\nimport type { Container } from '../../app/container.js';\nimport { NoActiveOrgError, resolveCallerFor, type Caller } from '../scope.js';\nimport { forbidden, unauthorized } from './challenge.js';\nimport { createToolServer } from './server.js';\nimport type { Tool, ToolContext } from './tool.js';\nimport { TOOLS } from './tools/index.js';\n\ntype Connection = { context: ToolContext; tools: readonly Tool[] };\n\nexport function registerMcp(\n app: FastifyInstance,\n container: Container,\n tools: readonly Tool[] = TOOLS,\n): void {\n const handler = createMcpHandler(\n (ctx) => {\n const connection = ctx.authInfo?.extra?.connection as Connection | undefined;\n if (!connection) {\n throw new Error('mcp connection missing');\n }\n return createToolServer(connection.context, connection.tools);\n },\n {\n legacy: 'stateless',\n onerror: (err) => container.logger.error('mcp handler error', { err }),\n },\n );\n\n const webRequest = async (request: FastifyRequest, reply: FastifyReply) => {\n const abort = new AbortController();\n reply.raw.on('close', () => {\n if (!reply.raw.writableFinished) {\n abort.abort();\n }\n });\n return toWebRequest(request.raw, request.body, { signal: abort.signal });\n };\n\n app.post('/mcp', { schema: { hide: true } }, async (request, reply) => {\n const grant = await container.auth.verifyAccess({\n method: request.method,\n url: container.mcp.resource,\n headers: request.headers,\n });\n if (!grant) {\n return reply.send(unauthorized(container.mcp));\n }\n let caller: Caller;\n try {\n caller = await resolveCallerFor(\n container,\n grant.userId,\n grant.orgId,\n 'roleKey' in grant.limit\n ? grant.limit\n : { scopes: grant.limit.scopes.filter(isPermission) },\n );\n } catch (error) {\n if (error instanceof NoActiveOrgError) {\n return reply.send(forbidden());\n }\n throw error;\n }\n const connection: Connection = {\n context: { container, caller, requestId: request.id },\n tools,\n };\n const response = await handler.fetch(await webRequest(request, reply), {\n authInfo: {\n token: '',\n clientId: grant.clientId,\n scopes: 'scopes' in grant.limit ? grant.limit.scopes : [],\n extra: { connection },\n },\n parsedBody: request.body,\n });\n return reply.send(response);\n });\n\n app.route({\n method: ['GET', 'DELETE'],\n url: '/mcp',\n schema: { hide: true },\n handler: async (_request, reply) =>\n reply\n .code(405)\n .header('allow', 'POST')\n .header('content-type', 'application/json')\n .send({\n jsonrpc: '2.0',\n error: { code: -32000, message: 'Method not allowed.' },\n id: null,\n }),\n });\n}\n"],"mappings":";;;;;;;;AAaA,SAAgB,YACd,KACA,WACA,QAAyB,OACnB;CACN,MAAM,UAAU,kBACb,QAAQ;EACP,MAAM,aAAa,IAAI,UAAU,OAAO;EACxC,IAAI,CAAC,YACH,MAAM,IAAI,MAAM,wBAAwB;EAE1C,OAAO,iBAAiB,WAAW,SAAS,WAAW,KAAK;CAC9D,GACA;EACE,QAAQ;EACR,UAAU,QAAQ,UAAU,OAAO,MAAM,qBAAqB,EAAE,IAAI,CAAC;CACvE,CACF;CAEA,MAAM,aAAa,OAAO,SAAyB,UAAwB;EACzE,MAAM,QAAQ,IAAI,gBAAgB;EAClC,MAAM,IAAI,GAAG,eAAe;GAC1B,IAAI,CAAC,MAAM,IAAI,kBACb,MAAM,MAAM;EAEhB,CAAC;EACD,OAAO,aAAa,QAAQ,KAAK,QAAQ,MAAM,EAAE,QAAQ,MAAM,OAAO,CAAC;CACzE;CAEA,IAAI,KAAK,QAAQ,EAAE,QAAQ,EAAE,MAAM,KAAK,EAAE,GAAG,OAAO,SAAS,UAAU;EACrE,MAAM,QAAQ,MAAM,UAAU,KAAK,aAAa;GAC9C,QAAQ,QAAQ;GAChB,KAAK,UAAU,IAAI;GACnB,SAAS,QAAQ;EACnB,CAAC;EACD,IAAI,CAAC,OACH,OAAO,MAAM,KAAK,aAAa,UAAU,GAAG,CAAC;EAE/C,IAAI;EACJ,IAAI;GACF,SAAS,MAAM,iBACb,WACA,MAAM,QACN,MAAM,OACN,aAAa,MAAM,QACf,MAAM,QACN,EAAE,QAAQ,MAAM,MAAM,OAAO,OAAO,YAAY,EAAE,CACxD;EACF,SAAS,OAAO;GACd,IAAI,iBAAiB,kBACnB,OAAO,MAAM,KAAK,UAAU,CAAC;GAE/B,MAAM;EACR;EACA,MAAM,aAAyB;GAC7B,SAAS;IAAE;IAAW;IAAQ,WAAW,QAAQ;GAAG;GACpD;EACF;EACA,MAAM,WAAW,MAAM,QAAQ,MAAM,MAAM,WAAW,SAAS,KAAK,GAAG;GACrE,UAAU;IACR,OAAO;IACP,UAAU,MAAM;IAChB,QAAQ,YAAY,MAAM,QAAQ,MAAM,MAAM,SAAS,CAAC;IACxD,OAAO,EAAE,WAAW;GACtB;GACA,YAAY,QAAQ;EACtB,CAAC;EACD,OAAO,MAAM,KAAK,QAAQ;CAC5B,CAAC;CAED,IAAI,MAAM;EACR,QAAQ,CAAC,OAAO,QAAQ;EACxB,KAAK;EACL,QAAQ,EAAE,MAAM,KAAK;EACrB,SAAS,OAAO,UAAU,UACxB,MACG,KAAK,GAAG,CAAC,CACT,OAAO,SAAS,MAAM,CAAC,CACvB,OAAO,gBAAgB,kBAAkB,CAAC,CAC1C,KAAK;GACJ,SAAS;GACT,OAAO;IAAE,MAAM;IAAQ,SAAS;GAAsB;GACtD,IAAI;EACN,CAAC;CACP,CAAC;AACH"}