@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.
- package/README.md +14 -0
- package/dist/app/community-mail.d.ts +10 -10
- package/dist/app/community-mail.js +72 -28
- package/dist/app/community-mail.js.map +1 -1
- package/dist/app/container.d.ts +48 -10
- package/dist/app/container.js +268 -48
- package/dist/app/container.js.map +1 -1
- package/dist/app/notification-channels.d.ts +47 -0
- package/dist/app/notification-channels.js +175 -0
- package/dist/app/notification-channels.js.map +1 -0
- package/dist/app/notification-reads.d.ts +23 -0
- package/dist/app/notification-reads.js +74 -0
- package/dist/app/notification-reads.js.map +1 -0
- package/dist/app/platform.d.ts +14 -0
- package/dist/app/platform.js +14 -0
- package/dist/app/platform.js.map +1 -0
- package/dist/http/config.d.ts +4 -0
- package/dist/http/errors.d.ts +1 -0
- package/dist/http/errors.js +24 -4
- package/dist/http/errors.js.map +1 -1
- package/dist/http/fastify.d.ts +8 -0
- package/dist/http/mcp/catalogue.d.ts +11 -9
- package/dist/http/mcp/catalogue.js +15 -21
- package/dist/http/mcp/catalogue.js.map +1 -1
- package/dist/http/mcp/challenge.js +3 -3
- package/dist/http/mcp/challenge.js.map +1 -1
- package/dist/http/mcp/route.js +7 -7
- package/dist/http/mcp/route.js.map +1 -1
- package/dist/http/mcp/test-support.d.ts +6 -5
- package/dist/http/mcp/test-support.js +32 -11
- package/dist/http/mcp/test-support.js.map +1 -1
- package/dist/http/mcp/tool.d.ts +3 -9
- package/dist/http/mcp/tool.js +14 -6
- package/dist/http/mcp/tool.js.map +1 -1
- package/dist/http/mcp/tools/automations.js +10 -20
- package/dist/http/mcp/tools/automations.js.map +1 -1
- package/dist/http/mcp/tools/catalog.js +25 -42
- package/dist/http/mcp/tools/catalog.js.map +1 -1
- package/dist/http/mcp/tools/comments.js +9 -19
- package/dist/http/mcp/tools/comments.js.map +1 -1
- package/dist/http/mcp/tools/communities.js +16 -31
- package/dist/http/mcp/tools/communities.js.map +1 -1
- package/dist/http/mcp/tools/community-members.js +9 -16
- package/dist/http/mcp/tools/community-members.js.map +1 -1
- package/dist/http/mcp/tools/community-moderation.js +13 -25
- package/dist/http/mcp/tools/community-moderation.js.map +1 -1
- package/dist/http/mcp/tools/courses.js +11 -21
- package/dist/http/mcp/tools/courses.js.map +1 -1
- package/dist/http/mcp/tools/events.d.ts +6 -0
- package/dist/http/mcp/tools/events.js +170 -0
- package/dist/http/mcp/tools/events.js.map +1 -0
- package/dist/http/mcp/tools/index.d.ts +5 -1
- package/dist/http/mcp/tools/index.js +45 -10
- package/dist/http/mcp/tools/index.js.map +1 -1
- package/dist/http/mcp/tools/organization.d.ts +6 -0
- package/dist/http/mcp/tools/organization.js +214 -0
- package/dist/http/mcp/tools/organization.js.map +1 -0
- package/dist/http/mcp/tools/people.js +30 -49
- package/dist/http/mcp/tools/people.js.map +1 -1
- package/dist/http/plugins/auth.js +37 -8
- package/dist/http/plugins/auth.js.map +1 -1
- package/dist/http/plugins/error-handler.d.ts +5 -1
- package/dist/http/plugins/error-handler.js +13 -3
- package/dist/http/plugins/error-handler.js.map +1 -1
- package/dist/http/plugins/openapi.js +18 -2
- package/dist/http/plugins/openapi.js.map +1 -1
- package/dist/http/plugins/platform.d.ts +7 -0
- package/dist/http/plugins/platform.js +13 -0
- package/dist/http/plugins/platform.js.map +1 -0
- package/dist/http/routes/access.js +54 -36
- package/dist/http/routes/access.js.map +1 -1
- package/dist/http/routes/activities.js +45 -45
- package/dist/http/routes/activities.js.map +1 -1
- package/dist/http/routes/assets.js +30 -65
- package/dist/http/routes/assets.js.map +1 -1
- package/dist/http/routes/automations.js +37 -42
- package/dist/http/routes/automations.js.map +1 -1
- package/dist/http/routes/bundles.js +33 -36
- package/dist/http/routes/bundles.js.map +1 -1
- package/dist/http/routes/communities.js +144 -136
- package/dist/http/routes/communities.js.map +1 -1
- package/dist/http/routes/community-moderation.js +83 -137
- package/dist/http/routes/community-moderation.js.map +1 -1
- package/dist/http/routes/course-analytics.js +32 -15
- package/dist/http/routes/course-analytics.js.map +1 -1
- package/dist/http/routes/courses.js +25 -32
- package/dist/http/routes/courses.js.map +1 -1
- package/dist/http/routes/dashboard.js +9 -9
- package/dist/http/routes/dashboard.js.map +1 -1
- package/dist/http/routes/discussion.js +21 -39
- package/dist/http/routes/discussion.js.map +1 -1
- package/dist/http/routes/downloads.js +41 -47
- package/dist/http/routes/downloads.js.map +1 -1
- package/dist/http/routes/entitlements.js +19 -23
- package/dist/http/routes/entitlements.js.map +1 -1
- package/dist/http/routes/events.d.ts +7 -0
- package/dist/http/routes/events.js +333 -0
- package/dist/http/routes/events.js.map +1 -0
- package/dist/http/routes/integrations.js +40 -73
- package/dist/http/routes/integrations.js.map +1 -1
- package/dist/http/routes/member/assets.js +10 -13
- package/dist/http/routes/member/assets.js.map +1 -1
- package/dist/http/routes/member/chats.js +177 -75
- package/dist/http/routes/member/chats.js.map +1 -1
- package/dist/http/routes/member/comments.js +65 -26
- package/dist/http/routes/member/comments.js.map +1 -1
- package/dist/http/routes/member/community.js +68 -51
- package/dist/http/routes/member/community.js.map +1 -1
- package/dist/http/routes/member/courses.js +12 -19
- package/dist/http/routes/member/courses.js.map +1 -1
- package/dist/http/routes/member/devices.d.ts +7 -0
- package/dist/http/routes/member/devices.js +97 -0
- package/dist/http/routes/member/devices.js.map +1 -0
- package/dist/http/routes/member/downloads.js +6 -9
- package/dist/http/routes/member/downloads.js.map +1 -1
- package/dist/http/routes/member/events.d.ts +7 -0
- package/dist/http/routes/member/events.js +386 -0
- package/dist/http/routes/member/events.js.map +1 -0
- package/dist/http/routes/member/files.js +71 -46
- package/dist/http/routes/member/files.js.map +1 -1
- package/dist/http/routes/member/index.js +6 -0
- package/dist/http/routes/member/index.js.map +1 -1
- package/dist/http/routes/member/notification-settings.d.ts +7 -0
- package/dist/http/routes/member/notification-settings.js +100 -0
- package/dist/http/routes/member/notification-settings.js.map +1 -0
- package/dist/http/routes/member/notifications.js +69 -45
- package/dist/http/routes/member/notifications.js.map +1 -1
- package/dist/http/routes/member/org.js +5 -15
- package/dist/http/routes/member/org.js.map +1 -1
- package/dist/http/routes/member/posts.js +94 -95
- package/dist/http/routes/member/posts.js.map +1 -1
- package/dist/http/routes/member/profile.js +15 -6
- package/dist/http/routes/member/profile.js.map +1 -1
- package/dist/http/routes/member/progress.js +7 -11
- package/dist/http/routes/member/progress.js.map +1 -1
- package/dist/http/routes/organizations.js +415 -110
- package/dist/http/routes/organizations.js.map +1 -1
- package/dist/http/routes/platform.d.ts +7 -0
- package/dist/http/routes/platform.js +61 -0
- package/dist/http/routes/platform.js.map +1 -0
- package/dist/http/routes/students.js +12 -75
- package/dist/http/routes/students.js.map +1 -1
- package/dist/http/routes/webhooks.js +30 -39
- package/dist/http/routes/webhooks.js.map +1 -1
- package/dist/http/routes.js +9 -0
- package/dist/http/routes.js.map +1 -1
- package/dist/http/schemas/access.d.ts +346 -64
- package/dist/http/schemas/access.js +39 -12
- package/dist/http/schemas/access.js.map +1 -1
- package/dist/http/schemas/assets.d.ts +13 -8
- package/dist/http/schemas/assets.js +6 -5
- package/dist/http/schemas/assets.js.map +1 -1
- package/dist/http/schemas/automations.d.ts +4 -8
- package/dist/http/schemas/chats.d.ts +238 -36
- package/dist/http/schemas/chats.js +14 -7
- package/dist/http/schemas/chats.js.map +1 -1
- package/dist/http/schemas/community.d.ts +140 -33
- package/dist/http/schemas/community.js +25 -9
- package/dist/http/schemas/community.js.map +1 -1
- package/dist/http/schemas/course-analytics.d.ts +51 -1
- package/dist/http/schemas/course-analytics.js +19 -1
- package/dist/http/schemas/course-analytics.js.map +1 -1
- package/dist/http/schemas/dashboard.d.ts +1 -0
- package/dist/http/schemas/dashboard.js +2 -1
- package/dist/http/schemas/dashboard.js.map +1 -1
- package/dist/http/schemas/discussion.d.ts +45 -73
- package/dist/http/schemas/discussion.js +16 -4
- package/dist/http/schemas/discussion.js.map +1 -1
- package/dist/http/schemas/entitlements.d.ts +9 -12
- package/dist/http/schemas/entitlements.js +11 -5
- package/dist/http/schemas/entitlements.js.map +1 -1
- package/dist/http/schemas/events.d.ts +527 -0
- package/dist/http/schemas/events.js +64 -0
- package/dist/http/schemas/events.js.map +1 -0
- package/dist/http/schemas/files.d.ts +15 -5
- package/dist/http/schemas/files.js +10 -2
- package/dist/http/schemas/files.js.map +1 -1
- package/dist/http/schemas/index.d.ts +14 -12
- package/dist/http/schemas/index.js +14 -12
- package/dist/http/schemas/invites.d.ts +3 -47
- package/dist/http/schemas/invites.js +4 -25
- package/dist/http/schemas/invites.js.map +1 -1
- package/dist/http/schemas/member.d.ts +94 -7
- package/dist/http/schemas/member.js +7 -3
- package/dist/http/schemas/member.js.map +1 -1
- package/dist/http/schemas/members.d.ts +329 -65
- package/dist/http/schemas/members.js +67 -25
- package/dist/http/schemas/members.js.map +1 -1
- package/dist/http/schemas/notifications.d.ts +502 -175
- package/dist/http/schemas/notifications.js +148 -28
- package/dist/http/schemas/notifications.js.map +1 -1
- package/dist/http/schemas/platform.d.ts +37 -0
- package/dist/http/schemas/platform.js +19 -0
- package/dist/http/schemas/platform.js.map +1 -0
- package/dist/http/schemas/posts.d.ts +92 -38
- package/dist/http/schemas/posts.js +4 -4
- package/dist/http/schemas/posts.js.map +1 -1
- package/dist/http/schemas/profile.d.ts +1 -1
- package/dist/http/schemas/shared.d.ts +10 -13
- package/dist/http/schemas/shared.js +3 -11
- package/dist/http/schemas/shared.js.map +1 -1
- package/dist/http/schemas/students.d.ts +22 -0
- package/dist/http/schemas/students.js +3 -1
- package/dist/http/schemas/students.js.map +1 -1
- package/dist/http/scope.d.ts +9 -12
- package/dist/http/scope.js +30 -35
- package/dist/http/scope.js.map +1 -1
- package/dist/http/server.js +9 -0
- package/dist/http/server.js.map +1 -1
- package/dist/http/shutdown.d.ts +11 -0
- package/dist/http/shutdown.js +40 -0
- package/dist/http/shutdown.js.map +1 -0
- package/dist/http/sse.d.ts +19 -0
- package/dist/http/sse.js +104 -0
- package/dist/http/sse.js.map +1 -0
- package/dist/http/test-caller.d.ts +13 -0
- package/dist/http/test-caller.js +14 -0
- package/dist/http/test-caller.js.map +1 -0
- package/dist/http/well-known.js +2 -2
- package/dist/http/well-known.js.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/packages/server/package.js +1 -1
- package/package.json +7 -6
- package/dist/http/routes/member/actor.d.ts +0 -14
- package/dist/http/routes/member/actor.js +0 -11
- package/dist/http/routes/member/actor.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"error-handler.js","names":[],"sources":["../../../src/http/plugins/error-handler.ts"],"sourcesContent":["import type { FastifyReply, FastifyRequest } from 'fastify';\nimport { ActionInvocationError } from '@headless-lms/core/integrations';\nimport { isResponseSerializationError } from 'fastify-type-provider-zod';\nimport { classifyError } from '../errors.js';\n\nfunction routeOf(request: FastifyRequest): string {\n return `${request.method} ${request.routeOptions.url ?? 'unmatched route'}`;\n}\n\nexport function errorHandler(error: unknown, request: FastifyRequest, reply: FastifyReply) {\n if (error instanceof ActionInvocationError) {\n request.log.error(\n { err: error, integrationId: error.integrationId, actionId: error.actionId },\n `integration action failed in ${routeOf(request)}`,\n );\n return reply.status(502).send({\n error: 'action_failed',\n message: 'The integration did not respond as expected',\n });\n }\n const refusal = classifyError(error);\n if (refusal) {\n request.log.info(\n {\n status: refusal.status,\n error: refusal.error,\n message: refusal.message,\n ...(refusal.issues && { issues: refusal.issues }),\n ...(refusal.error === 'bad_request' && { cause: (error as Error).message }),\n },\n `${routeOf(request)} refused`,\n );\n return reply.status(refusal.status).send({\n error: refusal.error,\n message: refusal.message,\n ...(refusal.issues && { issues: refusal.issues }),\n });\n }\n if (isResponseSerializationError(error)) {\n request.log.error(\n { err: error, issues: error.cause.issues },\n `response of ${routeOf(request)} does not match its schema`,\n );\n } else {\n request.log.error({ err: error }, `unhandled error in ${routeOf(request)}`);\n }\n return reply.status(500).send({\n error: 'internal_error',\n message: 'Something went wrong on our side. Quote the request id when reporting it.',\n requestId: request.id,\n });\n}\n\nexport function notFoundHandler(request: FastifyRequest, reply: FastifyReply) {\n request.log.info(\n { status: 404, error: 'not_found' },\n `${request.method} unmatched route refused`,\n );\n return reply\n .status(404)\n .send({ error: 'not_found', message: `No route for ${request.method} ${request.url}` });\n}\n"],"mappings":";;;;AAKA,SAAS,QAAQ,SAAiC;CAChD,OAAO,GAAG,QAAQ,OAAO,GAAG,QAAQ,aAAa,OAAO;AAC1D;AAEA,SAAgB,aAAa,OAAgB,SAAyB,OAAqB;CACzF,IAAI,iBAAiB,uBAAuB;EAC1C,QAAQ,IAAI,MACV;GAAE,KAAK;GAAO,eAAe,MAAM;GAAe,UAAU,MAAM;EAAS,GAC3E,gCAAgC,QAAQ,OAAO,GACjD;EACA,OAAO,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK;GAC5B,OAAO;GACP,SAAS;EACX,CAAC;CACH;CACA,MAAM,UAAU,cAAc,KAAK;CACnC,IAAI,SAAS;EACX,QAAQ,IAAI,KACV;GACE,QAAQ,QAAQ;GAChB,OAAO,QAAQ;GACf,SAAS,QAAQ;GACjB,GAAI,QAAQ,UAAU,EAAE,QAAQ,QAAQ,OAAO;GAC/C,GAAI,QAAQ,UAAU,iBAAiB,EAAE,OAAQ,MAAgB,QAAQ;
|
|
1
|
+
{"version":3,"file":"error-handler.js","names":[],"sources":["../../../src/http/plugins/error-handler.ts"],"sourcesContent":["import type { FastifyReply, FastifyRequest } from 'fastify';\nimport { ActionInvocationError } from '@headless-lms/core/integrations';\nimport { isResponseSerializationError } from 'fastify-type-provider-zod';\nimport { classifyError } from '../errors.js';\n\nfunction routeOf(request: FastifyRequest): string {\n return `${request.method} ${request.routeOptions.url ?? 'unmatched route'}`;\n}\n\nexport function refusalSubject(error: unknown): { permission?: string; contentId?: string } {\n const subject = error as { permission?: unknown; contentId?: unknown };\n return {\n ...(typeof subject?.permission === 'string' && { permission: subject.permission }),\n ...(typeof subject?.contentId === 'string' && { contentId: subject.contentId }),\n };\n}\n\nexport function errorHandler(error: unknown, request: FastifyRequest, reply: FastifyReply) {\n if (error instanceof ActionInvocationError) {\n request.log.error(\n { err: error, integrationId: error.integrationId, actionId: error.actionId },\n `integration action failed in ${routeOf(request)}`,\n );\n return reply.status(502).send({\n error: 'action_failed',\n message: 'The integration did not respond as expected',\n });\n }\n const refusal = classifyError(error);\n if (refusal) {\n request.log.info(\n {\n status: refusal.status,\n error: refusal.error,\n message: refusal.message,\n ...(refusal.issues && { issues: refusal.issues }),\n ...(refusal.details && { details: refusal.details }),\n ...(refusal.error === 'bad_request' && { cause: (error as Error).message }),\n ...refusalSubject(error),\n },\n `${routeOf(request)} refused`,\n );\n return reply.status(refusal.status).send({\n error: refusal.error,\n message: refusal.message,\n ...(refusal.issues && { issues: refusal.issues }),\n ...(refusal.details && { details: refusal.details }),\n });\n }\n if (isResponseSerializationError(error)) {\n request.log.error(\n { err: error, issues: error.cause.issues },\n `response of ${routeOf(request)} does not match its schema`,\n );\n } else {\n request.log.error({ err: error }, `unhandled error in ${routeOf(request)}`);\n }\n return reply.status(500).send({\n error: 'internal_error',\n message: 'Something went wrong on our side. Quote the request id when reporting it.',\n requestId: request.id,\n });\n}\n\nexport function notFoundHandler(request: FastifyRequest, reply: FastifyReply) {\n request.log.info(\n { status: 404, error: 'not_found' },\n `${request.method} unmatched route refused`,\n );\n return reply\n .status(404)\n .send({ error: 'not_found', message: `No route for ${request.method} ${request.url}` });\n}\n"],"mappings":";;;;AAKA,SAAS,QAAQ,SAAiC;CAChD,OAAO,GAAG,QAAQ,OAAO,GAAG,QAAQ,aAAa,OAAO;AAC1D;AAEA,SAAgB,eAAe,OAA6D;CAC1F,MAAM,UAAU;CAChB,OAAO;EACL,GAAI,OAAO,SAAS,eAAe,YAAY,EAAE,YAAY,QAAQ,WAAW;EAChF,GAAI,OAAO,SAAS,cAAc,YAAY,EAAE,WAAW,QAAQ,UAAU;CAC/E;AACF;AAEA,SAAgB,aAAa,OAAgB,SAAyB,OAAqB;CACzF,IAAI,iBAAiB,uBAAuB;EAC1C,QAAQ,IAAI,MACV;GAAE,KAAK;GAAO,eAAe,MAAM;GAAe,UAAU,MAAM;EAAS,GAC3E,gCAAgC,QAAQ,OAAO,GACjD;EACA,OAAO,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK;GAC5B,OAAO;GACP,SAAS;EACX,CAAC;CACH;CACA,MAAM,UAAU,cAAc,KAAK;CACnC,IAAI,SAAS;EACX,QAAQ,IAAI,KACV;GACE,QAAQ,QAAQ;GAChB,OAAO,QAAQ;GACf,SAAS,QAAQ;GACjB,GAAI,QAAQ,UAAU,EAAE,QAAQ,QAAQ,OAAO;GAC/C,GAAI,QAAQ,WAAW,EAAE,SAAS,QAAQ,QAAQ;GAClD,GAAI,QAAQ,UAAU,iBAAiB,EAAE,OAAQ,MAAgB,QAAQ;GACzE,GAAG,eAAe,KAAK;EACzB,GACA,GAAG,QAAQ,OAAO,EAAE,SACtB;EACA,OAAO,MAAM,OAAO,QAAQ,MAAM,CAAC,CAAC,KAAK;GACvC,OAAO,QAAQ;GACf,SAAS,QAAQ;GACjB,GAAI,QAAQ,UAAU,EAAE,QAAQ,QAAQ,OAAO;GAC/C,GAAI,QAAQ,WAAW,EAAE,SAAS,QAAQ,QAAQ;EACpD,CAAC;CACH;CACA,IAAI,6BAA6B,KAAK,GACpC,QAAQ,IAAI,MACV;EAAE,KAAK;EAAO,QAAQ,MAAM,MAAM;CAAO,GACzC,eAAe,QAAQ,OAAO,EAAE,2BAClC;MAEA,QAAQ,IAAI,MAAM,EAAE,KAAK,MAAM,GAAG,sBAAsB,QAAQ,OAAO,GAAG;CAE5E,OAAO,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK;EAC5B,OAAO;EACP,SAAS;EACT,WAAW,QAAQ;CACrB,CAAC;AACH;AAEA,SAAgB,gBAAgB,SAAyB,OAAqB;CAC5E,QAAQ,IAAI,KACV;EAAE,QAAQ;EAAK,OAAO;CAAY,GAClC,GAAG,QAAQ,OAAO,yBACpB;CACA,OAAO,MACJ,OAAO,GAAG,CAAC,CACX,KAAK;EAAE,OAAO;EAAa,SAAS,gBAAgB,QAAQ,OAAO,GAAG,QAAQ;CAAM,CAAC;AAC1F"}
|
|
@@ -22,7 +22,7 @@ function registerOpenApi(app, config) {
|
|
|
22
22
|
info: {
|
|
23
23
|
title: "Headless LMS API",
|
|
24
24
|
version,
|
|
25
|
-
description: "API documentation for the Headless LMS platform",
|
|
25
|
+
description: "API documentation for the Headless LMS platform. Every route under /api takes a signed-in session or an API key, sent in the `x-api-key` header or as `Authorization: Bearer <key>`. The platform routes take the platform key as `Authorization: Bearer <platform key>`.",
|
|
26
26
|
license: {
|
|
27
27
|
name: "MIT",
|
|
28
28
|
url: "https://opensource.org/licenses/MIT"
|
|
@@ -38,6 +38,10 @@ function registerOpenApi(app, config) {
|
|
|
38
38
|
name: "Content",
|
|
39
39
|
description: "Authored content: courses and downloads, their structure and delivery"
|
|
40
40
|
},
|
|
41
|
+
{
|
|
42
|
+
name: "Events",
|
|
43
|
+
description: "Live sessions at a set time: their details, location, recording and files"
|
|
44
|
+
},
|
|
41
45
|
{
|
|
42
46
|
name: "Entitlements",
|
|
43
47
|
description: "Access grants to content"
|
|
@@ -90,9 +94,21 @@ function registerOpenApi(app, config) {
|
|
|
90
94
|
name: "Reporting",
|
|
91
95
|
description: "Reporting and analytics"
|
|
92
96
|
},
|
|
97
|
+
{
|
|
98
|
+
name: "People",
|
|
99
|
+
description: "The organization's people, invites and the roles they hold"
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: "Roles",
|
|
103
|
+
description: "The organization's roles and their permissions"
|
|
104
|
+
},
|
|
93
105
|
{
|
|
94
106
|
name: "Access",
|
|
95
|
-
description: "
|
|
107
|
+
description: "Your permissions, connected apps and API keys"
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: "Platform",
|
|
111
|
+
description: "Organizations and their API keys, created with the platform key"
|
|
96
112
|
}
|
|
97
113
|
]
|
|
98
114
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"openapi.js","names":["pkg.version"],"sources":["../../../src/http/plugins/openapi.ts"],"sourcesContent":["// OpenAPI documentation. @fastify/swagger reads the Zod route schemas (via the\n// transform) to build the spec the frontend SDK is generated from; it must be\n// registered before the routes so its onRoute hook captures them. UI + JSON are\n// served under /docs.\nimport type { FastifyInstance } from 'fastify';\nimport swagger from '@fastify/swagger';\nimport swaggerUi from '@fastify/swagger-ui';\nimport { jsonSchemaTransform, jsonSchemaTransformObject } from 'fastify-type-provider-zod';\nimport type { ServerConfig } from '../config.js';\nimport { ErrorBody, ValidationErrorBody } from '../schemas/shared.js';\nimport pkg from '../../../package.json' with { type: 'json' };\n\nconst PUBLIC_ROUTES = new Set(['GET /api/organizations/invites/:token']);\n\ntype RouteSchema = {\n body?: unknown;\n querystring?: unknown;\n params?: unknown;\n response?: Record<string, unknown>;\n};\n\nfunction withDefaultResponses(\n schema: RouteSchema | undefined,\n method: string,\n url: string,\n): RouteSchema | undefined {\n if (!schema || !url.startsWith('/api/') || url.startsWith('/api/auth/')) {\n return schema;\n }\n const response: Record<string, unknown> = { ...(schema.response ?? {}) };\n if ((schema.body || schema.querystring || schema.params) && !response['400']) {\n response['400'] = ValidationErrorBody;\n }\n if (!PUBLIC_ROUTES.has(`${method} ${url}`) && !response['401']) {\n response['401'] = ErrorBody;\n }\n if (!response['500']) {\n response['500'] = ErrorBody;\n }\n return { ...schema, response };\n}\n\nexport function registerOpenApi(app: FastifyInstance, config: ServerConfig): void {\n app.register(swagger, {\n openapi: {\n info: {\n title: 'Headless LMS API',\n version: pkg.version,\n description
|
|
1
|
+
{"version":3,"file":"openapi.js","names":["pkg.version"],"sources":["../../../src/http/plugins/openapi.ts"],"sourcesContent":["// OpenAPI documentation. @fastify/swagger reads the Zod route schemas (via the\n// transform) to build the spec the frontend SDK is generated from; it must be\n// registered before the routes so its onRoute hook captures them. UI + JSON are\n// served under /docs.\nimport type { FastifyInstance } from 'fastify';\nimport swagger from '@fastify/swagger';\nimport swaggerUi from '@fastify/swagger-ui';\nimport { jsonSchemaTransform, jsonSchemaTransformObject } from 'fastify-type-provider-zod';\nimport type { ServerConfig } from '../config.js';\nimport { ErrorBody, ValidationErrorBody } from '../schemas/shared.js';\nimport pkg from '../../../package.json' with { type: 'json' };\n\nconst PUBLIC_ROUTES = new Set(['GET /api/organizations/invites/:token']);\n\ntype RouteSchema = {\n body?: unknown;\n querystring?: unknown;\n params?: unknown;\n response?: Record<string, unknown>;\n};\n\nfunction withDefaultResponses(\n schema: RouteSchema | undefined,\n method: string,\n url: string,\n): RouteSchema | undefined {\n if (!schema || !url.startsWith('/api/') || url.startsWith('/api/auth/')) {\n return schema;\n }\n const response: Record<string, unknown> = { ...(schema.response ?? {}) };\n if ((schema.body || schema.querystring || schema.params) && !response['400']) {\n response['400'] = ValidationErrorBody;\n }\n if (!PUBLIC_ROUTES.has(`${method} ${url}`) && !response['401']) {\n response['401'] = ErrorBody;\n }\n if (!response['500']) {\n response['500'] = ErrorBody;\n }\n return { ...schema, response };\n}\n\nexport function registerOpenApi(app: FastifyInstance, config: ServerConfig): void {\n app.register(swagger, {\n openapi: {\n info: {\n title: 'Headless LMS API',\n version: pkg.version,\n description:\n 'API documentation for the Headless LMS platform. Every route under /api takes a signed-in session or an API key, sent in the `x-api-key` header or as `Authorization: Bearer <key>`. The platform routes take the platform key as `Authorization: Bearer <platform key>`.',\n license: {\n name: 'MIT',\n url: 'https://opensource.org/licenses/MIT',\n },\n },\n servers: [{ url: config.publicUrl }],\n tags: [\n {\n name: 'Organizations',\n description: 'The tenant: its members, roles, invites and students',\n },\n {\n name: 'Content',\n description: 'Authored content: courses and downloads, their structure and delivery',\n },\n {\n name: 'Events',\n description: 'Live sessions at a set time: their details, location, recording and files',\n },\n { name: 'Entitlements', description: 'Access grants to content' },\n {\n name: 'Progress',\n description: 'Per-student progress and completion through course content',\n },\n { name: 'Discussion', description: 'Learner comments, replies, reactions and moderation' },\n {\n name: 'Communities',\n description: 'Communities, their spaces, topics, and direct-message settings',\n },\n { name: 'Posts', description: 'Posts in a space, reactions, and the moderation queue' },\n { name: 'Chats', description: 'Direct and group chats inside one community' },\n { name: 'Profile', description: 'The signed-in member profile' },\n {\n name: 'Notifications',\n description: 'What happened to the signed-in member, and how they want to be told',\n },\n { name: 'Assets', description: 'The org media library: uploads, serving and lifecycle' },\n {\n name: 'Files',\n description: 'Stable file URLs that check access and redirect to a signed storage URL',\n },\n { name: 'Automations', description: 'Workflows and webhooks that react to system events' },\n { name: 'Integrations', description: 'Connections to external services' },\n { name: 'Reporting', description: 'Reporting and analytics' },\n {\n name: 'People',\n description: \"The organization's people, invites and the roles they hold\",\n },\n { name: 'Roles', description: \"The organization's roles and their permissions\" },\n { name: 'Access', description: 'Your permissions, connected apps and API keys' },\n {\n name: 'Platform',\n description: 'Organizations and their API keys, created with the platform key',\n },\n ],\n },\n transform: (input) => {\n const method = String(\n Array.isArray(input.route.method) ? input.route.method[0] : input.route.method,\n );\n return jsonSchemaTransform({\n ...input,\n schema: withDefaultResponses(input.schema as RouteSchema | undefined, method, input.url),\n } as typeof input);\n },\n transformObject: jsonSchemaTransformObject,\n });\n app.register(swaggerUi, { routePrefix: '/docs' });\n}\n"],"mappings":";;;;;;AAYA,MAAM,gCAAgB,IAAI,IAAI,CAAC,uCAAuC,CAAC;AASvE,SAAS,qBACP,QACA,QACA,KACyB;CACzB,IAAI,CAAC,UAAU,CAAC,IAAI,WAAW,OAAO,KAAK,IAAI,WAAW,YAAY,GACpE,OAAO;CAET,MAAM,WAAoC,EAAE,GAAI,OAAO,YAAY,CAAC,EAAG;CACvE,KAAK,OAAO,QAAQ,OAAO,eAAe,OAAO,WAAW,CAAC,SAAS,QACpE,SAAS,SAAS;CAEpB,IAAI,CAAC,cAAc,IAAI,GAAG,OAAO,GAAG,KAAK,KAAK,CAAC,SAAS,QACtD,SAAS,SAAS;CAEpB,IAAI,CAAC,SAAS,QACZ,SAAS,SAAS;CAEpB,OAAO;EAAE,GAAG;EAAQ;CAAS;AAC/B;AAEA,SAAgB,gBAAgB,KAAsB,QAA4B;CAChF,IAAI,SAAS,SAAS;EACpB,SAAS;GACP,MAAM;IACJ,OAAO;IACEA;IACT,aACE;IACF,SAAS;KACP,MAAM;KACN,KAAK;IACP;GACF;GACA,SAAS,CAAC,EAAE,KAAK,OAAO,UAAU,CAAC;GACnC,MAAM;IACJ;KACE,MAAM;KACN,aAAa;IACf;IACA;KACE,MAAM;KACN,aAAa;IACf;IACA;KACE,MAAM;KACN,aAAa;IACf;IACA;KAAE,MAAM;KAAgB,aAAa;IAA2B;IAChE;KACE,MAAM;KACN,aAAa;IACf;IACA;KAAE,MAAM;KAAc,aAAa;IAAsD;IACzF;KACE,MAAM;KACN,aAAa;IACf;IACA;KAAE,MAAM;KAAS,aAAa;IAAwD;IACtF;KAAE,MAAM;KAAS,aAAa;IAA8C;IAC5E;KAAE,MAAM;KAAW,aAAa;IAA+B;IAC/D;KACE,MAAM;KACN,aAAa;IACf;IACA;KAAE,MAAM;KAAU,aAAa;IAAwD;IACvF;KACE,MAAM;KACN,aAAa;IACf;IACA;KAAE,MAAM;KAAe,aAAa;IAAqD;IACzF;KAAE,MAAM;KAAgB,aAAa;IAAmC;IACxE;KAAE,MAAM;KAAa,aAAa;IAA0B;IAC5D;KACE,MAAM;KACN,aAAa;IACf;IACA;KAAE,MAAM;KAAS,aAAa;IAAiD;IAC/E;KAAE,MAAM;KAAU,aAAa;IAAgD;IAC/E;KACE,MAAM;KACN,aAAa;IACf;GACF;EACF;EACA,YAAY,UAAU;GACpB,MAAM,SAAS,OACb,MAAM,QAAQ,MAAM,MAAM,MAAM,IAAI,MAAM,MAAM,OAAO,KAAK,MAAM,MAAM,MAC1E;GACA,OAAO,oBAAoB;IACzB,GAAG;IACH,QAAQ,qBAAqB,MAAM,QAAmC,QAAQ,MAAM,GAAG;GACzF,CAAiB;EACnB;EACA,iBAAiB;CACnB,CAAC;CACD,IAAI,SAAS,WAAW,EAAE,aAAa,QAAQ,CAAC;AAClD"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { PlatformKeyCheck } from "../../app/platform.js";
|
|
2
|
+
import { FastifyInstance } from "fastify";
|
|
3
|
+
//#region src/http/plugins/platform.d.ts
|
|
4
|
+
declare function registerPlatformAuth(app: FastifyInstance, platform: PlatformKeyCheck): void;
|
|
5
|
+
//#endregion
|
|
6
|
+
export { registerPlatformAuth };
|
|
7
|
+
//# sourceMappingURL=platform.d.ts.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { UnauthorizedError } from "./auth.js";
|
|
2
|
+
//#region src/http/plugins/platform.ts
|
|
3
|
+
const BEARER = /^Bearer\s+(\S+)$/i;
|
|
4
|
+
function registerPlatformAuth(app, platform) {
|
|
5
|
+
app.decorate("requirePlatform", async (request) => {
|
|
6
|
+
const match = BEARER.exec(request.headers.authorization ?? "");
|
|
7
|
+
if (!match || !platform.matches(match[1])) throw new UnauthorizedError("The platform key is not valid");
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
//#endregion
|
|
11
|
+
export { registerPlatformAuth };
|
|
12
|
+
|
|
13
|
+
//# sourceMappingURL=platform.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"platform.js","names":[],"sources":["../../../src/http/plugins/platform.ts"],"sourcesContent":["import type { FastifyInstance, FastifyRequest } from 'fastify';\nimport type { PlatformKeyCheck } from '../../app/platform.js';\nimport { UnauthorizedError } from './auth.js';\n\nconst BEARER = /^Bearer\\s+(\\S+)$/i;\n\nexport function registerPlatformAuth(app: FastifyInstance, platform: PlatformKeyCheck): void {\n app.decorate('requirePlatform', async (request: FastifyRequest) => {\n const match = BEARER.exec(request.headers.authorization ?? '');\n if (!match || !platform.matches(match[1]!)) {\n throw new UnauthorizedError('The platform key is not valid');\n }\n });\n}\n"],"mappings":";;AAIA,MAAM,SAAS;AAEf,SAAgB,qBAAqB,KAAsB,UAAkC;CAC3F,IAAI,SAAS,mBAAmB,OAAO,YAA4B;EACjE,MAAM,QAAQ,OAAO,KAAK,QAAQ,QAAQ,iBAAiB,EAAE;EAC7D,IAAI,CAAC,SAAS,CAAC,SAAS,QAAQ,MAAM,EAAG,GACvC,MAAM,IAAI,kBAAkB,+BAA+B;CAE/D,CAAC;AACH"}
|
|
@@ -1,31 +1,48 @@
|
|
|
1
1
|
import { refusals } from "../schemas/shared.js";
|
|
2
|
-
import {
|
|
3
|
-
import { ApiKeyIdParam, ApiKeyList, ClientIdParam, ConnectedAppList, CreateApiKey, CreatedApiKey,
|
|
2
|
+
import { resolveActor } from "../scope.js";
|
|
3
|
+
import { ApiKeyIdParam, ApiKeyList, ApiKeyRoleList, ClientIdParam, ConnectedAppList, CreateApiKey, CreatedApiKey, PermissionDefinitionList, ViewerPermissions } from "../schemas/access.js";
|
|
4
4
|
import "../schemas/index.js";
|
|
5
|
-
import { assertRoleHoldsScopes, scopeOptions } from "@headless-lms/core/organizations";
|
|
6
5
|
import { z } from "zod";
|
|
7
6
|
//#region src/http/routes/access.ts
|
|
8
7
|
async function accessRoutes(app, container) {
|
|
9
8
|
const r = app.withTypeProvider();
|
|
10
9
|
const tags = ["Access"];
|
|
10
|
+
const organizations = container.organizations;
|
|
11
11
|
r.route({
|
|
12
12
|
method: "GET",
|
|
13
|
-
url: "/api/access/
|
|
13
|
+
url: "/api/access/permissions/list",
|
|
14
14
|
preHandler: app.requireOrgSession,
|
|
15
15
|
schema: {
|
|
16
|
-
operationId: "
|
|
16
|
+
operationId: "listPermissions",
|
|
17
17
|
tags,
|
|
18
|
-
summary: "Every
|
|
19
|
-
description:
|
|
18
|
+
summary: "Every permission, with its label, description and level",
|
|
19
|
+
description: `Any active member of the organization. For role screens, the key-creation screen and the consent screen.`,
|
|
20
20
|
response: {
|
|
21
|
-
200:
|
|
21
|
+
200: PermissionDefinitionList,
|
|
22
22
|
...refusals(403)
|
|
23
23
|
}
|
|
24
24
|
},
|
|
25
25
|
handler: async (req) => {
|
|
26
|
-
|
|
26
|
+
await resolveActor(container, req);
|
|
27
|
+
return organizations.permissionList();
|
|
27
28
|
}
|
|
28
29
|
});
|
|
30
|
+
r.route({
|
|
31
|
+
method: "GET",
|
|
32
|
+
url: "/api/access/permissions",
|
|
33
|
+
preHandler: app.requireOrgSession,
|
|
34
|
+
schema: {
|
|
35
|
+
operationId: "getMyPermissions",
|
|
36
|
+
tags,
|
|
37
|
+
summary: "Your effective permissions in this organization",
|
|
38
|
+
description: `Any active member of the organization. Your org role, whether you own the organization, the permissions you hold on the organization, and the ones you hold on each content item.`,
|
|
39
|
+
response: {
|
|
40
|
+
200: ViewerPermissions,
|
|
41
|
+
...refusals(403)
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
handler: async (req) => organizations.viewerPermissions(await resolveActor(container, req))
|
|
45
|
+
});
|
|
29
46
|
r.route({
|
|
30
47
|
method: "GET",
|
|
31
48
|
url: "/api/access/apps",
|
|
@@ -34,16 +51,13 @@ async function accessRoutes(app, container) {
|
|
|
34
51
|
operationId: "listConnectedApps",
|
|
35
52
|
tags,
|
|
36
53
|
summary: "Apps connected to this organization as you",
|
|
37
|
-
description:
|
|
54
|
+
description: `Any active member of the organization. The apps you connected to this organization, each with the scopes you granted and when.`,
|
|
38
55
|
response: {
|
|
39
56
|
200: ConnectedAppList,
|
|
40
57
|
...refusals(403)
|
|
41
58
|
}
|
|
42
59
|
},
|
|
43
|
-
handler: async (req) =>
|
|
44
|
-
const scope = await resolveScope(container, req);
|
|
45
|
-
return container.auth.listConnectedApps(req.headers, scope.authOrgId);
|
|
46
|
-
}
|
|
60
|
+
handler: async (req) => organizations.listConnectedApps(await resolveActor(container, req))
|
|
47
61
|
});
|
|
48
62
|
r.route({
|
|
49
63
|
method: "DELETE",
|
|
@@ -53,7 +67,7 @@ async function accessRoutes(app, container) {
|
|
|
53
67
|
operationId: "revokeConnectedApp",
|
|
54
68
|
tags,
|
|
55
69
|
summary: "Disconnect an app",
|
|
56
|
-
description:
|
|
70
|
+
description: `Any active member of the organization. Revokes your consent and tokens for the app in this organization; its next call is refused.`,
|
|
57
71
|
params: ClientIdParam,
|
|
58
72
|
response: {
|
|
59
73
|
204: z.void(),
|
|
@@ -61,8 +75,7 @@ async function accessRoutes(app, container) {
|
|
|
61
75
|
}
|
|
62
76
|
},
|
|
63
77
|
handler: async (req, reply) => {
|
|
64
|
-
|
|
65
|
-
await container.auth.revokeConnectedApp(req.headers, scope.authOrgId, req.params.clientId);
|
|
78
|
+
await organizations.revokeConnectedApp(req.params.clientId, await resolveActor(container, req));
|
|
66
79
|
return reply.code(204).send();
|
|
67
80
|
}
|
|
68
81
|
});
|
|
@@ -74,16 +87,29 @@ async function accessRoutes(app, container) {
|
|
|
74
87
|
operationId: "listApiKeys",
|
|
75
88
|
tags,
|
|
76
89
|
summary: "Your API keys for this organization",
|
|
77
|
-
description:
|
|
90
|
+
description: `Requires \`api_keys:read\`. Your keys for this organization with their role and expiry. The key itself is never returned again after it is created.`,
|
|
78
91
|
response: {
|
|
79
92
|
200: ApiKeyList,
|
|
80
93
|
...refusals(403)
|
|
81
94
|
}
|
|
82
95
|
},
|
|
83
|
-
handler: async (req) =>
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
96
|
+
handler: async (req) => organizations.listApiKeys(await resolveActor(container, req))
|
|
97
|
+
});
|
|
98
|
+
r.route({
|
|
99
|
+
method: "GET",
|
|
100
|
+
url: "/api/access/api-keys/roles",
|
|
101
|
+
preHandler: app.requireOrgSession,
|
|
102
|
+
schema: {
|
|
103
|
+
operationId: "listApiKeyRoles",
|
|
104
|
+
tags,
|
|
105
|
+
summary: "The roles you can put on an API key",
|
|
106
|
+
description: `Requires \`api_keys:write\`. The organization's roles whose every permission you hold somewhere.`,
|
|
107
|
+
response: {
|
|
108
|
+
200: ApiKeyRoleList,
|
|
109
|
+
...refusals(403)
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
handler: async (req) => organizations.listApiKeyRoles(await resolveActor(container, req))
|
|
87
113
|
});
|
|
88
114
|
r.route({
|
|
89
115
|
method: "POST",
|
|
@@ -93,22 +119,15 @@ async function accessRoutes(app, container) {
|
|
|
93
119
|
operationId: "createApiKey",
|
|
94
120
|
tags,
|
|
95
121
|
summary: "Create an API key",
|
|
96
|
-
description:
|
|
122
|
+
description: `Requires \`api_keys:write\`. The key acts as you in this organization with the permissions of the role given, never beyond what you hold at the time of each request. Send it in the \`x-api-key\` header or as \`Authorization: Bearer <key>\`; every route under /api and /mcp accepts it. 403 when the role holds a permission you hold nowhere; 404 when the organization has no such role. The key is returned once.`,
|
|
97
123
|
body: CreateApiKey,
|
|
98
124
|
response: {
|
|
99
125
|
201: CreatedApiKey,
|
|
100
|
-
...refusals(400, 403)
|
|
126
|
+
...refusals(400, 403, 404)
|
|
101
127
|
}
|
|
102
128
|
},
|
|
103
129
|
handler: async (req, reply) => {
|
|
104
|
-
const
|
|
105
|
-
assertRoleHoldsScopes(scope.role, req.body.scopes);
|
|
106
|
-
const created = await container.auth.createApiKey({
|
|
107
|
-
authUserId: req.authUser.id,
|
|
108
|
-
orgId: scope.authOrgId,
|
|
109
|
-
name: req.body.name,
|
|
110
|
-
scopes: req.body.scopes
|
|
111
|
-
});
|
|
130
|
+
const created = await organizations.createApiKey(req.body, await resolveActor(container, req));
|
|
112
131
|
return reply.code(201).send(created);
|
|
113
132
|
}
|
|
114
133
|
});
|
|
@@ -119,8 +138,8 @@ async function accessRoutes(app, container) {
|
|
|
119
138
|
schema: {
|
|
120
139
|
operationId: "deleteApiKey",
|
|
121
140
|
tags,
|
|
122
|
-
summary: "
|
|
123
|
-
description:
|
|
141
|
+
summary: "Revoke an API key",
|
|
142
|
+
description: `Requires \`api_keys:delete\`. 404 when the key is not one of yours in this organization.`,
|
|
124
143
|
params: ApiKeyIdParam,
|
|
125
144
|
response: {
|
|
126
145
|
204: z.void(),
|
|
@@ -128,8 +147,7 @@ async function accessRoutes(app, container) {
|
|
|
128
147
|
}
|
|
129
148
|
},
|
|
130
149
|
handler: async (req, reply) => {
|
|
131
|
-
|
|
132
|
-
await container.auth.deleteApiKey(req.headers, scope.authOrgId, req.params.id);
|
|
150
|
+
await organizations.deleteApiKey(req.params.id, await resolveActor(container, req));
|
|
133
151
|
return reply.code(204).send();
|
|
134
152
|
}
|
|
135
153
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"access.js","names":[],"sources":["../../../src/http/routes/access.ts"],"sourcesContent":["import type { FastifyInstance } from 'fastify';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport { z } from 'zod';\nimport
|
|
1
|
+
{"version":3,"file":"access.js","names":[],"sources":["../../../src/http/routes/access.ts"],"sourcesContent":["import type { FastifyInstance } from 'fastify';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport { z } from 'zod';\nimport type { Container } from '../../app/container.js';\nimport {\n ApiKeyIdParam,\n ApiKeyList,\n ApiKeyRoleList,\n ClientIdParam,\n ConnectedAppList,\n CreateApiKey,\n CreatedApiKey,\n PermissionDefinitionList,\n ViewerPermissions,\n refusals,\n} from '../schemas/index.js';\nimport { resolveActor } from '../scope.js';\n\nexport async function accessRoutes(app: FastifyInstance, container: Container): Promise<void> {\n const r = app.withTypeProvider<ZodTypeProvider>();\n const tags = ['Access'];\n const organizations = container.organizations;\n\n r.route({\n method: 'GET',\n url: '/api/access/permissions/list',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listPermissions',\n tags,\n summary: 'Every permission, with its label, description and level',\n description: `Any active member of the organization. For role screens, the key-creation screen and the consent screen.`,\n response: { 200: PermissionDefinitionList, ...refusals(403) },\n },\n handler: async (req) => {\n await resolveActor(container, req);\n return organizations.permissionList();\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/access/permissions',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getMyPermissions',\n tags,\n summary: 'Your effective permissions in this organization',\n description: `Any active member of the organization. Your org role, whether you own the organization, the permissions you hold on the organization, and the ones you hold on each content item.`,\n response: { 200: ViewerPermissions, ...refusals(403) },\n },\n handler: async (req) => organizations.viewerPermissions(await resolveActor(container, req)),\n });\n\n r.route({\n method: 'GET',\n url: '/api/access/apps',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listConnectedApps',\n tags,\n summary: 'Apps connected to this organization as you',\n description: `Any active member of the organization. The apps you connected to this organization, each with the scopes you granted and when.`,\n response: { 200: ConnectedAppList, ...refusals(403) },\n },\n handler: async (req) => organizations.listConnectedApps(await resolveActor(container, req)),\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/access/apps/:clientId',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'revokeConnectedApp',\n tags,\n summary: 'Disconnect an app',\n description: `Any active member of the organization. Revokes your consent and tokens for the app in this organization; its next call is refused.`,\n params: ClientIdParam,\n response: { 204: z.void(), ...refusals(403) },\n },\n handler: async (req, reply) => {\n await organizations.revokeConnectedApp(\n req.params.clientId,\n await resolveActor(container, req),\n );\n return reply.code(204).send();\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/access/api-keys',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listApiKeys',\n tags,\n summary: 'Your API keys for this organization',\n description: `Requires \\`api_keys:read\\`. Your keys for this organization with their role and expiry. The key itself is never returned again after it is created.`,\n response: { 200: ApiKeyList, ...refusals(403) },\n },\n handler: async (req) => organizations.listApiKeys(await resolveActor(container, req)),\n });\n\n r.route({\n method: 'GET',\n url: '/api/access/api-keys/roles',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listApiKeyRoles',\n tags,\n summary: 'The roles you can put on an API key',\n description: `Requires \\`api_keys:write\\`. The organization's roles whose every permission you hold somewhere.`,\n response: { 200: ApiKeyRoleList, ...refusals(403) },\n },\n handler: async (req) => organizations.listApiKeyRoles(await resolveActor(container, req)),\n });\n\n r.route({\n method: 'POST',\n url: '/api/access/api-keys',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'createApiKey',\n tags,\n summary: 'Create an API key',\n description: `Requires \\`api_keys:write\\`. The key acts as you in this organization with the permissions of the role given, never beyond what you hold at the time of each request. Send it in the \\`x-api-key\\` header or as \\`Authorization: Bearer <key>\\`; every route under /api and /mcp accepts it. 403 when the role holds a permission you hold nowhere; 404 when the organization has no such role. The key is returned once.`,\n body: CreateApiKey,\n response: { 201: CreatedApiKey, ...refusals(400, 403, 404) },\n },\n handler: async (req, reply) => {\n const created = await organizations.createApiKey(\n req.body,\n await resolveActor(container, req),\n );\n return reply.code(201).send(created);\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/access/api-keys/:id',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'deleteApiKey',\n tags,\n summary: 'Revoke an API key',\n description: `Requires \\`api_keys:delete\\`. 404 when the key is not one of yours in this organization.`,\n params: ApiKeyIdParam,\n response: { 204: z.void(), ...refusals(403, 404) },\n },\n handler: async (req, reply) => {\n await organizations.deleteApiKey(req.params.id, await resolveActor(container, req));\n return reply.code(204).send();\n },\n });\n}\n"],"mappings":";;;;;;AAkBA,eAAsB,aAAa,KAAsB,WAAqC;CAC5F,MAAM,IAAI,IAAI,iBAAkC;CAChD,MAAM,OAAO,CAAC,QAAQ;CACtB,MAAM,gBAAgB,UAAU;CAEhC,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb;GACA,SAAS;GACT,aAAa;GACb,UAAU;IAAE,KAAK;IAA0B,GAAG,SAAS,GAAG;GAAE;EAC9D;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,aAAa,WAAW,GAAG;GACjC,OAAO,cAAc,eAAe;EACtC;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb;GACA,SAAS;GACT,aAAa;GACb,UAAU;IAAE,KAAK;IAAmB,GAAG,SAAS,GAAG;GAAE;EACvD;EACA,SAAS,OAAO,QAAQ,cAAc,kBAAkB,MAAM,aAAa,WAAW,GAAG,CAAC;CAC5F,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb;GACA,SAAS;GACT,aAAa;GACb,UAAU;IAAE,KAAK;IAAkB,GAAG,SAAS,GAAG;GAAE;EACtD;EACA,SAAS,OAAO,QAAQ,cAAc,kBAAkB,MAAM,aAAa,WAAW,GAAG,CAAC;CAC5F,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb;GACA,SAAS;GACT,aAAa;GACb,QAAQ;GACR,UAAU;IAAE,KAAK,EAAE,KAAK;IAAG,GAAG,SAAS,GAAG;GAAE;EAC9C;EACA,SAAS,OAAO,KAAK,UAAU;GAC7B,MAAM,cAAc,mBAClB,IAAI,OAAO,UACX,MAAM,aAAa,WAAW,GAAG,CACnC;GACA,OAAO,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK;EAC9B;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb;GACA,SAAS;GACT,aAAa;GACb,UAAU;IAAE,KAAK;IAAY,GAAG,SAAS,GAAG;GAAE;EAChD;EACA,SAAS,OAAO,QAAQ,cAAc,YAAY,MAAM,aAAa,WAAW,GAAG,CAAC;CACtF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb;GACA,SAAS;GACT,aAAa;GACb,UAAU;IAAE,KAAK;IAAgB,GAAG,SAAS,GAAG;GAAE;EACpD;EACA,SAAS,OAAO,QAAQ,cAAc,gBAAgB,MAAM,aAAa,WAAW,GAAG,CAAC;CAC1F,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb;GACA,SAAS;GACT,aAAa;GACb,MAAM;GACN,UAAU;IAAE,KAAK;IAAe,GAAG,SAAS,KAAK,KAAK,GAAG;GAAE;EAC7D;EACA,SAAS,OAAO,KAAK,UAAU;GAC7B,MAAM,UAAU,MAAM,cAAc,aAClC,IAAI,MACJ,MAAM,aAAa,WAAW,GAAG,CACnC;GACA,OAAO,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,OAAO;EACrC;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb;GACA,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,cAAc,aAAa,IAAI,OAAO,IAAI,MAAM,aAAa,WAAW,GAAG,CAAC;GAClF,OAAO,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK;EAC9B;CACF,CAAC;AACH"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { refusals } from "../schemas/shared.js";
|
|
2
|
-
import {
|
|
2
|
+
import { resolveActor } from "../scope.js";
|
|
3
3
|
import { ActivityAssetList, ActivityList, ActivityPathParam, CourseIdPathParam, CreateModule, ModuleList, ModulePathParam, ReorderInput, SaveActivity, UpdateModule } from "../schemas/activities.js";
|
|
4
4
|
import "../schemas/index.js";
|
|
5
5
|
//#region src/http/routes/activities.ts
|
|
@@ -14,8 +14,8 @@ async function activitiesRoutes(app, container) {
|
|
|
14
14
|
schema: {
|
|
15
15
|
operationId: "listModules",
|
|
16
16
|
tags,
|
|
17
|
-
summary: "List a course's modules
|
|
18
|
-
description:
|
|
17
|
+
summary: "List a course's modules",
|
|
18
|
+
description: `Requires \`courses:read\`. Modules in order, each with its activities.`,
|
|
19
19
|
params: CourseIdPathParam,
|
|
20
20
|
response: {
|
|
21
21
|
200: ModuleList,
|
|
@@ -23,8 +23,8 @@ async function activitiesRoutes(app, container) {
|
|
|
23
23
|
}
|
|
24
24
|
},
|
|
25
25
|
handler: async (req) => {
|
|
26
|
-
const
|
|
27
|
-
return content.listCourseModules(
|
|
26
|
+
const actor = await resolveActor(container, req);
|
|
27
|
+
return content.listCourseModules(req.params.courseId, actor);
|
|
28
28
|
}
|
|
29
29
|
});
|
|
30
30
|
r.route({
|
|
@@ -34,8 +34,8 @@ async function activitiesRoutes(app, container) {
|
|
|
34
34
|
schema: {
|
|
35
35
|
operationId: "listActivities",
|
|
36
36
|
tags,
|
|
37
|
-
summary: "List a course's activities
|
|
38
|
-
description:
|
|
37
|
+
summary: "List a course's activities",
|
|
38
|
+
description: `Requires \`courses:read\`. Every activity, published or not.`,
|
|
39
39
|
params: CourseIdPathParam,
|
|
40
40
|
response: {
|
|
41
41
|
200: ActivityList,
|
|
@@ -43,8 +43,8 @@ async function activitiesRoutes(app, container) {
|
|
|
43
43
|
}
|
|
44
44
|
},
|
|
45
45
|
handler: async (req) => {
|
|
46
|
-
const
|
|
47
|
-
return content.listCourseActivityViews(
|
|
46
|
+
const actor = await resolveActor(container, req);
|
|
47
|
+
return content.listCourseActivityViews(req.params.courseId, actor);
|
|
48
48
|
}
|
|
49
49
|
});
|
|
50
50
|
r.route({
|
|
@@ -54,8 +54,8 @@ async function activitiesRoutes(app, container) {
|
|
|
54
54
|
schema: {
|
|
55
55
|
operationId: "listActivityAssets",
|
|
56
56
|
tags,
|
|
57
|
-
summary: "List the library files each activity of a course uses
|
|
58
|
-
description:
|
|
57
|
+
summary: "List the library files each activity of a course uses",
|
|
58
|
+
description: `Requires \`courses:read\`. One row per activity and file, as recorded when the activity was saved.`,
|
|
59
59
|
params: CourseIdPathParam,
|
|
60
60
|
response: {
|
|
61
61
|
200: ActivityAssetList,
|
|
@@ -63,8 +63,8 @@ async function activitiesRoutes(app, container) {
|
|
|
63
63
|
}
|
|
64
64
|
},
|
|
65
65
|
handler: async (req) => {
|
|
66
|
-
const
|
|
67
|
-
return content.listCourseActivityAssets(
|
|
66
|
+
const actor = await resolveActor(container, req);
|
|
67
|
+
return content.listCourseActivityAssets(req.params.courseId, actor);
|
|
68
68
|
}
|
|
69
69
|
});
|
|
70
70
|
r.route({
|
|
@@ -74,8 +74,8 @@ async function activitiesRoutes(app, container) {
|
|
|
74
74
|
schema: {
|
|
75
75
|
operationId: "createModule",
|
|
76
76
|
tags,
|
|
77
|
-
summary: "Add a module
|
|
78
|
-
description:
|
|
77
|
+
summary: "Add a module",
|
|
78
|
+
description: `Requires \`courses:write\`. Added last. Returns the course's modules.`,
|
|
79
79
|
params: CourseIdPathParam,
|
|
80
80
|
body: CreateModule,
|
|
81
81
|
response: {
|
|
@@ -84,8 +84,8 @@ async function activitiesRoutes(app, container) {
|
|
|
84
84
|
}
|
|
85
85
|
},
|
|
86
86
|
handler: async (req) => {
|
|
87
|
-
const
|
|
88
|
-
return content.createModule(
|
|
87
|
+
const actor = await resolveActor(container, req);
|
|
88
|
+
return content.createModule(req.params.courseId, req.body.title, actor);
|
|
89
89
|
}
|
|
90
90
|
});
|
|
91
91
|
r.route({
|
|
@@ -95,8 +95,8 @@ async function activitiesRoutes(app, container) {
|
|
|
95
95
|
schema: {
|
|
96
96
|
operationId: "reorderModules",
|
|
97
97
|
tags,
|
|
98
|
-
summary: "Reorder a course's modules
|
|
99
|
-
description:
|
|
98
|
+
summary: "Reorder a course's modules",
|
|
99
|
+
description: `Requires \`courses:write\`. Ids that are not modules of the course are ignored. Returns the course's modules.`,
|
|
100
100
|
params: CourseIdPathParam,
|
|
101
101
|
body: ReorderInput,
|
|
102
102
|
response: {
|
|
@@ -105,8 +105,8 @@ async function activitiesRoutes(app, container) {
|
|
|
105
105
|
}
|
|
106
106
|
},
|
|
107
107
|
handler: async (req) => {
|
|
108
|
-
const
|
|
109
|
-
return content.reorderModules(
|
|
108
|
+
const actor = await resolveActor(container, req);
|
|
109
|
+
return content.reorderModules(req.params.courseId, req.body.orderedIds, actor);
|
|
110
110
|
}
|
|
111
111
|
});
|
|
112
112
|
r.route({
|
|
@@ -116,8 +116,8 @@ async function activitiesRoutes(app, container) {
|
|
|
116
116
|
schema: {
|
|
117
117
|
operationId: "updateModule",
|
|
118
118
|
tags,
|
|
119
|
-
summary: "Rename a module
|
|
120
|
-
description:
|
|
119
|
+
summary: "Rename a module",
|
|
120
|
+
description: `Requires \`courses:write\`. Returns the course's modules.`,
|
|
121
121
|
params: ModulePathParam,
|
|
122
122
|
body: UpdateModule,
|
|
123
123
|
response: {
|
|
@@ -126,8 +126,8 @@ async function activitiesRoutes(app, container) {
|
|
|
126
126
|
}
|
|
127
127
|
},
|
|
128
128
|
handler: async (req) => {
|
|
129
|
-
const
|
|
130
|
-
return content.updateModule(
|
|
129
|
+
const actor = await resolveActor(container, req);
|
|
130
|
+
return content.updateModule(req.params.courseId, req.params.moduleId, req.body.title, actor);
|
|
131
131
|
}
|
|
132
132
|
});
|
|
133
133
|
r.route({
|
|
@@ -137,8 +137,8 @@ async function activitiesRoutes(app, container) {
|
|
|
137
137
|
schema: {
|
|
138
138
|
operationId: "deleteModule",
|
|
139
139
|
tags,
|
|
140
|
-
summary: "Delete a module
|
|
141
|
-
description:
|
|
140
|
+
summary: "Delete a module",
|
|
141
|
+
description: `Requires \`courses:write\`. Returns the course's modules.`,
|
|
142
142
|
params: ModulePathParam,
|
|
143
143
|
response: {
|
|
144
144
|
200: ModuleList,
|
|
@@ -146,8 +146,8 @@ async function activitiesRoutes(app, container) {
|
|
|
146
146
|
}
|
|
147
147
|
},
|
|
148
148
|
handler: async (req) => {
|
|
149
|
-
const
|
|
150
|
-
return content.deleteModule(
|
|
149
|
+
const actor = await resolveActor(container, req);
|
|
150
|
+
return content.deleteModule(req.params.courseId, req.params.moduleId, actor);
|
|
151
151
|
}
|
|
152
152
|
});
|
|
153
153
|
r.route({
|
|
@@ -157,8 +157,8 @@ async function activitiesRoutes(app, container) {
|
|
|
157
157
|
schema: {
|
|
158
158
|
operationId: "reorderActivities",
|
|
159
159
|
tags,
|
|
160
|
-
summary: "Reorder the activities in a module
|
|
161
|
-
description:
|
|
160
|
+
summary: "Reorder the activities in a module",
|
|
161
|
+
description: `Requires \`courses:write\`. Send every activity id of the module once (409 otherwise). Returns the course's modules.`,
|
|
162
162
|
params: ModulePathParam,
|
|
163
163
|
body: ReorderInput,
|
|
164
164
|
response: {
|
|
@@ -167,8 +167,8 @@ async function activitiesRoutes(app, container) {
|
|
|
167
167
|
}
|
|
168
168
|
},
|
|
169
169
|
handler: async (req) => {
|
|
170
|
-
const
|
|
171
|
-
return content.reorderActivities(
|
|
170
|
+
const actor = await resolveActor(container, req);
|
|
171
|
+
return content.reorderActivities(req.params.courseId, req.params.moduleId, req.body.orderedIds, actor);
|
|
172
172
|
}
|
|
173
173
|
});
|
|
174
174
|
r.route({
|
|
@@ -178,8 +178,8 @@ async function activitiesRoutes(app, container) {
|
|
|
178
178
|
schema: {
|
|
179
179
|
operationId: "createActivity",
|
|
180
180
|
tags,
|
|
181
|
-
summary: "Add an activity
|
|
182
|
-
description:
|
|
181
|
+
summary: "Add an activity",
|
|
182
|
+
description: `Requires \`courses:write\`. Records the library files the activity uses; each must be a ready library file (409 otherwise). A file the course does not already use must be uploaded for the course, or the caller needs \`media:read\` (403 otherwise). Returns the course's modules.`,
|
|
183
183
|
params: ModulePathParam,
|
|
184
184
|
body: SaveActivity,
|
|
185
185
|
response: {
|
|
@@ -188,8 +188,8 @@ async function activitiesRoutes(app, container) {
|
|
|
188
188
|
}
|
|
189
189
|
},
|
|
190
190
|
handler: async (req) => {
|
|
191
|
-
const
|
|
192
|
-
return content.saveActivity(
|
|
191
|
+
const actor = await resolveActor(container, req);
|
|
192
|
+
return content.saveActivity(req.params.courseId, req.params.moduleId, req.body, null, actor);
|
|
193
193
|
}
|
|
194
194
|
});
|
|
195
195
|
r.route({
|
|
@@ -199,8 +199,8 @@ async function activitiesRoutes(app, container) {
|
|
|
199
199
|
schema: {
|
|
200
200
|
operationId: "updateActivity",
|
|
201
201
|
tags,
|
|
202
|
-
summary: "Update an activity
|
|
203
|
-
description:
|
|
202
|
+
summary: "Update an activity",
|
|
203
|
+
description: `Requires \`courses:write\`. Replaces its content and records the library files it uses; each must be a ready library file (409 otherwise). A file the course does not already use must be uploaded for the course, or the caller needs \`media:read\` (403 otherwise). Returns the course's modules.`,
|
|
204
204
|
params: ActivityPathParam,
|
|
205
205
|
body: SaveActivity,
|
|
206
206
|
response: {
|
|
@@ -209,8 +209,8 @@ async function activitiesRoutes(app, container) {
|
|
|
209
209
|
}
|
|
210
210
|
},
|
|
211
211
|
handler: async (req) => {
|
|
212
|
-
const
|
|
213
|
-
return content.saveActivity(
|
|
212
|
+
const actor = await resolveActor(container, req);
|
|
213
|
+
return content.saveActivity(req.params.courseId, req.params.moduleId, req.body, req.params.activityId, actor);
|
|
214
214
|
}
|
|
215
215
|
});
|
|
216
216
|
r.route({
|
|
@@ -220,8 +220,8 @@ async function activitiesRoutes(app, container) {
|
|
|
220
220
|
schema: {
|
|
221
221
|
operationId: "deleteActivity",
|
|
222
222
|
tags,
|
|
223
|
-
summary: "Delete an activity
|
|
224
|
-
description:
|
|
223
|
+
summary: "Delete an activity",
|
|
224
|
+
description: `Requires \`courses:write\`. Returns the course's modules.`,
|
|
225
225
|
params: ActivityPathParam,
|
|
226
226
|
response: {
|
|
227
227
|
200: ModuleList,
|
|
@@ -229,8 +229,8 @@ async function activitiesRoutes(app, container) {
|
|
|
229
229
|
}
|
|
230
230
|
},
|
|
231
231
|
handler: async (req) => {
|
|
232
|
-
const
|
|
233
|
-
return content.deleteActivity(
|
|
232
|
+
const actor = await resolveActor(container, req);
|
|
233
|
+
return content.deleteActivity(req.params.courseId, req.params.moduleId, req.params.activityId, actor);
|
|
234
234
|
}
|
|
235
235
|
});
|
|
236
236
|
}
|