@headless-lms/server 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. package/README.md +14 -0
  2. package/dist/app/community-mail.d.ts +10 -10
  3. package/dist/app/community-mail.js +72 -28
  4. package/dist/app/community-mail.js.map +1 -1
  5. package/dist/app/container.d.ts +48 -10
  6. package/dist/app/container.js +268 -48
  7. package/dist/app/container.js.map +1 -1
  8. package/dist/app/notification-channels.d.ts +47 -0
  9. package/dist/app/notification-channels.js +175 -0
  10. package/dist/app/notification-channels.js.map +1 -0
  11. package/dist/app/notification-reads.d.ts +23 -0
  12. package/dist/app/notification-reads.js +74 -0
  13. package/dist/app/notification-reads.js.map +1 -0
  14. package/dist/app/platform.d.ts +14 -0
  15. package/dist/app/platform.js +14 -0
  16. package/dist/app/platform.js.map +1 -0
  17. package/dist/http/config.d.ts +4 -0
  18. package/dist/http/errors.d.ts +1 -0
  19. package/dist/http/errors.js +24 -4
  20. package/dist/http/errors.js.map +1 -1
  21. package/dist/http/fastify.d.ts +8 -0
  22. package/dist/http/mcp/catalogue.d.ts +11 -9
  23. package/dist/http/mcp/catalogue.js +15 -21
  24. package/dist/http/mcp/catalogue.js.map +1 -1
  25. package/dist/http/mcp/challenge.js +3 -3
  26. package/dist/http/mcp/challenge.js.map +1 -1
  27. package/dist/http/mcp/route.js +7 -7
  28. package/dist/http/mcp/route.js.map +1 -1
  29. package/dist/http/mcp/test-support.d.ts +6 -5
  30. package/dist/http/mcp/test-support.js +32 -11
  31. package/dist/http/mcp/test-support.js.map +1 -1
  32. package/dist/http/mcp/tool.d.ts +3 -9
  33. package/dist/http/mcp/tool.js +14 -6
  34. package/dist/http/mcp/tool.js.map +1 -1
  35. package/dist/http/mcp/tools/automations.js +10 -20
  36. package/dist/http/mcp/tools/automations.js.map +1 -1
  37. package/dist/http/mcp/tools/catalog.js +25 -42
  38. package/dist/http/mcp/tools/catalog.js.map +1 -1
  39. package/dist/http/mcp/tools/comments.js +9 -19
  40. package/dist/http/mcp/tools/comments.js.map +1 -1
  41. package/dist/http/mcp/tools/communities.js +16 -31
  42. package/dist/http/mcp/tools/communities.js.map +1 -1
  43. package/dist/http/mcp/tools/community-members.js +9 -16
  44. package/dist/http/mcp/tools/community-members.js.map +1 -1
  45. package/dist/http/mcp/tools/community-moderation.js +13 -25
  46. package/dist/http/mcp/tools/community-moderation.js.map +1 -1
  47. package/dist/http/mcp/tools/courses.js +11 -21
  48. package/dist/http/mcp/tools/courses.js.map +1 -1
  49. package/dist/http/mcp/tools/events.d.ts +6 -0
  50. package/dist/http/mcp/tools/events.js +170 -0
  51. package/dist/http/mcp/tools/events.js.map +1 -0
  52. package/dist/http/mcp/tools/index.d.ts +5 -1
  53. package/dist/http/mcp/tools/index.js +45 -10
  54. package/dist/http/mcp/tools/index.js.map +1 -1
  55. package/dist/http/mcp/tools/organization.d.ts +6 -0
  56. package/dist/http/mcp/tools/organization.js +214 -0
  57. package/dist/http/mcp/tools/organization.js.map +1 -0
  58. package/dist/http/mcp/tools/people.js +30 -49
  59. package/dist/http/mcp/tools/people.js.map +1 -1
  60. package/dist/http/plugins/auth.js +37 -8
  61. package/dist/http/plugins/auth.js.map +1 -1
  62. package/dist/http/plugins/error-handler.d.ts +5 -1
  63. package/dist/http/plugins/error-handler.js +13 -3
  64. package/dist/http/plugins/error-handler.js.map +1 -1
  65. package/dist/http/plugins/openapi.js +18 -2
  66. package/dist/http/plugins/openapi.js.map +1 -1
  67. package/dist/http/plugins/platform.d.ts +7 -0
  68. package/dist/http/plugins/platform.js +13 -0
  69. package/dist/http/plugins/platform.js.map +1 -0
  70. package/dist/http/routes/access.js +54 -36
  71. package/dist/http/routes/access.js.map +1 -1
  72. package/dist/http/routes/activities.js +45 -45
  73. package/dist/http/routes/activities.js.map +1 -1
  74. package/dist/http/routes/assets.js +30 -65
  75. package/dist/http/routes/assets.js.map +1 -1
  76. package/dist/http/routes/automations.js +37 -42
  77. package/dist/http/routes/automations.js.map +1 -1
  78. package/dist/http/routes/bundles.js +33 -36
  79. package/dist/http/routes/bundles.js.map +1 -1
  80. package/dist/http/routes/communities.js +144 -136
  81. package/dist/http/routes/communities.js.map +1 -1
  82. package/dist/http/routes/community-moderation.js +83 -137
  83. package/dist/http/routes/community-moderation.js.map +1 -1
  84. package/dist/http/routes/course-analytics.js +32 -15
  85. package/dist/http/routes/course-analytics.js.map +1 -1
  86. package/dist/http/routes/courses.js +25 -32
  87. package/dist/http/routes/courses.js.map +1 -1
  88. package/dist/http/routes/dashboard.js +9 -9
  89. package/dist/http/routes/dashboard.js.map +1 -1
  90. package/dist/http/routes/discussion.js +21 -39
  91. package/dist/http/routes/discussion.js.map +1 -1
  92. package/dist/http/routes/downloads.js +41 -47
  93. package/dist/http/routes/downloads.js.map +1 -1
  94. package/dist/http/routes/entitlements.js +19 -23
  95. package/dist/http/routes/entitlements.js.map +1 -1
  96. package/dist/http/routes/events.d.ts +7 -0
  97. package/dist/http/routes/events.js +333 -0
  98. package/dist/http/routes/events.js.map +1 -0
  99. package/dist/http/routes/integrations.js +40 -73
  100. package/dist/http/routes/integrations.js.map +1 -1
  101. package/dist/http/routes/member/assets.js +10 -13
  102. package/dist/http/routes/member/assets.js.map +1 -1
  103. package/dist/http/routes/member/chats.js +177 -75
  104. package/dist/http/routes/member/chats.js.map +1 -1
  105. package/dist/http/routes/member/comments.js +65 -26
  106. package/dist/http/routes/member/comments.js.map +1 -1
  107. package/dist/http/routes/member/community.js +68 -51
  108. package/dist/http/routes/member/community.js.map +1 -1
  109. package/dist/http/routes/member/courses.js +12 -19
  110. package/dist/http/routes/member/courses.js.map +1 -1
  111. package/dist/http/routes/member/devices.d.ts +7 -0
  112. package/dist/http/routes/member/devices.js +97 -0
  113. package/dist/http/routes/member/devices.js.map +1 -0
  114. package/dist/http/routes/member/downloads.js +6 -9
  115. package/dist/http/routes/member/downloads.js.map +1 -1
  116. package/dist/http/routes/member/events.d.ts +7 -0
  117. package/dist/http/routes/member/events.js +386 -0
  118. package/dist/http/routes/member/events.js.map +1 -0
  119. package/dist/http/routes/member/files.js +71 -46
  120. package/dist/http/routes/member/files.js.map +1 -1
  121. package/dist/http/routes/member/index.js +6 -0
  122. package/dist/http/routes/member/index.js.map +1 -1
  123. package/dist/http/routes/member/notification-settings.d.ts +7 -0
  124. package/dist/http/routes/member/notification-settings.js +100 -0
  125. package/dist/http/routes/member/notification-settings.js.map +1 -0
  126. package/dist/http/routes/member/notifications.js +69 -45
  127. package/dist/http/routes/member/notifications.js.map +1 -1
  128. package/dist/http/routes/member/org.js +5 -15
  129. package/dist/http/routes/member/org.js.map +1 -1
  130. package/dist/http/routes/member/posts.js +94 -95
  131. package/dist/http/routes/member/posts.js.map +1 -1
  132. package/dist/http/routes/member/profile.js +15 -6
  133. package/dist/http/routes/member/profile.js.map +1 -1
  134. package/dist/http/routes/member/progress.js +7 -11
  135. package/dist/http/routes/member/progress.js.map +1 -1
  136. package/dist/http/routes/organizations.js +415 -110
  137. package/dist/http/routes/organizations.js.map +1 -1
  138. package/dist/http/routes/platform.d.ts +7 -0
  139. package/dist/http/routes/platform.js +61 -0
  140. package/dist/http/routes/platform.js.map +1 -0
  141. package/dist/http/routes/students.js +12 -75
  142. package/dist/http/routes/students.js.map +1 -1
  143. package/dist/http/routes/webhooks.js +30 -39
  144. package/dist/http/routes/webhooks.js.map +1 -1
  145. package/dist/http/routes.js +9 -0
  146. package/dist/http/routes.js.map +1 -1
  147. package/dist/http/schemas/access.d.ts +346 -64
  148. package/dist/http/schemas/access.js +39 -12
  149. package/dist/http/schemas/access.js.map +1 -1
  150. package/dist/http/schemas/assets.d.ts +13 -8
  151. package/dist/http/schemas/assets.js +6 -5
  152. package/dist/http/schemas/assets.js.map +1 -1
  153. package/dist/http/schemas/automations.d.ts +4 -8
  154. package/dist/http/schemas/chats.d.ts +238 -36
  155. package/dist/http/schemas/chats.js +14 -7
  156. package/dist/http/schemas/chats.js.map +1 -1
  157. package/dist/http/schemas/community.d.ts +140 -33
  158. package/dist/http/schemas/community.js +25 -9
  159. package/dist/http/schemas/community.js.map +1 -1
  160. package/dist/http/schemas/course-analytics.d.ts +51 -1
  161. package/dist/http/schemas/course-analytics.js +19 -1
  162. package/dist/http/schemas/course-analytics.js.map +1 -1
  163. package/dist/http/schemas/dashboard.d.ts +1 -0
  164. package/dist/http/schemas/dashboard.js +2 -1
  165. package/dist/http/schemas/dashboard.js.map +1 -1
  166. package/dist/http/schemas/discussion.d.ts +45 -73
  167. package/dist/http/schemas/discussion.js +16 -4
  168. package/dist/http/schemas/discussion.js.map +1 -1
  169. package/dist/http/schemas/entitlements.d.ts +9 -12
  170. package/dist/http/schemas/entitlements.js +11 -5
  171. package/dist/http/schemas/entitlements.js.map +1 -1
  172. package/dist/http/schemas/events.d.ts +527 -0
  173. package/dist/http/schemas/events.js +64 -0
  174. package/dist/http/schemas/events.js.map +1 -0
  175. package/dist/http/schemas/files.d.ts +15 -5
  176. package/dist/http/schemas/files.js +10 -2
  177. package/dist/http/schemas/files.js.map +1 -1
  178. package/dist/http/schemas/index.d.ts +14 -12
  179. package/dist/http/schemas/index.js +14 -12
  180. package/dist/http/schemas/invites.d.ts +3 -47
  181. package/dist/http/schemas/invites.js +4 -25
  182. package/dist/http/schemas/invites.js.map +1 -1
  183. package/dist/http/schemas/member.d.ts +94 -7
  184. package/dist/http/schemas/member.js +7 -3
  185. package/dist/http/schemas/member.js.map +1 -1
  186. package/dist/http/schemas/members.d.ts +329 -65
  187. package/dist/http/schemas/members.js +67 -25
  188. package/dist/http/schemas/members.js.map +1 -1
  189. package/dist/http/schemas/notifications.d.ts +502 -175
  190. package/dist/http/schemas/notifications.js +148 -28
  191. package/dist/http/schemas/notifications.js.map +1 -1
  192. package/dist/http/schemas/platform.d.ts +37 -0
  193. package/dist/http/schemas/platform.js +19 -0
  194. package/dist/http/schemas/platform.js.map +1 -0
  195. package/dist/http/schemas/posts.d.ts +92 -38
  196. package/dist/http/schemas/posts.js +4 -4
  197. package/dist/http/schemas/posts.js.map +1 -1
  198. package/dist/http/schemas/profile.d.ts +1 -1
  199. package/dist/http/schemas/shared.d.ts +10 -13
  200. package/dist/http/schemas/shared.js +3 -11
  201. package/dist/http/schemas/shared.js.map +1 -1
  202. package/dist/http/schemas/students.d.ts +22 -0
  203. package/dist/http/schemas/students.js +3 -1
  204. package/dist/http/schemas/students.js.map +1 -1
  205. package/dist/http/scope.d.ts +9 -12
  206. package/dist/http/scope.js +30 -35
  207. package/dist/http/scope.js.map +1 -1
  208. package/dist/http/server.js +9 -0
  209. package/dist/http/server.js.map +1 -1
  210. package/dist/http/shutdown.d.ts +11 -0
  211. package/dist/http/shutdown.js +40 -0
  212. package/dist/http/shutdown.js.map +1 -0
  213. package/dist/http/sse.d.ts +19 -0
  214. package/dist/http/sse.js +104 -0
  215. package/dist/http/sse.js.map +1 -0
  216. package/dist/http/test-caller.d.ts +13 -0
  217. package/dist/http/test-caller.js +14 -0
  218. package/dist/http/test-caller.js.map +1 -0
  219. package/dist/http/well-known.js +2 -2
  220. package/dist/http/well-known.js.map +1 -1
  221. package/dist/index.d.ts +5 -2
  222. package/dist/index.js +4 -2
  223. package/dist/index.js.map +1 -1
  224. package/dist/packages/server/package.js +1 -1
  225. package/package.json +7 -6
  226. package/dist/http/routes/member/actor.d.ts +0 -14
  227. package/dist/http/routes/member/actor.js +0 -11
  228. package/dist/http/routes/member/actor.js.map +0 -1
@@ -0,0 +1,214 @@
1
+ import { AssignmentList, ContentIdParam, CreateRole, MembersPage, MembersQuery, OrgRoleList, OrgRoleView, OrgUserContentParam, OrgUserIdParam, RoleKeyParam, SetRole, UpdateRole } from "../../schemas/members.js";
2
+ import { Organization, UpdateOrganization } from "../../schemas/organizations.js";
3
+ import { CreateInvite, Invite } from "../../schemas/invites.js";
4
+ import "../../schemas/index.js";
5
+ import { defineTool, noInput } from "../tool.js";
6
+ import { z } from "zod";
7
+ //#region src/http/mcp/tools/organization.ts
8
+ const organizationTools = [
9
+ defineTool({
10
+ name: "update_organization",
11
+ title: "Update organization",
12
+ description: "Change the organization's name or slug.",
13
+ kind: "write",
14
+ idempotent: true,
15
+ openWorld: false,
16
+ input: UpdateOrganization,
17
+ output: Organization,
18
+ call: async (input, { container, caller }) => {
19
+ const org = await container.organizations.updateOrganization(input, caller.actor);
20
+ return {
21
+ id: org.id,
22
+ name: org.name,
23
+ slug: org.slug,
24
+ createdAt: org.createdAt.toISOString()
25
+ };
26
+ }
27
+ }),
28
+ defineTool({
29
+ name: "list_roles",
30
+ title: "List roles",
31
+ description: "The organization's roles with their levels, permissions and how many people hold each.",
32
+ kind: "read",
33
+ idempotent: true,
34
+ openWorld: false,
35
+ input: noInput,
36
+ output: z.object({ roles: OrgRoleList }),
37
+ call: async (_input, { container, caller }) => ({ roles: await container.organizations.listRoles(caller.actor) })
38
+ }),
39
+ defineTool({
40
+ name: "get_role",
41
+ title: "Get role",
42
+ description: "One role with its levels and permissions.",
43
+ kind: "read",
44
+ idempotent: true,
45
+ openWorld: false,
46
+ input: RoleKeyParam,
47
+ output: OrgRoleView,
48
+ call: ({ key }, { container, caller }) => container.organizations.getRole(key, caller.actor)
49
+ }),
50
+ defineTool({
51
+ name: "update_role",
52
+ title: "Update role",
53
+ description: "Rename a role, or change its permissions, levels or team flag. Changes need every permission the role holds before and after.",
54
+ kind: "write",
55
+ idempotent: true,
56
+ openWorld: false,
57
+ input: UpdateRole.extend(RoleKeyParam.shape),
58
+ output: OrgRoleView,
59
+ call: ({ key, ...input }, { container, caller }) => container.organizations.updateRole(key, input, caller.actor)
60
+ }),
61
+ defineTool({
62
+ name: "create_role",
63
+ title: "Create role",
64
+ description: "Add a role to the organization, starting from a deployment role (base).",
65
+ kind: "write",
66
+ idempotent: false,
67
+ openWorld: false,
68
+ input: CreateRole,
69
+ output: OrgRoleView,
70
+ call: (input, { container, caller }) => container.organizations.createRole(input, caller.actor)
71
+ }),
72
+ defineTool({
73
+ name: "reset_role",
74
+ title: "Reset role",
75
+ description: "Reset a deployment role to the deployment's version.",
76
+ kind: "destructive",
77
+ idempotent: true,
78
+ openWorld: false,
79
+ input: RoleKeyParam,
80
+ output: OrgRoleView,
81
+ call: ({ key }, { container, caller }) => container.organizations.resetRole(key, caller.actor)
82
+ }),
83
+ defineTool({
84
+ name: "delete_role",
85
+ title: "Delete role",
86
+ description: "Delete a role the organization added. Refused while anyone holds it.",
87
+ kind: "destructive",
88
+ idempotent: false,
89
+ openWorld: false,
90
+ input: RoleKeyParam,
91
+ call: ({ key }, { container, caller }) => container.organizations.deleteRole(key, caller.actor)
92
+ }),
93
+ defineTool({
94
+ name: "list_team",
95
+ title: "List team",
96
+ description: "Everyone with an org role other than Member or a role on a content item, invited people included.",
97
+ kind: "read",
98
+ idempotent: true,
99
+ openWorld: false,
100
+ input: MembersQuery,
101
+ output: MembersPage,
102
+ call: (input, { container, caller }) => container.organizations.listTeam(input, caller.actor)
103
+ }),
104
+ defineTool({
105
+ name: "list_person_roles",
106
+ title: "List a person's roles",
107
+ description: "A person's org role and every role they hold on a content item.",
108
+ kind: "read",
109
+ idempotent: true,
110
+ openWorld: false,
111
+ input: OrgUserIdParam,
112
+ output: z.object({ roles: AssignmentList }),
113
+ call: async ({ orgUserId }, { container, caller }) => ({ roles: await container.organizations.listAssignments(orgUserId, caller.actor) })
114
+ }),
115
+ defineTool({
116
+ name: "list_roles_on_content",
117
+ title: "List roles on content",
118
+ description: "The people with a role on one course, download, community or event.",
119
+ kind: "read",
120
+ idempotent: true,
121
+ openWorld: false,
122
+ input: ContentIdParam,
123
+ output: z.object({ roles: AssignmentList }),
124
+ call: async ({ contentId }, { container, caller }) => ({ roles: await container.organizations.listAssignmentsOn(contentId, caller.actor) })
125
+ }),
126
+ defineTool({
127
+ name: "set_org_role",
128
+ title: "Set org role",
129
+ description: "Set a person's org role. Needs every permission the old and the new role hold.",
130
+ kind: "write",
131
+ idempotent: true,
132
+ openWorld: false,
133
+ input: SetRole.extend(OrgUserIdParam.shape),
134
+ output: z.object({ roles: AssignmentList }),
135
+ call: async ({ orgUserId, roleKey }, { container, caller }) => ({ roles: await container.organizations.setOrgRole(orgUserId, roleKey, caller.actor) })
136
+ }),
137
+ defineTool({
138
+ name: "assign_role",
139
+ title: "Give a role on content",
140
+ description: "Give a person a role on one course, download, community or event. The role must list the item's type.",
141
+ kind: "write",
142
+ idempotent: true,
143
+ openWorld: false,
144
+ input: SetRole.extend(OrgUserContentParam.shape),
145
+ output: z.object({ roles: AssignmentList }),
146
+ call: async ({ orgUserId, contentId, roleKey }, { container, caller }) => ({ roles: await container.organizations.assignRole(orgUserId, contentId, roleKey, caller.actor) })
147
+ }),
148
+ defineTool({
149
+ name: "unassign_role",
150
+ title: "Remove a role on content",
151
+ description: "Remove a person's role on one course, download, community or event.",
152
+ kind: "destructive",
153
+ idempotent: true,
154
+ openWorld: false,
155
+ input: OrgUserContentParam,
156
+ output: z.object({ roles: AssignmentList }),
157
+ call: async ({ orgUserId, contentId }, { container, caller }) => ({ roles: await container.organizations.unassignRole(orgUserId, contentId, caller.actor) })
158
+ }),
159
+ defineTool({
160
+ name: "invite_person",
161
+ title: "Invite person",
162
+ description: "Invite someone by email with an org role (default member). They exist as an invited person at once.",
163
+ kind: "write",
164
+ idempotent: false,
165
+ openWorld: true,
166
+ input: CreateInvite,
167
+ output: Invite,
168
+ call: async (input, { container, caller }) => {
169
+ const invite = await container.organizations.createInvite(input, caller.actor, caller.userId);
170
+ return {
171
+ id: invite.id,
172
+ email: invite.email,
173
+ roleKey: invite.roleKey,
174
+ status: invite.status,
175
+ expiresAt: invite.expiresAt,
176
+ createdAt: invite.createdAt
177
+ };
178
+ }
179
+ }),
180
+ defineTool({
181
+ name: "resend_invite",
182
+ title: "Resend invite",
183
+ description: "Send a person's pending invite email again, with a new link.",
184
+ kind: "write",
185
+ idempotent: false,
186
+ openWorld: true,
187
+ input: OrgUserIdParam,
188
+ call: ({ orgUserId }, { container, caller }) => container.organizations.resendInvite(orgUserId, caller.actor, caller.userId)
189
+ }),
190
+ defineTool({
191
+ name: "cancel_invite",
192
+ title: "Cancel invite",
193
+ description: "Cancel a person's pending invite.",
194
+ kind: "destructive",
195
+ idempotent: false,
196
+ openWorld: false,
197
+ input: OrgUserIdParam,
198
+ call: ({ orgUserId }, { container, caller }) => container.organizations.cancelInvite(orgUserId, caller.actor)
199
+ }),
200
+ defineTool({
201
+ name: "remove_person",
202
+ title: "Remove person",
203
+ description: "Remove a person from the organization, with their API keys and connected apps here. Needs every permission they hold.",
204
+ kind: "destructive",
205
+ idempotent: false,
206
+ openWorld: false,
207
+ input: OrgUserIdParam,
208
+ call: ({ orgUserId }, { container, caller }) => container.organizations.removePerson(orgUserId, caller.actor)
209
+ })
210
+ ];
211
+ //#endregion
212
+ export { organizationTools };
213
+
214
+ //# sourceMappingURL=organization.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"organization.js","names":[],"sources":["../../../../src/http/mcp/tools/organization.ts"],"sourcesContent":["import { z } from 'zod';\nimport {\n AssignmentList,\n ContentIdParam,\n CreateInvite,\n CreateRole,\n Invite,\n MembersPage,\n MembersQuery,\n OrgRoleList,\n OrgRoleView,\n OrgUserContentParam,\n OrgUserIdParam,\n Organization,\n RoleKeyParam,\n SetRole,\n UpdateOrganization,\n UpdateRole,\n} from '../../schemas/index.js';\nimport { defineTool, noInput, type Tool } from '../tool.js';\n\nexport const organizationTools: Tool[] = [\n defineTool({\n name: 'update_organization',\n title: 'Update organization',\n description: \"Change the organization's name or slug.\",\n kind: 'write',\n idempotent: true,\n openWorld: false,\n input: UpdateOrganization,\n output: Organization,\n call: async (input, { container, caller }) => {\n const org = await container.organizations.updateOrganization(input, caller.actor);\n return { id: org.id, name: org.name, slug: org.slug, createdAt: org.createdAt.toISOString() };\n },\n }),\n defineTool({\n name: 'list_roles',\n title: 'List roles',\n description:\n \"The organization's roles with their levels, permissions and how many people hold each.\",\n kind: 'read',\n idempotent: true,\n openWorld: false,\n input: noInput,\n output: z.object({ roles: OrgRoleList }),\n call: async (_input, { container, caller }) => ({\n roles: await container.organizations.listRoles(caller.actor),\n }),\n }),\n defineTool({\n name: 'get_role',\n title: 'Get role',\n description: 'One role with its levels and permissions.',\n kind: 'read',\n idempotent: true,\n openWorld: false,\n input: RoleKeyParam,\n output: OrgRoleView,\n call: ({ key }, { container, caller }) => container.organizations.getRole(key, caller.actor),\n }),\n defineTool({\n name: 'update_role',\n title: 'Update role',\n description:\n 'Rename a role, or change its permissions, levels or team flag. Changes need every permission the role holds before and after.',\n kind: 'write',\n idempotent: true,\n openWorld: false,\n input: UpdateRole.extend(RoleKeyParam.shape),\n output: OrgRoleView,\n call: ({ key, ...input }, { container, caller }) =>\n container.organizations.updateRole(key, input, caller.actor),\n }),\n defineTool({\n name: 'create_role',\n title: 'Create role',\n description: 'Add a role to the organization, starting from a deployment role (base).',\n kind: 'write',\n idempotent: false,\n openWorld: false,\n input: CreateRole,\n output: OrgRoleView,\n call: (input, { container, caller }) => container.organizations.createRole(input, caller.actor),\n }),\n defineTool({\n name: 'reset_role',\n title: 'Reset role',\n description: \"Reset a deployment role to the deployment's version.\",\n kind: 'destructive',\n idempotent: true,\n openWorld: false,\n input: RoleKeyParam,\n output: OrgRoleView,\n call: ({ key }, { container, caller }) => container.organizations.resetRole(key, caller.actor),\n }),\n defineTool({\n name: 'delete_role',\n title: 'Delete role',\n description: 'Delete a role the organization added. Refused while anyone holds it.',\n kind: 'destructive',\n idempotent: false,\n openWorld: false,\n input: RoleKeyParam,\n call: ({ key }, { container, caller }) => container.organizations.deleteRole(key, caller.actor),\n }),\n defineTool({\n name: 'list_team',\n title: 'List team',\n description:\n 'Everyone with an org role other than Member or a role on a content item, invited people included.',\n kind: 'read',\n idempotent: true,\n openWorld: false,\n input: MembersQuery,\n output: MembersPage,\n call: (input, { container, caller }) => container.organizations.listTeam(input, caller.actor),\n }),\n defineTool({\n name: 'list_person_roles',\n title: \"List a person's roles\",\n description: \"A person's org role and every role they hold on a content item.\",\n kind: 'read',\n idempotent: true,\n openWorld: false,\n input: OrgUserIdParam,\n output: z.object({ roles: AssignmentList }),\n call: async ({ orgUserId }, { container, caller }) => ({\n roles: await container.organizations.listAssignments(orgUserId, caller.actor),\n }),\n }),\n defineTool({\n name: 'list_roles_on_content',\n title: 'List roles on content',\n description: 'The people with a role on one course, download, community or event.',\n kind: 'read',\n idempotent: true,\n openWorld: false,\n input: ContentIdParam,\n output: z.object({ roles: AssignmentList }),\n call: async ({ contentId }, { container, caller }) => ({\n roles: await container.organizations.listAssignmentsOn(contentId, caller.actor),\n }),\n }),\n defineTool({\n name: 'set_org_role',\n title: 'Set org role',\n description: \"Set a person's org role. Needs every permission the old and the new role hold.\",\n kind: 'write',\n idempotent: true,\n openWorld: false,\n input: SetRole.extend(OrgUserIdParam.shape),\n output: z.object({ roles: AssignmentList }),\n call: async ({ orgUserId, roleKey }, { container, caller }) => ({\n roles: await container.organizations.setOrgRole(orgUserId, roleKey, caller.actor),\n }),\n }),\n defineTool({\n name: 'assign_role',\n title: 'Give a role on content',\n description:\n \"Give a person a role on one course, download, community or event. The role must list the item's type.\",\n kind: 'write',\n idempotent: true,\n openWorld: false,\n input: SetRole.extend(OrgUserContentParam.shape),\n output: z.object({ roles: AssignmentList }),\n call: async ({ orgUserId, contentId, roleKey }, { container, caller }) => ({\n roles: await container.organizations.assignRole(orgUserId, contentId, roleKey, caller.actor),\n }),\n }),\n defineTool({\n name: 'unassign_role',\n title: 'Remove a role on content',\n description: \"Remove a person's role on one course, download, community or event.\",\n kind: 'destructive',\n idempotent: true,\n openWorld: false,\n input: OrgUserContentParam,\n output: z.object({ roles: AssignmentList }),\n call: async ({ orgUserId, contentId }, { container, caller }) => ({\n roles: await container.organizations.unassignRole(orgUserId, contentId, caller.actor),\n }),\n }),\n defineTool({\n name: 'invite_person',\n title: 'Invite person',\n description:\n 'Invite someone by email with an org role (default member). They exist as an invited person at once.',\n kind: 'write',\n idempotent: false,\n openWorld: true,\n input: CreateInvite,\n output: Invite,\n call: async (input, { container, caller }) => {\n const invite = await container.organizations.createInvite(input, caller.actor, caller.userId);\n return {\n id: invite.id,\n email: invite.email,\n roleKey: invite.roleKey,\n status: invite.status,\n expiresAt: invite.expiresAt,\n createdAt: invite.createdAt,\n };\n },\n }),\n defineTool({\n name: 'resend_invite',\n title: 'Resend invite',\n description: \"Send a person's pending invite email again, with a new link.\",\n kind: 'write',\n idempotent: false,\n openWorld: true,\n input: OrgUserIdParam,\n call: ({ orgUserId }, { container, caller }) =>\n container.organizations.resendInvite(orgUserId, caller.actor, caller.userId),\n }),\n defineTool({\n name: 'cancel_invite',\n title: 'Cancel invite',\n description: \"Cancel a person's pending invite.\",\n kind: 'destructive',\n idempotent: false,\n openWorld: false,\n input: OrgUserIdParam,\n call: ({ orgUserId }, { container, caller }) =>\n container.organizations.cancelInvite(orgUserId, caller.actor),\n }),\n defineTool({\n name: 'remove_person',\n title: 'Remove person',\n description:\n 'Remove a person from the organization, with their API keys and connected apps here. Needs every permission they hold.',\n kind: 'destructive',\n idempotent: false,\n openWorld: false,\n input: OrgUserIdParam,\n call: ({ orgUserId }, { container, caller }) =>\n container.organizations.removePerson(orgUserId, caller.actor),\n }),\n];\n"],"mappings":"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"}
@@ -1,142 +1,123 @@
1
1
  import { CourseIdPathParam } from "../../schemas/activities.js";
2
2
  import { StudentIdParam, StudentsQuery } from "../../schemas/students.js";
3
- import { EntitlementIdParam, EntitlementsQuery, GrantEntitlement, SetEntitlementStatus } from "../../schemas/entitlements.js";
3
+ import { EntitlementsQuery, GrantEntitlement, RevokeEntitlement } from "../../schemas/entitlements.js";
4
4
  import { CreateInvite, Invite } from "../../schemas/invites.js";
5
5
  import { EnrollmentSeriesQuery } from "../../schemas/dashboard.js";
6
+ import { CourseLearnersQuery } from "../../schemas/course-analytics.js";
6
7
  import "../../schemas/index.js";
7
- import { actor, defineTool, noInput } from "../tool.js";
8
+ import { defineTool, noInput } from "../tool.js";
8
9
  //#region src/http/mcp/tools/people.ts
9
10
  const peopleTools = [
10
11
  defineTool({
11
12
  name: "list_students",
12
13
  title: "List students",
13
14
  description: "Search and page through the students with their access count, average progress, status and last activity.",
14
- scope: "students:read",
15
15
  kind: "read",
16
16
  idempotent: true,
17
17
  openWorld: false,
18
18
  input: StudentsQuery,
19
- call: (input, { container, scope }) => container.reporting.admin.listStudents(scope.orgId, input)
19
+ call: (input, { container, caller }) => container.reporting.admin.listStudents(input, caller.actor)
20
20
  }),
21
21
  defineTool({
22
22
  name: "invite_student",
23
23
  title: "Invite student",
24
24
  description: "Invite someone by email as a student. They exist as an invited student at once. sendEmail false skips the email.",
25
- scope: "students:invite",
26
25
  kind: "write",
27
26
  idempotent: false,
28
27
  openWorld: true,
29
- input: CreateInvite.omit({ role: true }),
28
+ input: CreateInvite.omit({ roleKey: true }),
30
29
  output: Invite,
31
- call: (input, { container, scope }) => container.organizations.createInvite(actor(scope), {
32
- orgId: scope.orgId,
33
- inviterUserId: scope.userId,
34
- role: "student",
35
- ...input
36
- })
37
- }),
38
- defineTool({
39
- name: "resend_student_invite",
40
- title: "Resend student invite",
41
- description: "Send a pending student's invite email again.",
42
- scope: "students:invite",
43
- kind: "write",
44
- idempotent: false,
45
- openWorld: true,
46
- input: StudentIdParam,
47
- call: ({ id }, { container, scope }) => container.organizations.resendStudentInvite(actor(scope), {
48
- orgId: scope.orgId,
49
- orgUserId: id,
50
- inviterUserId: scope.userId
51
- })
30
+ call: (input, { container, caller }) => container.organizations.createInvite(input, caller.actor, caller.userId)
52
31
  }),
53
32
  defineTool({
54
33
  name: "list_access",
55
34
  title: "List access",
56
35
  description: "Who has access to what. Filter by student (orgUserId), content, type or status.",
57
- scope: "access:read",
58
36
  kind: "read",
59
37
  idempotent: true,
60
38
  openWorld: false,
61
39
  input: EntitlementsQuery,
62
- call: (input, { container, scope }) => container.entitlements.list(scope.orgId, input)
40
+ call: (input, { container, caller }) => container.entitlements.list(input, caller.actor)
63
41
  }),
64
42
  defineTool({
65
43
  name: "grant_access",
66
44
  title: "Grant access",
67
- description: "Give a student access to one course, download or community (contentId) or to a bundle (bundleId), never both. expiresAt is a date-time, or null for no expiry.",
68
- scope: "access:write",
45
+ description: "Give a student access to one course, download, community or event (contentId) or to a bundle (bundleId), never both. expiresAt is a date-time, or null for no expiry.",
69
46
  kind: "write",
70
47
  idempotent: false,
71
48
  openWorld: false,
72
49
  input: GrantEntitlement,
73
- call: (input, { container, scope }) => container.entitlements.grant(scope.orgId, input)
50
+ call: (input, { container, caller }) => container.entitlements.grant(input, caller.actor)
74
51
  }),
75
52
  defineTool({
76
- name: "set_access_status",
77
- title: "Revoke or reinstate access",
78
- description: "Revoke an access grant, or reinstate a revoked one.",
79
- scope: "access:write",
53
+ name: "revoke_access",
54
+ title: "Revoke access",
55
+ description: "Revoke a student's direct grant to one course, download, community or event (contentId) or to a bundle (bundleId), never both. Access through a bundle stays. grant_access makes it active again.",
80
56
  kind: "destructive",
81
- idempotent: true,
57
+ idempotent: false,
82
58
  openWorld: false,
83
- input: SetEntitlementStatus.extend(EntitlementIdParam.shape),
84
- call: ({ id, status }, { container, scope }) => container.entitlements.setStatus(scope.orgId, id, status)
59
+ input: RevokeEntitlement,
60
+ call: (input, { container, caller }) => container.entitlements.revoke(input, caller.actor)
85
61
  }),
86
62
  defineTool({
87
63
  name: "get_dashboard",
88
64
  title: "Get dashboard",
89
65
  description: "Published and draft course counts, active students, and access grants expiring soon.",
90
- scope: "analytics:read",
91
66
  kind: "read",
92
67
  idempotent: true,
93
68
  openWorld: false,
94
69
  input: noInput,
95
- call: (_input, { container, scope }) => container.reporting.admin.overview(scope.orgId)
70
+ call: (_input, { container, caller }) => container.reporting.admin.overview(caller.actor)
96
71
  }),
97
72
  defineTool({
98
73
  name: "get_enrollment_trend",
99
74
  title: "Get enrollment trend",
100
75
  description: "Daily enrollments across the organization for the last days (default 30, at most 365).",
101
- scope: "analytics:read",
102
76
  kind: "read",
103
77
  idempotent: true,
104
78
  openWorld: false,
105
79
  input: EnrollmentSeriesQuery,
106
- call: ({ days }, { container, scope }) => container.reporting.admin.enrollments(scope.orgId, days)
80
+ call: ({ days }, { container, caller }) => container.reporting.admin.enrollments(days, caller.actor)
107
81
  }),
108
82
  defineTool({
109
83
  name: "get_course_analytics",
110
84
  title: "Get course analytics",
111
85
  description: "Enrolled, started and completed counts, average progress and per-activity engagement for one course.",
112
- scope: "analytics:read",
113
86
  kind: "read",
114
87
  idempotent: true,
115
88
  openWorld: false,
116
89
  input: CourseIdPathParam,
117
- call: ({ courseId }, { container, scope }) => container.reporting.admin.courseAnalytics(scope.orgId, courseId)
90
+ call: ({ courseId }, { container, caller }) => container.reporting.admin.courseAnalytics(courseId, caller.actor)
91
+ }),
92
+ defineTool({
93
+ name: "list_course_learners",
94
+ title: "List course learners",
95
+ description: "Page through a course's learners: everyone with a grant that opens it, directly or through a bundle, active or not, with that grant and their progress. Search by name or email.",
96
+ kind: "read",
97
+ idempotent: true,
98
+ openWorld: false,
99
+ input: CourseLearnersQuery.extend(CourseIdPathParam.shape),
100
+ call: ({ courseId, ...query }, { container, caller }) => container.reporting.admin.courseLearners(courseId, query, caller.actor)
118
101
  }),
119
102
  defineTool({
120
103
  name: "get_course_enrollment_trend",
121
104
  title: "Get course enrollment trend",
122
105
  description: "Daily enrollments in one course for the last days (default 30, at most 365).",
123
- scope: "analytics:read",
124
106
  kind: "read",
125
107
  idempotent: true,
126
108
  openWorld: false,
127
109
  input: EnrollmentSeriesQuery.extend(CourseIdPathParam.shape),
128
- call: ({ courseId, days }, { container, scope }) => container.reporting.admin.courseEnrollments(scope.orgId, courseId, days)
110
+ call: ({ courseId, days }, { container, caller }) => container.reporting.admin.courseEnrollments(courseId, days, caller.actor)
129
111
  }),
130
112
  defineTool({
131
113
  name: "get_student_analytics",
132
114
  title: "Get student analytics",
133
115
  description: "One student's enrolled, started and completed counts, average progress and progress in each course.",
134
- scope: "analytics:read",
135
116
  kind: "read",
136
117
  idempotent: true,
137
118
  openWorld: false,
138
119
  input: StudentIdParam,
139
- call: ({ id }, { container, scope }) => container.reporting.admin.studentAnalytics(scope.orgId, id)
120
+ call: ({ id }, { container, caller }) => container.reporting.admin.studentAnalytics(id, caller.actor)
140
121
  })
141
122
  ];
142
123
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"people.js","names":[],"sources":["../../../../src/http/mcp/tools/people.ts"],"sourcesContent":["import {\n CourseIdPathParam,\n CreateInvite,\n EnrollmentSeriesQuery,\n EntitlementIdParam,\n EntitlementsQuery,\n GrantEntitlement,\n Invite,\n SetEntitlementStatus,\n StudentIdParam,\n StudentsQuery,\n} from '../../schemas/index.js';\nimport { actor, defineTool, noInput, type Tool } from '../tool.js';\n\nexport const peopleTools: Tool[] = [\n defineTool({\n name: 'list_students',\n title: 'List students',\n description:\n 'Search and page through the students with their access count, average progress, status and last activity.',\n scope: 'students:read',\n kind: 'read',\n idempotent: true,\n openWorld: false,\n input: StudentsQuery,\n call: (input, { container, scope }) =>\n container.reporting.admin.listStudents(scope.orgId, input),\n }),\n defineTool({\n name: 'invite_student',\n title: 'Invite student',\n description:\n 'Invite someone by email as a student. They exist as an invited student at once. sendEmail false skips the email.',\n scope: 'students:invite',\n kind: 'write',\n idempotent: false,\n openWorld: true,\n input: CreateInvite.omit({ role: true }),\n output: Invite,\n call: (input, { container, scope }) =>\n container.organizations.createInvite(actor(scope), {\n orgId: scope.orgId,\n inviterUserId: scope.userId,\n role: 'student',\n ...input,\n }),\n }),\n defineTool({\n name: 'resend_student_invite',\n title: 'Resend student invite',\n description: \"Send a pending student's invite email again.\",\n scope: 'students:invite',\n kind: 'write',\n idempotent: false,\n openWorld: true,\n input: StudentIdParam,\n call: ({ id }, { container, scope }) =>\n container.organizations.resendStudentInvite(actor(scope), {\n orgId: scope.orgId,\n orgUserId: id,\n inviterUserId: scope.userId,\n }),\n }),\n defineTool({\n name: 'list_access',\n title: 'List access',\n description: 'Who has access to what. Filter by student (orgUserId), content, type or status.',\n scope: 'access:read',\n kind: 'read',\n idempotent: true,\n openWorld: false,\n input: EntitlementsQuery,\n call: (input, { container, scope }) => container.entitlements.list(scope.orgId, input),\n }),\n defineTool({\n name: 'grant_access',\n title: 'Grant access',\n description:\n 'Give a student access to one course, download or community (contentId) or to a bundle (bundleId), never both. expiresAt is a date-time, or null for no expiry.',\n scope: 'access:write',\n kind: 'write',\n idempotent: false,\n openWorld: false,\n input: GrantEntitlement,\n call: (input, { container, scope }) => container.entitlements.grant(scope.orgId, input),\n }),\n defineTool({\n name: 'set_access_status',\n title: 'Revoke or reinstate access',\n description: 'Revoke an access grant, or reinstate a revoked one.',\n scope: 'access:write',\n kind: 'destructive',\n idempotent: true,\n openWorld: false,\n input: SetEntitlementStatus.extend(EntitlementIdParam.shape),\n call: ({ id, status }, { container, scope }) =>\n container.entitlements.setStatus(scope.orgId, id, status),\n }),\n defineTool({\n name: 'get_dashboard',\n title: 'Get dashboard',\n description:\n 'Published and draft course counts, active students, and access grants expiring soon.',\n scope: 'analytics:read',\n kind: 'read',\n idempotent: true,\n openWorld: false,\n input: noInput,\n call: (_input, { container, scope }) => container.reporting.admin.overview(scope.orgId),\n }),\n defineTool({\n name: 'get_enrollment_trend',\n title: 'Get enrollment trend',\n description:\n 'Daily enrollments across the organization for the last days (default 30, at most 365).',\n scope: 'analytics:read',\n kind: 'read',\n idempotent: true,\n openWorld: false,\n input: EnrollmentSeriesQuery,\n call: ({ days }, { container, scope }) =>\n container.reporting.admin.enrollments(scope.orgId, days),\n }),\n defineTool({\n name: 'get_course_analytics',\n title: 'Get course analytics',\n description:\n 'Enrolled, started and completed counts, average progress and per-activity engagement for one course.',\n scope: 'analytics:read',\n kind: 'read',\n idempotent: true,\n openWorld: false,\n input: CourseIdPathParam,\n call: ({ courseId }, { container, scope }) =>\n container.reporting.admin.courseAnalytics(scope.orgId, courseId),\n }),\n defineTool({\n name: 'get_course_enrollment_trend',\n title: 'Get course enrollment trend',\n description: 'Daily enrollments in one course for the last days (default 30, at most 365).',\n scope: 'analytics:read',\n kind: 'read',\n idempotent: true,\n openWorld: false,\n input: EnrollmentSeriesQuery.extend(CourseIdPathParam.shape),\n call: ({ courseId, days }, { container, scope }) =>\n container.reporting.admin.courseEnrollments(scope.orgId, courseId, days),\n }),\n defineTool({\n name: 'get_student_analytics',\n title: 'Get student analytics',\n description:\n \"One student's enrolled, started and completed counts, average progress and progress in each course.\",\n scope: 'analytics:read',\n kind: 'read',\n idempotent: true,\n openWorld: false,\n input: StudentIdParam,\n call: ({ id }, { container, scope }) =>\n container.reporting.admin.studentAnalytics(scope.orgId, id),\n }),\n];\n"],"mappings":"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"}
1
+ {"version":3,"file":"people.js","names":[],"sources":["../../../../src/http/mcp/tools/people.ts"],"sourcesContent":["import {\n CourseIdPathParam,\n CourseLearnersQuery,\n CreateInvite,\n EnrollmentSeriesQuery,\n EntitlementsQuery,\n GrantEntitlement,\n Invite,\n RevokeEntitlement,\n StudentIdParam,\n StudentsQuery,\n} from '../../schemas/index.js';\nimport { defineTool, noInput, type Tool } from '../tool.js';\n\nexport const peopleTools: Tool[] = [\n defineTool({\n name: 'list_students',\n title: 'List students',\n description:\n 'Search and page through the students with their access count, average progress, status and last activity.',\n kind: 'read',\n idempotent: true,\n openWorld: false,\n input: StudentsQuery,\n call: (input, { container, caller }) =>\n container.reporting.admin.listStudents(input, caller.actor),\n }),\n defineTool({\n name: 'invite_student',\n title: 'Invite student',\n description:\n 'Invite someone by email as a student. They exist as an invited student at once. sendEmail false skips the email.',\n kind: 'write',\n idempotent: false,\n openWorld: true,\n input: CreateInvite.omit({ roleKey: true }),\n output: Invite,\n call: (input, { container, caller }) =>\n container.organizations.createInvite(input, caller.actor, caller.userId),\n }),\n defineTool({\n name: 'list_access',\n title: 'List access',\n description: 'Who has access to what. Filter by student (orgUserId), content, type or status.',\n kind: 'read',\n idempotent: true,\n openWorld: false,\n input: EntitlementsQuery,\n call: (input, { container, caller }) => container.entitlements.list(input, caller.actor),\n }),\n defineTool({\n name: 'grant_access',\n title: 'Grant access',\n description:\n 'Give a student access to one course, download, community or event (contentId) or to a bundle (bundleId), never both. expiresAt is a date-time, or null for no expiry.',\n kind: 'write',\n idempotent: false,\n openWorld: false,\n input: GrantEntitlement,\n call: (input, { container, caller }) => container.entitlements.grant(input, caller.actor),\n }),\n defineTool({\n name: 'revoke_access',\n title: 'Revoke access',\n description:\n \"Revoke a student's direct grant to one course, download, community or event (contentId) or to a bundle (bundleId), never both. Access through a bundle stays. grant_access makes it active again.\",\n kind: 'destructive',\n idempotent: false,\n openWorld: false,\n input: RevokeEntitlement,\n call: (input, { container, caller }) => container.entitlements.revoke(input, caller.actor),\n }),\n defineTool({\n name: 'get_dashboard',\n title: 'Get dashboard',\n description:\n 'Published and draft course counts, active students, and access grants expiring soon.',\n kind: 'read',\n idempotent: true,\n openWorld: false,\n input: noInput,\n call: (_input, { container, caller }) => container.reporting.admin.overview(caller.actor),\n }),\n defineTool({\n name: 'get_enrollment_trend',\n title: 'Get enrollment trend',\n description:\n 'Daily enrollments across the organization for the last days (default 30, at most 365).',\n kind: 'read',\n idempotent: true,\n openWorld: false,\n input: EnrollmentSeriesQuery,\n call: ({ days }, { container, caller }) =>\n container.reporting.admin.enrollments(days, caller.actor),\n }),\n defineTool({\n name: 'get_course_analytics',\n title: 'Get course analytics',\n description:\n 'Enrolled, started and completed counts, average progress and per-activity engagement for one course.',\n kind: 'read',\n idempotent: true,\n openWorld: false,\n input: CourseIdPathParam,\n call: ({ courseId }, { container, caller }) =>\n container.reporting.admin.courseAnalytics(courseId, caller.actor),\n }),\n defineTool({\n name: 'list_course_learners',\n title: 'List course learners',\n description:\n \"Page through a course's learners: everyone with a grant that opens it, directly or through a bundle, active or not, with that grant and their progress. Search by name or email.\",\n kind: 'read',\n idempotent: true,\n openWorld: false,\n input: CourseLearnersQuery.extend(CourseIdPathParam.shape),\n call: ({ courseId, ...query }, { container, caller }) =>\n container.reporting.admin.courseLearners(courseId, query, caller.actor),\n }),\n defineTool({\n name: 'get_course_enrollment_trend',\n title: 'Get course enrollment trend',\n description: 'Daily enrollments in one course for the last days (default 30, at most 365).',\n kind: 'read',\n idempotent: true,\n openWorld: false,\n input: EnrollmentSeriesQuery.extend(CourseIdPathParam.shape),\n call: ({ courseId, days }, { container, caller }) =>\n container.reporting.admin.courseEnrollments(courseId, days, caller.actor),\n }),\n defineTool({\n name: 'get_student_analytics',\n title: 'Get student analytics',\n description:\n \"One student's enrolled, started and completed counts, average progress and progress in each course.\",\n kind: 'read',\n idempotent: true,\n openWorld: false,\n input: StudentIdParam,\n call: ({ id }, { container, caller }) =>\n container.reporting.admin.studentAnalytics(id, caller.actor),\n }),\n];\n"],"mappings":";;;;;;;;;AAcA,MAAa,cAAsB;CACjC,WAAW;EACT,MAAM;EACN,OAAO;EACP,aACE;EACF,MAAM;EACN,YAAY;EACZ,WAAW;EACX,OAAO;EACP,OAAO,OAAO,EAAE,WAAW,aACzB,UAAU,UAAU,MAAM,aAAa,OAAO,OAAO,KAAK;CAC9D,CAAC;CACD,WAAW;EACT,MAAM;EACN,OAAO;EACP,aACE;EACF,MAAM;EACN,YAAY;EACZ,WAAW;EACX,OAAO,aAAa,KAAK,EAAE,SAAS,KAAK,CAAC;EAC1C,QAAQ;EACR,OAAO,OAAO,EAAE,WAAW,aACzB,UAAU,cAAc,aAAa,OAAO,OAAO,OAAO,OAAO,MAAM;CAC3E,CAAC;CACD,WAAW;EACT,MAAM;EACN,OAAO;EACP,aAAa;EACb,MAAM;EACN,YAAY;EACZ,WAAW;EACX,OAAO;EACP,OAAO,OAAO,EAAE,WAAW,aAAa,UAAU,aAAa,KAAK,OAAO,OAAO,KAAK;CACzF,CAAC;CACD,WAAW;EACT,MAAM;EACN,OAAO;EACP,aACE;EACF,MAAM;EACN,YAAY;EACZ,WAAW;EACX,OAAO;EACP,OAAO,OAAO,EAAE,WAAW,aAAa,UAAU,aAAa,MAAM,OAAO,OAAO,KAAK;CAC1F,CAAC;CACD,WAAW;EACT,MAAM;EACN,OAAO;EACP,aACE;EACF,MAAM;EACN,YAAY;EACZ,WAAW;EACX,OAAO;EACP,OAAO,OAAO,EAAE,WAAW,aAAa,UAAU,aAAa,OAAO,OAAO,OAAO,KAAK;CAC3F,CAAC;CACD,WAAW;EACT,MAAM;EACN,OAAO;EACP,aACE;EACF,MAAM;EACN,YAAY;EACZ,WAAW;EACX,OAAO;EACP,OAAO,QAAQ,EAAE,WAAW,aAAa,UAAU,UAAU,MAAM,SAAS,OAAO,KAAK;CAC1F,CAAC;CACD,WAAW;EACT,MAAM;EACN,OAAO;EACP,aACE;EACF,MAAM;EACN,YAAY;EACZ,WAAW;EACX,OAAO;EACP,OAAO,EAAE,QAAQ,EAAE,WAAW,aAC5B,UAAU,UAAU,MAAM,YAAY,MAAM,OAAO,KAAK;CAC5D,CAAC;CACD,WAAW;EACT,MAAM;EACN,OAAO;EACP,aACE;EACF,MAAM;EACN,YAAY;EACZ,WAAW;EACX,OAAO;EACP,OAAO,EAAE,YAAY,EAAE,WAAW,aAChC,UAAU,UAAU,MAAM,gBAAgB,UAAU,OAAO,KAAK;CACpE,CAAC;CACD,WAAW;EACT,MAAM;EACN,OAAO;EACP,aACE;EACF,MAAM;EACN,YAAY;EACZ,WAAW;EACX,OAAO,oBAAoB,OAAO,kBAAkB,KAAK;EACzD,OAAO,EAAE,UAAU,GAAG,SAAS,EAAE,WAAW,aAC1C,UAAU,UAAU,MAAM,eAAe,UAAU,OAAO,OAAO,KAAK;CAC1E,CAAC;CACD,WAAW;EACT,MAAM;EACN,OAAO;EACP,aAAa;EACb,MAAM;EACN,YAAY;EACZ,WAAW;EACX,OAAO,sBAAsB,OAAO,kBAAkB,KAAK;EAC3D,OAAO,EAAE,UAAU,QAAQ,EAAE,WAAW,aACtC,UAAU,UAAU,MAAM,kBAAkB,UAAU,MAAM,OAAO,KAAK;CAC5E,CAAC;CACD,WAAW;EACT,MAAM;EACN,OAAO;EACP,aACE;EACF,MAAM;EACN,YAAY;EACZ,WAAW;EACX,OAAO;EACP,OAAO,EAAE,MAAM,EAAE,WAAW,aAC1B,UAAU,UAAU,MAAM,iBAAiB,IAAI,OAAO,KAAK;CAC/D,CAAC;AACH"}
@@ -1,4 +1,5 @@
1
1
  import { bridgeWebResponse, toWebRequest } from "../web-bridge.js";
2
+ import { isPermission } from "@headless-lms/core/organizations";
2
3
  //#region src/http/plugins/auth.ts
3
4
  /** The request carries no session. The central error handler maps this to 401. */
4
5
  var UnauthorizedError = class extends Error {
@@ -7,6 +8,12 @@ var UnauthorizedError = class extends Error {
7
8
  this.name = "UnauthorizedError";
8
9
  }
9
10
  };
11
+ function carriesKey(request) {
12
+ return request.headers["x-api-key"] !== void 0 || request.headers.authorization !== void 0;
13
+ }
14
+ function callerLimit(limit) {
15
+ return "roleKey" in limit ? limit : { scopes: limit.scopes.filter(isPermission) };
16
+ }
10
17
  function registerAuth(app, appServer) {
11
18
  const { auth } = appServer;
12
19
  const origin = new URL(appServer.mcp.issuer).origin;
@@ -20,32 +27,54 @@ function registerAuth(app, appServer) {
20
27
  await bridgeWebResponse(await auth.handler(toWebRequest(request, origin)), reply);
21
28
  }
22
29
  });
30
+ async function requireKey(request) {
31
+ const grant = await appServer.auth.verifyApiKey(request.headers);
32
+ if (!grant) throw new UnauthorizedError("The API key is not valid");
33
+ const caller = await appServer.organizations.resolveCaller(grant.orgId, grant.userId, callerLimit(grant.limit));
34
+ if (!caller) throw new UnauthorizedError("The API key's organization no longer exists");
35
+ request.authUserId = grant.userId;
36
+ request.userId = caller.userId ?? grant.userId;
37
+ request.orgId = caller.orgId;
38
+ request.authOrgId = grant.orgId;
39
+ request.resolvedCaller = caller;
40
+ appServer.requestContext.set({ orgId: caller.orgId });
41
+ }
23
42
  app.decorate("requireSession", async (request) => {
43
+ if (carriesKey(request)) throw new UnauthorizedError("Sign in to continue; an API key is not accepted here");
24
44
  const session = await appServer.auth.verify(request.headers);
25
45
  if (!session) throw new UnauthorizedError();
26
46
  request.authUser = session.user;
47
+ request.authUserId = session.user.id;
27
48
  request.userId = session.user.id;
28
49
  const authOrgId = session.session.activeOrganizationId;
29
50
  if (authOrgId) {
30
51
  request.authOrgId = authOrgId;
31
- const org = await appServer.organizations.getByExternalId(authOrgId);
32
- if (org) {
33
- request.orgId = org.id;
34
- appServer.requestContext.set({ orgId: org.id });
52
+ const caller = await appServer.organizations.resolveCaller(authOrgId, session.user.id);
53
+ if (caller) {
54
+ request.resolvedCaller = caller;
55
+ request.orgId = caller.orgId;
56
+ appServer.requestContext.set({ orgId: caller.orgId });
35
57
  }
36
58
  }
37
59
  });
38
60
  app.decorate("requireOrgSession", async (request) => {
61
+ if (request.resolvedCaller && request.orgId) return;
62
+ if (carriesKey(request)) {
63
+ await requireKey(request);
64
+ return;
65
+ }
39
66
  const session = await appServer.auth.verify(request.headers);
40
67
  if (!session) throw new UnauthorizedError();
41
68
  const authOrgId = session.session.activeOrganizationId;
42
69
  if (!authOrgId) throw new UnauthorizedError("Choose an organization to continue");
43
- const org = await appServer.organizations.getByExternalId(authOrgId);
44
- if (!org) throw new UnauthorizedError("Your session points to an organization that no longer exists");
70
+ const caller = await appServer.organizations.resolveCaller(authOrgId, session.user.id);
71
+ if (!caller) throw new UnauthorizedError("Your session points to an organization that no longer exists");
45
72
  request.authUser = session.user;
73
+ request.authUserId = session.user.id;
46
74
  request.userId = session.user.id;
47
- request.orgId = org.id;
48
- appServer.requestContext.set({ orgId: org.id });
75
+ request.orgId = caller.orgId;
76
+ request.resolvedCaller = caller;
77
+ appServer.requestContext.set({ orgId: caller.orgId });
49
78
  request.authOrgId = authOrgId;
50
79
  });
51
80
  }
@@ -1 +1 @@
1
- {"version":3,"file":"auth.js","names":[],"sources":["../../../src/http/plugins/auth.ts"],"sourcesContent":["// Everything better-auth touches on the HTTP surface:\n// - the /api/auth/* catch-all bridged to better-auth's Web handler\n// - the `requireSession` / `requireOrgSession` decorators routes guard with\nimport type { FastifyInstance, FastifyRequest } from 'fastify';\nimport type { Container } from '../../app/container.js';\nimport { bridgeWebResponse, toWebRequest } from '../web-bridge.js';\n\n/** The request carries no session. The central error handler maps this to 401. */\nexport class UnauthorizedError extends Error {\n constructor(message = 'Sign in to continue') {\n super(message);\n this.name = 'UnauthorizedError';\n }\n}\n\nexport function registerAuth(app: FastifyInstance, appServer: Container): void {\n const { auth } = appServer;\n const origin = new URL(appServer.mcp.issuer).origin;\n\n app.addContentTypeParser(\n 'application/x-www-form-urlencoded',\n { parseAs: 'string' },\n (_request, body, done) => {\n done(null, body);\n },\n );\n\n // Mount better-auth: a catch-all that bridges Fastify <-> Web Request/Response.\n app.route({\n method: ['GET', 'POST'],\n url: '/api/auth/*',\n async handler(request, reply) {\n await bridgeWebResponse(await auth.handler(toWebRequest(request, origin)), reply);\n },\n });\n\n // Session only, no active org required. For the routes a caller reaches\n // *before* they have an org: creating their first organization, and accepting\n // an invitation (which is what stamps the active org onto the session).\n app.decorate('requireSession', async (request: FastifyRequest) => {\n const session = await appServer.auth.verify(request.headers);\n if (!session) {\n throw new UnauthorizedError();\n }\n request.authUser = session.user;\n request.userId = session.user.id;\n\n const authOrgId = session.session.activeOrganizationId;\n if (authOrgId) {\n request.authOrgId = authOrgId;\n const org = await appServer.organizations.getByExternalId(authOrgId);\n if (org) {\n request.orgId = org.id;\n appServer.requestContext.set({ orgId: org.id });\n }\n }\n });\n\n // Requires a valid session with an active org set — every org-scoped route.\n app.decorate('requireOrgSession', async (request: FastifyRequest) => {\n const session = await appServer.auth.verify(request.headers);\n if (!session) {\n throw new UnauthorizedError();\n }\n const authOrgId = session.session.activeOrganizationId;\n if (!authOrgId) {\n throw new UnauthorizedError('Choose an organization to continue');\n }\n\n const org = await appServer.organizations.getByExternalId(authOrgId);\n if (!org) {\n throw new UnauthorizedError('Your session points to an organization that no longer exists');\n }\n request.authUser = session.user;\n request.userId = session.user.id;\n request.orgId = org.id;\n appServer.requestContext.set({ orgId: org.id });\n // resolveScope requires the auth-provider id too — routes that write back\n // through better-auth key on it.\n request.authOrgId = authOrgId;\n });\n}\n"],"mappings":";;;AAQA,IAAa,oBAAb,cAAuC,MAAM;CAC3C,YAAY,UAAU,uBAAuB;EAC3C,MAAM,OAAO;EACb,KAAK,OAAO;CACd;AACF;AAEA,SAAgB,aAAa,KAAsB,WAA4B;CAC7E,MAAM,EAAE,SAAS;CACjB,MAAM,SAAS,IAAI,IAAI,UAAU,IAAI,MAAM,CAAC,CAAC;CAE7C,IAAI,qBACF,qCACA,EAAE,SAAS,SAAS,IACnB,UAAU,MAAM,SAAS;EACxB,KAAK,MAAM,IAAI;CACjB,CACF;CAGA,IAAI,MAAM;EACR,QAAQ,CAAC,OAAO,MAAM;EACtB,KAAK;EACL,MAAM,QAAQ,SAAS,OAAO;GAC5B,MAAM,kBAAkB,MAAM,KAAK,QAAQ,aAAa,SAAS,MAAM,CAAC,GAAG,KAAK;EAClF;CACF,CAAC;CAKD,IAAI,SAAS,kBAAkB,OAAO,YAA4B;EAChE,MAAM,UAAU,MAAM,UAAU,KAAK,OAAO,QAAQ,OAAO;EAC3D,IAAI,CAAC,SACH,MAAM,IAAI,kBAAkB;EAE9B,QAAQ,WAAW,QAAQ;EAC3B,QAAQ,SAAS,QAAQ,KAAK;EAE9B,MAAM,YAAY,QAAQ,QAAQ;EAClC,IAAI,WAAW;GACb,QAAQ,YAAY;GACpB,MAAM,MAAM,MAAM,UAAU,cAAc,gBAAgB,SAAS;GACnE,IAAI,KAAK;IACP,QAAQ,QAAQ,IAAI;IACpB,UAAU,eAAe,IAAI,EAAE,OAAO,IAAI,GAAG,CAAC;GAChD;EACF;CACF,CAAC;CAGD,IAAI,SAAS,qBAAqB,OAAO,YAA4B;EACnE,MAAM,UAAU,MAAM,UAAU,KAAK,OAAO,QAAQ,OAAO;EAC3D,IAAI,CAAC,SACH,MAAM,IAAI,kBAAkB;EAE9B,MAAM,YAAY,QAAQ,QAAQ;EAClC,IAAI,CAAC,WACH,MAAM,IAAI,kBAAkB,oCAAoC;EAGlE,MAAM,MAAM,MAAM,UAAU,cAAc,gBAAgB,SAAS;EACnE,IAAI,CAAC,KACH,MAAM,IAAI,kBAAkB,8DAA8D;EAE5F,QAAQ,WAAW,QAAQ;EAC3B,QAAQ,SAAS,QAAQ,KAAK;EAC9B,QAAQ,QAAQ,IAAI;EACpB,UAAU,eAAe,IAAI,EAAE,OAAO,IAAI,GAAG,CAAC;EAG9C,QAAQ,YAAY;CACtB,CAAC;AACH"}
1
+ {"version":3,"file":"auth.js","names":[],"sources":["../../../src/http/plugins/auth.ts"],"sourcesContent":["// Everything better-auth touches on the HTTP surface:\n// - the /api/auth/* catch-all bridged to better-auth's Web handler\n// - the `requireSession` / `requireOrgSession` decorators routes guard with\nimport type { FastifyInstance, FastifyRequest } from 'fastify';\nimport type { Container } from '../../app/container.js';\nimport { isPermission } from '@headless-lms/core/organizations';\nimport type { CallerLimit } from '@headless-lms/core/organizations';\nimport type { CredentialLimit } from '@headless-lms/core/types';\nimport { bridgeWebResponse, toWebRequest } from '../web-bridge.js';\n\n/** The request carries no session. The central error handler maps this to 401. */\nexport class UnauthorizedError extends Error {\n constructor(message = 'Sign in to continue') {\n super(message);\n this.name = 'UnauthorizedError';\n }\n}\n\nfunction carriesKey(request: FastifyRequest): boolean {\n return request.headers['x-api-key'] !== undefined || request.headers.authorization !== undefined;\n}\n\nfunction callerLimit(limit: CredentialLimit): CallerLimit {\n return 'roleKey' in limit ? limit : { scopes: limit.scopes.filter(isPermission) };\n}\n\nexport function registerAuth(app: FastifyInstance, appServer: Container): void {\n const { auth } = appServer;\n const origin = new URL(appServer.mcp.issuer).origin;\n\n app.addContentTypeParser(\n 'application/x-www-form-urlencoded',\n { parseAs: 'string' },\n (_request, body, done) => {\n done(null, body);\n },\n );\n\n // Mount better-auth: a catch-all that bridges Fastify <-> Web Request/Response.\n app.route({\n method: ['GET', 'POST'],\n url: '/api/auth/*',\n async handler(request, reply) {\n await bridgeWebResponse(await auth.handler(toWebRequest(request, origin)), reply);\n },\n });\n\n async function requireKey(request: FastifyRequest): Promise<void> {\n const grant = await appServer.auth.verifyApiKey(request.headers);\n if (!grant) {\n throw new UnauthorizedError('The API key is not valid');\n }\n const caller = await appServer.organizations.resolveCaller(\n grant.orgId,\n grant.userId,\n callerLimit(grant.limit),\n );\n if (!caller) {\n throw new UnauthorizedError(\"The API key's organization no longer exists\");\n }\n request.authUserId = grant.userId;\n request.userId = caller.userId ?? grant.userId;\n request.orgId = caller.orgId;\n request.authOrgId = grant.orgId;\n request.resolvedCaller = caller;\n appServer.requestContext.set({ orgId: caller.orgId });\n }\n\n // Session only, no active org required. For the routes a caller reaches\n // *before* they have an org: creating their first organization, and accepting\n // an invitation (which is what stamps the active org onto the session).\n app.decorate('requireSession', async (request: FastifyRequest) => {\n if (carriesKey(request)) {\n throw new UnauthorizedError('Sign in to continue; an API key is not accepted here');\n }\n const session = await appServer.auth.verify(request.headers);\n if (!session) {\n throw new UnauthorizedError();\n }\n request.authUser = session.user;\n request.authUserId = session.user.id;\n request.userId = session.user.id;\n\n const authOrgId = session.session.activeOrganizationId;\n if (authOrgId) {\n request.authOrgId = authOrgId;\n const caller = await appServer.organizations.resolveCaller(authOrgId, session.user.id);\n if (caller) {\n request.resolvedCaller = caller;\n request.orgId = caller.orgId;\n appServer.requestContext.set({ orgId: caller.orgId });\n }\n }\n });\n\n // Requires a valid API key, or a valid session with an active org set — every org-scoped route.\n app.decorate('requireOrgSession', async (request: FastifyRequest) => {\n if (request.resolvedCaller && request.orgId) {\n return;\n }\n if (carriesKey(request)) {\n await requireKey(request);\n return;\n }\n const session = await appServer.auth.verify(request.headers);\n if (!session) {\n throw new UnauthorizedError();\n }\n const authOrgId = session.session.activeOrganizationId;\n if (!authOrgId) {\n throw new UnauthorizedError('Choose an organization to continue');\n }\n\n const caller = await appServer.organizations.resolveCaller(authOrgId, session.user.id);\n if (!caller) {\n throw new UnauthorizedError('Your session points to an organization that no longer exists');\n }\n request.authUser = session.user;\n request.authUserId = session.user.id;\n request.userId = session.user.id;\n request.orgId = caller.orgId;\n request.resolvedCaller = caller;\n appServer.requestContext.set({ orgId: caller.orgId });\n // resolveScope requires the auth-provider id too — routes that write back\n // through better-auth key on it.\n request.authOrgId = authOrgId;\n });\n}\n"],"mappings":"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"}
@@ -1,7 +1,11 @@
1
1
  import { FastifyReply, FastifyRequest } from "fastify";
2
2
  //#region src/http/plugins/error-handler.d.ts
3
+ declare function refusalSubject(error: unknown): {
4
+ permission?: string;
5
+ contentId?: string;
6
+ };
3
7
  declare function errorHandler(error: unknown, request: FastifyRequest, reply: FastifyReply): FastifyReply<import("fastify").RouteGenericInterface, import("fastify").RawServerDefault, import("http").IncomingMessage, import("http").ServerResponse<import("http").IncomingMessage>, unknown, import("fastify").FastifySchema, import("fastify").FastifyTypeProviderDefault, unknown>;
4
8
  declare function notFoundHandler(request: FastifyRequest, reply: FastifyReply): FastifyReply<import("fastify").RouteGenericInterface, import("fastify").RawServerDefault, import("http").IncomingMessage, import("http").ServerResponse<import("http").IncomingMessage>, unknown, import("fastify").FastifySchema, import("fastify").FastifyTypeProviderDefault, unknown>;
5
9
  //#endregion
6
- export { errorHandler, notFoundHandler };
10
+ export { errorHandler, notFoundHandler, refusalSubject };
7
11
  //# sourceMappingURL=error-handler.d.ts.map
@@ -5,6 +5,13 @@ import { isResponseSerializationError } from "fastify-type-provider-zod";
5
5
  function routeOf(request) {
6
6
  return `${request.method} ${request.routeOptions.url ?? "unmatched route"}`;
7
7
  }
8
+ function refusalSubject(error) {
9
+ const subject = error;
10
+ return {
11
+ ...typeof subject?.permission === "string" && { permission: subject.permission },
12
+ ...typeof subject?.contentId === "string" && { contentId: subject.contentId }
13
+ };
14
+ }
8
15
  function errorHandler(error, request, reply) {
9
16
  if (error instanceof ActionInvocationError) {
10
17
  request.log.error({
@@ -24,12 +31,15 @@ function errorHandler(error, request, reply) {
24
31
  error: refusal.error,
25
32
  message: refusal.message,
26
33
  ...refusal.issues && { issues: refusal.issues },
27
- ...refusal.error === "bad_request" && { cause: error.message }
34
+ ...refusal.details && { details: refusal.details },
35
+ ...refusal.error === "bad_request" && { cause: error.message },
36
+ ...refusalSubject(error)
28
37
  }, `${routeOf(request)} refused`);
29
38
  return reply.status(refusal.status).send({
30
39
  error: refusal.error,
31
40
  message: refusal.message,
32
- ...refusal.issues && { issues: refusal.issues }
41
+ ...refusal.issues && { issues: refusal.issues },
42
+ ...refusal.details && { details: refusal.details }
33
43
  });
34
44
  }
35
45
  if (isResponseSerializationError(error)) request.log.error({
@@ -54,6 +64,6 @@ function notFoundHandler(request, reply) {
54
64
  });
55
65
  }
56
66
  //#endregion
57
- export { errorHandler, notFoundHandler };
67
+ export { errorHandler, notFoundHandler, refusalSubject };
58
68
 
59
69
  //# sourceMappingURL=error-handler.js.map