@headless-lms/server 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. package/README.md +14 -0
  2. package/dist/app/community-mail.d.ts +10 -10
  3. package/dist/app/community-mail.js +72 -28
  4. package/dist/app/community-mail.js.map +1 -1
  5. package/dist/app/container.d.ts +48 -10
  6. package/dist/app/container.js +268 -48
  7. package/dist/app/container.js.map +1 -1
  8. package/dist/app/notification-channels.d.ts +47 -0
  9. package/dist/app/notification-channels.js +175 -0
  10. package/dist/app/notification-channels.js.map +1 -0
  11. package/dist/app/notification-reads.d.ts +23 -0
  12. package/dist/app/notification-reads.js +74 -0
  13. package/dist/app/notification-reads.js.map +1 -0
  14. package/dist/app/platform.d.ts +14 -0
  15. package/dist/app/platform.js +14 -0
  16. package/dist/app/platform.js.map +1 -0
  17. package/dist/http/config.d.ts +4 -0
  18. package/dist/http/errors.d.ts +1 -0
  19. package/dist/http/errors.js +24 -4
  20. package/dist/http/errors.js.map +1 -1
  21. package/dist/http/fastify.d.ts +8 -0
  22. package/dist/http/mcp/catalogue.d.ts +11 -9
  23. package/dist/http/mcp/catalogue.js +15 -21
  24. package/dist/http/mcp/catalogue.js.map +1 -1
  25. package/dist/http/mcp/challenge.js +3 -3
  26. package/dist/http/mcp/challenge.js.map +1 -1
  27. package/dist/http/mcp/route.js +7 -7
  28. package/dist/http/mcp/route.js.map +1 -1
  29. package/dist/http/mcp/test-support.d.ts +6 -5
  30. package/dist/http/mcp/test-support.js +32 -11
  31. package/dist/http/mcp/test-support.js.map +1 -1
  32. package/dist/http/mcp/tool.d.ts +3 -9
  33. package/dist/http/mcp/tool.js +14 -6
  34. package/dist/http/mcp/tool.js.map +1 -1
  35. package/dist/http/mcp/tools/automations.js +10 -20
  36. package/dist/http/mcp/tools/automations.js.map +1 -1
  37. package/dist/http/mcp/tools/catalog.js +25 -42
  38. package/dist/http/mcp/tools/catalog.js.map +1 -1
  39. package/dist/http/mcp/tools/comments.js +9 -19
  40. package/dist/http/mcp/tools/comments.js.map +1 -1
  41. package/dist/http/mcp/tools/communities.js +16 -31
  42. package/dist/http/mcp/tools/communities.js.map +1 -1
  43. package/dist/http/mcp/tools/community-members.js +9 -16
  44. package/dist/http/mcp/tools/community-members.js.map +1 -1
  45. package/dist/http/mcp/tools/community-moderation.js +13 -25
  46. package/dist/http/mcp/tools/community-moderation.js.map +1 -1
  47. package/dist/http/mcp/tools/courses.js +11 -21
  48. package/dist/http/mcp/tools/courses.js.map +1 -1
  49. package/dist/http/mcp/tools/events.d.ts +6 -0
  50. package/dist/http/mcp/tools/events.js +170 -0
  51. package/dist/http/mcp/tools/events.js.map +1 -0
  52. package/dist/http/mcp/tools/index.d.ts +5 -1
  53. package/dist/http/mcp/tools/index.js +45 -10
  54. package/dist/http/mcp/tools/index.js.map +1 -1
  55. package/dist/http/mcp/tools/organization.d.ts +6 -0
  56. package/dist/http/mcp/tools/organization.js +214 -0
  57. package/dist/http/mcp/tools/organization.js.map +1 -0
  58. package/dist/http/mcp/tools/people.js +30 -49
  59. package/dist/http/mcp/tools/people.js.map +1 -1
  60. package/dist/http/plugins/auth.js +37 -8
  61. package/dist/http/plugins/auth.js.map +1 -1
  62. package/dist/http/plugins/error-handler.d.ts +5 -1
  63. package/dist/http/plugins/error-handler.js +13 -3
  64. package/dist/http/plugins/error-handler.js.map +1 -1
  65. package/dist/http/plugins/openapi.js +18 -2
  66. package/dist/http/plugins/openapi.js.map +1 -1
  67. package/dist/http/plugins/platform.d.ts +7 -0
  68. package/dist/http/plugins/platform.js +13 -0
  69. package/dist/http/plugins/platform.js.map +1 -0
  70. package/dist/http/routes/access.js +54 -36
  71. package/dist/http/routes/access.js.map +1 -1
  72. package/dist/http/routes/activities.js +45 -45
  73. package/dist/http/routes/activities.js.map +1 -1
  74. package/dist/http/routes/assets.js +30 -65
  75. package/dist/http/routes/assets.js.map +1 -1
  76. package/dist/http/routes/automations.js +37 -42
  77. package/dist/http/routes/automations.js.map +1 -1
  78. package/dist/http/routes/bundles.js +33 -36
  79. package/dist/http/routes/bundles.js.map +1 -1
  80. package/dist/http/routes/communities.js +144 -136
  81. package/dist/http/routes/communities.js.map +1 -1
  82. package/dist/http/routes/community-moderation.js +83 -137
  83. package/dist/http/routes/community-moderation.js.map +1 -1
  84. package/dist/http/routes/course-analytics.js +32 -15
  85. package/dist/http/routes/course-analytics.js.map +1 -1
  86. package/dist/http/routes/courses.js +25 -32
  87. package/dist/http/routes/courses.js.map +1 -1
  88. package/dist/http/routes/dashboard.js +9 -9
  89. package/dist/http/routes/dashboard.js.map +1 -1
  90. package/dist/http/routes/discussion.js +21 -39
  91. package/dist/http/routes/discussion.js.map +1 -1
  92. package/dist/http/routes/downloads.js +41 -47
  93. package/dist/http/routes/downloads.js.map +1 -1
  94. package/dist/http/routes/entitlements.js +19 -23
  95. package/dist/http/routes/entitlements.js.map +1 -1
  96. package/dist/http/routes/events.d.ts +7 -0
  97. package/dist/http/routes/events.js +333 -0
  98. package/dist/http/routes/events.js.map +1 -0
  99. package/dist/http/routes/integrations.js +40 -73
  100. package/dist/http/routes/integrations.js.map +1 -1
  101. package/dist/http/routes/member/assets.js +10 -13
  102. package/dist/http/routes/member/assets.js.map +1 -1
  103. package/dist/http/routes/member/chats.js +177 -75
  104. package/dist/http/routes/member/chats.js.map +1 -1
  105. package/dist/http/routes/member/comments.js +65 -26
  106. package/dist/http/routes/member/comments.js.map +1 -1
  107. package/dist/http/routes/member/community.js +68 -51
  108. package/dist/http/routes/member/community.js.map +1 -1
  109. package/dist/http/routes/member/courses.js +12 -19
  110. package/dist/http/routes/member/courses.js.map +1 -1
  111. package/dist/http/routes/member/devices.d.ts +7 -0
  112. package/dist/http/routes/member/devices.js +97 -0
  113. package/dist/http/routes/member/devices.js.map +1 -0
  114. package/dist/http/routes/member/downloads.js +6 -9
  115. package/dist/http/routes/member/downloads.js.map +1 -1
  116. package/dist/http/routes/member/events.d.ts +7 -0
  117. package/dist/http/routes/member/events.js +386 -0
  118. package/dist/http/routes/member/events.js.map +1 -0
  119. package/dist/http/routes/member/files.js +71 -46
  120. package/dist/http/routes/member/files.js.map +1 -1
  121. package/dist/http/routes/member/index.js +6 -0
  122. package/dist/http/routes/member/index.js.map +1 -1
  123. package/dist/http/routes/member/notification-settings.d.ts +7 -0
  124. package/dist/http/routes/member/notification-settings.js +100 -0
  125. package/dist/http/routes/member/notification-settings.js.map +1 -0
  126. package/dist/http/routes/member/notifications.js +69 -45
  127. package/dist/http/routes/member/notifications.js.map +1 -1
  128. package/dist/http/routes/member/org.js +5 -15
  129. package/dist/http/routes/member/org.js.map +1 -1
  130. package/dist/http/routes/member/posts.js +94 -95
  131. package/dist/http/routes/member/posts.js.map +1 -1
  132. package/dist/http/routes/member/profile.js +15 -6
  133. package/dist/http/routes/member/profile.js.map +1 -1
  134. package/dist/http/routes/member/progress.js +7 -11
  135. package/dist/http/routes/member/progress.js.map +1 -1
  136. package/dist/http/routes/organizations.js +415 -110
  137. package/dist/http/routes/organizations.js.map +1 -1
  138. package/dist/http/routes/platform.d.ts +7 -0
  139. package/dist/http/routes/platform.js +61 -0
  140. package/dist/http/routes/platform.js.map +1 -0
  141. package/dist/http/routes/students.js +12 -75
  142. package/dist/http/routes/students.js.map +1 -1
  143. package/dist/http/routes/webhooks.js +30 -39
  144. package/dist/http/routes/webhooks.js.map +1 -1
  145. package/dist/http/routes.js +9 -0
  146. package/dist/http/routes.js.map +1 -1
  147. package/dist/http/schemas/access.d.ts +346 -64
  148. package/dist/http/schemas/access.js +39 -12
  149. package/dist/http/schemas/access.js.map +1 -1
  150. package/dist/http/schemas/assets.d.ts +13 -8
  151. package/dist/http/schemas/assets.js +6 -5
  152. package/dist/http/schemas/assets.js.map +1 -1
  153. package/dist/http/schemas/automations.d.ts +4 -8
  154. package/dist/http/schemas/chats.d.ts +238 -36
  155. package/dist/http/schemas/chats.js +14 -7
  156. package/dist/http/schemas/chats.js.map +1 -1
  157. package/dist/http/schemas/community.d.ts +140 -33
  158. package/dist/http/schemas/community.js +25 -9
  159. package/dist/http/schemas/community.js.map +1 -1
  160. package/dist/http/schemas/course-analytics.d.ts +51 -1
  161. package/dist/http/schemas/course-analytics.js +19 -1
  162. package/dist/http/schemas/course-analytics.js.map +1 -1
  163. package/dist/http/schemas/dashboard.d.ts +1 -0
  164. package/dist/http/schemas/dashboard.js +2 -1
  165. package/dist/http/schemas/dashboard.js.map +1 -1
  166. package/dist/http/schemas/discussion.d.ts +45 -73
  167. package/dist/http/schemas/discussion.js +16 -4
  168. package/dist/http/schemas/discussion.js.map +1 -1
  169. package/dist/http/schemas/entitlements.d.ts +9 -12
  170. package/dist/http/schemas/entitlements.js +11 -5
  171. package/dist/http/schemas/entitlements.js.map +1 -1
  172. package/dist/http/schemas/events.d.ts +527 -0
  173. package/dist/http/schemas/events.js +64 -0
  174. package/dist/http/schemas/events.js.map +1 -0
  175. package/dist/http/schemas/files.d.ts +15 -5
  176. package/dist/http/schemas/files.js +10 -2
  177. package/dist/http/schemas/files.js.map +1 -1
  178. package/dist/http/schemas/index.d.ts +14 -12
  179. package/dist/http/schemas/index.js +14 -12
  180. package/dist/http/schemas/invites.d.ts +3 -47
  181. package/dist/http/schemas/invites.js +4 -25
  182. package/dist/http/schemas/invites.js.map +1 -1
  183. package/dist/http/schemas/member.d.ts +94 -7
  184. package/dist/http/schemas/member.js +7 -3
  185. package/dist/http/schemas/member.js.map +1 -1
  186. package/dist/http/schemas/members.d.ts +329 -65
  187. package/dist/http/schemas/members.js +67 -25
  188. package/dist/http/schemas/members.js.map +1 -1
  189. package/dist/http/schemas/notifications.d.ts +502 -175
  190. package/dist/http/schemas/notifications.js +148 -28
  191. package/dist/http/schemas/notifications.js.map +1 -1
  192. package/dist/http/schemas/platform.d.ts +37 -0
  193. package/dist/http/schemas/platform.js +19 -0
  194. package/dist/http/schemas/platform.js.map +1 -0
  195. package/dist/http/schemas/posts.d.ts +92 -38
  196. package/dist/http/schemas/posts.js +4 -4
  197. package/dist/http/schemas/posts.js.map +1 -1
  198. package/dist/http/schemas/profile.d.ts +1 -1
  199. package/dist/http/schemas/shared.d.ts +10 -13
  200. package/dist/http/schemas/shared.js +3 -11
  201. package/dist/http/schemas/shared.js.map +1 -1
  202. package/dist/http/schemas/students.d.ts +22 -0
  203. package/dist/http/schemas/students.js +3 -1
  204. package/dist/http/schemas/students.js.map +1 -1
  205. package/dist/http/scope.d.ts +9 -12
  206. package/dist/http/scope.js +30 -35
  207. package/dist/http/scope.js.map +1 -1
  208. package/dist/http/server.js +9 -0
  209. package/dist/http/server.js.map +1 -1
  210. package/dist/http/shutdown.d.ts +11 -0
  211. package/dist/http/shutdown.js +40 -0
  212. package/dist/http/shutdown.js.map +1 -0
  213. package/dist/http/sse.d.ts +19 -0
  214. package/dist/http/sse.js +104 -0
  215. package/dist/http/sse.js.map +1 -0
  216. package/dist/http/test-caller.d.ts +13 -0
  217. package/dist/http/test-caller.js +14 -0
  218. package/dist/http/test-caller.js.map +1 -0
  219. package/dist/http/well-known.js +2 -2
  220. package/dist/http/well-known.js.map +1 -1
  221. package/dist/index.d.ts +5 -2
  222. package/dist/index.js +4 -2
  223. package/dist/index.js.map +1 -1
  224. package/dist/packages/server/package.js +1 -1
  225. package/package.json +7 -6
  226. package/dist/http/routes/member/actor.d.ts +0 -14
  227. package/dist/http/routes/member/actor.js +0 -11
  228. package/dist/http/routes/member/actor.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"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
+ {"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 } 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 type { Container } from '../../app/container.js';\nimport { resolveActor } 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\n r.route({\n method: 'GET',\n url: '/api/comments',\n schema: {\n operationId: 'listComments',\n tags: ['Discussion'],\n summary: 'List course comments for moderation',\n description: `Requires \\`courses:moderate\\`. Comments on the activities of the courses you moderate, 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 const actor = await resolveActor(container, req);\n return discussion.listComments(req.query, actor);\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/comments/:commentId',\n schema: {\n operationId: 'moderateRemoveComment',\n tags: ['Discussion'],\n summary: 'Remove a course comment',\n description:\n 'Needs `courses:moderate` on the course, unless it is your own comment. 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 actor = await resolveActor(container, req);\n return discussion.remove(req.params.commentId, 'lesson', actor);\n },\n });\n\n r.route({\n method: 'PATCH',\n url: '/api/comments/:commentId',\n schema: {\n operationId: 'publishComment',\n tags: ['Discussion'],\n summary: 'Publish a course comment',\n description:\n 'Needs `courses:moderate` on the course. 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 actor = await resolveActor(container, req);\n return discussion.publish(req.params.commentId, 'lesson', actor);\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/comments/:commentId/reports',\n schema: {\n operationId: 'dismissCommentReports',\n tags: ['Discussion'],\n summary: 'Dismiss every open report on a course comment',\n description: 'Needs `courses:moderate` on the course. The comment stays up.',\n params: CommentIdParam,\n response: { 204: z.void(), ...refusals(403, 404) },\n },\n handler: async (req, reply) => {\n const actor = await resolveActor(container, req);\n await discussion.resolveReports(req.params.commentId, 'lesson', actor);\n return reply.code(204).send();\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/comments/:commentId/replies',\n schema: {\n operationId: 'replyToComment',\n tags: ['Discussion'],\n summary: 'Reply to a course comment from the moderation queue',\n description: `Requires \\`courses:moderate\\`. Needed on the comment's course. 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 actor = await resolveActor(container, req);\n return discussion.replyToComment(req.params.commentId, req.body.body, actor);\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,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,YAAY;GACnB,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,WAAW,aAAa,IAAI,OAAO,KAAK;EACjD;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,YAAY;GACnB,SAAS;GACT,aACE;GACF,QAAQ;GACR,UAAU;IAAE,KAAK;IAAa,GAAG,SAAS,KAAK,GAAG;GAAE;EACtD;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,OAAO,WAAW,OAAO,IAAI,OAAO,WAAW,UAAU,KAAK;EAChE;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,YAAY;GACnB,SAAS;GACT,aACE;GACF,QAAQ;GACR,MAAM;GACN,UAAU;IAAE,KAAK;IAAS,GAAG,SAAS,KAAK,GAAG;GAAE;EAClD;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,OAAO,WAAW,QAAQ,IAAI,OAAO,WAAW,UAAU,KAAK;EACjE;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,YAAY;GACnB,SAAS;GACT,aAAa;GACb,QAAQ;GACR,UAAU;IAAE,KAAK,EAAE,KAAK;IAAG,GAAG,SAAS,KAAK,GAAG;GAAE;EACnD;EACA,SAAS,OAAO,KAAK,UAAU;GAC7B,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,MAAM,WAAW,eAAe,IAAI,OAAO,WAAW,UAAU,KAAK;GACrE,OAAO,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK;EAC9B;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,YAAY;GACnB,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,OAAO,WAAW,eAAe,IAAI,OAAO,WAAW,IAAI,KAAK,MAAM,KAAK;EAC7E;CACF,CAAC;AACH"}
@@ -1,9 +1,8 @@
1
1
  import { refusals } from "../schemas/shared.js";
2
- import { resolveScope } from "../scope.js";
2
+ import { resolveActor } from "../scope.js";
3
3
  import { AddDownloadAsset, CreateDownload, Download, DownloadAssetParams, DownloadFile, DownloadIdParam, DownloadsPage, DownloadsQuery, RenameDownloadAsset, ReorderDownloadAssets, UpdateDownload } from "../schemas/downloads.js";
4
4
  import "../schemas/index.js";
5
5
  import { z } from "zod";
6
- import { NotFoundError } from "@headless-lms/core/shared/errors";
7
6
  //#region src/http/routes/downloads.ts
8
7
  async function downloadsRoutes(app, container) {
9
8
  const r = app.withTypeProvider();
@@ -15,8 +14,8 @@ async function downloadsRoutes(app, container) {
15
14
  schema: {
16
15
  operationId: "listDownloads",
17
16
  tags: ["Content"],
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.",
17
+ summary: "List downloads",
18
+ description: `Requires \`downloads:read\`. Every download, drafts included, filtered by status and category and searched by title.`,
20
19
  querystring: DownloadsQuery,
21
20
  response: {
22
21
  200: DownloadsPage,
@@ -24,8 +23,8 @@ async function downloadsRoutes(app, container) {
24
23
  }
25
24
  },
26
25
  handler: async (req) => {
27
- const scope = await resolveScope(container, req);
28
- return content.listDownloads(scope.orgId, req.query);
26
+ const actor = await resolveActor(container, req);
27
+ return content.listDownloads(req.query, actor);
29
28
  }
30
29
  });
31
30
  r.route({
@@ -35,8 +34,8 @@ async function downloadsRoutes(app, container) {
35
34
  schema: {
36
35
  operationId: "getDownload",
37
36
  tags: ["Content"],
38
- summary: "Get a download (staff)",
39
- description: "Staff only (owner, admin, instructor).",
37
+ summary: "Get a download",
38
+ description: "Requires `downloads:read`.",
40
39
  params: DownloadIdParam,
41
40
  response: {
42
41
  200: Download,
@@ -44,10 +43,8 @@ async function downloadsRoutes(app, container) {
44
43
  }
45
44
  },
46
45
  handler: async (req) => {
47
- const scope = await resolveScope(container, req);
48
- const download = await content.getDownload(scope.orgId, req.params.downloadId);
49
- if (!download) throw new NotFoundError("Download", req.params.downloadId);
50
- return download;
46
+ const actor = await resolveActor(container, req);
47
+ return content.getDownload(req.params.downloadId, actor);
51
48
  }
52
49
  });
53
50
  r.route({
@@ -57,8 +54,8 @@ async function downloadsRoutes(app, container) {
57
54
  schema: {
58
55
  operationId: "createDownload",
59
56
  tags: ["Content"],
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.",
57
+ summary: "Create a download",
58
+ description: `Requires \`downloads:write\`. The download starts as a draft. 409 when its slug is taken.`,
62
59
  body: CreateDownload,
63
60
  response: {
64
61
  201: Download,
@@ -66,8 +63,8 @@ async function downloadsRoutes(app, container) {
66
63
  }
67
64
  },
68
65
  handler: async (req, reply) => {
69
- const scope = await resolveScope(container, req);
70
- const download = await content.createDownload(scope.orgId, req.body);
66
+ const actor = await resolveActor(container, req);
67
+ const download = await content.createDownload(req.body, actor);
71
68
  return reply.code(201).send(download);
72
69
  }
73
70
  });
@@ -78,8 +75,8 @@ async function downloadsRoutes(app, container) {
78
75
  schema: {
79
76
  operationId: "updateDownload",
80
77
  tags: ["Content"],
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.",
78
+ summary: "Update a download",
79
+ description: `Requires \`downloads:write\`. Changes only the fields sent. \`thumbnailAssetId\` must be a ready library image (409 otherwise); null clears it. A file it does not already use must be uploaded for it, or the caller needs \`media:read\` (403 otherwise).`,
83
80
  params: DownloadIdParam,
84
81
  body: UpdateDownload,
85
82
  response: {
@@ -88,8 +85,8 @@ async function downloadsRoutes(app, container) {
88
85
  }
89
86
  },
90
87
  handler: async (req) => {
91
- const scope = await resolveScope(container, req);
92
- return content.updateDownload(scope.orgId, req.params.downloadId, req.body);
88
+ const actor = await resolveActor(container, req);
89
+ return content.updateDownload(req.params.downloadId, req.body, actor);
93
90
  }
94
91
  });
95
92
  r.route({
@@ -99,8 +96,8 @@ async function downloadsRoutes(app, container) {
99
96
  schema: {
100
97
  operationId: "deleteDownload",
101
98
  tags: ["Content"],
102
- summary: "Delete a download (staff)",
103
- description: "Staff only (owner, admin, instructor). Its library files survive.",
99
+ summary: "Delete a download",
100
+ description: `Requires \`downloads:delete\`. Its library files survive.`,
104
101
  params: DownloadIdParam,
105
102
  response: {
106
103
  204: z.void(),
@@ -108,11 +105,8 @@ async function downloadsRoutes(app, container) {
108
105
  }
109
106
  },
110
107
  handler: async (req, reply) => {
111
- const scope = await resolveScope(container, req);
112
- await content.removeDownload(scope.orgId, req.params.downloadId, {
113
- id: scope.orgUserId,
114
- role: scope.role
115
- });
108
+ const actor = await resolveActor(container, req);
109
+ await content.removeDownload(req.params.downloadId, actor);
116
110
  return reply.code(204).send();
117
111
  }
118
112
  });
@@ -123,8 +117,8 @@ async function downloadsRoutes(app, container) {
123
117
  schema: {
124
118
  operationId: "listDownloadAssets",
125
119
  tags: ["Content"],
126
- summary: "List a download's files in order (staff)",
127
- description: "Staff only (owner, admin, instructor).",
120
+ summary: "List a download's files in order",
121
+ description: "Requires `downloads:read`.",
128
122
  params: DownloadIdParam,
129
123
  response: {
130
124
  200: z.array(DownloadFile),
@@ -132,8 +126,8 @@ async function downloadsRoutes(app, container) {
132
126
  }
133
127
  },
134
128
  handler: async (req) => {
135
- const scope = await resolveScope(container, req);
136
- return content.listDownloadAssets(scope.orgId, req.params.downloadId);
129
+ const actor = await resolveActor(container, req);
130
+ return content.listDownloadAssets(req.params.downloadId, actor);
137
131
  }
138
132
  });
139
133
  r.route({
@@ -143,8 +137,8 @@ async function downloadsRoutes(app, container) {
143
137
  schema: {
144
138
  operationId: "addDownloadAsset",
145
139
  tags: ["Content"],
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.",
140
+ summary: "Add a library file to a download",
141
+ description: `Requires \`downloads:write\`. Added last. 409 when the file is not a ready library file or is already in the download. A file not uploaded for the download needs \`media:read\` (403 otherwise). Returns the download's files.`,
148
142
  params: DownloadIdParam,
149
143
  body: AddDownloadAsset,
150
144
  response: {
@@ -153,8 +147,8 @@ async function downloadsRoutes(app, container) {
153
147
  }
154
148
  },
155
149
  handler: async (req) => {
156
- const scope = await resolveScope(container, req);
157
- return content.addDownloadAsset(scope.orgId, req.params.downloadId, req.body);
150
+ const actor = await resolveActor(container, req);
151
+ return content.addDownloadAsset(req.params.downloadId, req.body, actor);
158
152
  }
159
153
  });
160
154
  r.route({
@@ -164,8 +158,8 @@ async function downloadsRoutes(app, container) {
164
158
  schema: {
165
159
  operationId: "reorderDownloadAssets",
166
160
  tags: ["Content"],
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.",
161
+ summary: "Reorder a download's files",
162
+ description: `Requires \`downloads:write\`. Send every file id of the download exactly once (409 otherwise). Returns the download's files.`,
169
163
  params: DownloadIdParam,
170
164
  body: ReorderDownloadAssets,
171
165
  response: {
@@ -174,8 +168,8 @@ async function downloadsRoutes(app, container) {
174
168
  }
175
169
  },
176
170
  handler: async (req) => {
177
- const scope = await resolveScope(container, req);
178
- return content.reorderDownloadAssets(scope.orgId, req.params.downloadId, req.body.assetIds);
171
+ const actor = await resolveActor(container, req);
172
+ return content.reorderDownloadAssets(req.params.downloadId, req.body.assetIds, actor);
179
173
  }
180
174
  });
181
175
  r.route({
@@ -185,8 +179,8 @@ async function downloadsRoutes(app, container) {
185
179
  schema: {
186
180
  operationId: "renameDownloadAsset",
187
181
  tags: ["Content"],
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.",
182
+ summary: "Set a file's display name within a download",
183
+ description: `Requires \`downloads:write\`. Null falls back to the filename. Returns the download's files.`,
190
184
  params: DownloadAssetParams,
191
185
  body: RenameDownloadAsset,
192
186
  response: {
@@ -195,8 +189,8 @@ async function downloadsRoutes(app, container) {
195
189
  }
196
190
  },
197
191
  handler: async (req) => {
198
- const scope = await resolveScope(container, req);
199
- return content.renameDownloadAsset(scope.orgId, req.params.downloadId, req.params.assetId, req.body.displayName);
192
+ const actor = await resolveActor(container, req);
193
+ return content.renameDownloadAsset(req.params.downloadId, req.params.assetId, req.body.displayName, actor);
200
194
  }
201
195
  });
202
196
  r.route({
@@ -206,8 +200,8 @@ async function downloadsRoutes(app, container) {
206
200
  schema: {
207
201
  operationId: "removeDownloadAsset",
208
202
  tags: ["Content"],
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.",
203
+ summary: "Remove a file from a download",
204
+ description: `Requires \`downloads:write\`. The library file itself survives. Returns the download's files.`,
211
205
  params: DownloadAssetParams,
212
206
  response: {
213
207
  200: z.array(DownloadFile),
@@ -215,8 +209,8 @@ async function downloadsRoutes(app, container) {
215
209
  }
216
210
  },
217
211
  handler: async (req) => {
218
- const scope = await resolveScope(container, req);
219
- return content.removeDownloadAsset(scope.orgId, req.params.downloadId, req.params.assetId);
212
+ const actor = await resolveActor(container, req);
213
+ return content.removeDownloadAsset(req.params.downloadId, req.params.assetId, actor);
220
214
  }
221
215
  });
222
216
  }
@@ -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 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
+ {"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 { resolveActor } 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 description: `Requires \\`downloads:read\\`. 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 actor = await resolveActor(container, req);\n return content.listDownloads(req.query, actor);\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',\n description: 'Requires `downloads:read`.',\n params: DownloadIdParam,\n response: { 200: Download, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return content.getDownload(req.params.downloadId, actor);\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 description: `Requires \\`downloads:write\\`. 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 actor = await resolveActor(container, req);\n const download = await content.createDownload(req.body, actor);\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 description: `Requires \\`downloads:write\\`. Changes only the fields sent. \\`thumbnailAssetId\\` must be a ready library image (409 otherwise); null clears it. A file it does not already use must be uploaded for it, or the caller needs \\`media:read\\` (403 otherwise).`,\n params: DownloadIdParam,\n body: UpdateDownload,\n response: { 200: Download, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return content.updateDownload(req.params.downloadId, req.body, actor);\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 description: `Requires \\`downloads:delete\\`. Its library files survive.`,\n params: DownloadIdParam,\n response: { 204: z.void(), ...refusals(403, 404) },\n },\n handler: async (req, reply) => {\n const actor = await resolveActor(container, req);\n await content.removeDownload(req.params.downloadId, actor);\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\",\n description: 'Requires `downloads:read`.',\n params: DownloadIdParam,\n response: { 200: z.array(DownloadFile), ...refusals(403) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return content.listDownloadAssets(req.params.downloadId, actor);\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',\n description: `Requires \\`downloads:write\\`. Added last. 409 when the file is not a ready library file or is already in the download. A file not uploaded for the download needs \\`media:read\\` (403 otherwise). 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 actor = await resolveActor(container, req);\n return content.addDownloadAsset(req.params.downloadId, req.body, actor);\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\",\n description: `Requires \\`downloads:write\\`. 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 actor = await resolveActor(container, req);\n return content.reorderDownloadAssets(req.params.downloadId, req.body.assetIds, actor);\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\",\n description: `Requires \\`downloads:write\\`. 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 actor = await resolveActor(container, req);\n return content.renameDownloadAsset(\n req.params.downloadId,\n req.params.assetId,\n req.body.displayName,\n actor,\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',\n description: `Requires \\`downloads:write\\`. 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 actor = await resolveActor(container, req);\n return content.removeDownloadAsset(req.params.downloadId, req.params.assetId, actor);\n },\n });\n}\n"],"mappings":";;;;;;AAuBA,eAAsB,gBAAgB,KAAsB,WAAqC;CAC/F,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,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,QAAQ,cAAc,IAAI,OAAO,KAAK;EAC/C;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,SAAS;GAChB,SAAS;GACT,aAAa;GACb,QAAQ;GACR,UAAU;IAAE,KAAK;IAAU,GAAG,SAAS,KAAK,GAAG;GAAE;EACnD;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,OAAO,QAAQ,YAAY,IAAI,OAAO,YAAY,KAAK;EACzD;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,SAAS;GAChB,SAAS;GACT,aAAa;GACb,MAAM;GACN,UAAU;IAAE,KAAK;IAAU,GAAG,SAAS,KAAK,GAAG;GAAE;EACnD;EACA,SAAS,OAAO,KAAK,UAAU;GAC7B,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,MAAM,WAAW,MAAM,QAAQ,eAAe,IAAI,MAAM,KAAK;GAC7D,OAAO,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,QAAQ;EACtC;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,SAAS;GAChB,SAAS;GACT,aAAa;GACb,QAAQ;GACR,MAAM;GACN,UAAU;IAAE,KAAK;IAAU,GAAG,SAAS,KAAK,KAAK,GAAG;GAAE;EACxD;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,OAAO,QAAQ,eAAe,IAAI,OAAO,YAAY,IAAI,MAAM,KAAK;EACtE;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,SAAS;GAChB,SAAS;GACT,aAAa;GACb,QAAQ;GACR,UAAU;IAAE,KAAK,EAAE,KAAK;IAAG,GAAG,SAAS,KAAK,GAAG;GAAE;EACnD;EACA,SAAS,OAAO,KAAK,UAAU;GAC7B,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,MAAM,QAAQ,eAAe,IAAI,OAAO,YAAY,KAAK;GACzD,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,SAAS;GAChB,SAAS;GACT,aAAa;GACb,QAAQ;GACR,UAAU;IAAE,KAAK,EAAE,MAAM,YAAY;IAAG,GAAG,SAAS,GAAG;GAAE;EAC3D;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,OAAO,QAAQ,mBAAmB,IAAI,OAAO,YAAY,KAAK;EAChE;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,SAAS;GAChB,SAAS;GACT,aAAa;GACb,QAAQ;GACR,MAAM;GACN,UAAU;IAAE,KAAK,EAAE,MAAM,YAAY;IAAG,GAAG,SAAS,KAAK,KAAK,GAAG;GAAE;EACrE;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,OAAO,QAAQ,iBAAiB,IAAI,OAAO,YAAY,IAAI,MAAM,KAAK;EACxE;CACF,CAAC;CAID,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,SAAS;GAChB,SAAS;GACT,aAAa;GACb,QAAQ;GACR,MAAM;GACN,UAAU;IAAE,KAAK,EAAE,MAAM,YAAY;IAAG,GAAG,SAAS,KAAK,KAAK,GAAG;GAAE;EACrE;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,OAAO,QAAQ,sBAAsB,IAAI,OAAO,YAAY,IAAI,KAAK,UAAU,KAAK;EACtF;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,SAAS;GAChB,SAAS;GACT,aAAa;GACb,QAAQ;GACR,MAAM;GACN,UAAU;IAAE,KAAK,EAAE,MAAM,YAAY;IAAG,GAAG,SAAS,KAAK,GAAG;GAAE;EAChE;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,OAAO,QAAQ,oBACb,IAAI,OAAO,YACX,IAAI,OAAO,SACX,IAAI,KAAK,aACT,KACF;EACF;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb,MAAM,CAAC,SAAS;GAChB,SAAS;GACT,aAAa;GACb,QAAQ;GACR,UAAU;IAAE,KAAK,EAAE,MAAM,YAAY;IAAG,GAAG,SAAS,KAAK,GAAG;GAAE;EAChE;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,OAAO,QAAQ,oBAAoB,IAAI,OAAO,YAAY,IAAI,OAAO,SAAS,KAAK;EACrF;CACF,CAAC;AACH"}
@@ -1,8 +1,7 @@
1
1
  import { refusals } from "../schemas/shared.js";
2
- import { resolveScope } from "../scope.js";
3
- import { Entitlement, EntitlementIdParam, EntitlementsPage, EntitlementsQuery, GrantEntitlement, SetEntitlementStatus } from "../schemas/entitlements.js";
2
+ import { resolveActor } from "../scope.js";
3
+ import { Entitlement, EntitlementsPage, EntitlementsQuery, GrantEntitlement, RevokeEntitlement } from "../schemas/entitlements.js";
4
4
  import "../schemas/index.js";
5
- import { NotFoundError } from "@headless-lms/core/shared/errors";
6
5
  //#region src/http/routes/entitlements.ts
7
6
  async function entitlementsRoutes(app, container) {
8
7
  const r = app.withTypeProvider();
@@ -14,8 +13,8 @@ async function entitlementsRoutes(app, container) {
14
13
  schema: {
15
14
  operationId: "listEntitlements",
16
15
  tags: ["Entitlements"],
17
- summary: "List entitlements (staff)",
18
- description: "Staff only (owner, admin, instructor).",
16
+ summary: "List entitlements",
17
+ description: `Requires one of \`access:read\`, \`progress:read\`. \`access:read\` lists every grant; \`progress:read\` on a course lists that course's grants when \`contentId\` names it.`,
19
18
  querystring: EntitlementsQuery,
20
19
  response: {
21
20
  200: EntitlementsPage,
@@ -23,8 +22,8 @@ async function entitlementsRoutes(app, container) {
23
22
  }
24
23
  },
25
24
  handler: async (req) => {
26
- const scope = await resolveScope(container, req);
27
- return entitlements.list(scope.orgId, req.query);
25
+ const actor = await resolveActor(container, req);
26
+ return entitlements.list(req.query, actor);
28
27
  }
29
28
  });
30
29
  r.route({
@@ -34,41 +33,38 @@ async function entitlementsRoutes(app, container) {
34
33
  schema: {
35
34
  operationId: "grantEntitlement",
36
35
  tags: ["Entitlements"],
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.",
36
+ summary: "Grant a person access to content or a bundle",
37
+ description: `Requires \`access:write\`; to grant an event, \`events:write\` on it is enough. Name exactly one of \`contentId\` and \`bundleId\`. A bundle grant covers whatever the bundle holds, now and later. Granting again makes a revoked or expired grant active with the new expiry. A grant to an event needs it published and not ended, with a place left when it has a capacity: 409 \`not_published\`, \`ended\` or \`full\` otherwise; granting it to someone who already holds it changes nothing. 404 when the content or the person does not exist.`,
39
38
  body: GrantEntitlement,
40
39
  response: {
41
40
  201: Entitlement,
42
- ...refusals(403, 409)
41
+ ...refusals(403, 404, 409)
43
42
  }
44
43
  },
45
44
  handler: async (req, reply) => {
46
- const scope = await resolveScope(container, req);
47
- const entitlement = await entitlements.grant(scope.orgId, req.body);
45
+ const actor = await resolveActor(container, req);
46
+ const entitlement = await entitlements.grant(req.body, actor);
48
47
  return reply.code(201).send(entitlement);
49
48
  }
50
49
  });
51
50
  r.route({
52
- method: "PATCH",
53
- url: "/api/entitlements/:id",
51
+ method: "POST",
52
+ url: "/api/entitlements/revoke",
54
53
  preHandler: app.requireOrgSession,
55
54
  schema: {
56
- operationId: "setEntitlementStatus",
55
+ operationId: "revokeEntitlement",
57
56
  tags: ["Entitlements"],
58
- summary: "Revoke or reinstate an entitlement (staff)",
59
- description: "Staff only (owner, admin, instructor). Access follows the status at once.",
60
- params: EntitlementIdParam,
61
- body: SetEntitlementStatus,
57
+ summary: "Revoke a person's grant to content or a bundle",
58
+ description: `Requires \`access:write\`; to revoke an event, \`events:write\` on it is enough, and a person may revoke their own. Name exactly one of \`contentId\` and \`bundleId\`. Revokes the person's direct grant; access through a bundle stays. Granting again makes it active. 404 when they have no direct active grant.`,
59
+ body: RevokeEntitlement,
62
60
  response: {
63
61
  200: Entitlement,
64
62
  ...refusals(403, 404)
65
63
  }
66
64
  },
67
65
  handler: async (req) => {
68
- const scope = await resolveScope(container, req);
69
- const entitlement = await entitlements.setStatus(scope.orgId, req.params.id, req.body.status);
70
- if (!entitlement) throw new NotFoundError("Entitlement", req.params.id);
71
- return entitlement;
66
+ const actor = await resolveActor(container, req);
67
+ return entitlements.revoke(req.body, actor);
72
68
  }
73
69
  });
74
70
  }
@@ -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 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"}
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 EntitlementsPage,\n EntitlementsQuery,\n GrantEntitlement,\n RevokeEntitlement,\n refusals,\n} from '../schemas/index.js';\nimport type { Container } from '../../app/container.js';\nimport { resolveActor } 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 description: `Requires one of \\`access:read\\`, \\`progress:read\\`. \\`access:read\\` lists every grant; \\`progress:read\\` on a course lists that course's grants when \\`contentId\\` names it.`,\n querystring: EntitlementsQuery,\n response: { 200: EntitlementsPage, ...refusals(403) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return entitlements.list(req.query, actor);\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',\n description: `Requires \\`access:write\\`; to grant an event, \\`events:write\\` on it is enough. Name exactly one of \\`contentId\\` and \\`bundleId\\`. A bundle grant covers whatever the bundle holds, now and later. Granting again makes a revoked or expired grant active with the new expiry. A grant to an event needs it published and not ended, with a place left when it has a capacity: 409 \\`not_published\\`, \\`ended\\` or \\`full\\` otherwise; granting it to someone who already holds it changes nothing. 404 when the content or the person does not exist.`,\n body: GrantEntitlement,\n response: { 201: Entitlement, ...refusals(403, 404, 409) },\n },\n handler: async (req, reply) => {\n const actor = await resolveActor(container, req);\n const entitlement = await entitlements.grant(req.body, actor);\n return reply.code(201).send(entitlement);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/entitlements/revoke',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'revokeEntitlement',\n tags: ['Entitlements'],\n summary: \"Revoke a person's grant to content or a bundle\",\n description: `Requires \\`access:write\\`; to revoke an event, \\`events:write\\` on it is enough, and a person may revoke their own. Name exactly one of \\`contentId\\` and \\`bundleId\\`. Revokes the person's direct grant; access through a bundle stays. Granting again makes it active. 404 when they have no direct active grant.`,\n body: RevokeEntitlement,\n response: { 200: Entitlement, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return entitlements.revoke(req.body, actor);\n },\n });\n}\n"],"mappings":";;;;;AAcA,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,IAAI,OAAO,KAAK;EAC3C;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,MAAM;GACN,UAAU;IAAE,KAAK;IAAa,GAAG,SAAS,KAAK,KAAK,GAAG;GAAE;EAC3D;EACA,SAAS,OAAO,KAAK,UAAU;GAC7B,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,MAAM,cAAc,MAAM,aAAa,MAAM,IAAI,MAAM,KAAK;GAC5D,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,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,OAAO,aAAa,OAAO,IAAI,MAAM,KAAK;EAC5C;CACF,CAAC;AACH"}
@@ -0,0 +1,7 @@
1
+ import { Container } from "../../app/container.js";
2
+ import { FastifyInstance } from "fastify";
3
+ //#region src/http/routes/events.d.ts
4
+ declare function eventsRoutes(app: FastifyInstance, container: Container): Promise<void>;
5
+ //#endregion
6
+ export { eventsRoutes };
7
+ //# sourceMappingURL=events.d.ts.map