@headless-lms/server 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/dist/app/community-mail.d.ts +6 -8
  2. package/dist/app/community-mail.js +13 -17
  3. package/dist/app/community-mail.js.map +1 -1
  4. package/dist/app/container.d.ts +27 -10
  5. package/dist/app/container.js +178 -22
  6. package/dist/app/container.js.map +1 -1
  7. package/dist/app/notification-channels.d.ts +47 -0
  8. package/dist/app/notification-channels.js +174 -0
  9. package/dist/app/notification-channels.js.map +1 -0
  10. package/dist/app/notification-reads.d.ts +23 -0
  11. package/dist/app/notification-reads.js +95 -0
  12. package/dist/app/notification-reads.js.map +1 -0
  13. package/dist/http/errors.d.ts +1 -0
  14. package/dist/http/errors.js +16 -1
  15. package/dist/http/errors.js.map +1 -1
  16. package/dist/http/mcp/tools/communities.js +1 -1
  17. package/dist/http/mcp/tools/communities.js.map +1 -1
  18. package/dist/http/plugins/error-handler.js +3 -1
  19. package/dist/http/plugins/error-handler.js.map +1 -1
  20. package/dist/http/routes/automations.js +3 -3
  21. package/dist/http/routes/automations.js.map +1 -1
  22. package/dist/http/routes/communities.js +67 -7
  23. package/dist/http/routes/communities.js.map +1 -1
  24. package/dist/http/routes/member/actor.d.ts +1 -1
  25. package/dist/http/routes/member/chats.js +9 -30
  26. package/dist/http/routes/member/chats.js.map +1 -1
  27. package/dist/http/routes/member/community.js +25 -1
  28. package/dist/http/routes/member/community.js.map +1 -1
  29. package/dist/http/routes/member/devices.d.ts +7 -0
  30. package/dist/http/routes/member/devices.js +98 -0
  31. package/dist/http/routes/member/devices.js.map +1 -0
  32. package/dist/http/routes/member/index.js +4 -0
  33. package/dist/http/routes/member/index.js.map +1 -1
  34. package/dist/http/routes/member/notification-settings.d.ts +7 -0
  35. package/dist/http/routes/member/notification-settings.js +108 -0
  36. package/dist/http/routes/member/notification-settings.js.map +1 -0
  37. package/dist/http/routes/member/notifications.js +66 -42
  38. package/dist/http/routes/member/notifications.js.map +1 -1
  39. package/dist/http/routes/member/posts.js +33 -33
  40. package/dist/http/routes/member/posts.js.map +1 -1
  41. package/dist/http/routes/organizations.js +122 -1
  42. package/dist/http/routes/organizations.js.map +1 -1
  43. package/dist/http/schemas/assets.d.ts +6 -6
  44. package/dist/http/schemas/automations.d.ts +0 -4
  45. package/dist/http/schemas/chats.d.ts +34 -11
  46. package/dist/http/schemas/chats.js +4 -6
  47. package/dist/http/schemas/chats.js.map +1 -1
  48. package/dist/http/schemas/community.d.ts +77 -17
  49. package/dist/http/schemas/community.js +6 -3
  50. package/dist/http/schemas/community.js.map +1 -1
  51. package/dist/http/schemas/discussion.d.ts +23 -1
  52. package/dist/http/schemas/discussion.js +14 -2
  53. package/dist/http/schemas/discussion.js.map +1 -1
  54. package/dist/http/schemas/index.d.ts +6 -6
  55. package/dist/http/schemas/index.js +6 -6
  56. package/dist/http/schemas/integrations.d.ts +4 -4
  57. package/dist/http/schemas/members.d.ts +5 -5
  58. package/dist/http/schemas/notifications.d.ts +498 -175
  59. package/dist/http/schemas/notifications.js +148 -28
  60. package/dist/http/schemas/notifications.js.map +1 -1
  61. package/dist/http/schemas/posts.d.ts +37 -3
  62. package/dist/http/schemas/profile.d.ts +2 -2
  63. package/dist/http/schemas/shared.d.ts +10 -3
  64. package/dist/http/schemas/shared.js +3 -2
  65. package/dist/http/schemas/shared.js.map +1 -1
  66. package/dist/http/schemas/students.d.ts +3 -3
  67. package/dist/http/scope.d.ts +2 -1
  68. package/dist/http/scope.js +7 -1
  69. package/dist/http/scope.js.map +1 -1
  70. package/dist/index.d.ts +2 -1
  71. package/dist/index.js.map +1 -1
  72. package/dist/packages/server/package.js +1 -1
  73. package/package.json +5 -5
@@ -1 +1 @@
1
- {"version":3,"file":"errors.js","names":[],"sources":["../../src/http/errors.ts"],"sourcesContent":["import {\n ConflictError,\n ForbiddenError,\n InvalidInputError,\n NotFoundError,\n} from '@headless-lms/core/shared/errors';\nimport { InviteError, OrganizationRuleError } from '@headless-lms/core/organizations';\nimport {\n ActionInvocationError,\n AlreadyConnectedError,\n InvalidConfigError,\n UnknownIntegrationError,\n} from '@headless-lms/core/integrations';\nimport {\n AutomationKindMismatchError,\n InvalidTriggerError,\n ReservedActionError,\n} from '@headless-lms/core/automations';\nimport { hasZodFastifySchemaValidationErrors } from 'fastify-type-provider-zod';\nimport { NoActiveOrgError } from './scope.js';\nimport { UnauthorizedError } from './plugins/auth.js';\n\nexport type Refusal = {\n status: number;\n error: string;\n message: string;\n issues?: { path: string; message: string }[];\n};\n\nconst BODY_ERRORS: Record<string, Refusal> = {\n FST_ERR_CTP_INVALID_JSON_BODY: {\n status: 400,\n error: 'invalid_body',\n message: 'The request body must be valid JSON',\n },\n FST_ERR_CTP_EMPTY_JSON_BODY: {\n status: 400,\n error: 'invalid_body',\n message: 'The request body must be valid JSON',\n },\n FST_ERR_CTP_INVALID_MEDIA_TYPE: {\n status: 415,\n error: 'unsupported_media_type',\n message: 'Send the request body as application/json',\n },\n FST_ERR_CTP_BODY_TOO_LARGE: {\n status: 413,\n error: 'payload_too_large',\n message: 'The request body is too large',\n },\n FST_ERR_NOT_FOUND: { status: 404, error: 'not_found', message: 'No such route' },\n};\n\nfunction issuePath(context: string, instancePath: string): string {\n const inner = instancePath\n .split('/')\n .filter((part) => part.length > 0)\n .map((part) => (/^\\d+$/.test(part) ? `[${part}]` : `.${part}`))\n .join('');\n return `${context}${inner}`;\n}\n\ntype IssueParams = {\n expected?: string;\n received?: string;\n minimum?: number | bigint;\n maximum?: number | bigint;\n inclusive?: boolean;\n origin?: string;\n keys?: string[];\n values?: unknown[];\n format?: string;\n};\n\nfunction describeIssue(keyword: string | undefined, params: IssueParams, fallback: string): string {\n switch (keyword) {\n case 'invalid_type':\n if (params.received === 'undefined' || params.received === 'null') {\n return 'is required';\n }\n return params.expected ? `must be ${withArticle(params.expected)}` : fallback;\n case 'too_small':\n return params.origin === 'string'\n ? Number(params.minimum) === 1\n ? 'must not be empty'\n : `must be at least ${params.minimum} characters`\n : params.origin === 'array'\n ? `must have at least ${params.minimum} ${Number(params.minimum) === 1 ? 'item' : 'items'}`\n : `must be ${params.inclusive === false ? 'greater than' : 'at least'} ${params.minimum}`;\n case 'too_big':\n return params.origin === 'string'\n ? `must be at most ${params.maximum} characters`\n : params.origin === 'array'\n ? `must have at most ${params.maximum} items`\n : `must be ${params.inclusive === false ? 'less than' : 'at most'} ${params.maximum}`;\n case 'unrecognized_keys':\n return `has ${params.keys && params.keys.length > 1 ? 'unknown fields' : 'an unknown field'} ${(params.keys ?? []).map((key) => `\"${key}\"`).join(', ')}`;\n case 'invalid_value':\n return `must be one of ${(params.values ?? []).map((value) => JSON.stringify(value)).join(', ')}`;\n case 'invalid_format':\n return params.format ? `must be a valid ${params.format.replace(/_/g, ' ')}` : fallback;\n case 'invalid_union':\n return 'is not in an accepted shape';\n default:\n return fallback;\n }\n}\n\nfunction withArticle(noun: string): string {\n return /^[aeiou]/i.test(noun) ? `an ${noun}` : `a ${noun}`;\n}\n\nexport function classifyError(error: unknown): Refusal | undefined {\n if (error instanceof NotFoundError) {\n return { status: 404, error: 'not_found', message: error.message };\n }\n if (error instanceof InvalidInputError) {\n return { status: 400, error: 'invalid_input', message: error.message };\n }\n if (error instanceof ConflictError || error instanceof OrganizationRuleError) {\n return { status: 409, error: 'conflict', message: error.message };\n }\n if (error instanceof ForbiddenError || error instanceof NoActiveOrgError) {\n return { status: 403, error: 'forbidden', message: error.message };\n }\n if (error instanceof UnauthorizedError) {\n return { status: 401, error: 'unauthorized', message: error.message };\n }\n if (error instanceof InviteError) {\n return { status: 400, error: 'invalid_invite', message: error.message };\n }\n if (error instanceof AlreadyConnectedError) {\n return { status: 409, error: 'already_connected', message: error.message };\n }\n if (error instanceof UnknownIntegrationError) {\n return { status: 400, error: 'unknown_integration', message: error.message };\n }\n if (error instanceof InvalidConfigError) {\n return { status: 400, error: 'invalid_config', message: error.message };\n }\n if (error instanceof InvalidTriggerError) {\n return { status: 400, error: 'invalid_trigger', message: error.message };\n }\n if (error instanceof ReservedActionError) {\n return { status: 400, error: 'reserved_action', message: error.message };\n }\n if (error instanceof ActionInvocationError) {\n return {\n status: 502,\n error: 'action_failed',\n message: 'The integration did not respond as expected',\n };\n }\n if (error instanceof AutomationKindMismatchError) {\n return { status: 409, error: 'kind_mismatch', message: error.message };\n }\n if (hasZodFastifySchemaValidationErrors(error)) {\n const context = (error as { validationContext?: string }).validationContext ?? 'request';\n const issues = error.validation.map((issue) => ({\n path: issuePath(context, issue.instancePath),\n message: describeIssue(\n issue.keyword,\n (issue.params ?? {}) as IssueParams,\n issue.message ?? 'is invalid',\n ),\n }));\n const first = issues[0];\n return {\n status: 400,\n error: 'validation',\n message: first ? `${first.path}: ${first.message}` : 'The request is invalid',\n issues,\n };\n }\n const err = error as { statusCode?: number; code?: string };\n if (err.code && BODY_ERRORS[err.code]) {\n return BODY_ERRORS[err.code];\n }\n if (typeof err.statusCode === 'number' && err.statusCode >= 400 && err.statusCode < 500) {\n return {\n status: err.statusCode,\n error: 'bad_request',\n message: 'The request could not be processed',\n };\n }\n return undefined;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"errors.js","names":[],"sources":["../../src/http/errors.ts"],"sourcesContent":["import {\n ConflictError,\n ForbiddenError,\n InvalidInputError,\n NotFoundError,\n UnavailableError,\n ValidationError,\n} from '@headless-lms/core/shared/errors';\nimport { InviteError, OrganizationRuleError } from '@headless-lms/core/organizations';\nimport {\n ActionInvocationError,\n AlreadyConnectedError,\n InvalidConfigError,\n UnknownIntegrationError,\n} from '@headless-lms/core/integrations';\nimport {\n AutomationKindMismatchError,\n InvalidTriggerError,\n ReservedActionError,\n} from '@headless-lms/core/automations';\nimport { hasZodFastifySchemaValidationErrors } from 'fastify-type-provider-zod';\nimport { NoActiveOrgError } from './scope.js';\nimport { UnauthorizedError } from './plugins/auth.js';\n\nexport type Refusal = {\n status: number;\n error: string;\n message: string;\n issues?: { path: string; message: string }[];\n details?: Record<string, string>;\n};\n\nconst BODY_ERRORS: Record<string, Refusal> = {\n FST_ERR_CTP_INVALID_JSON_BODY: {\n status: 400,\n error: 'invalid_body',\n message: 'The request body must be valid JSON',\n },\n FST_ERR_CTP_EMPTY_JSON_BODY: {\n status: 400,\n error: 'invalid_body',\n message: 'The request body must be valid JSON',\n },\n FST_ERR_CTP_INVALID_MEDIA_TYPE: {\n status: 415,\n error: 'unsupported_media_type',\n message: 'Send the request body as application/json',\n },\n FST_ERR_CTP_BODY_TOO_LARGE: {\n status: 413,\n error: 'payload_too_large',\n message: 'The request body is too large',\n },\n FST_ERR_NOT_FOUND: { status: 404, error: 'not_found', message: 'No such route' },\n};\n\nfunction issuePath(context: string, instancePath: string): string {\n const inner = instancePath\n .split('/')\n .filter((part) => part.length > 0)\n .map((part) => (/^\\d+$/.test(part) ? `[${part}]` : `.${part}`))\n .join('');\n return `${context}${inner}`;\n}\n\ntype IssueParams = {\n expected?: string;\n received?: string;\n minimum?: number | bigint;\n maximum?: number | bigint;\n inclusive?: boolean;\n origin?: string;\n keys?: string[];\n values?: unknown[];\n format?: string;\n};\n\nfunction describeIssue(keyword: string | undefined, params: IssueParams, fallback: string): string {\n switch (keyword) {\n case 'invalid_type':\n if (params.received === 'undefined' || params.received === 'null') {\n return 'is required';\n }\n return params.expected ? `must be ${withArticle(params.expected)}` : fallback;\n case 'too_small':\n return params.origin === 'string'\n ? Number(params.minimum) === 1\n ? 'must not be empty'\n : `must be at least ${params.minimum} characters`\n : params.origin === 'array'\n ? `must have at least ${params.minimum} ${Number(params.minimum) === 1 ? 'item' : 'items'}`\n : `must be ${params.inclusive === false ? 'greater than' : 'at least'} ${params.minimum}`;\n case 'too_big':\n return params.origin === 'string'\n ? `must be at most ${params.maximum} characters`\n : params.origin === 'array'\n ? `must have at most ${params.maximum} items`\n : `must be ${params.inclusive === false ? 'less than' : 'at most'} ${params.maximum}`;\n case 'unrecognized_keys':\n return `has ${params.keys && params.keys.length > 1 ? 'unknown fields' : 'an unknown field'} ${(params.keys ?? []).map((key) => `\"${key}\"`).join(', ')}`;\n case 'invalid_value':\n return `must be one of ${(params.values ?? []).map((value) => JSON.stringify(value)).join(', ')}`;\n case 'invalid_format':\n return params.format ? `must be a valid ${params.format.replace(/_/g, ' ')}` : fallback;\n case 'invalid_union':\n return 'is not in an accepted shape';\n default:\n return fallback;\n }\n}\n\nfunction withArticle(noun: string): string {\n return /^[aeiou]/i.test(noun) ? `an ${noun}` : `a ${noun}`;\n}\n\nexport function classifyError(error: unknown): Refusal | undefined {\n if (error instanceof NotFoundError) {\n return { status: 404, error: 'not_found', message: error.message };\n }\n if (error instanceof InvalidInputError) {\n return { status: 400, error: 'invalid_input', message: error.message };\n }\n if (error instanceof ValidationError) {\n return {\n status: 422,\n error: 'unprocessable',\n message: error.message,\n issues: error.problems.map((problem) => ({\n path: problem.path ? `body.${problem.path}` : 'body',\n message: problem.message,\n })),\n };\n }\n if (error instanceof ConflictError || error instanceof OrganizationRuleError) {\n return { status: 409, error: 'conflict', message: error.message };\n }\n if (error instanceof ForbiddenError || error instanceof NoActiveOrgError) {\n return { status: 403, error: 'forbidden', message: error.message };\n }\n if (error instanceof UnavailableError) {\n return {\n status: 503,\n error: 'unavailable',\n message: error.message,\n ...(error.details && { details: error.details }),\n };\n }\n if (error instanceof UnauthorizedError) {\n return { status: 401, error: 'unauthorized', message: error.message };\n }\n if (error instanceof InviteError) {\n return { status: 400, error: 'invalid_invite', message: error.message };\n }\n if (error instanceof AlreadyConnectedError) {\n return { status: 409, error: 'already_connected', message: error.message };\n }\n if (error instanceof UnknownIntegrationError) {\n return { status: 400, error: 'unknown_integration', message: error.message };\n }\n if (error instanceof InvalidConfigError) {\n return { status: 400, error: 'invalid_config', message: error.message };\n }\n if (error instanceof InvalidTriggerError) {\n return { status: 400, error: 'invalid_trigger', message: error.message };\n }\n if (error instanceof ReservedActionError) {\n return { status: 400, error: 'reserved_action', message: error.message };\n }\n if (error instanceof ActionInvocationError) {\n return {\n status: 502,\n error: 'action_failed',\n message: 'The integration did not respond as expected',\n };\n }\n if (error instanceof AutomationKindMismatchError) {\n return { status: 409, error: 'kind_mismatch', message: error.message };\n }\n if (hasZodFastifySchemaValidationErrors(error)) {\n const context = (error as { validationContext?: string }).validationContext ?? 'request';\n const issues = error.validation.map((issue) => ({\n path: issuePath(context, issue.instancePath),\n message: describeIssue(\n issue.keyword,\n (issue.params ?? {}) as IssueParams,\n issue.message ?? 'is invalid',\n ),\n }));\n const first = issues[0];\n return {\n status: 400,\n error: 'validation',\n message: first ? `${first.path}: ${first.message}` : 'The request is invalid',\n issues,\n };\n }\n const err = error as { statusCode?: number; code?: string };\n if (err.code && BODY_ERRORS[err.code]) {\n return BODY_ERRORS[err.code];\n }\n if (typeof err.statusCode === 'number' && err.statusCode >= 400 && err.statusCode < 500) {\n return {\n status: err.statusCode,\n error: 'bad_request',\n message: 'The request could not be processed',\n };\n }\n return undefined;\n}\n"],"mappings":"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"}
@@ -56,7 +56,7 @@ const communityTools = [
56
56
  idempotent: true,
57
57
  openWorld: false,
58
58
  input: PatchDmSettings.extend(CommunityIdParam.shape),
59
- call: ({ communityId, ...input }, { container, scope }) => container.content.patchDmSettings(scope.orgId, communityId, input, actor(scope))
59
+ call: ({ communityId, ...input }, { container, scope }) => container.content.patchCommunitySettings(scope.orgId, communityId, input, actor(scope))
60
60
  }),
61
61
  defineTool({
62
62
  name: "create_space",
@@ -1 +1 @@
1
- {"version":3,"file":"communities.js","names":[],"sources":["../../../../src/http/mcp/tools/communities.ts"],"sourcesContent":["import {\n CommunitiesQuery,\n CommunityIdParam,\n CommunitySpaceParam,\n CreateCommunity,\n CreateSpace,\n OrderedIds,\n PatchDmSettings,\n SetSpaceTopics,\n SpaceLinkBody,\n SpaceLinkParam,\n UpdateCommunity,\n UpdateSpace,\n} from '../../schemas/index.js';\nimport { actor, defineTool, type Tool } from '../tool.js';\n\nexport const communityTools: Tool[] = [\n defineTool({\n name: 'list_communities',\n title: 'List communities',\n description:\n 'Search and page through the communities, drafts included, each with its status and direct-message settings. Filter by status, search by title.',\n scope: 'communities:read',\n kind: 'read',\n idempotent: true,\n openWorld: false,\n input: CommunitiesQuery,\n call: (input, { container, scope }) => container.content.listCommunities(scope.orgId, input),\n }),\n defineTool({\n name: 'list_spaces',\n title: 'List spaces',\n description:\n 'The spaces of a community, in order, each with its access rule, topics and links.',\n scope: 'communities:read',\n kind: 'read',\n idempotent: true,\n openWorld: false,\n input: CommunityIdParam,\n call: ({ communityId }, { container, scope }) =>\n container.content.listSpaces(scope.orgId, communityId),\n }),\n defineTool({\n name: 'create_community',\n title: 'Create community',\n description:\n 'Create a draft community with the default direct-message settings and no spaces. Add spaces with create_space and publish it with update_community.',\n scope: 'communities:write',\n kind: 'write',\n idempotent: false,\n openWorld: false,\n input: CreateCommunity,\n call: (input, { container, scope }) =>\n container.content.createCommunity(scope.orgId, input, actor(scope)),\n }),\n defineTool({\n name: 'update_community',\n title: 'Update community',\n description:\n 'Change a community title, description or cover image, or publish and unpublish it with status. The cover must be a ready image from list_media; null clears it. Fields left out stay as they are.',\n scope: 'communities:write',\n kind: 'write',\n idempotent: true,\n openWorld: false,\n input: UpdateCommunity.extend(CommunityIdParam.shape),\n call: ({ communityId, ...input }, { container, scope }) =>\n container.content.updateCommunity(scope.orgId, communityId, input, actor(scope)),\n }),\n defineTool({\n name: 'update_community_messaging',\n title: 'Update community messaging',\n description:\n 'Change the direct-message settings of a community: who may message whom from now on. Settings left out stay as they are.',\n scope: 'communities:write',\n kind: 'write',\n idempotent: true,\n openWorld: false,\n input: PatchDmSettings.extend(CommunityIdParam.shape),\n call: ({ communityId, ...input }, { container, scope }) =>\n container.content.patchDmSettings(scope.orgId, communityId, input, actor(scope)),\n }),\n defineTool({\n name: 'create_space',\n title: 'Create space',\n description:\n 'Add a space at the end of a community. Its access rule decides who can open it: all members, by request or by invite. A space open to all members cannot have a capacity.',\n scope: 'communities:write',\n kind: 'write',\n idempotent: false,\n openWorld: false,\n input: CreateSpace.extend(CommunityIdParam.shape),\n call: ({ communityId, ...input }, { container, scope }) =>\n container.content.createSpace(scope.orgId, communityId, input, actor(scope)),\n }),\n defineTool({\n name: 'update_space',\n title: 'Update space',\n description:\n 'Change a space name, description, access rule, capacity or settings. settings merges into the current settings. Fields left out stay as they are.',\n scope: 'communities:write',\n kind: 'write',\n idempotent: true,\n openWorld: false,\n input: UpdateSpace.extend(CommunitySpaceParam.shape),\n call: ({ communityId, spaceId, ...input }, { container, scope }) =>\n container.content.updateSpace(scope.orgId, communityId, spaceId, input, actor(scope)),\n }),\n defineTool({\n name: 'reorder_spaces',\n title: 'Reorder spaces',\n description:\n 'Set the space order of a community. Pass every space id of the community in the new order.',\n scope: 'communities:write',\n kind: 'write',\n idempotent: true,\n openWorld: false,\n input: OrderedIds.extend(CommunityIdParam.shape),\n call: ({ communityId, ids }, { container, scope }) =>\n container.content.reorderSpaces(scope.orgId, communityId, ids, actor(scope)),\n }),\n defineTool({\n name: 'set_space_topics',\n title: 'Set space topics',\n description:\n 'Replace the topics of a space. An entry with an id keeps and renames that topic, one without adds a topic, and a topic left out is deleted. A topic that posts use cannot be deleted.',\n scope: 'communities:write',\n kind: 'write',\n idempotent: true,\n openWorld: false,\n input: SetSpaceTopics.extend(CommunitySpaceParam.shape),\n call: ({ communityId, spaceId, ...input }, { container, scope }) =>\n container.content.setSpaceTopics(scope.orgId, communityId, spaceId, input, actor(scope)),\n }),\n defineTool({\n name: 'add_space_link',\n title: 'Add space link',\n description:\n 'Link a course, download or community into a space, after its other links. Returns the space.',\n scope: 'communities:write',\n kind: 'write',\n idempotent: false,\n openWorld: false,\n input: SpaceLinkBody.extend(CommunitySpaceParam.shape),\n call: ({ communityId, spaceId, contentId }, { container, scope }) =>\n container.content.addSpaceLink(scope.orgId, communityId, spaceId, contentId, actor(scope)),\n }),\n defineTool({\n name: 'reorder_space_links',\n title: 'Reorder space links',\n description: 'Set the link order of a space. Pass every link id of the space in the new order.',\n scope: 'communities:write',\n kind: 'write',\n idempotent: true,\n openWorld: false,\n input: OrderedIds.extend(CommunitySpaceParam.shape),\n call: ({ communityId, spaceId, ids }, { container, scope }) =>\n container.content.reorderSpaceLinks(scope.orgId, communityId, spaceId, ids, actor(scope)),\n }),\n defineTool({\n name: 'remove_space_link',\n title: 'Remove space link',\n description:\n 'Take a link out of a space. The linked content itself is kept. Returns the space.',\n scope: 'communities:write',\n kind: 'destructive',\n idempotent: true,\n openWorld: false,\n input: SpaceLinkParam,\n call: ({ communityId, spaceId, linkId }, { container, scope }) =>\n container.content.removeSpaceLink(scope.orgId, communityId, spaceId, linkId, actor(scope)),\n }),\n defineTool({\n name: 'delete_space',\n title: 'Delete space',\n description: 'Permanently delete a space with its posts, comments and members.',\n scope: 'communities:delete',\n kind: 'destructive',\n idempotent: true,\n openWorld: false,\n input: CommunitySpaceParam,\n call: ({ communityId, spaceId }, { container, scope }) =>\n container.content.deleteSpace(scope.orgId, communityId, spaceId, actor(scope)),\n }),\n defineTool({\n name: 'delete_community',\n title: 'Delete community',\n description:\n 'Permanently delete a community with its spaces. Links to it from other spaces are removed.',\n scope: 'communities:delete',\n kind: 'destructive',\n idempotent: true,\n openWorld: false,\n input: CommunityIdParam,\n call: ({ communityId }, { container, scope }) =>\n container.content.deleteCommunity(scope.orgId, communityId, actor(scope)),\n }),\n defineTool({\n name: 'get_community_analytics',\n title: 'Get community analytics',\n description: 'Membership, activity and moderation counts for one community.',\n scope: 'analytics:read',\n kind: 'read',\n idempotent: true,\n openWorld: false,\n input: CommunityIdParam,\n call: ({ communityId }, { container, scope }) =>\n container.reporting.admin.communityAnalytics(scope.orgId, communityId),\n }),\n];\n"],"mappings":"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"}
1
+ {"version":3,"file":"communities.js","names":[],"sources":["../../../../src/http/mcp/tools/communities.ts"],"sourcesContent":["import {\n CommunitiesQuery,\n CommunityIdParam,\n CommunitySpaceParam,\n CreateCommunity,\n CreateSpace,\n OrderedIds,\n PatchDmSettings,\n SetSpaceTopics,\n SpaceLinkBody,\n SpaceLinkParam,\n UpdateCommunity,\n UpdateSpace,\n} from '../../schemas/index.js';\nimport { actor, defineTool, type Tool } from '../tool.js';\n\nexport const communityTools: Tool[] = [\n defineTool({\n name: 'list_communities',\n title: 'List communities',\n description:\n 'Search and page through the communities, drafts included, each with its status and direct-message settings. Filter by status, search by title.',\n scope: 'communities:read',\n kind: 'read',\n idempotent: true,\n openWorld: false,\n input: CommunitiesQuery,\n call: (input, { container, scope }) => container.content.listCommunities(scope.orgId, input),\n }),\n defineTool({\n name: 'list_spaces',\n title: 'List spaces',\n description:\n 'The spaces of a community, in order, each with its access rule, topics and links.',\n scope: 'communities:read',\n kind: 'read',\n idempotent: true,\n openWorld: false,\n input: CommunityIdParam,\n call: ({ communityId }, { container, scope }) =>\n container.content.listSpaces(scope.orgId, communityId),\n }),\n defineTool({\n name: 'create_community',\n title: 'Create community',\n description:\n 'Create a draft community with the default direct-message settings and no spaces. Add spaces with create_space and publish it with update_community.',\n scope: 'communities:write',\n kind: 'write',\n idempotent: false,\n openWorld: false,\n input: CreateCommunity,\n call: (input, { container, scope }) =>\n container.content.createCommunity(scope.orgId, input, actor(scope)),\n }),\n defineTool({\n name: 'update_community',\n title: 'Update community',\n description:\n 'Change a community title, description or cover image, or publish and unpublish it with status. The cover must be a ready image from list_media; null clears it. Fields left out stay as they are.',\n scope: 'communities:write',\n kind: 'write',\n idempotent: true,\n openWorld: false,\n input: UpdateCommunity.extend(CommunityIdParam.shape),\n call: ({ communityId, ...input }, { container, scope }) =>\n container.content.updateCommunity(scope.orgId, communityId, input, actor(scope)),\n }),\n defineTool({\n name: 'update_community_messaging',\n title: 'Update community messaging',\n description:\n 'Change the direct-message settings of a community: who may message whom from now on. Settings left out stay as they are.',\n scope: 'communities:write',\n kind: 'write',\n idempotent: true,\n openWorld: false,\n input: PatchDmSettings.extend(CommunityIdParam.shape),\n call: ({ communityId, ...input }, { container, scope }) =>\n container.content.patchCommunitySettings(scope.orgId, communityId, input, actor(scope)),\n }),\n defineTool({\n name: 'create_space',\n title: 'Create space',\n description:\n 'Add a space at the end of a community. Its access rule decides who can open it: all members, by request or by invite. A space open to all members cannot have a capacity.',\n scope: 'communities:write',\n kind: 'write',\n idempotent: false,\n openWorld: false,\n input: CreateSpace.extend(CommunityIdParam.shape),\n call: ({ communityId, ...input }, { container, scope }) =>\n container.content.createSpace(scope.orgId, communityId, input, actor(scope)),\n }),\n defineTool({\n name: 'update_space',\n title: 'Update space',\n description:\n 'Change a space name, description, access rule, capacity or settings. settings merges into the current settings. Fields left out stay as they are.',\n scope: 'communities:write',\n kind: 'write',\n idempotent: true,\n openWorld: false,\n input: UpdateSpace.extend(CommunitySpaceParam.shape),\n call: ({ communityId, spaceId, ...input }, { container, scope }) =>\n container.content.updateSpace(scope.orgId, communityId, spaceId, input, actor(scope)),\n }),\n defineTool({\n name: 'reorder_spaces',\n title: 'Reorder spaces',\n description:\n 'Set the space order of a community. Pass every space id of the community in the new order.',\n scope: 'communities:write',\n kind: 'write',\n idempotent: true,\n openWorld: false,\n input: OrderedIds.extend(CommunityIdParam.shape),\n call: ({ communityId, ids }, { container, scope }) =>\n container.content.reorderSpaces(scope.orgId, communityId, ids, actor(scope)),\n }),\n defineTool({\n name: 'set_space_topics',\n title: 'Set space topics',\n description:\n 'Replace the topics of a space. An entry with an id keeps and renames that topic, one without adds a topic, and a topic left out is deleted. A topic that posts use cannot be deleted.',\n scope: 'communities:write',\n kind: 'write',\n idempotent: true,\n openWorld: false,\n input: SetSpaceTopics.extend(CommunitySpaceParam.shape),\n call: ({ communityId, spaceId, ...input }, { container, scope }) =>\n container.content.setSpaceTopics(scope.orgId, communityId, spaceId, input, actor(scope)),\n }),\n defineTool({\n name: 'add_space_link',\n title: 'Add space link',\n description:\n 'Link a course, download or community into a space, after its other links. Returns the space.',\n scope: 'communities:write',\n kind: 'write',\n idempotent: false,\n openWorld: false,\n input: SpaceLinkBody.extend(CommunitySpaceParam.shape),\n call: ({ communityId, spaceId, contentId }, { container, scope }) =>\n container.content.addSpaceLink(scope.orgId, communityId, spaceId, contentId, actor(scope)),\n }),\n defineTool({\n name: 'reorder_space_links',\n title: 'Reorder space links',\n description: 'Set the link order of a space. Pass every link id of the space in the new order.',\n scope: 'communities:write',\n kind: 'write',\n idempotent: true,\n openWorld: false,\n input: OrderedIds.extend(CommunitySpaceParam.shape),\n call: ({ communityId, spaceId, ids }, { container, scope }) =>\n container.content.reorderSpaceLinks(scope.orgId, communityId, spaceId, ids, actor(scope)),\n }),\n defineTool({\n name: 'remove_space_link',\n title: 'Remove space link',\n description:\n 'Take a link out of a space. The linked content itself is kept. Returns the space.',\n scope: 'communities:write',\n kind: 'destructive',\n idempotent: true,\n openWorld: false,\n input: SpaceLinkParam,\n call: ({ communityId, spaceId, linkId }, { container, scope }) =>\n container.content.removeSpaceLink(scope.orgId, communityId, spaceId, linkId, actor(scope)),\n }),\n defineTool({\n name: 'delete_space',\n title: 'Delete space',\n description: 'Permanently delete a space with its posts, comments and members.',\n scope: 'communities:delete',\n kind: 'destructive',\n idempotent: true,\n openWorld: false,\n input: CommunitySpaceParam,\n call: ({ communityId, spaceId }, { container, scope }) =>\n container.content.deleteSpace(scope.orgId, communityId, spaceId, actor(scope)),\n }),\n defineTool({\n name: 'delete_community',\n title: 'Delete community',\n description:\n 'Permanently delete a community with its spaces. Links to it from other spaces are removed.',\n scope: 'communities:delete',\n kind: 'destructive',\n idempotent: true,\n openWorld: false,\n input: CommunityIdParam,\n call: ({ communityId }, { container, scope }) =>\n container.content.deleteCommunity(scope.orgId, communityId, actor(scope)),\n }),\n defineTool({\n name: 'get_community_analytics',\n title: 'Get community analytics',\n description: 'Membership, activity and moderation counts for one community.',\n scope: 'analytics:read',\n kind: 'read',\n idempotent: true,\n openWorld: false,\n input: CommunityIdParam,\n call: ({ communityId }, { container, scope }) =>\n container.reporting.admin.communityAnalytics(scope.orgId, communityId),\n }),\n];\n"],"mappings":"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"}
@@ -24,12 +24,14 @@ function errorHandler(error, request, reply) {
24
24
  error: refusal.error,
25
25
  message: refusal.message,
26
26
  ...refusal.issues && { issues: refusal.issues },
27
+ ...refusal.details && { details: refusal.details },
27
28
  ...refusal.error === "bad_request" && { cause: error.message }
28
29
  }, `${routeOf(request)} refused`);
29
30
  return reply.status(refusal.status).send({
30
31
  error: refusal.error,
31
32
  message: refusal.message,
32
- ...refusal.issues && { issues: refusal.issues }
33
+ ...refusal.issues && { issues: refusal.issues },
34
+ ...refusal.details && { details: refusal.details }
33
35
  });
34
36
  }
35
37
  if (isResponseSerializationError(error)) request.log.error({
@@ -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;EAC3E,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;EACjD,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"}
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.details && { details: refusal.details }),\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 ...(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,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;EAC3E,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"}
@@ -75,7 +75,7 @@ async function automationsRoutes(app, container) {
75
75
  operationId: "createAutomation",
76
76
  tags,
77
77
  summary: "Create a workflow automation (staff)",
78
- description: "Staff only (owner, admin, instructor). 400 when a trigger is reserved, not an event the system emits or an unsupported schedule, or an action is reserved for webhooks.",
78
+ description: "Staff only (owner, admin, instructor). 400 when a trigger is reserved, not an event the system emits or an unsupported schedule, or an action is reserved for webhooks or no longer available.",
79
79
  body: CreateAutomationBody,
80
80
  response: {
81
81
  201: Automation,
@@ -118,7 +118,7 @@ async function automationsRoutes(app, container) {
118
118
  operationId: "updateAutomation",
119
119
  tags,
120
120
  summary: "Update a workflow automation (staff)",
121
- description: "Staff only (owner, admin, instructor). Webhooks change through the webhook endpoints (409). 400 when a trigger is reserved, not an event the system emits or an unsupported schedule, or an action is reserved for webhooks.",
121
+ description: "Staff only (owner, admin, instructor). Webhooks change through the webhook endpoints (409). 400 when a trigger is reserved, not an event the system emits or an unsupported schedule, or an action is reserved for webhooks or no longer available.",
122
122
  params: AutomationIdParam,
123
123
  body: UpdateAutomationBody,
124
124
  response: {
@@ -141,7 +141,7 @@ async function automationsRoutes(app, container) {
141
141
  operationId: "deleteAutomation",
142
142
  tags,
143
143
  summary: "Delete a workflow automation (staff)",
144
- description: "Staff only (owner, admin, instructor). Webhooks are deleted through the webhook endpoints, and system automations cannot be deleted (409). Its runs stay readable.",
144
+ description: "Staff only (owner, admin, instructor). Webhooks are deleted through the webhook endpoints (409). Its runs stay readable.",
145
145
  params: AutomationIdParam,
146
146
  response: {
147
147
  204: z.void(),
@@ -1 +1 @@
1
- {"version":3,"file":"automations.js","names":[],"sources":["../../../src/http/routes/automations.ts"],"sourcesContent":["// HTTP routes for the automations context: rules that match a trigger (a\n// domain event type) against enabled automations and run an ordered list of\n// actions, plus the run history each automation accumulates.\nimport type { FastifyInstance } from 'fastify';\nimport { z } from 'zod';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport {\n Automation,\n AutomationIdParam,\n AutomationRun,\n AutomationRunParams,\n AutomationRunsPage,\n AutomationRunsQuery,\n AutomationsQuery,\n AvailableAction,\n AvailableTriggers,\n CreateAutomationBody,\n UpdateAutomationBody,\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 automationsRoutes(app: FastifyInstance, container: Container): Promise<void> {\n const r = app.withTypeProvider<ZodTypeProvider>();\n const automations = container.automations;\n const tags = ['Automations'];\n\n r.route({\n method: 'GET',\n url: '/api/automations',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listAutomations',\n tags,\n summary: 'List automations (staff)',\n description:\n 'Staff only (owner, admin, instructor). Optionally only workflows or only webhooks.',\n querystring: AutomationsQuery,\n response: { 200: z.array(Automation), ...refusals(403) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return automations.list(scope.orgId, req.query);\n },\n });\n\n // Before `/api/automations/:id` — otherwise `:id` would swallow these path segments.\n r.route({\n method: 'GET',\n url: '/api/automations/actions',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listAutomationActions',\n tags,\n summary: 'List the actions automations can use (staff)',\n description: 'Staff only (owner, admin, instructor).',\n response: { 200: z.array(AvailableAction), ...refusals(403) },\n },\n handler: async (req) => {\n await resolveScope(container, req);\n return automations.availableActions();\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/automations/triggers',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listAutomationTriggers',\n tags,\n summary: 'List the domain events automations can react to (staff)',\n description: 'Staff only (owner, admin, instructor).',\n response: { 200: AvailableTriggers, ...refusals(403) },\n },\n handler: async (req) => {\n await resolveScope(container, req);\n return automations.availableTriggers();\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/automations',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'createAutomation',\n tags,\n summary: 'Create a workflow automation (staff)',\n description:\n 'Staff only (owner, admin, instructor). 400 when a trigger is reserved, not an event the system emits or an unsupported schedule, or an action is reserved for webhooks.',\n body: CreateAutomationBody,\n response: { 201: Automation, ...refusals(400, 403) },\n },\n handler: async (req, reply) => {\n const scope = await resolveScope(container, req);\n const automation = await automations.create(scope.orgId, req.body);\n return reply.code(201).send(automation);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/automations/:id',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getAutomation',\n tags,\n summary: 'Get an automation (staff)',\n description: 'Staff only (owner, admin, instructor). Workflows and webhooks alike.',\n params: AutomationIdParam,\n response: { 200: Automation, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n const automation = await automations.get(scope.orgId, req.params.id);\n if (!automation) {\n throw new NotFoundError('Automation', req.params.id);\n }\n return automation;\n },\n });\n\n r.route({\n method: 'PATCH',\n url: '/api/automations/:id',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'updateAutomation',\n tags,\n summary: 'Update a workflow automation (staff)',\n description:\n 'Staff only (owner, admin, instructor). Webhooks change through the webhook endpoints (409). 400 when a trigger is reserved, not an event the system emits or an unsupported schedule, or an action is reserved for webhooks.',\n params: AutomationIdParam,\n body: UpdateAutomationBody,\n response: { 200: Automation, ...refusals(400, 403, 404, 409) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n const automation = await automations.update(scope.orgId, req.params.id, req.body);\n if (!automation) {\n throw new NotFoundError('Automation', req.params.id);\n }\n return automation;\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/automations/:id',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'deleteAutomation',\n tags,\n summary: 'Delete a workflow automation (staff)',\n description:\n 'Staff only (owner, admin, instructor). Webhooks are deleted through the webhook endpoints, and system automations cannot be deleted (409). Its runs stay readable.',\n params: AutomationIdParam,\n response: { 204: z.void(), ...refusals(403, 404, 409) },\n },\n handler: async (req, reply) => {\n const scope = await resolveScope(container, req);\n const deleted = await automations.delete(scope.orgId, req.params.id);\n if (!deleted) {\n throw new NotFoundError('Automation', req.params.id);\n }\n return reply.code(204).send();\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/automations/:id/runs',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listAutomationRuns',\n tags,\n summary: \"List an automation's runs (staff)\",\n description:\n \"Staff only (owner, admin, instructor). A deleted automation's runs remain reachable as an audit trail, so an unknown id answers an empty page rather than 404. A run's event carries the fields a webhook delivery of that event would.\",\n params: AutomationIdParam,\n querystring: AutomationRunsQuery,\n response: { 200: AutomationRunsPage, ...refusals(403) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n // No existence pre-check: runs deliberately survive automation deletion (audit trail).\n return automations.listRuns(scope.orgId, req.params.id, req.query);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/automations/:id/runs/:runId/rerun',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'rerunAutomationRun',\n tags,\n summary: 'Run a finished run again (staff)',\n description:\n \"Staff only (owner, admin, instructor). Dispatches the same run again with the automation's current steps; the new outcome replaces the run's previous one. For a webhook this resends the delivery to its current URL. A run that is still running can't be rerun (409).\",\n params: AutomationRunParams,\n response: { 200: AutomationRun, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return automations.rerun(scope.orgId, req.params.id, req.params.runId);\n },\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"automations.js","names":[],"sources":["../../../src/http/routes/automations.ts"],"sourcesContent":["// HTTP routes for the automations context: rules that match a trigger (a\n// domain event type) against enabled automations and run an ordered list of\n// actions, plus the run history each automation accumulates.\nimport type { FastifyInstance } from 'fastify';\nimport { z } from 'zod';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport {\n Automation,\n AutomationIdParam,\n AutomationRun,\n AutomationRunParams,\n AutomationRunsPage,\n AutomationRunsQuery,\n AutomationsQuery,\n AvailableAction,\n AvailableTriggers,\n CreateAutomationBody,\n UpdateAutomationBody,\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 automationsRoutes(app: FastifyInstance, container: Container): Promise<void> {\n const r = app.withTypeProvider<ZodTypeProvider>();\n const automations = container.automations;\n const tags = ['Automations'];\n\n r.route({\n method: 'GET',\n url: '/api/automations',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listAutomations',\n tags,\n summary: 'List automations (staff)',\n description:\n 'Staff only (owner, admin, instructor). Optionally only workflows or only webhooks.',\n querystring: AutomationsQuery,\n response: { 200: z.array(Automation), ...refusals(403) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return automations.list(scope.orgId, req.query);\n },\n });\n\n // Before `/api/automations/:id` — otherwise `:id` would swallow these path segments.\n r.route({\n method: 'GET',\n url: '/api/automations/actions',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listAutomationActions',\n tags,\n summary: 'List the actions automations can use (staff)',\n description: 'Staff only (owner, admin, instructor).',\n response: { 200: z.array(AvailableAction), ...refusals(403) },\n },\n handler: async (req) => {\n await resolveScope(container, req);\n return automations.availableActions();\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/automations/triggers',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listAutomationTriggers',\n tags,\n summary: 'List the domain events automations can react to (staff)',\n description: 'Staff only (owner, admin, instructor).',\n response: { 200: AvailableTriggers, ...refusals(403) },\n },\n handler: async (req) => {\n await resolveScope(container, req);\n return automations.availableTriggers();\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/automations',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'createAutomation',\n tags,\n summary: 'Create a workflow automation (staff)',\n description:\n 'Staff only (owner, admin, instructor). 400 when a trigger is reserved, not an event the system emits or an unsupported schedule, or an action is reserved for webhooks or no longer available.',\n body: CreateAutomationBody,\n response: { 201: Automation, ...refusals(400, 403) },\n },\n handler: async (req, reply) => {\n const scope = await resolveScope(container, req);\n const automation = await automations.create(scope.orgId, req.body);\n return reply.code(201).send(automation);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/automations/:id',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getAutomation',\n tags,\n summary: 'Get an automation (staff)',\n description: 'Staff only (owner, admin, instructor). Workflows and webhooks alike.',\n params: AutomationIdParam,\n response: { 200: Automation, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n const automation = await automations.get(scope.orgId, req.params.id);\n if (!automation) {\n throw new NotFoundError('Automation', req.params.id);\n }\n return automation;\n },\n });\n\n r.route({\n method: 'PATCH',\n url: '/api/automations/:id',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'updateAutomation',\n tags,\n summary: 'Update a workflow automation (staff)',\n description:\n 'Staff only (owner, admin, instructor). Webhooks change through the webhook endpoints (409). 400 when a trigger is reserved, not an event the system emits or an unsupported schedule, or an action is reserved for webhooks or no longer available.',\n params: AutomationIdParam,\n body: UpdateAutomationBody,\n response: { 200: Automation, ...refusals(400, 403, 404, 409) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n const automation = await automations.update(scope.orgId, req.params.id, req.body);\n if (!automation) {\n throw new NotFoundError('Automation', req.params.id);\n }\n return automation;\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/automations/:id',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'deleteAutomation',\n tags,\n summary: 'Delete a workflow automation (staff)',\n description:\n 'Staff only (owner, admin, instructor). Webhooks are deleted through the webhook endpoints (409). Its runs stay readable.',\n params: AutomationIdParam,\n response: { 204: z.void(), ...refusals(403, 404, 409) },\n },\n handler: async (req, reply) => {\n const scope = await resolveScope(container, req);\n const deleted = await automations.delete(scope.orgId, req.params.id);\n if (!deleted) {\n throw new NotFoundError('Automation', req.params.id);\n }\n return reply.code(204).send();\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/automations/:id/runs',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listAutomationRuns',\n tags,\n summary: \"List an automation's runs (staff)\",\n description:\n \"Staff only (owner, admin, instructor). A deleted automation's runs remain reachable as an audit trail, so an unknown id answers an empty page rather than 404. A run's event carries the fields a webhook delivery of that event would.\",\n params: AutomationIdParam,\n querystring: AutomationRunsQuery,\n response: { 200: AutomationRunsPage, ...refusals(403) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n // No existence pre-check: runs deliberately survive automation deletion (audit trail).\n return automations.listRuns(scope.orgId, req.params.id, req.query);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/automations/:id/runs/:runId/rerun',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'rerunAutomationRun',\n tags,\n summary: 'Run a finished run again (staff)',\n description:\n \"Staff only (owner, admin, instructor). Dispatches the same run again with the automation's current steps; the new outcome replaces the run's previous one. For a webhook this resends the delivery to its current URL. A run that is still running can't be rerun (409).\",\n params: AutomationRunParams,\n response: { 200: AutomationRun, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return automations.rerun(scope.orgId, req.params.id, req.params.runId);\n },\n });\n}\n"],"mappings":";;;;;;;AAwBA,eAAsB,kBAAkB,KAAsB,WAAqC;CACjG,MAAM,IAAI,IAAI,iBAAkC;CAChD,MAAM,cAAc,UAAU;CAC9B,MAAM,OAAO,CAAC,aAAa;CAE3B,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb;GACA,SAAS;GACT,aACE;GACF,aAAa;GACb,UAAU;IAAE,KAAK,EAAE,MAAM,UAAU;IAAG,GAAG,SAAS,GAAG;GAAE;EACzD;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,OAAO,YAAY,KAAK,MAAM,OAAO,IAAI,KAAK;EAChD;CACF,CAAC;CAGD,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb;GACA,SAAS;GACT,aAAa;GACb,UAAU;IAAE,KAAK,EAAE,MAAM,eAAe;IAAG,GAAG,SAAS,GAAG;GAAE;EAC9D;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,aAAa,WAAW,GAAG;GACjC,OAAO,YAAY,iBAAiB;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;GACtB,MAAM,aAAa,WAAW,GAAG;GACjC,OAAO,YAAY,kBAAkB;EACvC;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb;GACA,SAAS;GACT,aACE;GACF,MAAM;GACN,UAAU;IAAE,KAAK;IAAY,GAAG,SAAS,KAAK,GAAG;GAAE;EACrD;EACA,SAAS,OAAO,KAAK,UAAU;GAC7B,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,MAAM,aAAa,MAAM,YAAY,OAAO,MAAM,OAAO,IAAI,IAAI;GACjE,OAAO,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,UAAU;EACxC;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;IAAY,GAAG,SAAS,KAAK,GAAG;GAAE;EACrD;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,MAAM,aAAa,MAAM,YAAY,IAAI,MAAM,OAAO,IAAI,OAAO,EAAE;GACnE,IAAI,CAAC,YACH,MAAM,IAAI,cAAc,cAAc,IAAI,OAAO,EAAE;GAErD,OAAO;EACT;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb;GACA,SAAS;GACT,aACE;GACF,QAAQ;GACR,MAAM;GACN,UAAU;IAAE,KAAK;IAAY,GAAG,SAAS,KAAK,KAAK,KAAK,GAAG;GAAE;EAC/D;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,MAAM,aAAa,MAAM,YAAY,OAAO,MAAM,OAAO,IAAI,OAAO,IAAI,IAAI,IAAI;GAChF,IAAI,CAAC,YACH,MAAM,IAAI,cAAc,cAAc,IAAI,OAAO,EAAE;GAErD,OAAO;EACT;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb;GACA,SAAS;GACT,aACE;GACF,QAAQ;GACR,UAAU;IAAE,KAAK,EAAE,KAAK;IAAG,GAAG,SAAS,KAAK,KAAK,GAAG;GAAE;EACxD;EACA,SAAS,OAAO,KAAK,UAAU;GAC7B,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAE/C,IAAI,CAAC,MADiB,YAAY,OAAO,MAAM,OAAO,IAAI,OAAO,EAAE,GAEjE,MAAM,IAAI,cAAc,cAAc,IAAI,OAAO,EAAE;GAErD,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,aACE;GACF,QAAQ;GACR,aAAa;GACb,UAAU;IAAE,KAAK;IAAoB,GAAG,SAAS,GAAG;GAAE;EACxD;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAE/C,OAAO,YAAY,SAAS,MAAM,OAAO,IAAI,OAAO,IAAI,IAAI,KAAK;EACnE;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb;GACA,SAAS;GACT,aACE;GACF,QAAQ;GACR,UAAU;IAAE,KAAK;IAAe,GAAG,SAAS,KAAK,KAAK,GAAG;GAAE;EAC7D;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,aAAa,WAAW,GAAG;GAC/C,OAAO,YAAY,MAAM,MAAM,OAAO,IAAI,OAAO,IAAI,IAAI,OAAO,KAAK;EACvE;CACF,CAAC;AACH"}
@@ -1,7 +1,8 @@
1
1
  import { refusals } from "../schemas/shared.js";
2
- import { resolveScope } from "../scope.js";
3
- import { CommunitiesPage, CommunitiesQuery, Community, CommunityAnalytics, CommunityIdParam, CommunitySpaceParam, CreateCommunity, CreateSpace, OrderedIds, PatchDmSettings, SetSpaceTopics, SpaceDetail, SpaceLinkBody, SpaceLinkParam, UpdateCommunity, UpdateSpace } from "../schemas/community.js";
2
+ import { resolveConfiguratorScope, resolveScope } from "../scope.js";
3
+ import { CommunitiesPage, CommunitiesQuery, Community, CommunityAnalytics, CommunityIdParam, CommunitySpaceParam, CreateCommunity, CreateSpace, OrderedIds, PatchCommunitySettings, SetSpaceTopics, SpaceDetail, SpaceLinkBody, SpaceLinkParam, UpdateCommunity, UpdateSpace } from "../schemas/community.js";
4
4
  import { MessageQueue } from "../schemas/chats.js";
5
+ import { NotificationCategoryList, ReplaceNotificationCategories } from "../schemas/notifications.js";
5
6
  import "../schemas/index.js";
6
7
  import { z } from "zod";
7
8
  import { NotFoundError } from "@headless-lms/core/shared/errors";
@@ -124,12 +125,12 @@ async function communitiesRoutes(app, container) {
124
125
  method: "PATCH",
125
126
  url: "/api/communities/:communityId/settings",
126
127
  schema: {
127
- operationId: "patchCommunityDmSettings",
128
+ operationId: "patchCommunitySettings",
128
129
  tags,
129
- summary: "Change direct-message settings for a community (staff)",
130
- description: "Owner or admin only. Changes only the settings sent; the rest keep their values. They decide who may message whom from then on.",
130
+ summary: "Change the direct-message and digest settings of a community (staff)",
131
+ description: "Owner or admin only. Changes only the settings sent; the rest keep their values. The direct-message settings decide who may message whom from then on. `digest` sets when the community digest goes out, or `null` stops it; the first digest after a change goes out at the next slot.",
131
132
  params: CommunityIdParam,
132
- body: PatchDmSettings,
133
+ body: PatchCommunitySettings,
133
134
  response: {
134
135
  200: Community,
135
136
  ...refusals(403, 404)
@@ -137,12 +138,71 @@ async function communitiesRoutes(app, container) {
137
138
  },
138
139
  handler: async (req) => {
139
140
  const scope = await resolveScope(container, req);
140
- return content.patchDmSettings(scope.orgId, req.params.communityId, req.body, {
141
+ return content.patchCommunitySettings(scope.orgId, req.params.communityId, req.body, {
141
142
  id: scope.orgUserId,
142
143
  role: scope.role
143
144
  });
144
145
  }
145
146
  });
147
+ r.route({
148
+ method: "GET",
149
+ url: "/api/communities/:communityId/notification-categories",
150
+ schema: {
151
+ operationId: "getCommunityNotificationCategories",
152
+ tags: ["Notifications"],
153
+ summary: "Get a community's notification categories (staff)",
154
+ description: "Owner or admin only. The list in use for the community, with `source` saying whether it is the default or a stored list.",
155
+ params: CommunityIdParam,
156
+ response: {
157
+ 200: NotificationCategoryList,
158
+ ...refusals(403, 404)
159
+ }
160
+ },
161
+ handler: async (req) => {
162
+ const scope = await resolveConfiguratorScope(container, req);
163
+ return container.notificationCategories.community.get(scope.orgId, req.params.communityId);
164
+ }
165
+ });
166
+ r.route({
167
+ method: "PUT",
168
+ url: "/api/communities/:communityId/notification-categories",
169
+ schema: {
170
+ operationId: "replaceCommunityNotificationCategories",
171
+ tags: ["Notifications"],
172
+ summary: "Replace a community's notification categories (staff)",
173
+ description: "Owner or admin only. Stores the whole list for this community; other communities keep theirs. 422 when a key repeats, a type cannot be in a community list, a reason is not allowed for its type, a channel is not configured, `perSpace` is set on a type that cannot occur in a space, `spaceDefaults` appears without `perSpace` or names a channel the category does not offer, or earlier categories already cover everything a category covers.",
174
+ params: CommunityIdParam,
175
+ body: ReplaceNotificationCategories,
176
+ response: {
177
+ 200: NotificationCategoryList,
178
+ ...refusals(400, 403, 404, 422)
179
+ }
180
+ },
181
+ handler: async (req) => {
182
+ const scope = await resolveConfiguratorScope(container, req);
183
+ return container.notificationCategories.community.replace(scope.orgId, req.params.communityId, req.body.categories);
184
+ }
185
+ });
186
+ r.route({
187
+ method: "DELETE",
188
+ url: "/api/communities/:communityId/notification-categories",
189
+ schema: {
190
+ operationId: "resetCommunityNotificationCategories",
191
+ tags: ["Notifications"],
192
+ summary: "Go back to the default notification categories for a community (staff)",
193
+ description: "Owner or admin only. Removes the stored list; the default applies again.",
194
+ params: CommunityIdParam,
195
+ response: {
196
+ 204: z.void(),
197
+ ...refusals(403, 404)
198
+ }
199
+ },
200
+ handler: async (req, reply) => {
201
+ const scope = await resolveConfiguratorScope(container, req);
202
+ await container.notificationCategories.community.reset(scope.orgId, req.params.communityId);
203
+ return reply.code(204).send();
204
+ }
205
+ });
146
206
  r.route({
147
207
  method: "POST",
148
208
  url: "/api/communities/:communityId/publish",
@@ -1 +1 @@
1
- {"version":3,"file":"communities.js","names":[],"sources":["../../../src/http/routes/communities.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 { NotFoundError } from '@headless-lms/core/shared/errors';\nimport { resolveScope } from '../scope.js';\nimport {\n Community,\n CommunityAnalytics,\n CommunityIdParam,\n CommunitySpaceParam,\n CommunitiesPage,\n CommunitiesQuery,\n CreateCommunity,\n CreateSpace,\n MessageQueue,\n OrderedIds,\n PatchDmSettings,\n SetSpaceTopics,\n SpaceDetail,\n SpaceLinkBody,\n SpaceLinkParam,\n UpdateCommunity,\n UpdateSpace,\n refusals,\n} from '../schemas/index.js';\n\nexport async function communitiesRoutes(app: FastifyInstance, container: Container): Promise<void> {\n const r = app.withTypeProvider<ZodTypeProvider>();\n const content = container.content;\n const tags = ['Communities'];\n\n r.route({\n method: 'GET',\n url: '/api/communities',\n schema: {\n operationId: 'listCommunities',\n tags,\n summary: 'List communities (staff)',\n description:\n 'Staff only (owner, admin, instructor). Pages every community in the organization, drafts included, filtered by `status` and searched by title.',\n querystring: CommunitiesQuery,\n response: { 200: CommunitiesPage, ...refusals(403) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return content.listCommunities(scope.orgId, req.query);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/communities',\n schema: {\n operationId: 'createCommunity',\n tags,\n summary: 'Create a community (staff)',\n description:\n 'Owner or admin only. The community starts as a draft with the default direct-message settings and no spaces. Members see it once it is published and they hold it.',\n body: CreateCommunity,\n response: { 201: Community, ...refusals(403, 409) },\n },\n handler: async (req, reply) => {\n const scope = await resolveScope(container, req);\n const community = await content.createCommunity(scope.orgId, req.body, {\n id: scope.orgUserId,\n role: scope.role,\n });\n req.log.info({ communityId: community.id }, 'community created');\n return reply.code(201).send(community);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/communities/:communityId',\n schema: {\n operationId: 'getCommunity',\n tags,\n summary: 'Get a community (staff)',\n description:\n 'Staff only (owner, admin, instructor). Any community of the organization, drafts included.',\n params: CommunityIdParam,\n response: { 200: Community, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n const community = await content.getCommunity(scope.orgId, req.params.communityId);\n if (!community) {\n throw new NotFoundError('Community', req.params.communityId);\n }\n return community;\n },\n });\n\n r.route({\n method: 'PATCH',\n url: '/api/communities/:communityId',\n schema: {\n operationId: 'updateCommunity',\n tags,\n summary: 'Update a community (staff)',\n description:\n 'Owner or admin only. Changes only the fields sent. `thumbnailAssetId` must be a ready library image (409 otherwise); null clears it.',\n params: CommunityIdParam,\n body: UpdateCommunity,\n response: { 200: Community, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return content.updateCommunity(scope.orgId, req.params.communityId, req.body, {\n id: scope.orgUserId,\n role: scope.role,\n });\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/communities/:communityId',\n schema: {\n operationId: 'deleteCommunity',\n tags,\n summary: 'Delete a community (staff)',\n description:\n 'Owner or admin only. Links to the community from other spaces are removed with it.',\n params: CommunityIdParam,\n response: { 204: z.void(), ...refusals(403, 404) },\n },\n handler: async (req, reply) => {\n const scope = await resolveScope(container, req);\n await content.deleteCommunity(scope.orgId, req.params.communityId, {\n id: scope.orgUserId,\n role: scope.role,\n });\n return reply.code(204).send();\n },\n });\n\n r.route({\n method: 'PATCH',\n url: '/api/communities/:communityId/settings',\n schema: {\n operationId: 'patchCommunityDmSettings',\n tags,\n summary: 'Change direct-message settings for a community (staff)',\n description:\n 'Owner or admin only. Changes only the settings sent; the rest keep their values. They decide who may message whom from then on.',\n params: CommunityIdParam,\n body: PatchDmSettings,\n response: { 200: Community, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return content.patchDmSettings(scope.orgId, req.params.communityId, req.body, {\n id: scope.orgUserId,\n role: scope.role,\n });\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/communities/:communityId/publish',\n schema: {\n operationId: 'publishCommunity',\n tags,\n summary: 'Publish a community (staff)',\n description: 'Owner or admin only. Makes the community visible to the people who hold it.',\n params: CommunityIdParam,\n response: { 200: Community, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return content.publishCommunity(scope.orgId, req.params.communityId, {\n id: scope.orgUserId,\n role: scope.role,\n });\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/communities/:communityId/unpublish',\n schema: {\n operationId: 'unpublishCommunity',\n tags,\n summary: 'Unpublish a community (staff)',\n description:\n 'Owner or admin only. Hides the community from members again; nothing in it is deleted.',\n params: CommunityIdParam,\n response: { 200: Community, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return content.unpublishCommunity(scope.orgId, req.params.communityId, {\n id: scope.orgUserId,\n role: scope.role,\n });\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/communities/:communityId/analytics',\n schema: {\n operationId: 'getCommunityAnalytics',\n tags,\n summary: 'Staff analytics for a community',\n description:\n 'Staff only (owner, admin, instructor). Membership, activity and moderation counts for one community.',\n params: CommunityIdParam,\n response: { 200: CommunityAnalytics, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n const stats = await container.reporting.admin.communityAnalytics(\n scope.orgId,\n req.params.communityId,\n );\n if (!stats) {\n throw new NotFoundError('Community', req.params.communityId);\n }\n return stats;\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/communities/:communityId/message-queue',\n schema: {\n operationId: 'getCommunityMessageQueue',\n tags,\n summary: 'Open message flags in a community (staff)',\n description:\n \"Staff only (owner, admin, instructor). The messages in the community's chats with open flags, with the flags and the chat each is in. Holds at most the 100 oldest open flags; working the queue brings in the next ones.\",\n params: CommunityIdParam,\n response: { 200: MessageQueue, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return container.chats.messageQueue(\n scope.orgId,\n { id: scope.orgUserId, role: scope.role },\n req.params.communityId,\n );\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/communities/:communityId/spaces',\n schema: {\n operationId: 'listCommunitySpaces',\n tags,\n summary: 'List the spaces in a community (staff)',\n description:\n 'Staff only (owner, admin, instructor). Every space in order, with its topics and links.',\n params: CommunityIdParam,\n response: { 200: z.array(SpaceDetail), ...refusals(403, 404) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return content.listSpaces(scope.orgId, req.params.communityId);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/communities/:communityId/spaces',\n schema: {\n operationId: 'createSpace',\n tags,\n summary: 'Create a space (staff)',\n description:\n 'Owner or admin only. The space is added last. An `all_members` space cannot have a capacity (409).',\n params: CommunityIdParam,\n body: CreateSpace,\n response: { 201: SpaceDetail, ...refusals(403, 404, 409) },\n },\n handler: async (req, reply) => {\n const scope = await resolveScope(container, req);\n const space = await content.createSpace(scope.orgId, req.params.communityId, req.body, {\n id: scope.orgUserId,\n role: scope.role,\n });\n return reply.code(201).send(space);\n },\n });\n\n r.route({\n method: 'PUT',\n url: '/api/communities/:communityId/spaces/order',\n schema: {\n operationId: 'reorderSpaces',\n tags,\n summary: 'Reorder the spaces in a community (staff)',\n description:\n 'Owner or admin only. Send every space id of the community exactly once (409 otherwise).',\n params: CommunityIdParam,\n body: OrderedIds,\n response: { 200: z.array(SpaceDetail), ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return content.reorderSpaces(scope.orgId, req.params.communityId, req.body.ids, {\n id: scope.orgUserId,\n role: scope.role,\n });\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/communities/:communityId/spaces/:spaceId',\n schema: {\n operationId: 'getSpace',\n tags,\n summary: 'Get a space (staff)',\n description: 'Staff only (owner, admin, instructor). The space with its topics and links.',\n params: CommunitySpaceParam,\n response: { 200: SpaceDetail, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n const space = await content.getCommunitySpace(\n scope.orgId,\n req.params.communityId,\n req.params.spaceId,\n );\n if (!space) {\n throw new NotFoundError('Space', req.params.spaceId);\n }\n return space;\n },\n });\n\n r.route({\n method: 'PATCH',\n url: '/api/communities/:communityId/spaces/:spaceId',\n schema: {\n operationId: 'updateSpace',\n tags,\n summary: 'Update a space (staff)',\n description:\n 'Owner or admin only. Changes only the fields sent; `settings` merges into the current settings. An `all_members` space cannot have a capacity (409).',\n params: CommunitySpaceParam,\n body: UpdateSpace,\n response: { 200: SpaceDetail, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return content.updateSpace(\n scope.orgId,\n req.params.communityId,\n req.params.spaceId,\n req.body,\n { id: scope.orgUserId, role: scope.role },\n );\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/communities/:communityId/spaces/:spaceId',\n schema: {\n operationId: 'deleteSpace',\n tags,\n summary: 'Delete a space (staff)',\n description: 'Owner or admin only. The space and everything in it go.',\n params: CommunitySpaceParam,\n response: { 204: z.void(), ...refusals(403, 404) },\n },\n handler: async (req, reply) => {\n const scope = await resolveScope(container, req);\n await content.deleteSpace(scope.orgId, req.params.communityId, req.params.spaceId, {\n id: scope.orgUserId,\n role: scope.role,\n });\n return reply.code(204).send();\n },\n });\n\n r.route({\n method: 'PUT',\n url: '/api/communities/:communityId/spaces/:spaceId/topics',\n schema: {\n operationId: 'setSpaceTopics',\n tags,\n summary: 'Replace the topics on a space (staff)',\n description:\n 'Owner or admin only. The list replaces the current topics: an entry with an `id` keeps and renames that topic, one without adds a topic, and a topic left out is deleted. Names must be unique (409), and a topic that posts use cannot be deleted (409).',\n params: CommunitySpaceParam,\n body: SetSpaceTopics,\n response: { 200: SpaceDetail, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return content.setSpaceTopics(\n scope.orgId,\n req.params.communityId,\n req.params.spaceId,\n req.body,\n { id: scope.orgUserId, role: scope.role },\n );\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/communities/:communityId/spaces/:spaceId/links',\n schema: {\n operationId: 'addSpaceLink',\n tags,\n summary: 'Link a course, download, or community into a space (staff)',\n description:\n 'Owner or admin only. The link is added last. Linking the same content twice is a 409.',\n params: CommunitySpaceParam,\n body: SpaceLinkBody,\n response: { 201: SpaceDetail, ...refusals(403, 404, 409) },\n },\n handler: async (req, reply) => {\n const scope = await resolveScope(container, req);\n const space = await content.addSpaceLink(\n scope.orgId,\n req.params.communityId,\n req.params.spaceId,\n req.body.contentId,\n { id: scope.orgUserId, role: scope.role },\n );\n return reply.code(201).send(space);\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/communities/:communityId/spaces/:spaceId/links/:linkId',\n schema: {\n operationId: 'removeSpaceLink',\n tags,\n summary: 'Remove a link from a space (staff)',\n description: 'Owner or admin only. The linked content itself is untouched.',\n params: SpaceLinkParam,\n response: { 200: SpaceDetail, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return content.removeSpaceLink(\n scope.orgId,\n req.params.communityId,\n req.params.spaceId,\n req.params.linkId,\n { id: scope.orgUserId, role: scope.role },\n );\n },\n });\n\n r.route({\n method: 'PUT',\n url: '/api/communities/:communityId/spaces/:spaceId/links/order',\n schema: {\n operationId: 'reorderSpaceLinks',\n tags,\n summary: 'Reorder the links in a space (staff)',\n description:\n 'Owner or admin only. Send every link id of the space exactly once (409 otherwise).',\n params: CommunitySpaceParam,\n body: OrderedIds,\n response: { 200: SpaceDetail, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return content.reorderSpaceLinks(\n scope.orgId,\n req.params.communityId,\n req.params.spaceId,\n req.body.ids,\n { id: scope.orgUserId, role: scope.role },\n );\n },\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"communities.js","names":[],"sources":["../../../src/http/routes/communities.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 { NotFoundError } from '@headless-lms/core/shared/errors';\nimport { resolveConfiguratorScope, resolveScope } from '../scope.js';\nimport {\n Community,\n CommunityAnalytics,\n CommunityIdParam,\n CommunitySpaceParam,\n CommunitiesPage,\n CommunitiesQuery,\n CreateCommunity,\n CreateSpace,\n MessageQueue,\n NotificationCategoryList,\n OrderedIds,\n PatchCommunitySettings,\n ReplaceNotificationCategories,\n SetSpaceTopics,\n SpaceDetail,\n SpaceLinkBody,\n SpaceLinkParam,\n UpdateCommunity,\n UpdateSpace,\n refusals,\n} from '../schemas/index.js';\n\nexport async function communitiesRoutes(app: FastifyInstance, container: Container): Promise<void> {\n const r = app.withTypeProvider<ZodTypeProvider>();\n const content = container.content;\n const tags = ['Communities'];\n\n r.route({\n method: 'GET',\n url: '/api/communities',\n schema: {\n operationId: 'listCommunities',\n tags,\n summary: 'List communities (staff)',\n description:\n 'Staff only (owner, admin, instructor). Pages every community in the organization, drafts included, filtered by `status` and searched by title.',\n querystring: CommunitiesQuery,\n response: { 200: CommunitiesPage, ...refusals(403) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return content.listCommunities(scope.orgId, req.query);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/communities',\n schema: {\n operationId: 'createCommunity',\n tags,\n summary: 'Create a community (staff)',\n description:\n 'Owner or admin only. The community starts as a draft with the default direct-message settings and no spaces. Members see it once it is published and they hold it.',\n body: CreateCommunity,\n response: { 201: Community, ...refusals(403, 409) },\n },\n handler: async (req, reply) => {\n const scope = await resolveScope(container, req);\n const community = await content.createCommunity(scope.orgId, req.body, {\n id: scope.orgUserId,\n role: scope.role,\n });\n req.log.info({ communityId: community.id }, 'community created');\n return reply.code(201).send(community);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/communities/:communityId',\n schema: {\n operationId: 'getCommunity',\n tags,\n summary: 'Get a community (staff)',\n description:\n 'Staff only (owner, admin, instructor). Any community of the organization, drafts included.',\n params: CommunityIdParam,\n response: { 200: Community, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n const community = await content.getCommunity(scope.orgId, req.params.communityId);\n if (!community) {\n throw new NotFoundError('Community', req.params.communityId);\n }\n return community;\n },\n });\n\n r.route({\n method: 'PATCH',\n url: '/api/communities/:communityId',\n schema: {\n operationId: 'updateCommunity',\n tags,\n summary: 'Update a community (staff)',\n description:\n 'Owner or admin only. Changes only the fields sent. `thumbnailAssetId` must be a ready library image (409 otherwise); null clears it.',\n params: CommunityIdParam,\n body: UpdateCommunity,\n response: { 200: Community, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return content.updateCommunity(scope.orgId, req.params.communityId, req.body, {\n id: scope.orgUserId,\n role: scope.role,\n });\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/communities/:communityId',\n schema: {\n operationId: 'deleteCommunity',\n tags,\n summary: 'Delete a community (staff)',\n description:\n 'Owner or admin only. Links to the community from other spaces are removed with it.',\n params: CommunityIdParam,\n response: { 204: z.void(), ...refusals(403, 404) },\n },\n handler: async (req, reply) => {\n const scope = await resolveScope(container, req);\n await content.deleteCommunity(scope.orgId, req.params.communityId, {\n id: scope.orgUserId,\n role: scope.role,\n });\n return reply.code(204).send();\n },\n });\n\n r.route({\n method: 'PATCH',\n url: '/api/communities/:communityId/settings',\n schema: {\n operationId: 'patchCommunitySettings',\n tags,\n summary: 'Change the direct-message and digest settings of a community (staff)',\n description:\n 'Owner or admin only. Changes only the settings sent; the rest keep their values. The direct-message settings decide who may message whom from then on. `digest` sets when the community digest goes out, or `null` stops it; the first digest after a change goes out at the next slot.',\n params: CommunityIdParam,\n body: PatchCommunitySettings,\n response: { 200: Community, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return content.patchCommunitySettings(scope.orgId, req.params.communityId, req.body, {\n id: scope.orgUserId,\n role: scope.role,\n });\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/communities/:communityId/notification-categories',\n schema: {\n operationId: 'getCommunityNotificationCategories',\n tags: ['Notifications'],\n summary: \"Get a community's notification categories (staff)\",\n description:\n 'Owner or admin only. The list in use for the community, with `source` saying whether it is the default or a stored list.',\n params: CommunityIdParam,\n response: { 200: NotificationCategoryList, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const scope = await resolveConfiguratorScope(container, req);\n return container.notificationCategories.community.get(scope.orgId, req.params.communityId);\n },\n });\n\n r.route({\n method: 'PUT',\n url: '/api/communities/:communityId/notification-categories',\n schema: {\n operationId: 'replaceCommunityNotificationCategories',\n tags: ['Notifications'],\n summary: \"Replace a community's notification categories (staff)\",\n description:\n 'Owner or admin only. Stores the whole list for this community; other communities keep theirs. 422 when a key repeats, a type cannot be in a community list, a reason is not allowed for its type, a channel is not configured, `perSpace` is set on a type that cannot occur in a space, `spaceDefaults` appears without `perSpace` or names a channel the category does not offer, or earlier categories already cover everything a category covers.',\n params: CommunityIdParam,\n body: ReplaceNotificationCategories,\n response: { 200: NotificationCategoryList, ...refusals(400, 403, 404, 422) },\n },\n handler: async (req) => {\n const scope = await resolveConfiguratorScope(container, req);\n return container.notificationCategories.community.replace(\n scope.orgId,\n req.params.communityId,\n req.body.categories,\n );\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/communities/:communityId/notification-categories',\n schema: {\n operationId: 'resetCommunityNotificationCategories',\n tags: ['Notifications'],\n summary: 'Go back to the default notification categories for a community (staff)',\n description: 'Owner or admin only. Removes the stored list; the default applies again.',\n params: CommunityIdParam,\n response: { 204: z.void(), ...refusals(403, 404) },\n },\n handler: async (req, reply) => {\n const scope = await resolveConfiguratorScope(container, req);\n await container.notificationCategories.community.reset(scope.orgId, req.params.communityId);\n return reply.code(204).send();\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/communities/:communityId/publish',\n schema: {\n operationId: 'publishCommunity',\n tags,\n summary: 'Publish a community (staff)',\n description: 'Owner or admin only. Makes the community visible to the people who hold it.',\n params: CommunityIdParam,\n response: { 200: Community, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return content.publishCommunity(scope.orgId, req.params.communityId, {\n id: scope.orgUserId,\n role: scope.role,\n });\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/communities/:communityId/unpublish',\n schema: {\n operationId: 'unpublishCommunity',\n tags,\n summary: 'Unpublish a community (staff)',\n description:\n 'Owner or admin only. Hides the community from members again; nothing in it is deleted.',\n params: CommunityIdParam,\n response: { 200: Community, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return content.unpublishCommunity(scope.orgId, req.params.communityId, {\n id: scope.orgUserId,\n role: scope.role,\n });\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/communities/:communityId/analytics',\n schema: {\n operationId: 'getCommunityAnalytics',\n tags,\n summary: 'Staff analytics for a community',\n description:\n 'Staff only (owner, admin, instructor). Membership, activity and moderation counts for one community.',\n params: CommunityIdParam,\n response: { 200: CommunityAnalytics, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n const stats = await container.reporting.admin.communityAnalytics(\n scope.orgId,\n req.params.communityId,\n );\n if (!stats) {\n throw new NotFoundError('Community', req.params.communityId);\n }\n return stats;\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/communities/:communityId/message-queue',\n schema: {\n operationId: 'getCommunityMessageQueue',\n tags,\n summary: 'Open message flags in a community (staff)',\n description:\n \"Staff only (owner, admin, instructor). The messages in the community's chats with open flags, with the flags and the chat each is in. Holds at most the 100 oldest open flags; working the queue brings in the next ones.\",\n params: CommunityIdParam,\n response: { 200: MessageQueue, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return container.chats.messageQueue(\n scope.orgId,\n { id: scope.orgUserId, role: scope.role },\n req.params.communityId,\n );\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/communities/:communityId/spaces',\n schema: {\n operationId: 'listCommunitySpaces',\n tags,\n summary: 'List the spaces in a community (staff)',\n description:\n 'Staff only (owner, admin, instructor). Every space in order, with its topics and links.',\n params: CommunityIdParam,\n response: { 200: z.array(SpaceDetail), ...refusals(403, 404) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return content.listSpaces(scope.orgId, req.params.communityId);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/communities/:communityId/spaces',\n schema: {\n operationId: 'createSpace',\n tags,\n summary: 'Create a space (staff)',\n description:\n 'Owner or admin only. The space is added last. An `all_members` space cannot have a capacity (409).',\n params: CommunityIdParam,\n body: CreateSpace,\n response: { 201: SpaceDetail, ...refusals(403, 404, 409) },\n },\n handler: async (req, reply) => {\n const scope = await resolveScope(container, req);\n const space = await content.createSpace(scope.orgId, req.params.communityId, req.body, {\n id: scope.orgUserId,\n role: scope.role,\n });\n return reply.code(201).send(space);\n },\n });\n\n r.route({\n method: 'PUT',\n url: '/api/communities/:communityId/spaces/order',\n schema: {\n operationId: 'reorderSpaces',\n tags,\n summary: 'Reorder the spaces in a community (staff)',\n description:\n 'Owner or admin only. Send every space id of the community exactly once (409 otherwise).',\n params: CommunityIdParam,\n body: OrderedIds,\n response: { 200: z.array(SpaceDetail), ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return content.reorderSpaces(scope.orgId, req.params.communityId, req.body.ids, {\n id: scope.orgUserId,\n role: scope.role,\n });\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/communities/:communityId/spaces/:spaceId',\n schema: {\n operationId: 'getSpace',\n tags,\n summary: 'Get a space (staff)',\n description: 'Staff only (owner, admin, instructor). The space with its topics and links.',\n params: CommunitySpaceParam,\n response: { 200: SpaceDetail, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n const space = await content.getCommunitySpace(\n scope.orgId,\n req.params.communityId,\n req.params.spaceId,\n );\n if (!space) {\n throw new NotFoundError('Space', req.params.spaceId);\n }\n return space;\n },\n });\n\n r.route({\n method: 'PATCH',\n url: '/api/communities/:communityId/spaces/:spaceId',\n schema: {\n operationId: 'updateSpace',\n tags,\n summary: 'Update a space (staff)',\n description:\n 'Owner or admin only. Changes only the fields sent; `settings` merges into the current settings. An `all_members` space cannot have a capacity (409).',\n params: CommunitySpaceParam,\n body: UpdateSpace,\n response: { 200: SpaceDetail, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return content.updateSpace(\n scope.orgId,\n req.params.communityId,\n req.params.spaceId,\n req.body,\n { id: scope.orgUserId, role: scope.role },\n );\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/communities/:communityId/spaces/:spaceId',\n schema: {\n operationId: 'deleteSpace',\n tags,\n summary: 'Delete a space (staff)',\n description: 'Owner or admin only. The space and everything in it go.',\n params: CommunitySpaceParam,\n response: { 204: z.void(), ...refusals(403, 404) },\n },\n handler: async (req, reply) => {\n const scope = await resolveScope(container, req);\n await content.deleteSpace(scope.orgId, req.params.communityId, req.params.spaceId, {\n id: scope.orgUserId,\n role: scope.role,\n });\n return reply.code(204).send();\n },\n });\n\n r.route({\n method: 'PUT',\n url: '/api/communities/:communityId/spaces/:spaceId/topics',\n schema: {\n operationId: 'setSpaceTopics',\n tags,\n summary: 'Replace the topics on a space (staff)',\n description:\n 'Owner or admin only. The list replaces the current topics: an entry with an `id` keeps and renames that topic, one without adds a topic, and a topic left out is deleted. Names must be unique (409), and a topic that posts use cannot be deleted (409).',\n params: CommunitySpaceParam,\n body: SetSpaceTopics,\n response: { 200: SpaceDetail, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return content.setSpaceTopics(\n scope.orgId,\n req.params.communityId,\n req.params.spaceId,\n req.body,\n { id: scope.orgUserId, role: scope.role },\n );\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/communities/:communityId/spaces/:spaceId/links',\n schema: {\n operationId: 'addSpaceLink',\n tags,\n summary: 'Link a course, download, or community into a space (staff)',\n description:\n 'Owner or admin only. The link is added last. Linking the same content twice is a 409.',\n params: CommunitySpaceParam,\n body: SpaceLinkBody,\n response: { 201: SpaceDetail, ...refusals(403, 404, 409) },\n },\n handler: async (req, reply) => {\n const scope = await resolveScope(container, req);\n const space = await content.addSpaceLink(\n scope.orgId,\n req.params.communityId,\n req.params.spaceId,\n req.body.contentId,\n { id: scope.orgUserId, role: scope.role },\n );\n return reply.code(201).send(space);\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/communities/:communityId/spaces/:spaceId/links/:linkId',\n schema: {\n operationId: 'removeSpaceLink',\n tags,\n summary: 'Remove a link from a space (staff)',\n description: 'Owner or admin only. The linked content itself is untouched.',\n params: SpaceLinkParam,\n response: { 200: SpaceDetail, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return content.removeSpaceLink(\n scope.orgId,\n req.params.communityId,\n req.params.spaceId,\n req.params.linkId,\n { id: scope.orgUserId, role: scope.role },\n );\n },\n });\n\n r.route({\n method: 'PUT',\n url: '/api/communities/:communityId/spaces/:spaceId/links/order',\n schema: {\n operationId: 'reorderSpaceLinks',\n tags,\n summary: 'Reorder the links in a space (staff)',\n description:\n 'Owner or admin only. Send every link id of the space exactly once (409 otherwise).',\n params: CommunitySpaceParam,\n body: OrderedIds,\n response: { 200: SpaceDetail, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return content.reorderSpaceLinks(\n scope.orgId,\n req.params.communityId,\n req.params.spaceId,\n req.body.ids,\n { id: scope.orgUserId, role: scope.role },\n );\n },\n });\n}\n"],"mappings":"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"}
@@ -5,7 +5,7 @@ declare function actorOf(container: Container, orgId: string, userId: string): P
5
5
  orgId: string;
6
6
  userId: string;
7
7
  role: "owner" | "admin" | "instructor" | "moderator" | "student";
8
- status: "invited" | "active";
8
+ status: "active" | "invited";
9
9
  createdAt: Date;
10
10
  updatedAt: Date;
11
11
  }>;
@@ -1,7 +1,7 @@
1
1
  import { PageQuery, refusals } from "../../schemas/shared.js";
2
- import { Reactions, SetReaction } from "../../schemas/discussion.js";
2
+ import { Reactions, SetMute, SetReaction } from "../../schemas/discussion.js";
3
3
  import { FlagReason } from "../../schemas/posts.js";
4
- import { BlockParam, Chat, ChatIdParam, ChatsPage, CommunityParam, EditMessage, MarkRead, MessageFlag, MessageIdParam, MessageView, MessagesPage, MuteChat, OrgUserBody, ResolveMessageFlags, SendMessage, StartDirectChat, StartGroupChat } from "../../schemas/chats.js";
4
+ import { BlockParam, Chat, ChatIdParam, ChatsPage, CommunityParam, EditMessage, MarkRead, MessageFlag, MessageIdParam, MessageView, MessagesPage, OrgUserBody, ResolveMessageFlags, SendMessage, StartDirectChat, StartGroupChat } from "../../schemas/chats.js";
5
5
  import "../../schemas/index.js";
6
6
  import { actorOf } from "./actor.js";
7
7
  import { z } from "zod";
@@ -325,37 +325,16 @@ async function memberChatRoutes(app, container) {
325
325
  }
326
326
  });
327
327
  r.route({
328
- method: "POST",
329
- url: "/api/member/chats/:chatId/mute",
330
- preHandler: app.requireOrgSession,
331
- schema: {
332
- operationId: "muteChat",
333
- tags,
334
- summary: "Mute a chat until a time, or until unmuted",
335
- description: "For a participant. Without `until` the chat stays muted until you unmute it.",
336
- params: ChatIdParam,
337
- body: MuteChat,
338
- response: {
339
- 204: z.void(),
340
- ...refusals(403, 404)
341
- }
342
- },
343
- handler: async (req, reply) => {
344
- const actor = await actorOf(container, req.orgId, req.userId);
345
- await chats.mute(req.orgId, actor, req.params.chatId, req.body.until);
346
- return reply.code(204).send();
347
- }
348
- });
349
- r.route({
350
- method: "POST",
351
- url: "/api/member/chats/:chatId/unmute",
328
+ method: "PUT",
329
+ url: "/api/member/chats/:chatId/subscription",
352
330
  preHandler: app.requireOrgSession,
353
331
  schema: {
354
- operationId: "unmuteChat",
332
+ operationId: "setChatSubscription",
355
333
  tags,
356
- summary: "Unmute a chat",
357
- description: "For a participant.",
334
+ summary: "Mute a chat, or clear your mute",
335
+ description: "For a participant. `muted` stops its messages notifying you, until `mutedUntil` when given; a mention still notifies you. `none` clears the mute.",
358
336
  params: ChatIdParam,
337
+ body: SetMute,
359
338
  response: {
360
339
  204: z.void(),
361
340
  ...refusals(403, 404)
@@ -363,7 +342,7 @@ async function memberChatRoutes(app, container) {
363
342
  },
364
343
  handler: async (req, reply) => {
365
344
  const actor = await actorOf(container, req.orgId, req.userId);
366
- await chats.unmute(req.orgId, actor, req.params.chatId);
345
+ await container.subscriptions.setChat(req.orgId, actor, req.params.chatId, req.body);
367
346
  return reply.code(204).send();
368
347
  }
369
348
  });