@headless-lms/server 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (351) hide show
  1. package/dist/_virtual/_rolldown/runtime.js +5 -0
  2. package/dist/app/community-mail.d.ts +24 -0
  3. package/dist/app/community-mail.js +92 -0
  4. package/dist/app/community-mail.js.map +1 -0
  5. package/dist/app/container.d.ts +40 -19
  6. package/dist/app/container.js +294 -45
  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/app/notification-channels.d.ts +47 -0
  12. package/dist/app/notification-channels.js +174 -0
  13. package/dist/app/notification-channels.js.map +1 -0
  14. package/dist/app/notification-reads.d.ts +23 -0
  15. package/dist/app/notification-reads.js +95 -0
  16. package/dist/app/notification-reads.js.map +1 -0
  17. package/dist/http/config.d.ts +2 -1
  18. package/dist/http/errors.d.ts +15 -0
  19. package/dist/http/errors.js +162 -0
  20. package/dist/http/errors.js.map +1 -0
  21. package/dist/http/mcp/catalogue.d.ts +37 -0
  22. package/dist/http/mcp/catalogue.js +69 -0
  23. package/dist/http/mcp/catalogue.js.map +1 -0
  24. package/dist/http/mcp/challenge.d.ts +7 -0
  25. package/dist/http/mcp/challenge.js +35 -0
  26. package/dist/http/mcp/challenge.js.map +1 -0
  27. package/dist/http/mcp/input-schema.d.ts +7 -0
  28. package/dist/http/mcp/input-schema.js +28 -0
  29. package/dist/http/mcp/input-schema.js.map +1 -0
  30. package/dist/http/mcp/route.d.ts +8 -0
  31. package/dist/http/mcp/route.js +75 -0
  32. package/dist/http/mcp/route.js.map +1 -0
  33. package/dist/http/mcp/server.d.ts +8 -0
  34. package/dist/http/mcp/server.js +31 -0
  35. package/dist/http/mcp/server.js.map +1 -0
  36. package/dist/http/mcp/test-support.d.ts +55 -0
  37. package/dist/http/mcp/test-support.js +151 -0
  38. package/dist/http/mcp/test-support.js.map +1 -0
  39. package/dist/http/mcp/tool.d.ts +48 -0
  40. package/dist/http/mcp/tool.js +58 -0
  41. package/dist/http/mcp/tool.js.map +1 -0
  42. package/dist/http/mcp/tools/automations.d.ts +6 -0
  43. package/dist/http/mcp/tools/automations.js +120 -0
  44. package/dist/http/mcp/tools/automations.js.map +1 -0
  45. package/dist/http/mcp/tools/catalog.d.ts +6 -0
  46. package/dist/http/mcp/tools/catalog.js +200 -0
  47. package/dist/http/mcp/tools/catalog.js.map +1 -0
  48. package/dist/http/mcp/tools/comments.d.ts +6 -0
  49. package/dist/http/mcp/tools/comments.js +70 -0
  50. package/dist/http/mcp/tools/comments.js.map +1 -0
  51. package/dist/http/mcp/tools/communities.d.ts +6 -0
  52. package/dist/http/mcp/tools/communities.js +175 -0
  53. package/dist/http/mcp/tools/communities.js.map +1 -0
  54. package/dist/http/mcp/tools/community-members.d.ts +6 -0
  55. package/dist/http/mcp/tools/community-members.js +89 -0
  56. package/dist/http/mcp/tools/community-members.js.map +1 -0
  57. package/dist/http/mcp/tools/community-moderation.d.ts +6 -0
  58. package/dist/http/mcp/tools/community-moderation.js +144 -0
  59. package/dist/http/mcp/tools/community-moderation.js.map +1 -0
  60. package/dist/http/mcp/tools/courses.d.ts +6 -0
  61. package/dist/http/mcp/tools/courses.js +121 -0
  62. package/dist/http/mcp/tools/courses.js.map +1 -0
  63. package/dist/http/mcp/tools/index.d.ts +6 -0
  64. package/dist/http/mcp/tools/index.js +23 -0
  65. package/dist/http/mcp/tools/index.js.map +1 -0
  66. package/dist/http/mcp/tools/people.d.ts +6 -0
  67. package/dist/http/mcp/tools/people.js +145 -0
  68. package/dist/http/mcp/tools/people.js.map +1 -0
  69. package/dist/http/plugins/auth.js +13 -5
  70. package/dist/http/plugins/auth.js.map +1 -1
  71. package/dist/http/plugins/error-handler.d.ts +2 -1
  72. package/dist/http/plugins/error-handler.js +51 -65
  73. package/dist/http/plugins/error-handler.js.map +1 -1
  74. package/dist/http/plugins/openapi.js +49 -2
  75. package/dist/http/plugins/openapi.js.map +1 -1
  76. package/dist/http/routes/access.d.ts +7 -0
  77. package/dist/http/routes/access.js +140 -0
  78. package/dist/http/routes/access.js.map +1 -0
  79. package/dist/http/routes/activities.js +68 -23
  80. package/dist/http/routes/activities.js.map +1 -1
  81. package/dist/http/routes/assets.js +60 -55
  82. package/dist/http/routes/assets.js.map +1 -1
  83. package/dist/http/routes/automations.js +44 -24
  84. package/dist/http/routes/automations.js.map +1 -1
  85. package/dist/http/routes/bundles.js +29 -18
  86. package/dist/http/routes/bundles.js.map +1 -1
  87. package/dist/http/routes/communities.d.ts +7 -0
  88. package/dist/http/routes/communities.js +522 -0
  89. package/dist/http/routes/communities.js.map +1 -0
  90. package/dist/http/routes/community-moderation.d.ts +7 -0
  91. package/dist/http/routes/community-moderation.js +425 -0
  92. package/dist/http/routes/community-moderation.js.map +1 -0
  93. package/dist/http/routes/course-analytics.js +9 -7
  94. package/dist/http/routes/course-analytics.js.map +1 -1
  95. package/dist/http/routes/courses.js +30 -15
  96. package/dist/http/routes/courses.js.map +1 -1
  97. package/dist/http/routes/dashboard.js +15 -6
  98. package/dist/http/routes/dashboard.js.map +1 -1
  99. package/dist/http/routes/discussion.js +34 -26
  100. package/dist/http/routes/discussion.js.map +1 -1
  101. package/dist/http/routes/downloads.js +48 -30
  102. package/dist/http/routes/downloads.js.map +1 -1
  103. package/dist/http/routes/entitlements.js +16 -7
  104. package/dist/http/routes/entitlements.js.map +1 -1
  105. package/dist/http/routes/integrations.js +38 -41
  106. package/dist/http/routes/integrations.js.map +1 -1
  107. package/dist/http/routes/member/actor.d.ts +14 -0
  108. package/dist/http/routes/member/actor.js +11 -0
  109. package/dist/http/routes/member/actor.js.map +1 -0
  110. package/dist/http/routes/member/assets.d.ts +7 -0
  111. package/dist/http/routes/member/assets.js +79 -0
  112. package/dist/http/routes/member/assets.js.map +1 -0
  113. package/dist/http/routes/member/chats.d.ts +7 -0
  114. package/dist/http/routes/member/chats.js +435 -0
  115. package/dist/http/routes/member/chats.js.map +1 -0
  116. package/dist/http/routes/member/comments.d.ts +7 -0
  117. package/dist/http/routes/member/comments.js +187 -0
  118. package/dist/http/routes/member/comments.js.map +1 -0
  119. package/dist/http/routes/member/community.d.ts +7 -0
  120. package/dist/http/routes/member/community.js +312 -0
  121. package/dist/http/routes/member/community.js.map +1 -0
  122. package/dist/http/routes/member/courses.d.ts +7 -0
  123. package/dist/http/routes/member/courses.js +101 -0
  124. package/dist/http/routes/member/courses.js.map +1 -0
  125. package/dist/http/routes/member/devices.d.ts +7 -0
  126. package/dist/http/routes/member/devices.js +98 -0
  127. package/dist/http/routes/member/devices.js.map +1 -0
  128. package/dist/http/routes/member/downloads.d.ts +7 -0
  129. package/dist/http/routes/member/downloads.js +56 -0
  130. package/dist/http/routes/member/downloads.js.map +1 -0
  131. package/dist/http/routes/member/files.d.ts +7 -0
  132. package/dist/http/routes/member/files.js +184 -0
  133. package/dist/http/routes/member/files.js.map +1 -0
  134. package/dist/http/routes/member/index.d.ts +7 -0
  135. package/dist/http/routes/member/index.js +35 -0
  136. package/dist/http/routes/member/index.js.map +1 -0
  137. package/dist/http/routes/member/notification-settings.d.ts +7 -0
  138. package/dist/http/routes/member/notification-settings.js +108 -0
  139. package/dist/http/routes/member/notification-settings.js.map +1 -0
  140. package/dist/http/routes/member/notifications.d.ts +7 -0
  141. package/dist/http/routes/member/notifications.js +125 -0
  142. package/dist/http/routes/member/notifications.js.map +1 -0
  143. package/dist/http/routes/member/org.d.ts +7 -0
  144. package/dist/http/routes/member/org.js +56 -0
  145. package/dist/http/routes/member/org.js.map +1 -0
  146. package/dist/http/routes/member/posts.d.ts +7 -0
  147. package/dist/http/routes/member/posts.js +612 -0
  148. package/dist/http/routes/member/posts.js.map +1 -0
  149. package/dist/http/routes/member/profile.d.ts +7 -0
  150. package/dist/http/routes/member/profile.js +48 -0
  151. package/dist/http/routes/member/profile.js.map +1 -0
  152. package/dist/http/routes/member/progress.d.ts +7 -0
  153. package/dist/http/routes/member/progress.js +60 -0
  154. package/dist/http/routes/member/progress.js.map +1 -0
  155. package/dist/http/routes/organizations.js +171 -25
  156. package/dist/http/routes/organizations.js.map +1 -1
  157. package/dist/http/routes/students.js +32 -47
  158. package/dist/http/routes/students.js.map +1 -1
  159. package/dist/http/routes/webhooks.js +26 -18
  160. package/dist/http/routes/webhooks.js.map +1 -1
  161. package/dist/http/routes.js +8 -2
  162. package/dist/http/routes.js.map +1 -1
  163. package/dist/http/schemas/access.d.ts +146 -0
  164. package/dist/http/schemas/access.js +36 -0
  165. package/dist/http/schemas/access.js.map +1 -0
  166. package/dist/http/schemas/activities.d.ts +3 -1
  167. package/dist/http/schemas/activities.js +4 -4
  168. package/dist/http/schemas/activities.js.map +1 -1
  169. package/dist/http/schemas/assets.d.ts +79 -80
  170. package/dist/http/schemas/assets.js +23 -13
  171. package/dist/http/schemas/assets.js.map +1 -1
  172. package/dist/http/schemas/automations.d.ts +6 -0
  173. package/dist/http/schemas/bundles.js +6 -6
  174. package/dist/http/schemas/bundles.js.map +1 -1
  175. package/dist/http/schemas/chats.d.ts +409 -0
  176. package/dist/http/schemas/chats.js +56 -0
  177. package/dist/http/schemas/chats.js.map +1 -0
  178. package/dist/http/schemas/community.d.ts +597 -0
  179. package/dist/http/schemas/community.js +132 -0
  180. package/dist/http/schemas/community.js.map +1 -0
  181. package/dist/http/schemas/content.d.ts +4 -2
  182. package/dist/http/schemas/content.js +11 -8
  183. package/dist/http/schemas/content.js.map +1 -1
  184. package/dist/http/schemas/discussion.d.ts +98 -14
  185. package/dist/http/schemas/discussion.js +25 -12
  186. package/dist/http/schemas/discussion.js.map +1 -1
  187. package/dist/http/schemas/downloads.d.ts +11 -5
  188. package/dist/http/schemas/downloads.js +14 -11
  189. package/dist/http/schemas/downloads.js.map +1 -1
  190. package/dist/http/schemas/entitlements.d.ts +6 -6
  191. package/dist/http/schemas/entitlements.js +6 -6
  192. package/dist/http/schemas/entitlements.js.map +1 -1
  193. package/dist/http/schemas/files.d.ts +45 -0
  194. package/dist/http/schemas/files.js +25 -0
  195. package/dist/http/schemas/files.js.map +1 -0
  196. package/dist/http/schemas/index.d.ts +15 -8
  197. package/dist/http/schemas/index.js +15 -8
  198. package/dist/http/schemas/integrations.d.ts +4 -4
  199. package/dist/http/schemas/invites.d.ts +4 -0
  200. package/dist/http/schemas/invites.js +1 -0
  201. package/dist/http/schemas/invites.js.map +1 -1
  202. package/dist/http/schemas/{learn.d.ts → member.d.ts} +31 -18
  203. package/dist/http/schemas/{learn.js → member.js} +17 -13
  204. package/dist/http/schemas/member.js.map +1 -0
  205. package/dist/http/schemas/members.d.ts +35 -2
  206. package/dist/http/schemas/members.js +17 -3
  207. package/dist/http/schemas/members.js.map +1 -1
  208. package/dist/http/schemas/notifications.d.ts +550 -0
  209. package/dist/http/schemas/notifications.js +164 -0
  210. package/dist/http/schemas/notifications.js.map +1 -0
  211. package/dist/http/schemas/posts.d.ts +350 -0
  212. package/dist/http/schemas/posts.js +47 -0
  213. package/dist/http/schemas/posts.js.map +1 -0
  214. package/dist/http/schemas/profile.d.ts +23 -0
  215. package/dist/http/schemas/profile.js +15 -0
  216. package/dist/http/schemas/profile.js.map +1 -0
  217. package/dist/http/schemas/shared.d.ts +32 -1
  218. package/dist/http/schemas/shared.js +20 -2
  219. package/dist/http/schemas/shared.js.map +1 -1
  220. package/dist/http/schemas/students.d.ts +1 -7
  221. package/dist/http/schemas/students.js +1 -7
  222. package/dist/http/schemas/students.js.map +1 -1
  223. package/dist/http/scope.d.ts +3 -1
  224. package/dist/http/scope.js +26 -4
  225. package/dist/http/scope.js.map +1 -1
  226. package/dist/http/server.js +6 -1
  227. package/dist/http/server.js.map +1 -1
  228. package/dist/http/web-bridge.d.ts +1 -1
  229. package/dist/http/web-bridge.js +2 -2
  230. package/dist/http/web-bridge.js.map +1 -1
  231. package/dist/http/well-known.d.ts +7 -0
  232. package/dist/http/well-known.js +22 -0
  233. package/dist/http/well-known.js.map +1 -0
  234. package/dist/index.d.ts +3 -1
  235. package/dist/index.js +4 -2
  236. package/dist/index.js.map +1 -1
  237. package/dist/node_modules/.pnpm/@jridgewell_sourcemap-codec@1.5.5/node_modules/@jridgewell/sourcemap-codec/dist/sourcemap-codec.js +78 -0
  238. package/dist/node_modules/.pnpm/@jridgewell_sourcemap-codec@1.5.5/node_modules/@jridgewell/sourcemap-codec/dist/sourcemap-codec.js.map +1 -0
  239. package/dist/node_modules/.pnpm/@vitest_expect@2.1.9/node_modules/@vitest/expect/dist/index.js +1267 -0
  240. package/dist/node_modules/.pnpm/@vitest_expect@2.1.9/node_modules/@vitest/expect/dist/index.js.map +1 -0
  241. package/dist/node_modules/.pnpm/@vitest_pretty-format@2.1.9/node_modules/@vitest/pretty-format/dist/index.js +814 -0
  242. package/dist/node_modules/.pnpm/@vitest_pretty-format@2.1.9/node_modules/@vitest/pretty-format/dist/index.js.map +1 -0
  243. package/dist/node_modules/.pnpm/@vitest_runner@2.1.9/node_modules/@vitest/runner/dist/chunk-tasks.js +43 -0
  244. package/dist/node_modules/.pnpm/@vitest_runner@2.1.9/node_modules/@vitest/runner/dist/chunk-tasks.js.map +1 -0
  245. package/dist/node_modules/.pnpm/@vitest_runner@2.1.9/node_modules/@vitest/runner/dist/index.js +527 -0
  246. package/dist/node_modules/.pnpm/@vitest_runner@2.1.9/node_modules/@vitest/runner/dist/index.js.map +1 -0
  247. package/dist/node_modules/.pnpm/@vitest_runner@2.1.9/node_modules/@vitest/runner/dist/utils.js +2 -0
  248. package/dist/node_modules/.pnpm/@vitest_snapshot@2.1.9/node_modules/@vitest/snapshot/dist/index.js +1737 -0
  249. package/dist/node_modules/.pnpm/@vitest_snapshot@2.1.9/node_modules/@vitest/snapshot/dist/index.js.map +1 -0
  250. package/dist/node_modules/.pnpm/@vitest_spy@2.1.9/node_modules/@vitest/spy/dist/index.js +137 -0
  251. package/dist/node_modules/.pnpm/@vitest_spy@2.1.9/node_modules/@vitest/spy/dist/index.js.map +1 -0
  252. package/dist/node_modules/.pnpm/@vitest_utils@2.1.9/node_modules/@vitest/utils/dist/chunk-_commonjsHelpers.js +118 -0
  253. package/dist/node_modules/.pnpm/@vitest_utils@2.1.9/node_modules/@vitest/utils/dist/chunk-_commonjsHelpers.js.map +1 -0
  254. package/dist/node_modules/.pnpm/@vitest_utils@2.1.9/node_modules/@vitest/utils/dist/diff.js +1222 -0
  255. package/dist/node_modules/.pnpm/@vitest_utils@2.1.9/node_modules/@vitest/utils/dist/diff.js.map +1 -0
  256. package/dist/node_modules/.pnpm/@vitest_utils@2.1.9/node_modules/@vitest/utils/dist/error.js +91 -0
  257. package/dist/node_modules/.pnpm/@vitest_utils@2.1.9/node_modules/@vitest/utils/dist/error.js.map +1 -0
  258. package/dist/node_modules/.pnpm/@vitest_utils@2.1.9/node_modules/@vitest/utils/dist/helpers.js +113 -0
  259. package/dist/node_modules/.pnpm/@vitest_utils@2.1.9/node_modules/@vitest/utils/dist/helpers.js.map +1 -0
  260. package/dist/node_modules/.pnpm/@vitest_utils@2.1.9/node_modules/@vitest/utils/dist/index.js +449 -0
  261. package/dist/node_modules/.pnpm/@vitest_utils@2.1.9/node_modules/@vitest/utils/dist/index.js.map +1 -0
  262. package/dist/node_modules/.pnpm/@vitest_utils@2.1.9/node_modules/@vitest/utils/dist/source-map.js +163 -0
  263. package/dist/node_modules/.pnpm/@vitest_utils@2.1.9/node_modules/@vitest/utils/dist/source-map.js.map +1 -0
  264. package/dist/node_modules/.pnpm/chai@5.3.3/node_modules/chai/index.js +3187 -0
  265. package/dist/node_modules/.pnpm/chai@5.3.3/node_modules/chai/index.js.map +1 -0
  266. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/arguments.js +11 -0
  267. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/arguments.js.map +1 -0
  268. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/array.js +16 -0
  269. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/array.js.map +1 -0
  270. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/bigint.js +11 -0
  271. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/bigint.js.map +1 -0
  272. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/class.js +15 -0
  273. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/class.js.map +1 -0
  274. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/date.js +13 -0
  275. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/date.js.map +1 -0
  276. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/error.js +34 -0
  277. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/error.js.map +1 -0
  278. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/function.js +12 -0
  279. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/function.js.map +1 -0
  280. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/helpers.js +122 -0
  281. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/helpers.js.map +1 -0
  282. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/html.js +39 -0
  283. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/html.js.map +1 -0
  284. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/index.js +99 -0
  285. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/index.js.map +1 -0
  286. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/map.js +25 -0
  287. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/map.js.map +1 -0
  288. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/number.js +14 -0
  289. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/number.js.map +1 -0
  290. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/object.js +21 -0
  291. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/object.js.map +1 -0
  292. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/promise.js +6 -0
  293. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/promise.js.map +1 -0
  294. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/regexp.js +12 -0
  295. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/regexp.js.map +1 -0
  296. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/set.js +18 -0
  297. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/set.js.map +1 -0
  298. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/string.js +24 -0
  299. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/string.js.map +1 -0
  300. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/symbol.js +9 -0
  301. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/symbol.js.map +1 -0
  302. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/typedarray.js +30 -0
  303. package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/typedarray.js.map +1 -0
  304. package/dist/node_modules/.pnpm/magic-string@0.30.21/node_modules/magic-string/dist/magic-string.es.js +939 -0
  305. package/dist/node_modules/.pnpm/magic-string@0.30.21/node_modules/magic-string/dist/magic-string.es.js.map +1 -0
  306. package/dist/node_modules/.pnpm/pathe@1.1.2/node_modules/pathe/dist/shared/pathe.ff20891b.js +82 -0
  307. package/dist/node_modules/.pnpm/pathe@1.1.2/node_modules/pathe/dist/shared/pathe.ff20891b.js.map +1 -0
  308. package/dist/node_modules/.pnpm/tinyrainbow@1.2.0/node_modules/tinyrainbow/dist/chunk-BVHSVHOK.js +82 -0
  309. package/dist/node_modules/.pnpm/tinyrainbow@1.2.0/node_modules/tinyrainbow/dist/chunk-BVHSVHOK.js.map +1 -0
  310. package/dist/node_modules/.pnpm/tinyrainbow@1.2.0/node_modules/tinyrainbow/dist/node.js +8 -0
  311. package/dist/node_modules/.pnpm/tinyrainbow@1.2.0/node_modules/tinyrainbow/dist/node.js.map +1 -0
  312. package/dist/node_modules/.pnpm/tinyspy@3.0.2/node_modules/tinyspy/dist/index.js +83 -0
  313. package/dist/node_modules/.pnpm/tinyspy@3.0.2/node_modules/tinyspy/dist/index.js.map +1 -0
  314. 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
  315. 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
  316. 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
  317. 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
  318. 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
  319. 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
  320. 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
  321. 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
  322. package/dist/packages/server/package.js +6 -0
  323. package/dist/packages/server/package.js.map +1 -0
  324. package/package.json +9 -7
  325. package/dist/app/notifications.d.ts +0 -7
  326. package/dist/app/notifications.js +0 -26
  327. package/dist/app/notifications.js.map +0 -1
  328. package/dist/http/routes/learn/assets.d.ts +0 -8
  329. package/dist/http/routes/learn/assets.js +0 -32
  330. package/dist/http/routes/learn/assets.js.map +0 -1
  331. package/dist/http/routes/learn/comments.d.ts +0 -7
  332. package/dist/http/routes/learn/comments.js +0 -175
  333. package/dist/http/routes/learn/comments.js.map +0 -1
  334. package/dist/http/routes/learn/courses.d.ts +0 -7
  335. package/dist/http/routes/learn/courses.js +0 -98
  336. package/dist/http/routes/learn/courses.js.map +0 -1
  337. package/dist/http/routes/learn/downloads.d.ts +0 -7
  338. package/dist/http/routes/learn/downloads.js +0 -76
  339. package/dist/http/routes/learn/downloads.js.map +0 -1
  340. package/dist/http/routes/learn/index.d.ts +0 -7
  341. package/dist/http/routes/learn/index.js +0 -19
  342. package/dist/http/routes/learn/index.js.map +0 -1
  343. package/dist/http/routes/learn/org.d.ts +0 -7
  344. package/dist/http/routes/learn/org.js +0 -50
  345. package/dist/http/routes/learn/org.js.map +0 -1
  346. package/dist/http/routes/learn/progress.d.ts +0 -7
  347. package/dist/http/routes/learn/progress.js +0 -63
  348. package/dist/http/routes/learn/progress.js.map +0 -1
  349. package/dist/http/schemas/learn.js.map +0 -1
  350. package/dist/package.js +0 -6
  351. package/dist/package.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"courses.js","names":[],"sources":["../../../src/http/routes/courses.ts"],"sourcesContent":["// HTTP routes for the courses context. Request + response are validated against\n// the shared contract schemas by the Zod type provider, and @fastify/swagger\n// reads the same schemas to build the OpenAPI spec the SDK is generated from.\nimport type { FastifyInstance } from 'fastify';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport {\n Course,\n CourseIdParam,\n CourseSettings,\n CoursesPage,\n CoursesQuery,\n CreateCourse,\n ErrorBody,\n PatchCourseSettings,\n UpdateCourse,\n} from '../schemas/index.js';\nimport { z } from 'zod';\nimport type { Container } from '../../app/container.js';\nimport { NotFoundError } from '@headless-lms/core/shared/errors';\nimport { resolveScope } from '../scope.js';\n\nexport async function coursesRoutes(app: FastifyInstance, container: Container): Promise<void> {\n const r = app.withTypeProvider<ZodTypeProvider>();\n const courses = container.content;\n\n r.route({\n method: 'GET',\n url: '/api/courses',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listCourses',\n tags: ['Content'],\n summary: 'List courses',\n querystring: CoursesQuery,\n response: { 200: CoursesPage },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return courses.listCourses(scope.orgId, req.query);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/courses/:id',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getCourse',\n tags: ['Content'],\n summary: 'Get a course by id',\n params: CourseIdParam,\n response: { 200: Course, 404: ErrorBody },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n const course = await courses.getCourse(scope.orgId, req.params.id);\n if (!course) {\n throw new NotFoundError('Course', req.params.id);\n }\n return course;\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/courses',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'createCourse',\n tags: ['Content'],\n summary: 'Create a course',\n body: CreateCourse,\n response: { 201: Course },\n },\n handler: async (req, reply) => {\n const scope = await resolveScope(container, req);\n const course = await courses.createCourse(scope.orgId, req.body);\n return reply.code(201).send(course);\n },\n });\n\n r.route({\n method: 'PATCH',\n url: '/api/courses/:id',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'updateCourse',\n tags: ['Content'],\n summary: 'Update a course',\n params: CourseIdParam,\n body: UpdateCourse,\n response: { 200: Course, 404: ErrorBody },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return courses.updateCourse(scope.orgId, req.params.id, req.body);\n },\n });\n\n r.route({\n method: 'PATCH',\n url: '/api/courses/:id/settings',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'updateCourseSettings',\n tags: ['Content'],\n summary: 'Update a course settings',\n params: CourseIdParam,\n body: PatchCourseSettings,\n response: { 200: CourseSettings, 404: ErrorBody },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n req.log.info({ body: req.body }, 'update course settings');\n return courses.patchCourseSettings(scope.orgId, req.params.id, req.body);\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/courses/:id',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'deleteCourse',\n tags: ['Content'],\n summary: 'Delete a course',\n params: CourseIdParam,\n response: { 204: z.void(), 404: ErrorBody },\n },\n handler: async (req, reply) => {\n const scope = await resolveScope(container, req);\n await courses.deleteCourse(scope.orgId, req.params.id);\n return reply.code(204).send();\n },\n });\n}\n"],"mappings":";;;;;;;AAqBA,eAAsB,cAAc,KAAsB,WAAqC;CAC7F,MAAM,IAAI,IAAI,iBAAkC;CAChD,MAAM,UAAU,UAAU;CAE1B,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,SAAS;GAChB,SAAS;GACT,aAAa;GACb,UAAU,EAAE,KAAK,YAAY;EAC/B;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,OAAO,QAAQ,YAAY,MAAM,OAAO,IAAI,KAAK;EACnD;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,SAAS;GAChB,SAAS;GACT,QAAQ;GACR,UAAU;IAAE,KAAK;IAAQ,KAAK;GAAU;EAC1C;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,MAAM,SAAS,MAAM,QAAQ,UAAU,MAAM,OAAO,IAAI,OAAO,EAAE;GACjE,IAAI,CAAC,QACH,MAAM,IAAI,cAAc,UAAU,IAAI,OAAO,EAAE;GAEjD,OAAO;EACT;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,SAAS;GAChB,SAAS;GACT,MAAM;GACN,UAAU,EAAE,KAAK,OAAO;EAC1B;EACA,SAAS,OAAO,KAAK,UAAU;GAC7B,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,MAAM,SAAS,MAAM,QAAQ,aAAa,MAAM,OAAO,IAAI,IAAI;GAC/D,OAAO,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,MAAM;EACpC;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,SAAS;GAChB,SAAS;GACT,QAAQ;GACR,MAAM;GACN,UAAU;IAAE,KAAK;IAAQ,KAAK;GAAU;EAC1C;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,OAAO,QAAQ,aAAa,MAAM,OAAO,IAAI,OAAO,IAAI,IAAI,IAAI;EAClE;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,SAAS;GAChB,SAAS;GACT,QAAQ;GACR,MAAM;GACN,UAAU;IAAE,KAAK;IAAgB,KAAK;GAAU;EAClD;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,IAAI,IAAI,KAAK,EAAE,MAAM,IAAI,KAAK,GAAG,wBAAwB;GACzD,OAAO,QAAQ,oBAAoB,MAAM,OAAO,IAAI,OAAO,IAAI,IAAI,IAAI;EACzE;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,SAAS;GAChB,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,QAAQ,aAAa,MAAM,OAAO,IAAI,OAAO,EAAE;GACrD,OAAO,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK;EAC9B;CACF,CAAC;AACH"}
1
+ {"version":3,"file":"courses.js","names":[],"sources":["../../../src/http/routes/courses.ts"],"sourcesContent":["// HTTP routes for the courses context. Request + response are validated against\n// the shared contract schemas by the Zod type provider, and @fastify/swagger\n// reads the same schemas to build the OpenAPI spec the SDK is generated from.\nimport type { FastifyInstance } from 'fastify';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport {\n Course,\n CourseIdParam,\n CourseSettings,\n CoursesPage,\n CoursesQuery,\n CreateCourse,\n PatchCourseSettings,\n UpdateCourse,\n refusals,\n} from '../schemas/index.js';\nimport { z } from 'zod';\nimport type { Container } from '../../app/container.js';\nimport { NotFoundError } from '@headless-lms/core/shared/errors';\nimport { resolveScope } from '../scope.js';\n\nexport async function coursesRoutes(app: FastifyInstance, container: Container): Promise<void> {\n const r = app.withTypeProvider<ZodTypeProvider>();\n const courses = container.content;\n\n r.route({\n method: 'GET',\n url: '/api/courses',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listCourses',\n tags: ['Content'],\n summary: 'List courses (staff)',\n description:\n 'Staff only (owner, admin, instructor). Every course in the organization, drafts included, filtered by status and category and searched by title.',\n querystring: CoursesQuery,\n response: { 200: CoursesPage, ...refusals(403) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return courses.listCourses(scope.orgId, req.query);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/courses/:id',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getCourse',\n tags: ['Content'],\n summary: 'Get a course (staff)',\n description: 'Staff only (owner, admin, instructor).',\n params: CourseIdParam,\n response: { 200: Course, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n const course = await courses.getCourse(scope.orgId, req.params.id);\n if (!course) {\n throw new NotFoundError('Course', req.params.id);\n }\n return course;\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/courses',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'createCourse',\n tags: ['Content'],\n summary: 'Create a course (staff)',\n description:\n 'Staff only (owner, admin, instructor). The course starts as a draft. 409 when its slug is taken.',\n body: CreateCourse,\n response: { 201: Course, ...refusals(403, 409) },\n },\n handler: async (req, reply) => {\n const scope = await resolveScope(container, req);\n const course = await courses.createCourse(scope.orgId, req.body);\n return reply.code(201).send(course);\n },\n });\n\n r.route({\n method: 'PATCH',\n url: '/api/courses/:id',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'updateCourse',\n tags: ['Content'],\n summary: 'Update a course (staff)',\n description:\n 'Staff only (owner, admin, instructor). Changes only the fields sent. `thumbnailAssetId` must be a ready library image (409 otherwise); null clears it.',\n params: CourseIdParam,\n body: UpdateCourse,\n response: { 200: Course, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return courses.updateCourse(scope.orgId, req.params.id, req.body);\n },\n });\n\n r.route({\n method: 'PATCH',\n url: '/api/courses/:id/settings',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'updateCourseSettings',\n tags: ['Content'],\n summary: \"Update a course's settings (staff)\",\n description:\n 'Staff only (owner, admin, instructor). Merges the settings sent into the current ones.',\n params: CourseIdParam,\n body: PatchCourseSettings,\n response: { 200: CourseSettings, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n req.log.info({ body: req.body }, 'update course settings');\n return courses.patchCourseSettings(scope.orgId, req.params.id, req.body);\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/courses/:id',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'deleteCourse',\n tags: ['Content'],\n summary: 'Delete a course (staff)',\n description: 'Staff only (owner, admin, instructor).',\n params: CourseIdParam,\n response: { 204: z.void(), ...refusals(403, 404) },\n },\n handler: async (req, reply) => {\n const scope = await resolveScope(container, req);\n await courses.deleteCourse(scope.orgId, req.params.id, {\n id: scope.orgUserId,\n role: scope.role,\n });\n return reply.code(204).send();\n },\n });\n}\n"],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import { refusals } from "../schemas/shared.js";
1
2
  import { resolveScope } from "../scope.js";
2
3
  import { EnrollmentSeries, EnrollmentSeriesQuery, OverviewStats } from "../schemas/dashboard.js";
3
4
  import "../schemas/index.js";
@@ -11,12 +12,16 @@ async function dashboardRoutes(app, container) {
11
12
  schema: {
12
13
  operationId: "getOverview",
13
14
  tags: ["Reporting"],
14
- summary: "Back-office overview stats",
15
- response: { 200: OverviewStats }
15
+ summary: "Back-office overview stats (staff)",
16
+ description: "Staff only (owner, admin, instructor).",
17
+ response: {
18
+ 200: OverviewStats,
19
+ ...refusals(403)
20
+ }
16
21
  },
17
22
  handler: async (req) => {
18
23
  const scope = await resolveScope(container, req);
19
- return container.reporting.dashboard.overview(scope.orgId);
24
+ return container.reporting.admin.overview(scope.orgId);
20
25
  }
21
26
  });
22
27
  r.route({
@@ -26,13 +31,17 @@ async function dashboardRoutes(app, container) {
26
31
  schema: {
27
32
  operationId: "getEnrollmentSeries",
28
33
  tags: ["Reporting"],
29
- summary: "Enrollments granted per day over a trailing window",
34
+ summary: "Enrollments granted per day over a trailing window (staff)",
35
+ description: "Staff only (owner, admin, instructor).",
30
36
  querystring: EnrollmentSeriesQuery,
31
- response: { 200: EnrollmentSeries }
37
+ response: {
38
+ 200: EnrollmentSeries,
39
+ ...refusals(403)
40
+ }
32
41
  },
33
42
  handler: async (req) => {
34
43
  const scope = await resolveScope(container, req);
35
- return container.reporting.dashboard.enrollments(scope.orgId, req.query.days);
44
+ return container.reporting.admin.enrollments(scope.orgId, req.query.days);
36
45
  }
37
46
  });
38
47
  }
@@ -1 +1 @@
1
- {"version":3,"file":"dashboard.js","names":[],"sources":["../../../src/http/routes/dashboard.ts"],"sourcesContent":["// HTTP routes for the dashboard (overview) context.\nimport type { FastifyInstance } from 'fastify';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport { EnrollmentSeries, EnrollmentSeriesQuery, OverviewStats } from '../schemas/index.js';\nimport type { Container } from '../../app/container.js';\nimport { resolveScope } from '../scope.js';\n\nexport async function dashboardRoutes(app: FastifyInstance, container: Container): Promise<void> {\n const r = app.withTypeProvider<ZodTypeProvider>();\n\n r.route({\n method: 'GET',\n url: '/api/overview',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getOverview',\n tags: ['Reporting'],\n summary: 'Back-office overview stats',\n response: { 200: OverviewStats },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return container.reporting.dashboard.overview(scope.orgId);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/overview/enrollments',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getEnrollmentSeries',\n tags: ['Reporting'],\n summary: 'Enrollments granted per day over a trailing window',\n querystring: EnrollmentSeriesQuery,\n response: { 200: EnrollmentSeries },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return container.reporting.dashboard.enrollments(scope.orgId, req.query.days);\n },\n });\n}\n"],"mappings":";;;;AAOA,eAAsB,gBAAgB,KAAsB,WAAqC;CAC/F,MAAM,IAAI,IAAI,iBAAkC;CAEhD,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,WAAW;GAClB,SAAS;GACT,UAAU,EAAE,KAAK,cAAc;EACjC;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,OAAO,UAAU,UAAU,UAAU,SAAS,MAAM,KAAK;EAC3D;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,UAAU,EAAE,KAAK,iBAAiB;EACpC;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,OAAO,UAAU,UAAU,UAAU,YAAY,MAAM,OAAO,IAAI,MAAM,IAAI;EAC9E;CACF,CAAC;AACH"}
1
+ {"version":3,"file":"dashboard.js","names":[],"sources":["../../../src/http/routes/dashboard.ts"],"sourcesContent":["// HTTP routes for the dashboard (overview) context.\nimport type { FastifyInstance } from 'fastify';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport {\n EnrollmentSeries,\n EnrollmentSeriesQuery,\n OverviewStats,\n refusals,\n} from '../schemas/index.js';\nimport type { Container } from '../../app/container.js';\nimport { resolveScope } from '../scope.js';\n\nexport async function dashboardRoutes(app: FastifyInstance, container: Container): Promise<void> {\n const r = app.withTypeProvider<ZodTypeProvider>();\n\n r.route({\n method: 'GET',\n url: '/api/overview',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getOverview',\n tags: ['Reporting'],\n summary: 'Back-office overview stats (staff)',\n description: 'Staff only (owner, admin, instructor).',\n response: { 200: OverviewStats, ...refusals(403) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return container.reporting.admin.overview(scope.orgId);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/overview/enrollments',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getEnrollmentSeries',\n tags: ['Reporting'],\n summary: 'Enrollments granted per day over a trailing window (staff)',\n description: 'Staff only (owner, admin, instructor).',\n querystring: EnrollmentSeriesQuery,\n response: { 200: EnrollmentSeries, ...refusals(403) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return container.reporting.admin.enrollments(scope.orgId, req.query.days);\n },\n });\n}\n"],"mappings":";;;;;AAYA,eAAsB,gBAAgB,KAAsB,WAAqC;CAC/F,MAAM,IAAI,IAAI,iBAAkC;CAEhD,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,WAAW;GAClB,SAAS;GACT,aAAa;GACb,UAAU;IAAE,KAAK;IAAe,GAAG,SAAS,GAAG;GAAE;EACnD;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,OAAO,UAAU,UAAU,MAAM,SAAS,MAAM,KAAK;EACvD;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,aAAa;GACb,UAAU;IAAE,KAAK;IAAkB,GAAG,SAAS,GAAG;GAAE;EACtD;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,OAAO,UAAU,UAAU,MAAM,YAAY,MAAM,OAAO,IAAI,MAAM,IAAI;EAC1E;CACF,CAAC;AACH"}
@@ -1,22 +1,30 @@
1
- import { ErrorBody } from "../schemas/shared.js";
1
+ import { refusals } from "../schemas/shared.js";
2
+ import { resolveScope } from "../scope.js";
2
3
  import { Comment, CommentIdParam, CommentView, CommentsPage, CommentsQuery, PatchComment, ReplyToComment } from "../schemas/discussion.js";
3
4
  import "../schemas/index.js";
4
- import { NotFoundError } from "@headless-lms/core/shared/errors";
5
5
  import { z } from "zod";
6
+ import { NotFoundError } from "@headless-lms/core/shared/errors";
6
7
  //#region src/http/routes/discussion.ts
7
8
  async function discussionRoutes(app, container) {
8
9
  const r = app.withTypeProvider();
9
10
  const discussion = container.discussion;
11
+ const staff = async (req) => {
12
+ await resolveScope(container, req);
13
+ };
10
14
  r.route({
11
15
  method: "GET",
12
16
  url: "/api/comments",
13
- preHandler: app.requireOrgSession,
17
+ preHandler: staff,
14
18
  schema: {
15
19
  operationId: "listComments",
16
20
  tags: ["Discussion"],
17
- summary: "List comments, filtered by course, activity, author, status or reports",
21
+ summary: "List course comments for moderation (staff)",
22
+ description: "Staff only (owner, admin, instructor). Comments on course activities, filtered by course, activity, author, status or open reports; each row carries the author's email and the open reports.",
18
23
  querystring: CommentsQuery,
19
- response: { 200: CommentsPage }
24
+ response: {
25
+ 200: CommentsPage,
26
+ ...refusals(403)
27
+ }
20
28
  },
21
29
  handler: async (req) => {
22
30
  return discussion.listComments(req.orgId, req.query);
@@ -25,88 +33,88 @@ async function discussionRoutes(app, container) {
25
33
  r.route({
26
34
  method: "DELETE",
27
35
  url: "/api/comments/:commentId",
28
- preHandler: app.requireOrgSession,
36
+ preHandler: staff,
29
37
  schema: {
30
38
  operationId: "moderateRemoveComment",
31
39
  tags: ["Discussion"],
32
- summary: "Remove a comment as a moderator",
40
+ summary: "Remove a course comment (staff)",
41
+ description: "Staff only (owner, admin, instructor). Hides the comment and resolves its open reports as `removed`. Removing a removed comment returns it unchanged.",
33
42
  params: CommentIdParam,
34
43
  response: {
35
- 200: Comment,
36
- 403: ErrorBody,
37
- 404: ErrorBody
44
+ 200: CommentView,
45
+ ...refusals(403, 404)
38
46
  }
39
47
  },
40
48
  handler: async (req) => {
41
49
  const orgUser = await container.organizations.getOrgUser(req.orgId, req.userId);
42
- return discussion.remove(req.orgId, req.params.commentId, orgUser);
50
+ return discussion.remove(req.orgId, req.params.commentId, orgUser, null);
43
51
  }
44
52
  });
45
53
  r.route({
46
54
  method: "PATCH",
47
55
  url: "/api/comments/:commentId",
48
- preHandler: app.requireOrgSession,
56
+ preHandler: staff,
49
57
  schema: {
50
58
  operationId: "publishComment",
51
59
  tags: ["Discussion"],
52
- summary: "Publish a comment: approves a pending one, restores a removed one",
60
+ summary: "Publish a course comment (staff)",
61
+ description: "Staff only (owner, admin, instructor). Approves a pending comment or restores a removed one; a published comment is returned unchanged.",
53
62
  params: CommentIdParam,
54
63
  body: PatchComment,
55
64
  response: {
56
65
  200: Comment,
57
- 403: ErrorBody,
58
- 404: ErrorBody
66
+ ...refusals(403, 404)
59
67
  }
60
68
  },
61
69
  handler: async (req) => {
62
70
  const orgUser = await container.organizations.getOrgUser(req.orgId, req.userId);
63
71
  if (!orgUser) throw new NotFoundError("OrgUser", req.userId);
64
- return discussion.publish(req.orgId, req.params.commentId, orgUser);
72
+ return discussion.publish(req.orgId, req.params.commentId, orgUser, null);
65
73
  }
66
74
  });
67
75
  r.route({
68
76
  method: "DELETE",
69
77
  url: "/api/comments/:commentId/reports",
70
- preHandler: app.requireOrgSession,
78
+ preHandler: staff,
71
79
  schema: {
72
80
  operationId: "dismissCommentReports",
73
81
  tags: ["Discussion"],
74
- summary: "Dismiss every open report on a comment",
82
+ summary: "Dismiss every open report on a course comment (staff)",
83
+ description: "Staff only (owner, admin, instructor). The comment stays up.",
75
84
  params: CommentIdParam,
76
85
  response: {
77
86
  204: z.void(),
78
- 403: ErrorBody,
79
- 404: ErrorBody
87
+ ...refusals(403, 404)
80
88
  }
81
89
  },
82
90
  handler: async (req, reply) => {
83
91
  const orgUser = await container.organizations.getOrgUser(req.orgId, req.userId);
84
92
  if (!orgUser) throw new NotFoundError("OrgUser", req.userId);
85
- await discussion.resolveReports(req.orgId, req.params.commentId, orgUser);
93
+ await discussion.resolveReports(req.orgId, req.params.commentId, orgUser, null);
86
94
  return reply.code(204).send();
87
95
  }
88
96
  });
89
97
  r.route({
90
98
  method: "POST",
91
99
  url: "/api/comments/:commentId/replies",
92
- preHandler: app.requireOrgSession,
100
+ preHandler: staff,
93
101
  schema: {
94
102
  operationId: "replyToComment",
95
103
  tags: ["Discussion"],
96
- summary: "Reply to a comment from the moderation queue",
104
+ summary: "Reply to a course comment from the moderation queue (staff)",
105
+ description: "Staff only (owner, admin, instructor). Replies nest one level: answering a reply attaches to its root comment.",
97
106
  params: CommentIdParam,
98
107
  body: ReplyToComment,
99
108
  response: {
100
109
  200: CommentView,
101
- 403: ErrorBody,
102
- 404: ErrorBody
110
+ ...refusals(403, 404)
103
111
  }
104
112
  },
105
113
  handler: async (req) => {
106
114
  const orgUser = await container.organizations.getOrgUser(req.orgId, req.userId);
107
115
  if (!orgUser) throw new NotFoundError("OrgUser", req.userId);
108
116
  const parent = await discussion.getComment(req.orgId, req.params.commentId);
109
- if (!parent) throw new NotFoundError("Comment", req.params.commentId);
117
+ if (!parent || parent.activityId === null) throw new NotFoundError("Comment", req.params.commentId);
110
118
  return discussion.postComment(req.orgId, {
111
119
  actor: orgUser,
112
120
  activityId: parent.activityId,
@@ -1 +1 @@
1
- {"version":3,"file":"discussion.js","names":[],"sources":["../../../src/http/routes/discussion.ts"],"sourcesContent":["// Moderator-facing discussion routes. The learner surface (everything under\n// `/api/learn`) lives in routes/learn/comments.ts.\nimport type { FastifyInstance } from 'fastify';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport { z } from 'zod';\nimport {\n Comment,\n CommentIdParam,\n CommentsPage,\n CommentsQuery,\n CommentView,\n ErrorBody,\n PatchComment,\n ReplyToComment,\n} from '../schemas/index.js';\nimport { NotFoundError } from '@headless-lms/core/shared/errors';\nimport type { Container } from '../../app/container.js';\n\nexport async function discussionRoutes(app: FastifyInstance, container: Container): Promise<void> {\n const r = app.withTypeProvider<ZodTypeProvider>();\n const discussion = container.discussion;\n\n r.route({\n method: 'GET',\n url: '/api/comments',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listComments',\n tags: ['Discussion'],\n summary: 'List comments, filtered by course, activity, author, status or reports',\n querystring: CommentsQuery,\n response: { 200: CommentsPage },\n },\n handler: async (req) => {\n return discussion.listComments(req.orgId, req.query);\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/comments/:commentId',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'moderateRemoveComment',\n tags: ['Discussion'],\n summary: 'Remove a comment as a moderator',\n params: CommentIdParam,\n response: { 200: Comment, 403: ErrorBody, 404: ErrorBody },\n },\n handler: async (req) => {\n const orgUser = await container.organizations.getOrgUser(req.orgId, req.userId);\n return discussion.remove(req.orgId, req.params.commentId, orgUser!);\n },\n });\n\n r.route({\n method: 'PATCH',\n url: '/api/comments/:commentId',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'publishComment',\n tags: ['Discussion'],\n summary: 'Publish a comment: approves a pending one, restores a removed one',\n params: CommentIdParam,\n body: PatchComment,\n response: { 200: Comment, 403: ErrorBody, 404: ErrorBody },\n },\n handler: async (req) => {\n const orgUser = await container.organizations.getOrgUser(req.orgId, req.userId);\n if (!orgUser) {\n throw new NotFoundError('OrgUser', req.userId);\n }\n return discussion.publish(req.orgId, req.params.commentId, orgUser);\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/comments/:commentId/reports',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'dismissCommentReports',\n tags: ['Discussion'],\n summary: 'Dismiss every open report on a comment',\n params: CommentIdParam,\n response: { 204: z.void(), 403: ErrorBody, 404: ErrorBody },\n },\n handler: async (req, reply) => {\n const orgUser = await container.organizations.getOrgUser(req.orgId, req.userId);\n if (!orgUser) {\n throw new NotFoundError('OrgUser', req.userId);\n }\n await discussion.resolveReports(req.orgId, req.params.commentId, orgUser);\n return reply.code(204).send();\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/comments/:commentId/replies',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'replyToComment',\n tags: ['Discussion'],\n summary: 'Reply to a comment from the moderation queue',\n params: CommentIdParam,\n body: ReplyToComment,\n response: { 200: CommentView, 403: ErrorBody, 404: ErrorBody },\n },\n handler: async (req) => {\n const orgUser = await container.organizations.getOrgUser(req.orgId, req.userId);\n if (!orgUser) {\n throw new NotFoundError('OrgUser', req.userId);\n }\n const parent = await discussion.getComment(req.orgId, req.params.commentId);\n if (!parent) {\n throw new NotFoundError('Comment', req.params.commentId);\n }\n // Replies nest one level: answering a reply attaches to its root.\n return discussion.postComment(req.orgId, {\n actor: orgUser,\n activityId: parent.activityId,\n parentId: parent.parentId ?? parent.id,\n body: req.body.body,\n });\n },\n });\n}\n"],"mappings":";;;;;;AAkBA,eAAsB,iBAAiB,KAAsB,WAAqC;CAChG,MAAM,IAAI,IAAI,iBAAkC;CAChD,MAAM,aAAa,UAAU;CAE7B,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,YAAY;GACnB,SAAS;GACT,aAAa;GACb,UAAU,EAAE,KAAK,aAAa;EAChC;EACA,SAAS,OAAO,QAAQ;GACtB,OAAO,WAAW,aAAa,IAAI,OAAO,IAAI,KAAK;EACrD;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,YAAY;GACnB,SAAS;GACT,QAAQ;GACR,UAAU;IAAE,KAAK;IAAS,KAAK;IAAW,KAAK;GAAU;EAC3D;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,UAAU,MAAM,UAAU,cAAc,WAAW,IAAI,OAAO,IAAI,MAAM;GAC9E,OAAO,WAAW,OAAO,IAAI,OAAO,IAAI,OAAO,WAAW,OAAQ;EACpE;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,YAAY;GACnB,SAAS;GACT,QAAQ;GACR,MAAM;GACN,UAAU;IAAE,KAAK;IAAS,KAAK;IAAW,KAAK;GAAU;EAC3D;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,UAAU,MAAM,UAAU,cAAc,WAAW,IAAI,OAAO,IAAI,MAAM;GAC9E,IAAI,CAAC,SACH,MAAM,IAAI,cAAc,WAAW,IAAI,MAAM;GAE/C,OAAO,WAAW,QAAQ,IAAI,OAAO,IAAI,OAAO,WAAW,OAAO;EACpE;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,YAAY;GACnB,SAAS;GACT,QAAQ;GACR,UAAU;IAAE,KAAK,EAAE,KAAK;IAAG,KAAK;IAAW,KAAK;GAAU;EAC5D;EACA,SAAS,OAAO,KAAK,UAAU;GAC7B,MAAM,UAAU,MAAM,UAAU,cAAc,WAAW,IAAI,OAAO,IAAI,MAAM;GAC9E,IAAI,CAAC,SACH,MAAM,IAAI,cAAc,WAAW,IAAI,MAAM;GAE/C,MAAM,WAAW,eAAe,IAAI,OAAO,IAAI,OAAO,WAAW,OAAO;GACxE,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,YAAY;GACnB,SAAS;GACT,QAAQ;GACR,MAAM;GACN,UAAU;IAAE,KAAK;IAAa,KAAK;IAAW,KAAK;GAAU;EAC/D;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,UAAU,MAAM,UAAU,cAAc,WAAW,IAAI,OAAO,IAAI,MAAM;GAC9E,IAAI,CAAC,SACH,MAAM,IAAI,cAAc,WAAW,IAAI,MAAM;GAE/C,MAAM,SAAS,MAAM,WAAW,WAAW,IAAI,OAAO,IAAI,OAAO,SAAS;GAC1E,IAAI,CAAC,QACH,MAAM,IAAI,cAAc,WAAW,IAAI,OAAO,SAAS;GAGzD,OAAO,WAAW,YAAY,IAAI,OAAO;IACvC,OAAO;IACP,YAAY,OAAO;IACnB,UAAU,OAAO,YAAY,OAAO;IACpC,MAAM,IAAI,KAAK;GACjB,CAAC;EACH;CACF,CAAC;AACH"}
1
+ {"version":3,"file":"discussion.js","names":[],"sources":["../../../src/http/routes/discussion.ts"],"sourcesContent":["// Moderator-facing discussion routes. The learner surface (everything under\n// `/api/member`) lives in routes/member/comments.ts.\nimport type { FastifyInstance, FastifyRequest } from 'fastify';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport { z } from 'zod';\nimport {\n Comment,\n CommentIdParam,\n CommentsPage,\n CommentsQuery,\n CommentView,\n PatchComment,\n ReplyToComment,\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 discussionRoutes(app: FastifyInstance, container: Container): Promise<void> {\n const r = app.withTypeProvider<ZodTypeProvider>();\n const discussion = container.discussion;\n const staff = async (req: FastifyRequest) => {\n await resolveScope(container, req);\n };\n\n r.route({\n method: 'GET',\n url: '/api/comments',\n preHandler: staff,\n schema: {\n operationId: 'listComments',\n tags: ['Discussion'],\n summary: 'List course comments for moderation (staff)',\n description:\n \"Staff only (owner, admin, instructor). Comments on course activities, filtered by course, activity, author, status or open reports; each row carries the author's email and the open reports.\",\n querystring: CommentsQuery,\n response: { 200: CommentsPage, ...refusals(403) },\n },\n handler: async (req) => {\n return discussion.listComments(req.orgId, req.query);\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/comments/:commentId',\n preHandler: staff,\n schema: {\n operationId: 'moderateRemoveComment',\n tags: ['Discussion'],\n summary: 'Remove a course comment (staff)',\n description:\n 'Staff only (owner, admin, instructor). Hides the comment and resolves its open reports as `removed`. Removing a removed comment returns it unchanged.',\n params: CommentIdParam,\n response: { 200: CommentView, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const orgUser = await container.organizations.getOrgUser(req.orgId, req.userId);\n return discussion.remove(req.orgId, req.params.commentId, orgUser!, null);\n },\n });\n\n r.route({\n method: 'PATCH',\n url: '/api/comments/:commentId',\n preHandler: staff,\n schema: {\n operationId: 'publishComment',\n tags: ['Discussion'],\n summary: 'Publish a course comment (staff)',\n description:\n 'Staff only (owner, admin, instructor). Approves a pending comment or restores a removed one; a published comment is returned unchanged.',\n params: CommentIdParam,\n body: PatchComment,\n response: { 200: Comment, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const orgUser = await container.organizations.getOrgUser(req.orgId, req.userId);\n if (!orgUser) {\n throw new NotFoundError('OrgUser', req.userId);\n }\n return discussion.publish(req.orgId, req.params.commentId, orgUser, null);\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/comments/:commentId/reports',\n preHandler: staff,\n schema: {\n operationId: 'dismissCommentReports',\n tags: ['Discussion'],\n summary: 'Dismiss every open report on a course comment (staff)',\n description: 'Staff only (owner, admin, instructor). The comment stays up.',\n params: CommentIdParam,\n response: { 204: z.void(), ...refusals(403, 404) },\n },\n handler: async (req, reply) => {\n const orgUser = await container.organizations.getOrgUser(req.orgId, req.userId);\n if (!orgUser) {\n throw new NotFoundError('OrgUser', req.userId);\n }\n await discussion.resolveReports(req.orgId, req.params.commentId, orgUser, null);\n return reply.code(204).send();\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/comments/:commentId/replies',\n preHandler: staff,\n schema: {\n operationId: 'replyToComment',\n tags: ['Discussion'],\n summary: 'Reply to a course comment from the moderation queue (staff)',\n description:\n 'Staff only (owner, admin, instructor). Replies nest one level: answering a reply attaches to its root comment.',\n params: CommentIdParam,\n body: ReplyToComment,\n response: { 200: CommentView, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const orgUser = await container.organizations.getOrgUser(req.orgId, req.userId);\n if (!orgUser) {\n throw new NotFoundError('OrgUser', req.userId);\n }\n const parent = await discussion.getComment(req.orgId, req.params.commentId);\n if (!parent || parent.activityId === null) {\n throw new NotFoundError('Comment', req.params.commentId);\n }\n // Replies nest one level: answering a reply attaches to its root.\n return discussion.postComment(req.orgId, {\n actor: orgUser,\n activityId: parent.activityId,\n parentId: parent.parentId ?? parent.id,\n body: req.body.body,\n });\n },\n });\n}\n"],"mappings":"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"}
@@ -1,9 +1,9 @@
1
+ import { refusals } from "../schemas/shared.js";
1
2
  import { resolveScope } from "../scope.js";
2
- import { ErrorBody } from "../schemas/shared.js";
3
- import { AddDownloadAsset, CreateDownload, Download, DownloadAsset, DownloadAssetParams, DownloadIdParam, DownloadsPage, DownloadsQuery, RenameDownloadAsset, ReorderDownloadAssets, UpdateDownload } from "../schemas/downloads.js";
3
+ import { AddDownloadAsset, CreateDownload, Download, DownloadAssetParams, DownloadFile, DownloadIdParam, DownloadsPage, DownloadsQuery, RenameDownloadAsset, ReorderDownloadAssets, UpdateDownload } from "../schemas/downloads.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/downloads.ts
8
8
  async function downloadsRoutes(app, container) {
9
9
  const r = app.withTypeProvider();
@@ -15,9 +15,13 @@ async function downloadsRoutes(app, container) {
15
15
  schema: {
16
16
  operationId: "listDownloads",
17
17
  tags: ["Content"],
18
- summary: "List downloads",
18
+ summary: "List downloads (staff)",
19
+ description: "Staff only (owner, admin, instructor). Every download, drafts included, filtered by status and category and searched by title.",
19
20
  querystring: DownloadsQuery,
20
- response: { 200: DownloadsPage }
21
+ response: {
22
+ 200: DownloadsPage,
23
+ ...refusals(403)
24
+ }
21
25
  },
22
26
  handler: async (req) => {
23
27
  const scope = await resolveScope(container, req);
@@ -31,11 +35,12 @@ async function downloadsRoutes(app, container) {
31
35
  schema: {
32
36
  operationId: "getDownload",
33
37
  tags: ["Content"],
34
- summary: "Get a download by id",
38
+ summary: "Get a download (staff)",
39
+ description: "Staff only (owner, admin, instructor).",
35
40
  params: DownloadIdParam,
36
41
  response: {
37
42
  200: Download,
38
- 404: ErrorBody
43
+ ...refusals(403, 404)
39
44
  }
40
45
  },
41
46
  handler: async (req) => {
@@ -52,9 +57,13 @@ async function downloadsRoutes(app, container) {
52
57
  schema: {
53
58
  operationId: "createDownload",
54
59
  tags: ["Content"],
55
- summary: "Create a download",
60
+ summary: "Create a download (staff)",
61
+ description: "Staff only (owner, admin, instructor). The download starts as a draft. 409 when its slug is taken.",
56
62
  body: CreateDownload,
57
- response: { 201: Download }
63
+ response: {
64
+ 201: Download,
65
+ ...refusals(403, 409)
66
+ }
58
67
  },
59
68
  handler: async (req, reply) => {
60
69
  const scope = await resolveScope(container, req);
@@ -69,12 +78,13 @@ async function downloadsRoutes(app, container) {
69
78
  schema: {
70
79
  operationId: "updateDownload",
71
80
  tags: ["Content"],
72
- summary: "Update a download",
81
+ summary: "Update a download (staff)",
82
+ description: "Staff only (owner, admin, instructor). Changes only the fields sent. `thumbnailAssetId` must be a ready library image (409 otherwise); null clears it.",
73
83
  params: DownloadIdParam,
74
84
  body: UpdateDownload,
75
85
  response: {
76
86
  200: Download,
77
- 404: ErrorBody
87
+ ...refusals(403, 404, 409)
78
88
  }
79
89
  },
80
90
  handler: async (req) => {
@@ -89,16 +99,20 @@ async function downloadsRoutes(app, container) {
89
99
  schema: {
90
100
  operationId: "deleteDownload",
91
101
  tags: ["Content"],
92
- summary: "Delete a download",
102
+ summary: "Delete a download (staff)",
103
+ description: "Staff only (owner, admin, instructor). Its library files survive.",
93
104
  params: DownloadIdParam,
94
105
  response: {
95
106
  204: z.void(),
96
- 404: ErrorBody
107
+ ...refusals(403, 404)
97
108
  }
98
109
  },
99
110
  handler: async (req, reply) => {
100
111
  const scope = await resolveScope(container, req);
101
- await content.removeDownload(scope.orgId, req.params.downloadId);
112
+ await content.removeDownload(scope.orgId, req.params.downloadId, {
113
+ id: scope.orgUserId,
114
+ role: scope.role
115
+ });
102
116
  return reply.code(204).send();
103
117
  }
104
118
  });
@@ -109,11 +123,12 @@ async function downloadsRoutes(app, container) {
109
123
  schema: {
110
124
  operationId: "listDownloadAssets",
111
125
  tags: ["Content"],
112
- summary: "List a download's assets in order",
126
+ summary: "List a download's files in order (staff)",
127
+ description: "Staff only (owner, admin, instructor).",
113
128
  params: DownloadIdParam,
114
129
  response: {
115
- 200: z.array(DownloadAsset),
116
- 404: ErrorBody
130
+ 200: z.array(DownloadFile),
131
+ ...refusals(403)
117
132
  }
118
133
  },
119
134
  handler: async (req) => {
@@ -128,12 +143,13 @@ async function downloadsRoutes(app, container) {
128
143
  schema: {
129
144
  operationId: "addDownloadAsset",
130
145
  tags: ["Content"],
131
- summary: "Link a media-library asset to a download",
146
+ summary: "Add a library file to a download (staff)",
147
+ description: "Staff only (owner, admin, instructor). Added last. 409 when the file is not a ready library file or is already in the download. Returns the download's files.",
132
148
  params: DownloadIdParam,
133
149
  body: AddDownloadAsset,
134
150
  response: {
135
- 200: z.array(DownloadAsset),
136
- 404: ErrorBody
151
+ 200: z.array(DownloadFile),
152
+ ...refusals(403, 404, 409)
137
153
  }
138
154
  },
139
155
  handler: async (req) => {
@@ -148,13 +164,13 @@ async function downloadsRoutes(app, container) {
148
164
  schema: {
149
165
  operationId: "reorderDownloadAssets",
150
166
  tags: ["Content"],
151
- summary: "Reorder a download's assets (send the complete set)",
167
+ summary: "Reorder a download's files (staff)",
168
+ description: "Staff only (owner, admin, instructor). Send every file id of the download exactly once (409 otherwise). Returns the download's files.",
152
169
  params: DownloadIdParam,
153
170
  body: ReorderDownloadAssets,
154
171
  response: {
155
- 200: z.array(DownloadAsset),
156
- 404: ErrorBody,
157
- 409: ErrorBody
172
+ 200: z.array(DownloadFile),
173
+ ...refusals(403, 404, 409)
158
174
  }
159
175
  },
160
176
  handler: async (req) => {
@@ -169,12 +185,13 @@ async function downloadsRoutes(app, container) {
169
185
  schema: {
170
186
  operationId: "renameDownloadAsset",
171
187
  tags: ["Content"],
172
- summary: "Set an asset's display name within a download",
188
+ summary: "Set a file's display name within a download (staff)",
189
+ description: "Staff only (owner, admin, instructor). Null falls back to the filename. Returns the download's files.",
173
190
  params: DownloadAssetParams,
174
191
  body: RenameDownloadAsset,
175
192
  response: {
176
- 200: z.array(DownloadAsset),
177
- 404: ErrorBody
193
+ 200: z.array(DownloadFile),
194
+ ...refusals(403, 404)
178
195
  }
179
196
  },
180
197
  handler: async (req) => {
@@ -189,11 +206,12 @@ async function downloadsRoutes(app, container) {
189
206
  schema: {
190
207
  operationId: "removeDownloadAsset",
191
208
  tags: ["Content"],
192
- summary: "Unlink an asset from a download (the asset itself survives)",
209
+ summary: "Remove a file from a download (staff)",
210
+ description: "Staff only (owner, admin, instructor). The library file itself survives. Returns the download's files.",
193
211
  params: DownloadAssetParams,
194
212
  response: {
195
- 200: z.array(DownloadAsset),
196
- 404: ErrorBody
213
+ 200: z.array(DownloadFile),
214
+ ...refusals(403, 404)
197
215
  }
198
216
  },
199
217
  handler: async (req) => {
@@ -1 +1 @@
1
- {"version":3,"file":"downloads.js","names":[],"sources":["../../../src/http/routes/downloads.ts"],"sourcesContent":["// HTTP routes for the download content type. Request + response are validated\n// against the shared contract schemas by the Zod type provider, and\n// @fastify/swagger reads the same schemas to build the OpenAPI spec.\nimport type { FastifyInstance } from 'fastify';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport {\n AddDownloadAsset,\n Download,\n DownloadAsset,\n DownloadAssetParams,\n DownloadIdParam,\n DownloadsPage,\n DownloadsQuery,\n CreateDownload,\n ErrorBody,\n RenameDownloadAsset,\n ReorderDownloadAssets,\n UpdateDownload,\n} from '../schemas/index.js';\nimport { z } from 'zod';\nimport type { Container } from '../../app/container.js';\nimport { NotFoundError } from '@headless-lms/core/shared/errors';\nimport { resolveScope } from '../scope.js';\n\nexport async function downloadsRoutes(app: FastifyInstance, container: Container): Promise<void> {\n const r = app.withTypeProvider<ZodTypeProvider>();\n const content = container.content;\n\n r.route({\n method: 'GET',\n url: '/api/downloads',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listDownloads',\n tags: ['Content'],\n summary: 'List downloads',\n querystring: DownloadsQuery,\n response: { 200: DownloadsPage },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return content.listDownloads(scope.orgId, req.query);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/downloads/:downloadId',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getDownload',\n tags: ['Content'],\n summary: 'Get a download by id',\n params: DownloadIdParam,\n response: { 200: Download, 404: ErrorBody },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n const download = await content.getDownload(scope.orgId, req.params.downloadId);\n if (!download) {\n throw new NotFoundError('Download', req.params.downloadId);\n }\n return download;\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/downloads',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'createDownload',\n tags: ['Content'],\n summary: 'Create a download',\n body: CreateDownload,\n response: { 201: Download },\n },\n handler: async (req, reply) => {\n const scope = await resolveScope(container, req);\n const download = await content.createDownload(scope.orgId, req.body);\n return reply.code(201).send(download);\n },\n });\n\n r.route({\n method: 'PATCH',\n url: '/api/downloads/:downloadId',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'updateDownload',\n tags: ['Content'],\n summary: 'Update a download',\n params: DownloadIdParam,\n body: UpdateDownload,\n response: { 200: Download, 404: ErrorBody },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return content.updateDownload(scope.orgId, req.params.downloadId, req.body);\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/downloads/:downloadId',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'deleteDownload',\n tags: ['Content'],\n summary: 'Delete a download',\n params: DownloadIdParam,\n response: { 204: z.void(), 404: ErrorBody },\n },\n handler: async (req, reply) => {\n const scope = await resolveScope(container, req);\n await content.removeDownload(scope.orgId, req.params.downloadId);\n return reply.code(204).send();\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/downloads/:downloadId/assets',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listDownloadAssets',\n tags: ['Content'],\n summary: \"List a download's assets in order\",\n params: DownloadIdParam,\n response: { 200: z.array(DownloadAsset), 404: ErrorBody },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return content.listDownloadAssets(scope.orgId, req.params.downloadId);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/downloads/:downloadId/assets',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'addDownloadAsset',\n tags: ['Content'],\n summary: 'Link a media-library asset to a download',\n params: DownloadIdParam,\n body: AddDownloadAsset,\n response: { 200: z.array(DownloadAsset), 404: ErrorBody },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return content.addDownloadAsset(scope.orgId, req.params.downloadId, req.body);\n },\n });\n\n // Static segment before the parameterised sibling. Fastify's radix router\n // prefers static either way; the ordering documents the intent.\n r.route({\n method: 'PUT',\n url: '/api/downloads/:downloadId/assets/order',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'reorderDownloadAssets',\n tags: ['Content'],\n summary: \"Reorder a download's assets (send the complete set)\",\n params: DownloadIdParam,\n body: ReorderDownloadAssets,\n response: { 200: z.array(DownloadAsset), 404: ErrorBody, 409: ErrorBody },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return content.reorderDownloadAssets(scope.orgId, req.params.downloadId, req.body.assetIds);\n },\n });\n\n r.route({\n method: 'PATCH',\n url: '/api/downloads/:downloadId/assets/:assetId',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'renameDownloadAsset',\n tags: ['Content'],\n summary: \"Set an asset's display name within a download\",\n params: DownloadAssetParams,\n body: RenameDownloadAsset,\n response: { 200: z.array(DownloadAsset), 404: ErrorBody },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return content.renameDownloadAsset(\n scope.orgId,\n req.params.downloadId,\n req.params.assetId,\n req.body.displayName,\n );\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/downloads/:downloadId/assets/:assetId',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'removeDownloadAsset',\n tags: ['Content'],\n summary: 'Unlink an asset from a download (the asset itself survives)',\n params: DownloadAssetParams,\n response: { 200: z.array(DownloadAsset), 404: ErrorBody },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return content.removeDownloadAsset(scope.orgId, req.params.downloadId, req.params.assetId);\n },\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"downloads.js","names":[],"sources":["../../../src/http/routes/downloads.ts"],"sourcesContent":["// HTTP routes for the download content type. Request + response are validated\n// against the shared contract schemas by the Zod type provider, and\n// @fastify/swagger reads the same schemas to build the OpenAPI spec.\nimport type { FastifyInstance } from 'fastify';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport {\n AddDownloadAsset,\n Download,\n DownloadFile,\n DownloadAssetParams,\n DownloadIdParam,\n DownloadsPage,\n DownloadsQuery,\n CreateDownload,\n RenameDownloadAsset,\n ReorderDownloadAssets,\n UpdateDownload,\n refusals,\n} from '../schemas/index.js';\nimport { z } from 'zod';\nimport type { Container } from '../../app/container.js';\nimport { NotFoundError } from '@headless-lms/core/shared/errors';\nimport { resolveScope } from '../scope.js';\n\nexport async function downloadsRoutes(app: FastifyInstance, container: Container): Promise<void> {\n const r = app.withTypeProvider<ZodTypeProvider>();\n const content = container.content;\n\n r.route({\n method: 'GET',\n url: '/api/downloads',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listDownloads',\n tags: ['Content'],\n summary: 'List downloads (staff)',\n description:\n 'Staff only (owner, admin, instructor). Every download, drafts included, filtered by status and category and searched by title.',\n querystring: DownloadsQuery,\n response: { 200: DownloadsPage, ...refusals(403) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return content.listDownloads(scope.orgId, req.query);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/downloads/:downloadId',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getDownload',\n tags: ['Content'],\n summary: 'Get a download (staff)',\n description: 'Staff only (owner, admin, instructor).',\n params: DownloadIdParam,\n response: { 200: Download, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n const download = await content.getDownload(scope.orgId, req.params.downloadId);\n if (!download) {\n throw new NotFoundError('Download', req.params.downloadId);\n }\n return download;\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/downloads',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'createDownload',\n tags: ['Content'],\n summary: 'Create a download (staff)',\n description:\n 'Staff only (owner, admin, instructor). The download starts as a draft. 409 when its slug is taken.',\n body: CreateDownload,\n response: { 201: Download, ...refusals(403, 409) },\n },\n handler: async (req, reply) => {\n const scope = await resolveScope(container, req);\n const download = await content.createDownload(scope.orgId, req.body);\n return reply.code(201).send(download);\n },\n });\n\n r.route({\n method: 'PATCH',\n url: '/api/downloads/:downloadId',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'updateDownload',\n tags: ['Content'],\n summary: 'Update a download (staff)',\n description:\n 'Staff only (owner, admin, instructor). Changes only the fields sent. `thumbnailAssetId` must be a ready library image (409 otherwise); null clears it.',\n params: DownloadIdParam,\n body: UpdateDownload,\n response: { 200: Download, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return content.updateDownload(scope.orgId, req.params.downloadId, req.body);\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/downloads/:downloadId',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'deleteDownload',\n tags: ['Content'],\n summary: 'Delete a download (staff)',\n description: 'Staff only (owner, admin, instructor). Its library files survive.',\n params: DownloadIdParam,\n response: { 204: z.void(), ...refusals(403, 404) },\n },\n handler: async (req, reply) => {\n const scope = await resolveScope(container, req);\n await content.removeDownload(scope.orgId, req.params.downloadId, {\n id: scope.orgUserId,\n role: scope.role,\n });\n return reply.code(204).send();\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/downloads/:downloadId/assets',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listDownloadAssets',\n tags: ['Content'],\n summary: \"List a download's files in order (staff)\",\n description: 'Staff only (owner, admin, instructor).',\n params: DownloadIdParam,\n response: { 200: z.array(DownloadFile), ...refusals(403) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return content.listDownloadAssets(scope.orgId, req.params.downloadId);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/downloads/:downloadId/assets',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'addDownloadAsset',\n tags: ['Content'],\n summary: 'Add a library file to a download (staff)',\n description:\n \"Staff only (owner, admin, instructor). Added last. 409 when the file is not a ready library file or is already in the download. Returns the download's files.\",\n params: DownloadIdParam,\n body: AddDownloadAsset,\n response: { 200: z.array(DownloadFile), ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return content.addDownloadAsset(scope.orgId, req.params.downloadId, req.body);\n },\n });\n\n // Static segment before the parameterised sibling. Fastify's radix router\n // prefers static either way; the ordering documents the intent.\n r.route({\n method: 'PUT',\n url: '/api/downloads/:downloadId/assets/order',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'reorderDownloadAssets',\n tags: ['Content'],\n summary: \"Reorder a download's files (staff)\",\n description:\n \"Staff only (owner, admin, instructor). Send every file id of the download exactly once (409 otherwise). Returns the download's files.\",\n params: DownloadIdParam,\n body: ReorderDownloadAssets,\n response: { 200: z.array(DownloadFile), ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return content.reorderDownloadAssets(scope.orgId, req.params.downloadId, req.body.assetIds);\n },\n });\n\n r.route({\n method: 'PATCH',\n url: '/api/downloads/:downloadId/assets/:assetId',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'renameDownloadAsset',\n tags: ['Content'],\n summary: \"Set a file's display name within a download (staff)\",\n description:\n \"Staff only (owner, admin, instructor). Null falls back to the filename. Returns the download's files.\",\n params: DownloadAssetParams,\n body: RenameDownloadAsset,\n response: { 200: z.array(DownloadFile), ...refusals(403, 404) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return content.renameDownloadAsset(\n scope.orgId,\n req.params.downloadId,\n req.params.assetId,\n req.body.displayName,\n );\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/downloads/:downloadId/assets/:assetId',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'removeDownloadAsset',\n tags: ['Content'],\n summary: 'Remove a file from a download (staff)',\n description:\n \"Staff only (owner, admin, instructor). The library file itself survives. Returns the download's files.\",\n params: DownloadAssetParams,\n response: { 200: z.array(DownloadFile), ...refusals(403, 404) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return content.removeDownloadAsset(scope.orgId, req.params.downloadId, req.params.assetId);\n },\n });\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
+ import { refusals } from "../schemas/shared.js";
1
2
  import { resolveScope } from "../scope.js";
2
- import { ErrorBody } from "../schemas/shared.js";
3
3
  import { Entitlement, EntitlementIdParam, EntitlementsPage, EntitlementsQuery, GrantEntitlement, SetEntitlementStatus } from "../schemas/entitlements.js";
4
4
  import "../schemas/index.js";
5
5
  import { NotFoundError } from "@headless-lms/core/shared/errors";
@@ -14,9 +14,13 @@ async function entitlementsRoutes(app, container) {
14
14
  schema: {
15
15
  operationId: "listEntitlements",
16
16
  tags: ["Entitlements"],
17
- summary: "List entitlements",
17
+ summary: "List entitlements (staff)",
18
+ description: "Staff only (owner, admin, instructor).",
18
19
  querystring: EntitlementsQuery,
19
- response: { 200: EntitlementsPage }
20
+ response: {
21
+ 200: EntitlementsPage,
22
+ ...refusals(403)
23
+ }
20
24
  },
21
25
  handler: async (req) => {
22
26
  const scope = await resolveScope(container, req);
@@ -30,9 +34,13 @@ async function entitlementsRoutes(app, container) {
30
34
  schema: {
31
35
  operationId: "grantEntitlement",
32
36
  tags: ["Entitlements"],
33
- summary: "Grant a student access to a piece of content",
37
+ summary: "Grant a person access to content or a bundle (staff)",
38
+ description: "Staff only (owner, admin, instructor). Name exactly one of `contentId` and `bundleId`. A bundle grant covers whatever the bundle holds, now and later.",
34
39
  body: GrantEntitlement,
35
- response: { 201: Entitlement }
40
+ response: {
41
+ 201: Entitlement,
42
+ ...refusals(403, 409)
43
+ }
36
44
  },
37
45
  handler: async (req, reply) => {
38
46
  const scope = await resolveScope(container, req);
@@ -47,12 +55,13 @@ async function entitlementsRoutes(app, container) {
47
55
  schema: {
48
56
  operationId: "setEntitlementStatus",
49
57
  tags: ["Entitlements"],
50
- summary: "Revoke or reinstate an entitlement",
58
+ summary: "Revoke or reinstate an entitlement (staff)",
59
+ description: "Staff only (owner, admin, instructor). Access follows the status at once.",
51
60
  params: EntitlementIdParam,
52
61
  body: SetEntitlementStatus,
53
62
  response: {
54
63
  200: Entitlement,
55
- 404: ErrorBody
64
+ ...refusals(403, 404)
56
65
  }
57
66
  },
58
67
  handler: async (req) => {
@@ -1 +1 @@
1
- {"version":3,"file":"entitlements.js","names":[],"sources":["../../../src/http/routes/entitlements.ts"],"sourcesContent":["// HTTP routes for the entitlements context (a student's access grant to a piece of content).\nimport type { FastifyInstance } from 'fastify';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport {\n Entitlement,\n EntitlementIdParam,\n EntitlementsPage,\n EntitlementsQuery,\n ErrorBody,\n GrantEntitlement,\n SetEntitlementStatus,\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 entitlementsRoutes(\n app: FastifyInstance,\n container: Container,\n): Promise<void> {\n const r = app.withTypeProvider<ZodTypeProvider>();\n const entitlements = container.entitlements;\n\n r.route({\n method: 'GET',\n url: '/api/entitlements',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listEntitlements',\n tags: ['Entitlements'],\n summary: 'List entitlements',\n querystring: EntitlementsQuery,\n response: { 200: EntitlementsPage },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return entitlements.list(scope.orgId, req.query);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/entitlements',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'grantEntitlement',\n tags: ['Entitlements'],\n summary: 'Grant a student access to a piece of content',\n body: GrantEntitlement,\n response: { 201: Entitlement },\n },\n handler: async (req, reply) => {\n const scope = await resolveScope(container, req);\n const entitlement = await entitlements.grant(scope.orgId, req.body);\n return reply.code(201).send(entitlement);\n },\n });\n\n r.route({\n method: 'PATCH',\n url: '/api/entitlements/:id',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'setEntitlementStatus',\n tags: ['Entitlements'],\n summary: 'Revoke or reinstate an entitlement',\n params: EntitlementIdParam,\n body: SetEntitlementStatus,\n response: { 200: Entitlement, 404: ErrorBody },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n const entitlement = await entitlements.setStatus(scope.orgId, req.params.id, req.body.status);\n if (!entitlement) {\n throw new NotFoundError('Entitlement', req.params.id);\n }\n return entitlement;\n },\n });\n}\n"],"mappings":";;;;;;AAgBA,eAAsB,mBACpB,KACA,WACe;CACf,MAAM,IAAI,IAAI,iBAAkC;CAChD,MAAM,eAAe,UAAU;CAE/B,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,cAAc;GACrB,SAAS;GACT,aAAa;GACb,UAAU,EAAE,KAAK,iBAAiB;EACpC;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,OAAO,aAAa,KAAK,MAAM,OAAO,IAAI,KAAK;EACjD;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,cAAc;GACrB,SAAS;GACT,MAAM;GACN,UAAU,EAAE,KAAK,YAAY;EAC/B;EACA,SAAS,OAAO,KAAK,UAAU;GAC7B,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,MAAM,cAAc,MAAM,aAAa,MAAM,MAAM,OAAO,IAAI,IAAI;GAClE,OAAO,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,WAAW;EACzC;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,cAAc;GACrB,SAAS;GACT,QAAQ;GACR,MAAM;GACN,UAAU;IAAE,KAAK;IAAa,KAAK;GAAU;EAC/C;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,MAAM,cAAc,MAAM,aAAa,UAAU,MAAM,OAAO,IAAI,OAAO,IAAI,IAAI,KAAK,MAAM;GAC5F,IAAI,CAAC,aACH,MAAM,IAAI,cAAc,eAAe,IAAI,OAAO,EAAE;GAEtD,OAAO;EACT;CACF,CAAC;AACH"}
1
+ {"version":3,"file":"entitlements.js","names":[],"sources":["../../../src/http/routes/entitlements.ts"],"sourcesContent":["// HTTP routes for the entitlements context (a student's access grant to a piece of content).\nimport type { FastifyInstance } from 'fastify';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport {\n Entitlement,\n EntitlementIdParam,\n EntitlementsPage,\n EntitlementsQuery,\n GrantEntitlement,\n SetEntitlementStatus,\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 entitlementsRoutes(\n app: FastifyInstance,\n container: Container,\n): Promise<void> {\n const r = app.withTypeProvider<ZodTypeProvider>();\n const entitlements = container.entitlements;\n\n r.route({\n method: 'GET',\n url: '/api/entitlements',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listEntitlements',\n tags: ['Entitlements'],\n summary: 'List entitlements (staff)',\n description: 'Staff only (owner, admin, instructor).',\n querystring: EntitlementsQuery,\n response: { 200: EntitlementsPage, ...refusals(403) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return entitlements.list(scope.orgId, req.query);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/entitlements',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'grantEntitlement',\n tags: ['Entitlements'],\n summary: 'Grant a person access to content or a bundle (staff)',\n description:\n 'Staff only (owner, admin, instructor). Name exactly one of `contentId` and `bundleId`. A bundle grant covers whatever the bundle holds, now and later.',\n body: GrantEntitlement,\n response: { 201: Entitlement, ...refusals(403, 409) },\n },\n handler: async (req, reply) => {\n const scope = await resolveScope(container, req);\n const entitlement = await entitlements.grant(scope.orgId, req.body);\n return reply.code(201).send(entitlement);\n },\n });\n\n r.route({\n method: 'PATCH',\n url: '/api/entitlements/:id',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'setEntitlementStatus',\n tags: ['Entitlements'],\n summary: 'Revoke or reinstate an entitlement (staff)',\n description: 'Staff only (owner, admin, instructor). Access follows the status at once.',\n params: EntitlementIdParam,\n body: SetEntitlementStatus,\n response: { 200: Entitlement, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n const entitlement = await entitlements.setStatus(scope.orgId, req.params.id, req.body.status);\n if (!entitlement) {\n throw new NotFoundError('Entitlement', req.params.id);\n }\n return entitlement;\n },\n });\n}\n"],"mappings":";;;;;;AAgBA,eAAsB,mBACpB,KACA,WACe;CACf,MAAM,IAAI,IAAI,iBAAkC;CAChD,MAAM,eAAe,UAAU;CAE/B,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,cAAc;GACrB,SAAS;GACT,aAAa;GACb,aAAa;GACb,UAAU;IAAE,KAAK;IAAkB,GAAG,SAAS,GAAG;GAAE;EACtD;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,OAAO,aAAa,KAAK,MAAM,OAAO,IAAI,KAAK;EACjD;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,cAAc;GACrB,SAAS;GACT,aACE;GACF,MAAM;GACN,UAAU;IAAE,KAAK;IAAa,GAAG,SAAS,KAAK,GAAG;GAAE;EACtD;EACA,SAAS,OAAO,KAAK,UAAU;GAC7B,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,MAAM,cAAc,MAAM,aAAa,MAAM,MAAM,OAAO,IAAI,IAAI;GAClE,OAAO,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,WAAW;EACzC;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,cAAc;GACrB,SAAS;GACT,aAAa;GACb,QAAQ;GACR,MAAM;GACN,UAAU;IAAE,KAAK;IAAa,GAAG,SAAS,KAAK,GAAG;GAAE;EACtD;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,MAAM,cAAc,MAAM,aAAa,UAAU,MAAM,OAAO,IAAI,OAAO,IAAI,IAAI,KAAK,MAAM;GAC5F,IAAI,CAAC,aACH,MAAM,IAAI,cAAc,eAAe,IAAI,OAAO,EAAE;GAEtD,OAAO;EACT;CACF,CAAC;AACH"}