@headless-lms/server 0.2.0 → 0.3.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 (339) hide show
  1. package/dist/_virtual/_rolldown/runtime.js +5 -0
  2. package/dist/app/community-mail.d.ts +26 -0
  3. package/dist/app/community-mail.js +96 -0
  4. package/dist/app/community-mail.js.map +1 -0
  5. package/dist/app/container.d.ts +18 -14
  6. package/dist/app/container.js +136 -43
  7. package/dist/app/container.js.map +1 -1
  8. package/dist/app/mcp-endpoints.d.ts +13 -0
  9. package/dist/app/mcp-endpoints.js +25 -0
  10. package/dist/app/mcp-endpoints.js.map +1 -0
  11. package/dist/http/config.d.ts +2 -1
  12. package/dist/http/errors.d.ts +14 -0
  13. package/dist/http/errors.js +147 -0
  14. package/dist/http/errors.js.map +1 -0
  15. package/dist/http/mcp/catalogue.d.ts +37 -0
  16. package/dist/http/mcp/catalogue.js +69 -0
  17. package/dist/http/mcp/catalogue.js.map +1 -0
  18. package/dist/http/mcp/challenge.d.ts +7 -0
  19. package/dist/http/mcp/challenge.js +35 -0
  20. package/dist/http/mcp/challenge.js.map +1 -0
  21. package/dist/http/mcp/input-schema.d.ts +7 -0
  22. package/dist/http/mcp/input-schema.js +28 -0
  23. package/dist/http/mcp/input-schema.js.map +1 -0
  24. package/dist/http/mcp/route.d.ts +8 -0
  25. package/dist/http/mcp/route.js +75 -0
  26. package/dist/http/mcp/route.js.map +1 -0
  27. package/dist/http/mcp/server.d.ts +8 -0
  28. package/dist/http/mcp/server.js +31 -0
  29. package/dist/http/mcp/server.js.map +1 -0
  30. package/dist/http/mcp/test-support.d.ts +55 -0
  31. package/dist/http/mcp/test-support.js +151 -0
  32. package/dist/http/mcp/test-support.js.map +1 -0
  33. package/dist/http/mcp/tool.d.ts +48 -0
  34. package/dist/http/mcp/tool.js +58 -0
  35. package/dist/http/mcp/tool.js.map +1 -0
  36. package/dist/http/mcp/tools/automations.d.ts +6 -0
  37. package/dist/http/mcp/tools/automations.js +120 -0
  38. package/dist/http/mcp/tools/automations.js.map +1 -0
  39. package/dist/http/mcp/tools/catalog.d.ts +6 -0
  40. package/dist/http/mcp/tools/catalog.js +200 -0
  41. package/dist/http/mcp/tools/catalog.js.map +1 -0
  42. package/dist/http/mcp/tools/comments.d.ts +6 -0
  43. package/dist/http/mcp/tools/comments.js +70 -0
  44. package/dist/http/mcp/tools/comments.js.map +1 -0
  45. package/dist/http/mcp/tools/communities.d.ts +6 -0
  46. package/dist/http/mcp/tools/communities.js +175 -0
  47. package/dist/http/mcp/tools/communities.js.map +1 -0
  48. package/dist/http/mcp/tools/community-members.d.ts +6 -0
  49. package/dist/http/mcp/tools/community-members.js +89 -0
  50. package/dist/http/mcp/tools/community-members.js.map +1 -0
  51. package/dist/http/mcp/tools/community-moderation.d.ts +6 -0
  52. package/dist/http/mcp/tools/community-moderation.js +144 -0
  53. package/dist/http/mcp/tools/community-moderation.js.map +1 -0
  54. package/dist/http/mcp/tools/courses.d.ts +6 -0
  55. package/dist/http/mcp/tools/courses.js +121 -0
  56. package/dist/http/mcp/tools/courses.js.map +1 -0
  57. package/dist/http/mcp/tools/index.d.ts +6 -0
  58. package/dist/http/mcp/tools/index.js +23 -0
  59. package/dist/http/mcp/tools/index.js.map +1 -0
  60. package/dist/http/mcp/tools/people.d.ts +6 -0
  61. package/dist/http/mcp/tools/people.js +145 -0
  62. package/dist/http/mcp/tools/people.js.map +1 -0
  63. package/dist/http/plugins/auth.js +13 -5
  64. package/dist/http/plugins/auth.js.map +1 -1
  65. package/dist/http/plugins/error-handler.d.ts +2 -1
  66. package/dist/http/plugins/error-handler.js +49 -65
  67. package/dist/http/plugins/error-handler.js.map +1 -1
  68. package/dist/http/plugins/openapi.js +49 -2
  69. package/dist/http/plugins/openapi.js.map +1 -1
  70. package/dist/http/routes/access.d.ts +7 -0
  71. package/dist/http/routes/access.js +140 -0
  72. package/dist/http/routes/access.js.map +1 -0
  73. package/dist/http/routes/activities.js +68 -23
  74. package/dist/http/routes/activities.js.map +1 -1
  75. package/dist/http/routes/assets.js +60 -55
  76. package/dist/http/routes/assets.js.map +1 -1
  77. package/dist/http/routes/automations.js +44 -24
  78. package/dist/http/routes/automations.js.map +1 -1
  79. package/dist/http/routes/bundles.js +29 -18
  80. package/dist/http/routes/bundles.js.map +1 -1
  81. package/dist/http/routes/communities.d.ts +7 -0
  82. package/dist/http/routes/communities.js +462 -0
  83. package/dist/http/routes/communities.js.map +1 -0
  84. package/dist/http/routes/community-moderation.d.ts +7 -0
  85. package/dist/http/routes/community-moderation.js +425 -0
  86. package/dist/http/routes/community-moderation.js.map +1 -0
  87. package/dist/http/routes/course-analytics.js +9 -7
  88. package/dist/http/routes/course-analytics.js.map +1 -1
  89. package/dist/http/routes/courses.js +30 -15
  90. package/dist/http/routes/courses.js.map +1 -1
  91. package/dist/http/routes/dashboard.js +15 -6
  92. package/dist/http/routes/dashboard.js.map +1 -1
  93. package/dist/http/routes/discussion.js +34 -26
  94. package/dist/http/routes/discussion.js.map +1 -1
  95. package/dist/http/routes/downloads.js +48 -30
  96. package/dist/http/routes/downloads.js.map +1 -1
  97. package/dist/http/routes/entitlements.js +16 -7
  98. package/dist/http/routes/entitlements.js.map +1 -1
  99. package/dist/http/routes/integrations.js +38 -41
  100. package/dist/http/routes/integrations.js.map +1 -1
  101. package/dist/http/routes/member/actor.d.ts +14 -0
  102. package/dist/http/routes/member/actor.js +11 -0
  103. package/dist/http/routes/member/actor.js.map +1 -0
  104. package/dist/http/routes/member/assets.d.ts +7 -0
  105. package/dist/http/routes/member/assets.js +79 -0
  106. package/dist/http/routes/member/assets.js.map +1 -0
  107. package/dist/http/routes/member/chats.d.ts +7 -0
  108. package/dist/http/routes/member/chats.js +456 -0
  109. package/dist/http/routes/member/chats.js.map +1 -0
  110. package/dist/http/routes/member/comments.d.ts +7 -0
  111. package/dist/http/routes/member/comments.js +187 -0
  112. package/dist/http/routes/member/comments.js.map +1 -0
  113. package/dist/http/routes/member/community.d.ts +7 -0
  114. package/dist/http/routes/member/community.js +288 -0
  115. package/dist/http/routes/member/community.js.map +1 -0
  116. package/dist/http/routes/member/courses.d.ts +7 -0
  117. package/dist/http/routes/member/courses.js +101 -0
  118. package/dist/http/routes/member/courses.js.map +1 -0
  119. package/dist/http/routes/member/downloads.d.ts +7 -0
  120. package/dist/http/routes/member/downloads.js +56 -0
  121. package/dist/http/routes/member/downloads.js.map +1 -0
  122. package/dist/http/routes/member/files.d.ts +7 -0
  123. package/dist/http/routes/member/files.js +184 -0
  124. package/dist/http/routes/member/files.js.map +1 -0
  125. package/dist/http/routes/member/index.d.ts +7 -0
  126. package/dist/http/routes/member/index.js +31 -0
  127. package/dist/http/routes/member/index.js.map +1 -0
  128. package/dist/http/routes/member/notifications.d.ts +7 -0
  129. package/dist/http/routes/member/notifications.js +101 -0
  130. package/dist/http/routes/member/notifications.js.map +1 -0
  131. package/dist/http/routes/member/org.d.ts +7 -0
  132. package/dist/http/routes/member/org.js +56 -0
  133. package/dist/http/routes/member/org.js.map +1 -0
  134. package/dist/http/routes/member/posts.d.ts +7 -0
  135. package/dist/http/routes/member/posts.js +612 -0
  136. package/dist/http/routes/member/posts.js.map +1 -0
  137. package/dist/http/routes/member/profile.d.ts +7 -0
  138. package/dist/http/routes/member/profile.js +48 -0
  139. package/dist/http/routes/member/profile.js.map +1 -0
  140. package/dist/http/routes/member/progress.d.ts +7 -0
  141. package/dist/http/routes/member/progress.js +60 -0
  142. package/dist/http/routes/member/progress.js.map +1 -0
  143. package/dist/http/routes/organizations.js +49 -24
  144. package/dist/http/routes/organizations.js.map +1 -1
  145. package/dist/http/routes/students.js +32 -47
  146. package/dist/http/routes/students.js.map +1 -1
  147. package/dist/http/routes/webhooks.js +26 -18
  148. package/dist/http/routes/webhooks.js.map +1 -1
  149. package/dist/http/routes.js +8 -2
  150. package/dist/http/routes.js.map +1 -1
  151. package/dist/http/schemas/access.d.ts +146 -0
  152. package/dist/http/schemas/access.js +36 -0
  153. package/dist/http/schemas/access.js.map +1 -0
  154. package/dist/http/schemas/activities.d.ts +3 -1
  155. package/dist/http/schemas/activities.js +4 -4
  156. package/dist/http/schemas/activities.js.map +1 -1
  157. package/dist/http/schemas/assets.d.ts +73 -74
  158. package/dist/http/schemas/assets.js +23 -13
  159. package/dist/http/schemas/assets.js.map +1 -1
  160. package/dist/http/schemas/automations.d.ts +10 -0
  161. package/dist/http/schemas/bundles.js +6 -6
  162. package/dist/http/schemas/bundles.js.map +1 -1
  163. package/dist/http/schemas/chats.d.ts +386 -0
  164. package/dist/http/schemas/chats.js +58 -0
  165. package/dist/http/schemas/chats.js.map +1 -0
  166. package/dist/http/schemas/community.d.ts +537 -0
  167. package/dist/http/schemas/community.js +129 -0
  168. package/dist/http/schemas/community.js.map +1 -0
  169. package/dist/http/schemas/content.d.ts +4 -2
  170. package/dist/http/schemas/content.js +11 -8
  171. package/dist/http/schemas/content.js.map +1 -1
  172. package/dist/http/schemas/discussion.d.ts +76 -14
  173. package/dist/http/schemas/discussion.js +12 -11
  174. package/dist/http/schemas/discussion.js.map +1 -1
  175. package/dist/http/schemas/downloads.d.ts +11 -5
  176. package/dist/http/schemas/downloads.js +14 -11
  177. package/dist/http/schemas/downloads.js.map +1 -1
  178. package/dist/http/schemas/entitlements.d.ts +6 -6
  179. package/dist/http/schemas/entitlements.js +6 -6
  180. package/dist/http/schemas/entitlements.js.map +1 -1
  181. package/dist/http/schemas/files.d.ts +45 -0
  182. package/dist/http/schemas/files.js +25 -0
  183. package/dist/http/schemas/files.js.map +1 -0
  184. package/dist/http/schemas/index.d.ts +15 -8
  185. package/dist/http/schemas/index.js +15 -8
  186. package/dist/http/schemas/integrations.d.ts +2 -2
  187. package/dist/http/schemas/invites.d.ts +4 -0
  188. package/dist/http/schemas/invites.js +1 -0
  189. package/dist/http/schemas/invites.js.map +1 -1
  190. package/dist/http/schemas/{learn.d.ts → member.d.ts} +31 -18
  191. package/dist/http/schemas/{learn.js → member.js} +17 -13
  192. package/dist/http/schemas/member.js.map +1 -0
  193. package/dist/http/schemas/members.d.ts +39 -6
  194. package/dist/http/schemas/members.js +17 -3
  195. package/dist/http/schemas/members.js.map +1 -1
  196. package/dist/http/schemas/notifications.d.ts +227 -0
  197. package/dist/http/schemas/notifications.js +44 -0
  198. package/dist/http/schemas/notifications.js.map +1 -0
  199. package/dist/http/schemas/posts.d.ts +316 -0
  200. package/dist/http/schemas/posts.js +47 -0
  201. package/dist/http/schemas/posts.js.map +1 -0
  202. package/dist/http/schemas/profile.d.ts +23 -0
  203. package/dist/http/schemas/profile.js +15 -0
  204. package/dist/http/schemas/profile.js.map +1 -0
  205. package/dist/http/schemas/shared.d.ts +25 -1
  206. package/dist/http/schemas/shared.js +19 -2
  207. package/dist/http/schemas/shared.js.map +1 -1
  208. package/dist/http/schemas/students.d.ts +4 -10
  209. package/dist/http/schemas/students.js +1 -7
  210. package/dist/http/schemas/students.js.map +1 -1
  211. package/dist/http/scope.d.ts +2 -1
  212. package/dist/http/scope.js +20 -4
  213. package/dist/http/scope.js.map +1 -1
  214. package/dist/http/server.js +6 -1
  215. package/dist/http/server.js.map +1 -1
  216. package/dist/http/web-bridge.d.ts +1 -1
  217. package/dist/http/web-bridge.js +2 -2
  218. package/dist/http/web-bridge.js.map +1 -1
  219. package/dist/http/well-known.d.ts +7 -0
  220. package/dist/http/well-known.js +22 -0
  221. package/dist/http/well-known.js.map +1 -0
  222. package/dist/index.d.ts +2 -1
  223. package/dist/index.js +4 -2
  224. package/dist/index.js.map +1 -1
  225. package/dist/node_modules/.pnpm/@jridgewell_sourcemap-codec@1.5.5/node_modules/@jridgewell/sourcemap-codec/dist/sourcemap-codec.js +78 -0
  226. package/dist/node_modules/.pnpm/@jridgewell_sourcemap-codec@1.5.5/node_modules/@jridgewell/sourcemap-codec/dist/sourcemap-codec.js.map +1 -0
  227. package/dist/node_modules/.pnpm/@vitest_expect@2.1.9/node_modules/@vitest/expect/dist/index.js +1267 -0
  228. package/dist/node_modules/.pnpm/@vitest_expect@2.1.9/node_modules/@vitest/expect/dist/index.js.map +1 -0
  229. package/dist/node_modules/.pnpm/@vitest_pretty-format@2.1.9/node_modules/@vitest/pretty-format/dist/index.js +814 -0
  230. package/dist/node_modules/.pnpm/@vitest_pretty-format@2.1.9/node_modules/@vitest/pretty-format/dist/index.js.map +1 -0
  231. package/dist/node_modules/.pnpm/@vitest_runner@2.1.9/node_modules/@vitest/runner/dist/chunk-tasks.js +43 -0
  232. package/dist/node_modules/.pnpm/@vitest_runner@2.1.9/node_modules/@vitest/runner/dist/chunk-tasks.js.map +1 -0
  233. package/dist/node_modules/.pnpm/@vitest_runner@2.1.9/node_modules/@vitest/runner/dist/index.js +527 -0
  234. package/dist/node_modules/.pnpm/@vitest_runner@2.1.9/node_modules/@vitest/runner/dist/index.js.map +1 -0
  235. package/dist/node_modules/.pnpm/@vitest_runner@2.1.9/node_modules/@vitest/runner/dist/utils.js +2 -0
  236. package/dist/node_modules/.pnpm/@vitest_snapshot@2.1.9/node_modules/@vitest/snapshot/dist/index.js +1737 -0
  237. package/dist/node_modules/.pnpm/@vitest_snapshot@2.1.9/node_modules/@vitest/snapshot/dist/index.js.map +1 -0
  238. package/dist/node_modules/.pnpm/@vitest_spy@2.1.9/node_modules/@vitest/spy/dist/index.js +137 -0
  239. package/dist/node_modules/.pnpm/@vitest_spy@2.1.9/node_modules/@vitest/spy/dist/index.js.map +1 -0
  240. package/dist/node_modules/.pnpm/@vitest_utils@2.1.9/node_modules/@vitest/utils/dist/chunk-_commonjsHelpers.js +118 -0
  241. package/dist/node_modules/.pnpm/@vitest_utils@2.1.9/node_modules/@vitest/utils/dist/chunk-_commonjsHelpers.js.map +1 -0
  242. package/dist/node_modules/.pnpm/@vitest_utils@2.1.9/node_modules/@vitest/utils/dist/diff.js +1222 -0
  243. package/dist/node_modules/.pnpm/@vitest_utils@2.1.9/node_modules/@vitest/utils/dist/diff.js.map +1 -0
  244. package/dist/node_modules/.pnpm/@vitest_utils@2.1.9/node_modules/@vitest/utils/dist/error.js +91 -0
  245. package/dist/node_modules/.pnpm/@vitest_utils@2.1.9/node_modules/@vitest/utils/dist/error.js.map +1 -0
  246. package/dist/node_modules/.pnpm/@vitest_utils@2.1.9/node_modules/@vitest/utils/dist/helpers.js +113 -0
  247. package/dist/node_modules/.pnpm/@vitest_utils@2.1.9/node_modules/@vitest/utils/dist/helpers.js.map +1 -0
  248. package/dist/node_modules/.pnpm/@vitest_utils@2.1.9/node_modules/@vitest/utils/dist/index.js +449 -0
  249. package/dist/node_modules/.pnpm/@vitest_utils@2.1.9/node_modules/@vitest/utils/dist/index.js.map +1 -0
  250. package/dist/node_modules/.pnpm/@vitest_utils@2.1.9/node_modules/@vitest/utils/dist/source-map.js +163 -0
  251. package/dist/node_modules/.pnpm/@vitest_utils@2.1.9/node_modules/@vitest/utils/dist/source-map.js.map +1 -0
  252. package/dist/node_modules/.pnpm/chai@5.3.3/node_modules/chai/index.js +3187 -0
  253. package/dist/node_modules/.pnpm/chai@5.3.3/node_modules/chai/index.js.map +1 -0
  254. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/arguments.js +11 -0
  255. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/arguments.js.map +1 -0
  256. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/array.js +16 -0
  257. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/array.js.map +1 -0
  258. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/bigint.js +11 -0
  259. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/bigint.js.map +1 -0
  260. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/class.js +15 -0
  261. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/class.js.map +1 -0
  262. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/date.js +13 -0
  263. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/date.js.map +1 -0
  264. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/error.js +34 -0
  265. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/error.js.map +1 -0
  266. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/function.js +12 -0
  267. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/function.js.map +1 -0
  268. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/helpers.js +122 -0
  269. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/helpers.js.map +1 -0
  270. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/html.js +39 -0
  271. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/html.js.map +1 -0
  272. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/index.js +99 -0
  273. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/index.js.map +1 -0
  274. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/map.js +25 -0
  275. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/map.js.map +1 -0
  276. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/number.js +14 -0
  277. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/number.js.map +1 -0
  278. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/object.js +21 -0
  279. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/object.js.map +1 -0
  280. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/promise.js +6 -0
  281. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/promise.js.map +1 -0
  282. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/regexp.js +12 -0
  283. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/regexp.js.map +1 -0
  284. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/set.js +18 -0
  285. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/set.js.map +1 -0
  286. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/string.js +24 -0
  287. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/string.js.map +1 -0
  288. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/symbol.js +9 -0
  289. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/symbol.js.map +1 -0
  290. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/typedarray.js +30 -0
  291. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/typedarray.js.map +1 -0
  292. package/dist/node_modules/.pnpm/magic-string@0.30.21/node_modules/magic-string/dist/magic-string.es.js +939 -0
  293. package/dist/node_modules/.pnpm/magic-string@0.30.21/node_modules/magic-string/dist/magic-string.es.js.map +1 -0
  294. package/dist/node_modules/.pnpm/pathe@1.1.2/node_modules/pathe/dist/shared/pathe.ff20891b.js +82 -0
  295. package/dist/node_modules/.pnpm/pathe@1.1.2/node_modules/pathe/dist/shared/pathe.ff20891b.js.map +1 -0
  296. package/dist/node_modules/.pnpm/tinyrainbow@1.2.0/node_modules/tinyrainbow/dist/chunk-BVHSVHOK.js +82 -0
  297. package/dist/node_modules/.pnpm/tinyrainbow@1.2.0/node_modules/tinyrainbow/dist/chunk-BVHSVHOK.js.map +1 -0
  298. package/dist/node_modules/.pnpm/tinyrainbow@1.2.0/node_modules/tinyrainbow/dist/node.js +8 -0
  299. package/dist/node_modules/.pnpm/tinyrainbow@1.2.0/node_modules/tinyrainbow/dist/node.js.map +1 -0
  300. package/dist/node_modules/.pnpm/tinyspy@3.0.2/node_modules/tinyspy/dist/index.js +83 -0
  301. package/dist/node_modules/.pnpm/tinyspy@3.0.2/node_modules/tinyspy/dist/index.js.map +1 -0
  302. package/dist/node_modules/.pnpm/vitest@2.1.9_@types_node@22.20.0_jsdom@25.0.1_lightningcss@1.32.0/node_modules/vitest/dist/chunks/_commonjsHelpers.BFTU3MAI.js +9 -0
  303. package/dist/node_modules/.pnpm/vitest@2.1.9_@types_node@22.20.0_jsdom@25.0.1_lightningcss@1.32.0/node_modules/vitest/dist/chunks/_commonjsHelpers.BFTU3MAI.js.map +1 -0
  304. package/dist/node_modules/.pnpm/vitest@2.1.9_@types_node@22.20.0_jsdom@25.0.1_lightningcss@1.32.0/node_modules/vitest/dist/chunks/date.W2xKR2qe.js +51 -0
  305. package/dist/node_modules/.pnpm/vitest@2.1.9_@types_node@22.20.0_jsdom@25.0.1_lightningcss@1.32.0/node_modules/vitest/dist/chunks/date.W2xKR2qe.js.map +1 -0
  306. package/dist/node_modules/.pnpm/vitest@2.1.9_@types_node@22.20.0_jsdom@25.0.1_lightningcss@1.32.0/node_modules/vitest/dist/chunks/utils.C8RiOc4B.js +48 -0
  307. package/dist/node_modules/.pnpm/vitest@2.1.9_@types_node@22.20.0_jsdom@25.0.1_lightningcss@1.32.0/node_modules/vitest/dist/chunks/utils.C8RiOc4B.js.map +1 -0
  308. package/dist/node_modules/.pnpm/vitest@2.1.9_@types_node@22.20.0_jsdom@25.0.1_lightningcss@1.32.0/node_modules/vitest/dist/chunks/vi.DgezovHB.js +2426 -0
  309. package/dist/node_modules/.pnpm/vitest@2.1.9_@types_node@22.20.0_jsdom@25.0.1_lightningcss@1.32.0/node_modules/vitest/dist/chunks/vi.DgezovHB.js.map +1 -0
  310. package/dist/packages/server/package.js +6 -0
  311. package/dist/packages/server/package.js.map +1 -0
  312. package/package.json +9 -7
  313. package/dist/app/notifications.d.ts +0 -7
  314. package/dist/app/notifications.js +0 -26
  315. package/dist/app/notifications.js.map +0 -1
  316. package/dist/http/routes/learn/assets.d.ts +0 -8
  317. package/dist/http/routes/learn/assets.js +0 -32
  318. package/dist/http/routes/learn/assets.js.map +0 -1
  319. package/dist/http/routes/learn/comments.d.ts +0 -7
  320. package/dist/http/routes/learn/comments.js +0 -175
  321. package/dist/http/routes/learn/comments.js.map +0 -1
  322. package/dist/http/routes/learn/courses.d.ts +0 -7
  323. package/dist/http/routes/learn/courses.js +0 -98
  324. package/dist/http/routes/learn/courses.js.map +0 -1
  325. package/dist/http/routes/learn/downloads.d.ts +0 -7
  326. package/dist/http/routes/learn/downloads.js +0 -76
  327. package/dist/http/routes/learn/downloads.js.map +0 -1
  328. package/dist/http/routes/learn/index.d.ts +0 -7
  329. package/dist/http/routes/learn/index.js +0 -19
  330. package/dist/http/routes/learn/index.js.map +0 -1
  331. package/dist/http/routes/learn/org.d.ts +0 -7
  332. package/dist/http/routes/learn/org.js +0 -50
  333. package/dist/http/routes/learn/org.js.map +0 -1
  334. package/dist/http/routes/learn/progress.d.ts +0 -7
  335. package/dist/http/routes/learn/progress.js +0 -63
  336. package/dist/http/routes/learn/progress.js.map +0 -1
  337. package/dist/http/schemas/learn.js.map +0 -1
  338. package/dist/package.js +0 -6
  339. package/dist/package.js.map +0 -1
@@ -0,0 +1,48 @@
1
+ import { refusals } from "../../schemas/shared.js";
2
+ import { UpdateProfile, UserProfile } from "../../schemas/profile.js";
3
+ import "../../schemas/index.js";
4
+ import { actorOf } from "./actor.js";
5
+ //#region src/http/routes/member/profile.ts
6
+ async function memberProfileRoutes(app, container) {
7
+ const r = app.withTypeProvider();
8
+ const tags = ["Profile"];
9
+ r.route({
10
+ method: "GET",
11
+ url: "/api/member/profile",
12
+ preHandler: app.requireOrgSession,
13
+ schema: {
14
+ operationId: "getMemberProfile",
15
+ tags,
16
+ summary: "Read your profile",
17
+ description: "For any signed-in person. The profile is yours across organizations; `imageUrl` is set only when the photo was uploaded in this organization. Before you save one, every field is empty.",
18
+ response: { 200: UserProfile }
19
+ },
20
+ handler: async (req) => {
21
+ return container.identity.profileOf(req.orgId, req.userId);
22
+ }
23
+ });
24
+ r.route({
25
+ method: "PATCH",
26
+ url: "/api/member/profile",
27
+ preHandler: app.requireOrgSession,
28
+ schema: {
29
+ operationId: "updateMemberProfile",
30
+ tags,
31
+ summary: "Update your profile",
32
+ description: "For a member of the organization. Changes only the fields sent; null clears one. 404 when `imageAssetId` is not a finalized image you uploaded here.",
33
+ body: UpdateProfile,
34
+ response: {
35
+ 200: UserProfile,
36
+ ...refusals(403, 404)
37
+ }
38
+ },
39
+ handler: async (req) => {
40
+ const orgUser = await actorOf(container, req.orgId, req.userId);
41
+ return container.identity.updateProfile(req.orgId, req.userId, req.userId, orgUser.id, req.body);
42
+ }
43
+ });
44
+ }
45
+ //#endregion
46
+ export { memberProfileRoutes };
47
+
48
+ //# sourceMappingURL=profile.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"profile.js","names":[],"sources":["../../../../src/http/routes/member/profile.ts"],"sourcesContent":["import type { FastifyInstance } from 'fastify';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport type { Container } from '../../../app/container.js';\nimport { actorOf } from './actor.js';\nimport { UpdateProfile, UserProfile, refusals } from '../../schemas/index.js';\n\nexport async function memberProfileRoutes(\n app: FastifyInstance,\n container: Container,\n): Promise<void> {\n const r = app.withTypeProvider<ZodTypeProvider>();\n const tags = ['Profile'];\n\n r.route({\n method: 'GET',\n url: '/api/member/profile',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getMemberProfile',\n tags,\n summary: 'Read your profile',\n description:\n 'For any signed-in person. The profile is yours across organizations; `imageUrl` is set only when the photo was uploaded in this organization. Before you save one, every field is empty.',\n response: { 200: UserProfile },\n },\n handler: async (req) => {\n return container.identity.profileOf(req.orgId, req.userId);\n },\n });\n\n r.route({\n method: 'PATCH',\n url: '/api/member/profile',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'updateMemberProfile',\n tags,\n summary: 'Update your profile',\n description:\n 'For a member of the organization. Changes only the fields sent; null clears one. 404 when `imageAssetId` is not a finalized image you uploaded here.',\n body: UpdateProfile,\n response: { 200: UserProfile, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const orgUser = await actorOf(container, req.orgId, req.userId);\n return container.identity.updateProfile(\n req.orgId,\n req.userId,\n req.userId,\n orgUser.id,\n req.body,\n );\n },\n });\n}\n"],"mappings":";;;;;AAMA,eAAsB,oBACpB,KACA,WACe;CACf,MAAM,IAAI,IAAI,iBAAkC;CAChD,MAAM,OAAO,CAAC,SAAS;CAEvB,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb;GACA,SAAS;GACT,aACE;GACF,UAAU,EAAE,KAAK,YAAY;EAC/B;EACA,SAAS,OAAO,QAAQ;GACtB,OAAO,UAAU,SAAS,UAAU,IAAI,OAAO,IAAI,MAAM;EAC3D;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb;GACA,SAAS;GACT,aACE;GACF,MAAM;GACN,UAAU;IAAE,KAAK;IAAa,GAAG,SAAS,KAAK,GAAG;GAAE;EACtD;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,UAAU,MAAM,QAAQ,WAAW,IAAI,OAAO,IAAI,MAAM;GAC9D,OAAO,UAAU,SAAS,cACxB,IAAI,OACJ,IAAI,QACJ,IAAI,QACJ,QAAQ,IACR,IAAI,IACN;EACF;CACF,CAAC;AACH"}
@@ -0,0 +1,7 @@
1
+ import { Container } from "../../../app/container.js";
2
+ import { FastifyInstance } from "fastify";
3
+ //#region src/http/routes/member/progress.d.ts
4
+ declare function memberProgressRoutes(app: FastifyInstance, container: Container): Promise<void>;
5
+ //#endregion
6
+ export { memberProgressRoutes };
7
+ //# sourceMappingURL=progress.d.ts.map
@@ -0,0 +1,60 @@
1
+ import { refusals } from "../../schemas/shared.js";
2
+ import { CourseProgress, MemberCourseIdParam, ProgressStatus, ReportProgress } from "../../schemas/member.js";
3
+ import "../../schemas/index.js";
4
+ import { actorOf } from "./actor.js";
5
+ import { NotFoundError } from "@headless-lms/core/shared/errors";
6
+ //#region src/http/routes/member/progress.ts
7
+ async function memberProgressRoutes(app, container) {
8
+ const r = app.withTypeProvider();
9
+ const member = container.reporting.member;
10
+ r.route({
11
+ method: "POST",
12
+ url: "/api/member/progress",
13
+ preHandler: app.requireOrgSession,
14
+ schema: {
15
+ operationId: "reportProgress",
16
+ tags: ["Progress"],
17
+ summary: "Report usage on an activity; the progress service decides completion",
18
+ description: "For a member who can open the course. Reports facts, never a decision: the service decides whether the activity is complete. 404 when the activity or its course is not published, or you cannot open it.",
19
+ body: ReportProgress,
20
+ response: {
21
+ 200: ProgressStatus,
22
+ ...refusals(403, 404)
23
+ }
24
+ },
25
+ handler: async (req) => {
26
+ const orgUser = await actorOf(container, req.orgId, req.userId);
27
+ return { status: (await container.progress.report(req.orgId, {
28
+ orgUserId: orgUser.id,
29
+ activityId: req.body.activity,
30
+ reports: req.body.reports
31
+ })).completedAt ? "completed" : "in-progress" };
32
+ }
33
+ });
34
+ r.route({
35
+ method: "GET",
36
+ url: "/api/member/courses/:courseId/progress",
37
+ preHandler: app.requireOrgSession,
38
+ schema: {
39
+ operationId: "getMemberCourseProgress",
40
+ tags: ["Progress"],
41
+ summary: "Your progress in one course",
42
+ description: "For a member of the organization, derived from the course's current structure. 404 when the course is not published or you do not hold it.",
43
+ params: MemberCourseIdParam,
44
+ response: {
45
+ 200: CourseProgress,
46
+ ...refusals(403, 404)
47
+ }
48
+ },
49
+ handler: async (req) => {
50
+ const orgUser = await actorOf(container, req.orgId, req.userId);
51
+ const view = await member.courseProgress(req.orgId, orgUser.id, req.params.courseId);
52
+ if (!view) throw new NotFoundError("Course", req.params.courseId);
53
+ return view;
54
+ }
55
+ });
56
+ }
57
+ //#endregion
58
+ export { memberProgressRoutes };
59
+
60
+ //# sourceMappingURL=progress.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"progress.js","names":[],"sources":["../../../../src/http/routes/member/progress.ts"],"sourcesContent":["import type { FastifyInstance } from 'fastify';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport {\n CourseProgress,\n MemberCourseIdParam,\n ProgressStatus,\n ReportProgress,\n refusals,\n} from '../../schemas/index.js';\nimport { NotFoundError } from '@headless-lms/core/shared/errors';\nimport type { Container } from '../../../app/container.js';\nimport { actorOf } from './actor.js';\n\nexport async function memberProgressRoutes(\n app: FastifyInstance,\n container: Container,\n): Promise<void> {\n const r = app.withTypeProvider<ZodTypeProvider>();\n const member = container.reporting.member;\n\n r.route({\n method: 'POST',\n url: '/api/member/progress',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'reportProgress',\n tags: ['Progress'],\n summary: 'Report usage on an activity; the progress service decides completion',\n description:\n 'For a member who can open the course. Reports facts, never a decision: the service decides whether the activity is complete. 404 when the activity or its course is not published, or you cannot open it.',\n body: ReportProgress,\n response: { 200: ProgressStatus, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const orgUser = await actorOf(container, req.orgId, req.userId);\n const record = await container.progress.report(req.orgId, {\n orgUserId: orgUser.id,\n activityId: req.body.activity,\n reports: req.body.reports,\n });\n return { status: record.completedAt ? ('completed' as const) : ('in-progress' as const) };\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/member/courses/:courseId/progress',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getMemberCourseProgress',\n tags: ['Progress'],\n summary: 'Your progress in one course',\n description:\n \"For a member of the organization, derived from the course's current structure. 404 when the course is not published or you do not hold it.\",\n params: MemberCourseIdParam,\n response: { 200: CourseProgress, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const orgUser = await actorOf(container, req.orgId, req.userId);\n const view = await member.courseProgress(req.orgId, orgUser.id, req.params.courseId);\n if (!view) {\n throw new NotFoundError('Course', req.params.courseId);\n }\n return view;\n },\n });\n}\n"],"mappings":";;;;;;AAaA,eAAsB,qBACpB,KACA,WACe;CACf,MAAM,IAAI,IAAI,iBAAkC;CAChD,MAAM,SAAS,UAAU,UAAU;CAEnC,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,UAAU;GACjB,SAAS;GACT,aACE;GACF,MAAM;GACN,UAAU;IAAE,KAAK;IAAgB,GAAG,SAAS,KAAK,GAAG;GAAE;EACzD;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,UAAU,MAAM,QAAQ,WAAW,IAAI,OAAO,IAAI,MAAM;GAM9D,OAAO,EAAE,SAAQ,MALI,UAAU,SAAS,OAAO,IAAI,OAAO;IACxD,WAAW,QAAQ;IACnB,YAAY,IAAI,KAAK;IACrB,SAAS,IAAI,KAAK;GACpB,CAAC,EAAA,CACuB,cAAe,cAAyB,cAAwB;EAC1F;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,UAAU;GACjB,SAAS;GACT,aACE;GACF,QAAQ;GACR,UAAU;IAAE,KAAK;IAAgB,GAAG,SAAS,KAAK,GAAG;GAAE;EACzD;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,UAAU,MAAM,QAAQ,WAAW,IAAI,OAAO,IAAI,MAAM;GAC9D,MAAM,OAAO,MAAM,OAAO,eAAe,IAAI,OAAO,QAAQ,IAAI,IAAI,OAAO,QAAQ;GACnF,IAAI,CAAC,MACH,MAAM,IAAI,cAAc,UAAU,IAAI,OAAO,QAAQ;GAEvD,OAAO;EACT;CACF,CAAC;AACH"}
@@ -1,11 +1,11 @@
1
+ import { refusals } from "../schemas/shared.js";
1
2
  import { resolveScope } from "../scope.js";
2
- import { ErrorBody } from "../schemas/shared.js";
3
3
  import { CreateOrganization, Organization, UpdateOrganization } from "../schemas/organizations.js";
4
- import { Member, MemberIdParam, MembersPage, MembersQuery, UpdateMemberRole } from "../schemas/members.js";
4
+ import { MemberIdParam, MemberRoleChange, MembersPage, MembersQuery, UpdateMemberRole } from "../schemas/members.js";
5
5
  import { AcceptInvite, AcceptInviteResult, CreateInvite, GetInviteResult, Invite, InviteTokenParam } from "../schemas/invites.js";
6
6
  import "../schemas/index.js";
7
- import { NotFoundError } from "@headless-lms/core/shared/errors";
8
7
  import { z } from "zod";
8
+ import { NotFoundError } from "@headless-lms/core/shared/errors";
9
9
  //#region src/http/routes/organizations.ts
10
10
  async function organizationsRoutes(app, container) {
11
11
  const r = app.withTypeProvider();
@@ -19,6 +19,7 @@ async function organizationsRoutes(app, container) {
19
19
  operationId: "createOrganization",
20
20
  tags,
21
21
  summary: "Create an organization and make it active",
22
+ description: "For any signed-in person, including one with no organization yet. The caller becomes its owner.",
22
23
  body: CreateOrganization,
23
24
  response: { 201: Organization }
24
25
  },
@@ -39,11 +40,12 @@ async function organizationsRoutes(app, container) {
39
40
  schema: {
40
41
  operationId: "updateOrganization",
41
42
  tags,
42
- summary: "Update the active organization",
43
+ summary: "Update the active organization (staff)",
44
+ description: "Owner or admin. Changes its name or slug; 409 when the slug is taken.",
43
45
  body: UpdateOrganization,
44
46
  response: {
45
47
  200: Organization,
46
- 409: ErrorBody
48
+ ...refusals(403, 409)
47
49
  }
48
50
  },
49
51
  handler: async (req, reply) => {
@@ -64,9 +66,13 @@ async function organizationsRoutes(app, container) {
64
66
  schema: {
65
67
  operationId: "listMembers",
66
68
  tags,
67
- summary: "List organization members",
69
+ summary: "List the organization's team members (staff)",
70
+ description: "Staff only (owner, admin, instructor). The team: everyone in the organization who is not a student.",
68
71
  querystring: MembersQuery,
69
- response: { 200: MembersPage }
72
+ response: {
73
+ 200: MembersPage,
74
+ ...refusals(403)
75
+ }
70
76
  },
71
77
  handler: async (req) => {
72
78
  const scope = await resolveScope(container, req);
@@ -80,13 +86,13 @@ async function organizationsRoutes(app, container) {
80
86
  schema: {
81
87
  operationId: "updateMemberRole",
82
88
  tags,
83
- summary: "Change a member's role",
89
+ summary: "Change a member's role (staff)",
90
+ description: "Owner or admin only. The owner role can be neither granted nor changed (409), and only a moderator can become a student.",
84
91
  params: MemberIdParam,
85
92
  body: UpdateMemberRole,
86
93
  response: {
87
- 200: Member,
88
- 404: ErrorBody,
89
- 409: ErrorBody
94
+ 200: MemberRoleChange,
95
+ ...refusals(403, 404, 409)
90
96
  }
91
97
  },
92
98
  handler: async (req) => {
@@ -96,7 +102,10 @@ async function organizationsRoutes(app, container) {
96
102
  authOrgId: scope.authOrgId,
97
103
  headers: req.headers
98
104
  };
99
- const member = await organizations.updateMemberRole(ctx, req.params.id, req.body.role);
105
+ const member = await organizations.updateMemberRole(ctx, {
106
+ id: scope.orgUserId,
107
+ role: scope.role
108
+ }, req.params.id, req.body.role);
100
109
  if (!member) throw new NotFoundError("Member", req.params.id);
101
110
  return member;
102
111
  }
@@ -108,12 +117,12 @@ async function organizationsRoutes(app, container) {
108
117
  schema: {
109
118
  operationId: "removeMember",
110
119
  tags,
111
- summary: "Remove an organization member",
120
+ summary: "Remove an organization member (staff)",
121
+ description: "Owner or admin only. Removing a pending member cancels their invite. The owner cannot be removed (409).",
112
122
  params: MemberIdParam,
113
123
  response: {
114
124
  204: z.void(),
115
- 404: ErrorBody,
116
- 409: ErrorBody
125
+ ...refusals(403, 404, 409)
117
126
  }
118
127
  },
119
128
  handler: async (req, reply) => {
@@ -123,7 +132,10 @@ async function organizationsRoutes(app, container) {
123
132
  authOrgId: scope.authOrgId,
124
133
  headers: req.headers
125
134
  };
126
- if (!await organizations.removeMember(ctx, req.params.id)) throw new NotFoundError("Member", req.params.id);
135
+ if (!await organizations.removeMember(ctx, {
136
+ id: scope.orgUserId,
137
+ role: scope.role
138
+ }, req.params.id)) throw new NotFoundError("Member", req.params.id);
127
139
  return reply.code(204).send();
128
140
  }
129
141
  });
@@ -134,16 +146,20 @@ async function organizationsRoutes(app, container) {
134
146
  schema: {
135
147
  operationId: "createInvite",
136
148
  tags,
137
- summary: "Invite a member or student into the active organization",
149
+ summary: "Invite a member or student into the active organization (staff)",
150
+ description: "Owner or admin only. A student is created by this call, so their access can be granted before they accept. 409 when a student with that email is already in the organization.",
138
151
  body: CreateInvite,
139
152
  response: {
140
153
  201: Invite,
141
- 409: ErrorBody
154
+ ...refusals(403, 409)
142
155
  }
143
156
  },
144
- handler: async (req) => {
157
+ handler: async (req, reply) => {
145
158
  const scope = await resolveScope(container, req);
146
159
  const invite = await organizations.createInvite({
160
+ id: scope.orgUserId,
161
+ role: scope.role
162
+ }, {
147
163
  orgId: scope.orgId,
148
164
  inviterUserId: scope.userId,
149
165
  email: req.body.email,
@@ -152,8 +168,15 @@ async function organizationsRoutes(app, container) {
152
168
  ...req.body.lastName !== void 0 && { lastName: req.body.lastName },
153
169
  sendEmail: req.body.sendEmail
154
170
  });
155
- req.log.debug({ invite }, "created invite");
156
- return invite;
171
+ req.log.debug({ inviteId: invite.id }, "created invite");
172
+ return reply.code(201).send({
173
+ id: invite.id,
174
+ email: invite.email,
175
+ role: invite.role,
176
+ status: invite.status,
177
+ expiresAt: invite.expiresAt,
178
+ createdAt: invite.createdAt
179
+ });
157
180
  }
158
181
  });
159
182
  r.route({
@@ -163,10 +186,11 @@ async function organizationsRoutes(app, container) {
163
186
  operationId: "getInvite",
164
187
  tags,
165
188
  summary: "Read an invite by its token",
189
+ description: "Public: no session needed. The same 404 answers forged, expired and accepted tokens, so a probe learns nothing.",
166
190
  params: InviteTokenParam,
167
191
  response: {
168
192
  200: GetInviteResult,
169
- 404: ErrorBody
193
+ ...refusals(404)
170
194
  }
171
195
  },
172
196
  handler: async (req) => {
@@ -187,11 +211,12 @@ async function organizationsRoutes(app, container) {
187
211
  schema: {
188
212
  operationId: "acceptInvite",
189
213
  tags,
190
- summary: "Accept an invite with the logged-in account",
214
+ summary: "Accept an invite with the signed-in account",
215
+ description: "For any signed-in person whose email matches the invitation. Makes the invite's organization the session's active one. 400 when the token is invalid or expired, or the email does not match.",
191
216
  body: AcceptInvite,
192
217
  response: {
193
218
  200: AcceptInviteResult,
194
- 400: ErrorBody
219
+ ...refusals(400, 404)
195
220
  }
196
221
  },
197
222
  handler: async (req) => {
@@ -1 +1 @@
1
- {"version":3,"file":"organizations.js","names":[],"sources":["../../../src/http/routes/organizations.ts"],"sourcesContent":["import type { FastifyInstance } from 'fastify';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport { z } from 'zod';\nimport {\n AcceptInvite,\n AcceptInviteResult,\n CreateInvite,\n CreateOrganization,\n ErrorBody,\n Invite,\n Member,\n MemberIdParam,\n MembersPage,\n MembersQuery,\n GetInviteResult,\n InviteTokenParam,\n Organization,\n UpdateMemberRole,\n UpdateOrganization,\n} from '../schemas/index.js';\nimport type { MemberWriteContext } from '@headless-lms/core/organizations';\nimport { NotFoundError } from '@headless-lms/core/shared/errors';\nimport type { Container } from '../../app/container.js';\nimport { resolveScope } from '../scope.js';\n\nexport async function organizationsRoutes(\n app: FastifyInstance,\n container: Container,\n): Promise<void> {\n const r = app.withTypeProvider<ZodTypeProvider>();\n const organizations = container.organizations;\n const tags = ['Organizations'];\n\n // Create a new organization on the caller's behalf and make it their active\n // org. This is the API's own front door for org creation — it drives Better\n // Auth internally, so callers use the typed SDK, not the auth namespace. No\n // resolveScope here: the caller has no active org yet, only a session.\n r.route({\n method: 'POST',\n url: '/api/organizations',\n // Session only: a first-time signup has no active org yet, so requiring one\n // here would 401 the very call that produces it.\n preHandler: app.requireSession,\n schema: {\n operationId: 'createOrganization',\n tags,\n summary: 'Create an organization and make it active',\n body: CreateOrganization,\n response: { 201: Organization },\n },\n handler: async (req, reply) => {\n const org = await container.orgProvider.createOrganization(req.headers, req.body);\n return reply.code(201).send({\n name: org.name,\n slug: org.slug,\n id: org.id,\n createdAt: org.createdAt.toISOString(),\n });\n },\n });\n\n // Update the caller's active organization (name/slug). Writes go through Better\n // Auth, which enforces the caller's org-update permission (owner/admin).\n r.route({\n method: 'PATCH',\n url: '/api/organizations',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'updateOrganization',\n tags,\n summary: 'Update the active organization',\n body: UpdateOrganization,\n response: { 200: Organization, 409: ErrorBody },\n },\n handler: async (req, reply) => {\n const scope = await resolveScope(container, req);\n const org = await container.orgProvider.updateOrganization(\n req.headers,\n scope.authOrgId,\n req.body,\n );\n return reply.send({\n id: org.id,\n name: org.name,\n slug: org.slug,\n createdAt: org.createdAt.toISOString(),\n });\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/organizations/members',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listMembers',\n tags,\n summary: 'List organization members',\n querystring: MembersQuery,\n response: { 200: MembersPage },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return organizations.listMembers(scope.orgId, req.query);\n },\n });\n\n r.route({\n method: 'PATCH',\n url: '/api/organizations/members/:id/role',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'updateMemberRole',\n tags,\n summary: \"Change a member's role\",\n params: MemberIdParam,\n body: UpdateMemberRole,\n response: { 200: Member, 404: ErrorBody, 409: ErrorBody },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n const ctx: MemberWriteContext = {\n orgId: scope.orgId,\n authOrgId: scope.authOrgId,\n headers: req.headers,\n };\n const member = await organizations.updateMemberRole(ctx, req.params.id, req.body.role);\n if (!member) {\n throw new NotFoundError('Member', req.params.id);\n }\n return member;\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/organizations/members/:id',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'removeMember',\n tags,\n summary: 'Remove an organization member',\n params: MemberIdParam,\n response: { 204: z.void(), 404: ErrorBody, 409: ErrorBody },\n },\n handler: async (req, reply) => {\n const scope = await resolveScope(container, req);\n const ctx: MemberWriteContext = {\n orgId: scope.orgId,\n authOrgId: scope.authOrgId,\n headers: req.headers,\n };\n const removed = await organizations.removeMember(ctx, req.params.id);\n if (!removed) {\n throw new NotFoundError('Member', req.params.id);\n }\n return reply.code(204).send();\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/organizations/invites',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'createInvite',\n tags,\n summary: 'Invite a member or student into the active organization',\n body: CreateInvite,\n response: { 201: Invite, 409: ErrorBody },\n },\n handler: async (req) => {\n // Domain ids, not the session's auth ids: invites FK to organizations.id\n // and users.id.\n const scope = await resolveScope(container, req);\n const invite = await organizations.createInvite({\n orgId: scope.orgId,\n inviterUserId: scope.userId,\n email: req.body.email,\n role: req.body.role,\n ...(req.body.firstName !== undefined && { firstName: req.body.firstName }),\n ...(req.body.lastName !== undefined && { lastName: req.body.lastName }),\n sendEmail: req.body.sendEmail,\n });\n\n req.log.debug({ invite }, 'created invite');\n return invite;\n },\n });\n\n // The invite landing page's first call, made before the invitee has any\n // session — so no guard. It turns the emailed token into the two facts the\n // page needs: who the invitation is for, and whether that address already has\n // an account. The email comes from the invitation row rather than the URL, so\n // it cannot be steered by editing the link.\n r.route({\n method: 'GET',\n url: '/api/organizations/invites/:token',\n schema: {\n operationId: 'getInvite',\n tags,\n summary: 'Read an invite by its token',\n params: InviteTokenParam,\n response: { 200: GetInviteResult, 404: ErrorBody },\n },\n handler: async (req) => {\n const invite = await organizations.peekInvite(req.params.token);\n if (!invite) {\n // Same answer for forged, expired, and already-accepted tokens: a\n // probe learns nothing about which invitations exist.\n throw new NotFoundError('Invite', 'token');\n }\n // Whether the address already has a login is deliberately not answered\n // here. The mirror cannot say — an admin's student is a person long\n // before they are an account — and asking the auth engine would mean\n // reading its tables for one boolean. The sign-up form discovers it\n // instead: a duplicate signup comes back 422 and the page flips to\n // sign-in.\n const person = await container.identity.getUserByEmail(invite.email);\n return {\n email: invite.email,\n firstName: person?.firstName ?? null,\n lastName: person?.lastName ?? null,\n };\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/organizations/invites/accept',\n preHandler: app.requireSession,\n schema: {\n operationId: 'acceptInvite',\n tags,\n summary: 'Accept an invite with the logged-in account',\n body: AcceptInvite,\n response: { 200: AcceptInviteResult, 400: ErrorBody },\n },\n handler: async (req) => {\n // No active org yet, so no resolveScope — but acceptInvite links a domain\n // users.id, never the auth account id.\n const user = await container.identity.getUserByExternalId(req.authUser.id);\n if (!user) {\n throw new NotFoundError('User', req.authUser.id);\n }\n const accepted = await container.organizations.acceptInvite({\n token: req.body.token,\n userId: user.id,\n email: req.authUser.email,\n });\n\n // The session predates this grant, so it carries no active org and every\n // org-scoped read would 401. Students are never org members, so the org\n // plugin's set-active cannot do it — stamp the session row directly.\n const org = await container.organizations.getById(accepted.orgId);\n if (!org) {\n throw new NotFoundError('Organization', accepted.orgId);\n }\n const stamped = await container.sessions.stampActiveOrganization(req.headers, org.id);\n\n req.log.info({ accepted, stamped }, 'accepted invite');\n return {};\n },\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"organizations.js","names":[],"sources":["../../../src/http/routes/organizations.ts"],"sourcesContent":["import type { FastifyInstance } from 'fastify';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport { z } from 'zod';\nimport {\n AcceptInvite,\n AcceptInviteResult,\n CreateInvite,\n CreateOrganization,\n Invite,\n MemberIdParam,\n MemberRoleChange,\n MembersPage,\n MembersQuery,\n GetInviteResult,\n InviteTokenParam,\n Organization,\n UpdateMemberRole,\n UpdateOrganization,\n refusals,\n} from '../schemas/index.js';\nimport type { MemberWriteContext } from '@headless-lms/core/organizations';\nimport { NotFoundError } from '@headless-lms/core/shared/errors';\nimport type { Container } from '../../app/container.js';\nimport { resolveScope } from '../scope.js';\n\nexport async function organizationsRoutes(\n app: FastifyInstance,\n container: Container,\n): Promise<void> {\n const r = app.withTypeProvider<ZodTypeProvider>();\n const organizations = container.organizations;\n const tags = ['Organizations'];\n\n // Create a new organization on the caller's behalf and make it their active\n // org. This is the API's own front door for org creation — it drives Better\n // Auth internally, so callers use the typed SDK, not the auth namespace. No\n // resolveScope here: the caller has no active org yet, only a session.\n r.route({\n method: 'POST',\n url: '/api/organizations',\n // Session only: a first-time signup has no active org yet, so requiring one\n // here would 401 the very call that produces it.\n preHandler: app.requireSession,\n schema: {\n operationId: 'createOrganization',\n tags,\n summary: 'Create an organization and make it active',\n description:\n 'For any signed-in person, including one with no organization yet. The caller becomes its owner.',\n body: CreateOrganization,\n response: { 201: Organization },\n },\n handler: async (req, reply) => {\n const org = await container.orgProvider.createOrganization(req.headers, req.body);\n return reply.code(201).send({\n name: org.name,\n slug: org.slug,\n id: org.id,\n createdAt: org.createdAt.toISOString(),\n });\n },\n });\n\n // Update the caller's active organization (name/slug). Writes go through Better\n // Auth, which enforces the caller's org-update permission (owner/admin).\n r.route({\n method: 'PATCH',\n url: '/api/organizations',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'updateOrganization',\n tags,\n summary: 'Update the active organization (staff)',\n description: 'Owner or admin. Changes its name or slug; 409 when the slug is taken.',\n body: UpdateOrganization,\n response: { 200: Organization, ...refusals(403, 409) },\n },\n handler: async (req, reply) => {\n const scope = await resolveScope(container, req);\n const org = await container.orgProvider.updateOrganization(\n req.headers,\n scope.authOrgId,\n req.body,\n );\n return reply.send({\n id: org.id,\n name: org.name,\n slug: org.slug,\n createdAt: org.createdAt.toISOString(),\n });\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/organizations/members',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listMembers',\n tags,\n summary: \"List the organization's team members (staff)\",\n description:\n 'Staff only (owner, admin, instructor). The team: everyone in the organization who is not a student.',\n querystring: MembersQuery,\n response: { 200: MembersPage, ...refusals(403) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return organizations.listMembers(scope.orgId, req.query);\n },\n });\n\n r.route({\n method: 'PATCH',\n url: '/api/organizations/members/:id/role',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'updateMemberRole',\n tags,\n summary: \"Change a member's role (staff)\",\n description:\n 'Owner or admin only. The owner role can be neither granted nor changed (409), and only a moderator can become a student.',\n params: MemberIdParam,\n body: UpdateMemberRole,\n response: { 200: MemberRoleChange, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n const ctx: MemberWriteContext = {\n orgId: scope.orgId,\n authOrgId: scope.authOrgId,\n headers: req.headers,\n };\n const member = await organizations.updateMemberRole(\n ctx,\n { id: scope.orgUserId, role: scope.role },\n req.params.id,\n req.body.role,\n );\n if (!member) {\n throw new NotFoundError('Member', req.params.id);\n }\n return member;\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/organizations/members/:id',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'removeMember',\n tags,\n summary: 'Remove an organization member (staff)',\n description:\n 'Owner or admin only. Removing a pending member cancels their invite. The owner cannot be removed (409).',\n params: MemberIdParam,\n response: { 204: z.void(), ...refusals(403, 404, 409) },\n },\n handler: async (req, reply) => {\n const scope = await resolveScope(container, req);\n const ctx: MemberWriteContext = {\n orgId: scope.orgId,\n authOrgId: scope.authOrgId,\n headers: req.headers,\n };\n const removed = await organizations.removeMember(\n ctx,\n { id: scope.orgUserId, role: scope.role },\n req.params.id,\n );\n if (!removed) {\n throw new NotFoundError('Member', req.params.id);\n }\n return reply.code(204).send();\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/organizations/invites',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'createInvite',\n tags,\n summary: 'Invite a member or student into the active organization (staff)',\n description:\n 'Owner or admin only. A student is created by this call, so their access can be granted before they accept. 409 when a student with that email is already in the organization.',\n body: CreateInvite,\n response: { 201: Invite, ...refusals(403, 409) },\n },\n handler: async (req, reply) => {\n // Domain ids, not the session's auth ids: invites FK to organizations.id\n // and users.id.\n const scope = await resolveScope(container, req);\n const invite = await organizations.createInvite(\n { id: scope.orgUserId, role: scope.role },\n {\n orgId: scope.orgId,\n inviterUserId: scope.userId,\n email: req.body.email,\n role: req.body.role,\n ...(req.body.firstName !== undefined && { firstName: req.body.firstName }),\n ...(req.body.lastName !== undefined && { lastName: req.body.lastName }),\n sendEmail: req.body.sendEmail,\n },\n );\n\n req.log.debug({ inviteId: invite.id }, 'created invite');\n return reply.code(201).send({\n id: invite.id,\n email: invite.email,\n role: invite.role,\n status: invite.status,\n expiresAt: invite.expiresAt,\n createdAt: invite.createdAt,\n });\n },\n });\n\n // The invite landing page's first call, made before the invitee has any\n // session — so no guard. It turns the emailed token into the two facts the\n // page needs: who the invitation is for, and whether that address already has\n // an account. The email comes from the invitation row rather than the URL, so\n // it cannot be steered by editing the link.\n r.route({\n method: 'GET',\n url: '/api/organizations/invites/:token',\n schema: {\n operationId: 'getInvite',\n tags,\n summary: 'Read an invite by its token',\n description:\n 'Public: no session needed. The same 404 answers forged, expired and accepted tokens, so a probe learns nothing.',\n params: InviteTokenParam,\n response: { 200: GetInviteResult, ...refusals(404) },\n },\n handler: async (req) => {\n const invite = await organizations.peekInvite(req.params.token);\n if (!invite) {\n // Same answer for forged, expired, and already-accepted tokens: a\n // probe learns nothing about which invitations exist.\n throw new NotFoundError('Invite', 'token');\n }\n // Whether the address already has a login is deliberately not answered\n // here. The mirror cannot say — an admin's student is a person long\n // before they are an account — and asking the auth engine would mean\n // reading its tables for one boolean. The sign-up form discovers it\n // instead: a duplicate signup comes back 422 and the page flips to\n // sign-in.\n const person = await container.identity.getUserByEmail(invite.email);\n return {\n email: invite.email,\n firstName: person?.firstName ?? null,\n lastName: person?.lastName ?? null,\n };\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/organizations/invites/accept',\n preHandler: app.requireSession,\n schema: {\n operationId: 'acceptInvite',\n tags,\n summary: 'Accept an invite with the signed-in account',\n description:\n \"For any signed-in person whose email matches the invitation. Makes the invite's organization the session's active one. 400 when the token is invalid or expired, or the email does not match.\",\n body: AcceptInvite,\n response: { 200: AcceptInviteResult, ...refusals(400, 404) },\n },\n handler: async (req) => {\n // No active org yet, so no resolveScope — but acceptInvite links a domain\n // users.id, never the auth account id.\n const user = await container.identity.getUserByExternalId(req.authUser.id);\n if (!user) {\n throw new NotFoundError('User', req.authUser.id);\n }\n const accepted = await container.organizations.acceptInvite({\n token: req.body.token,\n userId: user.id,\n email: req.authUser.email,\n });\n\n // The session predates this grant, so it carries no active org and every\n // org-scoped read would 401. Students are never org members, so the org\n // plugin's set-active cannot do it — stamp the session row directly.\n const org = await container.organizations.getById(accepted.orgId);\n if (!org) {\n throw new NotFoundError('Organization', accepted.orgId);\n }\n const stamped = await container.sessions.stampActiveOrganization(req.headers, org.id);\n\n req.log.info({ accepted, stamped }, 'accepted invite');\n return {};\n },\n });\n}\n"],"mappings":"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"}
@@ -1,13 +1,13 @@
1
+ import { refusals } from "../schemas/shared.js";
1
2
  import { resolveScope } from "../scope.js";
2
- import { ErrorBody } from "../schemas/shared.js";
3
- import { Student, StudentAnalytics, StudentIdParam, StudentsPage, StudentsQuery, UpdateStudentBody } from "../schemas/students.js";
3
+ import { Student, StudentAnalytics, StudentIdParam, StudentsPage, StudentsQuery } from "../schemas/students.js";
4
4
  import "../schemas/index.js";
5
- import { NotFoundError } from "@headless-lms/core/shared/errors";
6
5
  import { z } from "zod";
6
+ import { NotFoundError } from "@headless-lms/core/shared/errors";
7
7
  //#region src/http/routes/students.ts
8
8
  async function studentsRoutes(app, container) {
9
9
  const r = app.withTypeProvider();
10
- const students = container.reporting.students;
10
+ const students = container.reporting.admin;
11
11
  r.route({
12
12
  method: "GET",
13
13
  url: "/api/students",
@@ -15,13 +15,17 @@ async function studentsRoutes(app, container) {
15
15
  schema: {
16
16
  operationId: "listStudents",
17
17
  tags: ["Organizations"],
18
- summary: "List students",
18
+ summary: "List students (staff)",
19
+ description: "Staff only (owner, admin, instructor).",
19
20
  querystring: StudentsQuery,
20
- response: { 200: StudentsPage }
21
+ response: {
22
+ 200: StudentsPage,
23
+ ...refusals(403)
24
+ }
21
25
  },
22
26
  handler: async (req) => {
23
27
  const scope = await resolveScope(container, req);
24
- return students.list(scope.orgId, req.query);
28
+ return students.listStudents(scope.orgId, req.query);
25
29
  }
26
30
  });
27
31
  r.route({
@@ -31,16 +35,17 @@ async function studentsRoutes(app, container) {
31
35
  schema: {
32
36
  operationId: "getStudent",
33
37
  tags: ["Organizations"],
34
- summary: "Get a student by id",
38
+ summary: "Get a student (staff)",
39
+ description: "Staff only (owner, admin, instructor).",
35
40
  params: StudentIdParam,
36
41
  response: {
37
42
  200: Student,
38
- 404: ErrorBody
43
+ ...refusals(403, 404)
39
44
  }
40
45
  },
41
46
  handler: async (req) => {
42
47
  const scope = await resolveScope(container, req);
43
- const student = await students.get(scope.orgId, req.params.id);
48
+ const student = await students.getStudent(scope.orgId, req.params.id);
44
49
  if (!student) throw new NotFoundError("Student", req.params.id);
45
50
  return student;
46
51
  }
@@ -52,48 +57,21 @@ async function studentsRoutes(app, container) {
52
57
  schema: {
53
58
  operationId: "getStudentAnalytics",
54
59
  tags: ["Reporting"],
55
- summary: "A student's learner record: per-course progress and completion",
60
+ summary: "A student's learner record (staff)",
61
+ description: "Staff only (owner, admin, instructor). Per-course progress and completion.",
56
62
  params: StudentIdParam,
57
63
  response: {
58
64
  200: StudentAnalytics,
59
- 404: ErrorBody
65
+ ...refusals(403, 404)
60
66
  }
61
67
  },
62
68
  handler: async (req) => {
63
69
  const scope = await resolveScope(container, req);
64
- const analytics = await students.analytics(scope.orgId, req.params.id);
70
+ const analytics = await students.studentAnalytics(scope.orgId, req.params.id);
65
71
  if (!analytics) throw new NotFoundError("Student", req.params.id);
66
72
  return analytics;
67
73
  }
68
74
  });
69
- r.route({
70
- method: "PATCH",
71
- url: "/api/students/:id",
72
- preHandler: app.requireOrgSession,
73
- schema: {
74
- operationId: "updateStudent",
75
- tags: ["Organizations"],
76
- summary: "Update a student's profile",
77
- params: StudentIdParam,
78
- body: UpdateStudentBody,
79
- response: {
80
- 200: Student,
81
- 404: ErrorBody,
82
- 409: ErrorBody
83
- }
84
- },
85
- handler: async (req) => {
86
- const scope = await resolveScope(container, req);
87
- await container.organizations.updateStudent({
88
- orgId: scope.orgId,
89
- orgUserId: req.params.id,
90
- ...req.body
91
- });
92
- const student = await students.get(scope.orgId, req.params.id);
93
- if (!student) throw new NotFoundError("Student", req.params.id);
94
- return student;
95
- }
96
- });
97
75
  r.route({
98
76
  method: "POST",
99
77
  url: "/api/students/:id/invite/resend",
@@ -101,17 +79,20 @@ async function studentsRoutes(app, container) {
101
79
  schema: {
102
80
  operationId: "resendStudentInvite",
103
81
  tags: ["Organizations"],
104
- summary: "Re-send a pending student's invite email",
82
+ summary: "Re-send a pending student's invite email (staff)",
83
+ description: "Owner or admin only. Issues a new token, so the previous link stops working. 404 when the student has no pending invite; 409 when it was already accepted.",
105
84
  params: StudentIdParam,
106
85
  response: {
107
86
  204: z.void(),
108
- 404: ErrorBody,
109
- 409: ErrorBody
87
+ ...refusals(403, 404, 409)
110
88
  }
111
89
  },
112
90
  handler: async (req, reply) => {
113
91
  const scope = await resolveScope(container, req);
114
92
  await container.organizations.resendStudentInvite({
93
+ id: scope.orgUserId,
94
+ role: scope.role
95
+ }, {
115
96
  orgId: scope.orgId,
116
97
  orgUserId: req.params.id,
117
98
  inviterUserId: scope.userId
@@ -126,16 +107,20 @@ async function studentsRoutes(app, container) {
126
107
  schema: {
127
108
  operationId: "deleteStudent",
128
109
  tags: ["Organizations"],
129
- summary: "Delete a student",
110
+ summary: "Delete a student (staff)",
111
+ description: "Owner or admin only. 409 when the person has written posts, comments or messages.",
130
112
  params: StudentIdParam,
131
113
  response: {
132
114
  204: z.void(),
133
- 404: ErrorBody
115
+ ...refusals(403, 404, 409)
134
116
  }
135
117
  },
136
118
  handler: async (req, reply) => {
137
119
  const scope = await resolveScope(container, req);
138
- await container.organizations.deleteOrgUser(scope.orgId, req.params.id);
120
+ await container.organizations.deleteOrgUser(scope.orgId, {
121
+ id: scope.orgUserId,
122
+ role: scope.role
123
+ }, req.params.id);
139
124
  return reply.code(204).send();
140
125
  }
141
126
  });
@@ -1 +1 @@
1
- {"version":3,"file":"students.js","names":[],"sources":["../../../src/http/routes/students.ts"],"sourcesContent":["// HTTP routes for students: the reporting-backed list/read endpoints, the\n// delete, and re-issuing a pending invite. There is no create here — adding a\n// student IS inviting one, at /api/organizations/invites, which provisions the\n// student record up front.\nimport type { FastifyInstance } from 'fastify';\nimport { z } from 'zod';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport {\n ErrorBody,\n Student,\n StudentAnalytics,\n StudentIdParam,\n StudentsPage,\n StudentsQuery,\n UpdateStudentBody,\n} from '../schemas/index.js';\nimport { NotFoundError } from '@headless-lms/core/shared/errors';\nimport type { Container } from '../../app/container.js';\nimport { resolveScope } from '../scope.js';\n\nexport async function studentsRoutes(app: FastifyInstance, container: Container): Promise<void> {\n const r = app.withTypeProvider<ZodTypeProvider>();\n const students = container.reporting.students;\n\n r.route({\n method: 'GET',\n url: '/api/students',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listStudents',\n tags: ['Organizations'],\n summary: 'List students',\n querystring: StudentsQuery,\n response: { 200: StudentsPage },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return students.list(scope.orgId, req.query);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/students/:id',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getStudent',\n tags: ['Organizations'],\n summary: 'Get a student by id',\n params: StudentIdParam,\n response: { 200: Student, 404: ErrorBody },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n const student = await students.get(scope.orgId, req.params.id);\n if (!student) {\n throw new NotFoundError('Student', req.params.id);\n }\n return student;\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/students/:id/analytics',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getStudentAnalytics',\n tags: ['Reporting'],\n summary: \"A student's learner record: per-course progress and completion\",\n params: StudentIdParam,\n response: { 200: StudentAnalytics, 404: ErrorBody },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n const analytics = await students.analytics(scope.orgId, req.params.id);\n if (!analytics) {\n throw new NotFoundError('Student', req.params.id);\n }\n return analytics;\n },\n });\n\n // Corrects the person behind the student row. The write lands in identity\n // (the person is global); organizations owns the rule that this org user is\n // a student here, and reconciles their pending invite when the address moves.\n r.route({\n method: 'PATCH',\n url: '/api/students/:id',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'updateStudent',\n tags: ['Organizations'],\n summary: \"Update a student's profile\",\n params: StudentIdParam,\n body: UpdateStudentBody,\n response: { 200: Student, 404: ErrorBody, 409: ErrorBody },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n await container.organizations.updateStudent({\n orgId: scope.orgId,\n orgUserId: req.params.id,\n ...req.body,\n });\n // Re-read through the report so the response is the same shape the list\n // and detail GETs return, aggregates included.\n const student = await students.get(scope.orgId, req.params.id);\n if (!student) {\n throw new NotFoundError('Student', req.params.id);\n }\n return student;\n },\n });\n\n // Re-mints the token on the student's pending invite and emails it. Lives\n // here rather than under invites because the admin app knows the student,\n // not the invitation.\n r.route({\n method: 'POST',\n url: '/api/students/:id/invite/resend',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'resendStudentInvite',\n tags: ['Organizations'],\n summary: \"Re-send a pending student's invite email\",\n params: StudentIdParam,\n response: { 204: z.void(), 404: ErrorBody, 409: ErrorBody },\n },\n handler: async (req, reply) => {\n const scope = await resolveScope(container, req);\n await container.organizations.resendStudentInvite({\n orgId: scope.orgId,\n orgUserId: req.params.id,\n inviterUserId: scope.userId,\n });\n return reply.code(204).send();\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/students/:id',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'deleteStudent',\n tags: ['Organizations'],\n summary: 'Delete a student',\n params: StudentIdParam,\n response: { 204: z.void(), 404: ErrorBody },\n },\n handler: async (req, reply) => {\n const scope = await resolveScope(container, req);\n await container.organizations.deleteOrgUser(scope.orgId, req.params.id);\n return reply.code(204).send();\n },\n });\n}\n"],"mappings":";;;;;;;AAoBA,eAAsB,eAAe,KAAsB,WAAqC;CAC9F,MAAM,IAAI,IAAI,iBAAkC;CAChD,MAAM,WAAW,UAAU,UAAU;CAErC,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,eAAe;GACtB,SAAS;GACT,aAAa;GACb,UAAU,EAAE,KAAK,aAAa;EAChC;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,OAAO,SAAS,KAAK,MAAM,OAAO,IAAI,KAAK;EAC7C;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,eAAe;GACtB,SAAS;GACT,QAAQ;GACR,UAAU;IAAE,KAAK;IAAS,KAAK;GAAU;EAC3C;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,MAAM,UAAU,MAAM,SAAS,IAAI,MAAM,OAAO,IAAI,OAAO,EAAE;GAC7D,IAAI,CAAC,SACH,MAAM,IAAI,cAAc,WAAW,IAAI,OAAO,EAAE;GAElD,OAAO;EACT;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,WAAW;GAClB,SAAS;GACT,QAAQ;GACR,UAAU;IAAE,KAAK;IAAkB,KAAK;GAAU;EACpD;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,MAAM,YAAY,MAAM,SAAS,UAAU,MAAM,OAAO,IAAI,OAAO,EAAE;GACrE,IAAI,CAAC,WACH,MAAM,IAAI,cAAc,WAAW,IAAI,OAAO,EAAE;GAElD,OAAO;EACT;CACF,CAAC;CAKD,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,eAAe;GACtB,SAAS;GACT,QAAQ;GACR,MAAM;GACN,UAAU;IAAE,KAAK;IAAS,KAAK;IAAW,KAAK;GAAU;EAC3D;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,MAAM,UAAU,cAAc,cAAc;IAC1C,OAAO,MAAM;IACb,WAAW,IAAI,OAAO;IACtB,GAAG,IAAI;GACT,CAAC;GAGD,MAAM,UAAU,MAAM,SAAS,IAAI,MAAM,OAAO,IAAI,OAAO,EAAE;GAC7D,IAAI,CAAC,SACH,MAAM,IAAI,cAAc,WAAW,IAAI,OAAO,EAAE;GAElD,OAAO;EACT;CACF,CAAC;CAKD,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,eAAe;GACtB,SAAS;GACT,QAAQ;GACR,UAAU;IAAE,KAAK,EAAE,KAAK;IAAG,KAAK;IAAW,KAAK;GAAU;EAC5D;EACA,SAAS,OAAO,KAAK,UAAU;GAC7B,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,MAAM,UAAU,cAAc,oBAAoB;IAChD,OAAO,MAAM;IACb,WAAW,IAAI,OAAO;IACtB,eAAe,MAAM;GACvB,CAAC;GACD,OAAO,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK;EAC9B;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,eAAe;GACtB,SAAS;GACT,QAAQ;GACR,UAAU;IAAE,KAAK,EAAE,KAAK;IAAG,KAAK;GAAU;EAC5C;EACA,SAAS,OAAO,KAAK,UAAU;GAC7B,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,MAAM,UAAU,cAAc,cAAc,MAAM,OAAO,IAAI,OAAO,EAAE;GACtE,OAAO,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK;EAC9B;CACF,CAAC;AACH"}
1
+ {"version":3,"file":"students.js","names":[],"sources":["../../../src/http/routes/students.ts"],"sourcesContent":["// HTTP routes for students: the reporting-backed list/read endpoints, the\n// delete, and re-issuing a pending invite. There is no create here — adding a\n// student IS inviting one, at /api/organizations/invites, which provisions the\n// student record up front.\nimport type { FastifyInstance } from 'fastify';\nimport { z } from 'zod';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport {\n Student,\n StudentAnalytics,\n StudentIdParam,\n StudentsPage,\n StudentsQuery,\n refusals,\n} from '../schemas/index.js';\nimport { NotFoundError } from '@headless-lms/core/shared/errors';\nimport type { Container } from '../../app/container.js';\nimport { resolveScope } from '../scope.js';\n\nexport async function studentsRoutes(app: FastifyInstance, container: Container): Promise<void> {\n const r = app.withTypeProvider<ZodTypeProvider>();\n const students = container.reporting.admin;\n\n r.route({\n method: 'GET',\n url: '/api/students',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listStudents',\n tags: ['Organizations'],\n summary: 'List students (staff)',\n description: 'Staff only (owner, admin, instructor).',\n querystring: StudentsQuery,\n response: { 200: StudentsPage, ...refusals(403) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return students.listStudents(scope.orgId, req.query);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/students/:id',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getStudent',\n tags: ['Organizations'],\n summary: 'Get a student (staff)',\n description: 'Staff only (owner, admin, instructor).',\n params: StudentIdParam,\n response: { 200: Student, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n const student = await students.getStudent(scope.orgId, req.params.id);\n if (!student) {\n throw new NotFoundError('Student', req.params.id);\n }\n return student;\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/students/:id/analytics',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getStudentAnalytics',\n tags: ['Reporting'],\n summary: \"A student's learner record (staff)\",\n description: 'Staff only (owner, admin, instructor). Per-course progress and completion.',\n params: StudentIdParam,\n response: { 200: StudentAnalytics, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n const analytics = await students.studentAnalytics(scope.orgId, req.params.id);\n if (!analytics) {\n throw new NotFoundError('Student', req.params.id);\n }\n return analytics;\n },\n });\n\n // Re-mints the token on the student's pending invite and emails it. Lives\n // here rather than under invites because the admin app knows the student,\n // not the invitation.\n r.route({\n method: 'POST',\n url: '/api/students/:id/invite/resend',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'resendStudentInvite',\n tags: ['Organizations'],\n summary: \"Re-send a pending student's invite email (staff)\",\n description:\n 'Owner or admin only. Issues a new token, so the previous link stops working. 404 when the student has no pending invite; 409 when it was already accepted.',\n params: StudentIdParam,\n response: { 204: z.void(), ...refusals(403, 404, 409) },\n },\n handler: async (req, reply) => {\n const scope = await resolveScope(container, req);\n await container.organizations.resendStudentInvite(\n { id: scope.orgUserId, role: scope.role },\n {\n orgId: scope.orgId,\n orgUserId: req.params.id,\n inviterUserId: scope.userId,\n },\n );\n return reply.code(204).send();\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/students/:id',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'deleteStudent',\n tags: ['Organizations'],\n summary: 'Delete a student (staff)',\n description:\n 'Owner or admin only. 409 when the person has written posts, comments or messages.',\n params: StudentIdParam,\n response: { 204: z.void(), ...refusals(403, 404, 409) },\n },\n handler: async (req, reply) => {\n const scope = await resolveScope(container, req);\n await container.organizations.deleteOrgUser(\n scope.orgId,\n { id: scope.orgUserId, role: scope.role },\n req.params.id,\n );\n return reply.code(204).send();\n },\n });\n}\n"],"mappings":";;;;;;;AAmBA,eAAsB,eAAe,KAAsB,WAAqC;CAC9F,MAAM,IAAI,IAAI,iBAAkC;CAChD,MAAM,WAAW,UAAU,UAAU;CAErC,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,eAAe;GACtB,SAAS;GACT,aAAa;GACb,aAAa;GACb,UAAU;IAAE,KAAK;IAAc,GAAG,SAAS,GAAG;GAAE;EAClD;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,OAAO,SAAS,aAAa,MAAM,OAAO,IAAI,KAAK;EACrD;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,eAAe;GACtB,SAAS;GACT,aAAa;GACb,QAAQ;GACR,UAAU;IAAE,KAAK;IAAS,GAAG,SAAS,KAAK,GAAG;GAAE;EAClD;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,MAAM,UAAU,MAAM,SAAS,WAAW,MAAM,OAAO,IAAI,OAAO,EAAE;GACpE,IAAI,CAAC,SACH,MAAM,IAAI,cAAc,WAAW,IAAI,OAAO,EAAE;GAElD,OAAO;EACT;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,WAAW;GAClB,SAAS;GACT,aAAa;GACb,QAAQ;GACR,UAAU;IAAE,KAAK;IAAkB,GAAG,SAAS,KAAK,GAAG;GAAE;EAC3D;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,MAAM,YAAY,MAAM,SAAS,iBAAiB,MAAM,OAAO,IAAI,OAAO,EAAE;GAC5E,IAAI,CAAC,WACH,MAAM,IAAI,cAAc,WAAW,IAAI,OAAO,EAAE;GAElD,OAAO;EACT;CACF,CAAC;CAKD,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,eAAe;GACtB,SAAS;GACT,aACE;GACF,QAAQ;GACR,UAAU;IAAE,KAAK,EAAE,KAAK;IAAG,GAAG,SAAS,KAAK,KAAK,GAAG;GAAE;EACxD;EACA,SAAS,OAAO,KAAK,UAAU;GAC7B,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,MAAM,UAAU,cAAc,oBAC5B;IAAE,IAAI,MAAM;IAAW,MAAM,MAAM;GAAK,GACxC;IACE,OAAO,MAAM;IACb,WAAW,IAAI,OAAO;IACtB,eAAe,MAAM;GACvB,CACF;GACA,OAAO,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK;EAC9B;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,eAAe;GACtB,SAAS;GACT,aACE;GACF,QAAQ;GACR,UAAU;IAAE,KAAK,EAAE,KAAK;IAAG,GAAG,SAAS,KAAK,KAAK,GAAG;GAAE;EACxD;EACA,SAAS,OAAO,KAAK,UAAU;GAC7B,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,MAAM,UAAU,cAAc,cAC5B,MAAM,OACN;IAAE,IAAI,MAAM;IAAW,MAAM,MAAM;GAAK,GACxC,IAAI,OAAO,EACb;GACA,OAAO,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK;EAC9B;CACF,CAAC;AACH"}