@headless-lms/core 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (249) hide show
  1. package/dist/assets/events.d.ts +90 -9
  2. package/dist/assets/events.js +19 -3
  3. package/dist/assets/events.js.map +1 -1
  4. package/dist/assets/file-urls.d.ts +20 -0
  5. package/dist/assets/file-urls.js +25 -0
  6. package/dist/assets/file-urls.js.map +1 -0
  7. package/dist/assets/index.d.ts +5 -4
  8. package/dist/assets/index.js +3 -2
  9. package/dist/assets/model.d.ts +2 -2
  10. package/dist/assets/ports.d.ts +45 -15
  11. package/dist/assets/service.d.ts +39 -7
  12. package/dist/assets/service.js +292 -37
  13. package/dist/assets/service.js.map +1 -1
  14. package/dist/automations/actions.d.ts +12 -15
  15. package/dist/automations/actions.js +188 -30
  16. package/dist/automations/actions.js.map +1 -1
  17. package/dist/automations/catalog.d.ts +5 -1
  18. package/dist/automations/catalog.js +514 -197
  19. package/dist/automations/catalog.js.map +1 -1
  20. package/dist/automations/community-derivations.d.ts +14 -0
  21. package/dist/automations/community-derivations.js +439 -0
  22. package/dist/automations/community-derivations.js.map +1 -0
  23. package/dist/automations/community-mail.d.ts +112 -0
  24. package/dist/automations/community-mail.js +90 -0
  25. package/dist/automations/community-mail.js.map +1 -0
  26. package/dist/automations/community-notifications.d.ts +16 -0
  27. package/dist/automations/community-notifications.js +84 -0
  28. package/dist/automations/community-notifications.js.map +1 -0
  29. package/dist/automations/events.d.ts +74 -24
  30. package/dist/automations/events.js +5 -5
  31. package/dist/automations/events.js.map +1 -1
  32. package/dist/automations/index.d.ts +7 -5
  33. package/dist/automations/index.js +3 -2
  34. package/dist/automations/model.d.ts +20 -3
  35. package/dist/automations/model.js +36 -3
  36. package/dist/automations/model.js.map +1 -1
  37. package/dist/automations/ports.d.ts +27 -8
  38. package/dist/automations/schedule.d.ts +6 -0
  39. package/dist/automations/schedule.js +41 -0
  40. package/dist/automations/schedule.js.map +1 -0
  41. package/dist/automations/seeds.d.ts +17 -0
  42. package/dist/automations/seeds.js +235 -0
  43. package/dist/automations/seeds.js.map +1 -0
  44. package/dist/automations/service.d.ts +41 -6
  45. package/dist/automations/service.js +542 -59
  46. package/dist/automations/service.js.map +1 -1
  47. package/dist/automations/types.d.ts +2 -2
  48. package/dist/automations/webhook-signing.d.ts +7 -0
  49. package/dist/automations/webhook-signing.js +22 -0
  50. package/dist/automations/webhook-signing.js.map +1 -0
  51. package/dist/content/events.d.ts +1013 -25
  52. package/dist/content/events.js +168 -19
  53. package/dist/content/events.js.map +1 -1
  54. package/dist/content/index.d.ts +5 -5
  55. package/dist/content/index.js +2 -2
  56. package/dist/content/model.d.ts +2 -2
  57. package/dist/content/ports.d.ts +109 -25
  58. package/dist/content/service.d.ts +68 -23
  59. package/dist/content/service.js +647 -60
  60. package/dist/content/service.js.map +1 -1
  61. package/dist/content/types.d.ts +2 -2
  62. package/dist/discussion/authors.d.ts +10 -0
  63. package/dist/discussion/authors.js +23 -0
  64. package/dist/discussion/authors.js.map +1 -0
  65. package/dist/discussion/chats.d.ts +104 -0
  66. package/dist/discussion/chats.js +798 -0
  67. package/dist/discussion/chats.js.map +1 -0
  68. package/dist/discussion/events.d.ts +633 -14
  69. package/dist/discussion/events.js +134 -5
  70. package/dist/discussion/events.js.map +1 -1
  71. package/dist/discussion/index.d.ts +7 -3
  72. package/dist/discussion/index.js +4 -1
  73. package/dist/discussion/model.d.ts +1 -2
  74. package/dist/discussion/ports.d.ts +288 -27
  75. package/dist/discussion/ports.js +6 -1
  76. package/dist/discussion/ports.js.map +1 -0
  77. package/dist/discussion/posts.d.ts +103 -0
  78. package/dist/discussion/posts.js +723 -0
  79. package/dist/discussion/posts.js.map +1 -0
  80. package/dist/discussion/service.d.ts +33 -11
  81. package/dist/discussion/service.js +297 -81
  82. package/dist/discussion/service.js.map +1 -1
  83. package/dist/entitlements/events.d.ts +264 -13
  84. package/dist/entitlements/events.js +49 -5
  85. package/dist/entitlements/events.js.map +1 -1
  86. package/dist/entitlements/index.d.ts +5 -4
  87. package/dist/entitlements/index.js +2 -1
  88. package/dist/entitlements/model.d.ts +3 -2
  89. package/dist/entitlements/ports.d.ts +124 -11
  90. package/dist/entitlements/rules.d.ts +12 -0
  91. package/dist/entitlements/rules.js +17 -0
  92. package/dist/entitlements/rules.js.map +1 -0
  93. package/dist/entitlements/service.d.ts +54 -3
  94. package/dist/entitlements/service.js +473 -2
  95. package/dist/entitlements/service.js.map +1 -1
  96. package/dist/identity/access.d.ts +6 -0
  97. package/dist/identity/access.js +7 -0
  98. package/dist/identity/access.js.map +1 -0
  99. package/dist/identity/events.d.ts +17 -3
  100. package/dist/identity/events.js +12 -2
  101. package/dist/identity/events.js.map +1 -1
  102. package/dist/identity/index.d.ts +6 -4
  103. package/dist/identity/index.js +3 -1
  104. package/dist/identity/model.d.ts +2 -1
  105. package/dist/identity/notifications.d.ts +27 -0
  106. package/dist/identity/notifications.js +101 -0
  107. package/dist/identity/notifications.js.map +1 -0
  108. package/dist/identity/ports.d.ts +76 -3
  109. package/dist/identity/service.d.ts +17 -3
  110. package/dist/identity/service.js +73 -5
  111. package/dist/identity/service.js.map +1 -1
  112. package/dist/integrations/events.d.ts +27 -3
  113. package/dist/integrations/events.js +15 -3
  114. package/dist/integrations/events.js.map +1 -1
  115. package/dist/integrations/model.js +1 -1
  116. package/dist/integrations/model.js.map +1 -1
  117. package/dist/integrations/service.js +2 -2
  118. package/dist/integrations/service.js.map +1 -1
  119. package/dist/organizations/access-scopes.d.ts +19 -0
  120. package/dist/organizations/access-scopes.js +89 -0
  121. package/dist/organizations/access-scopes.js.map +1 -0
  122. package/dist/organizations/events.d.ts +234 -25
  123. package/dist/organizations/events.js +58 -9
  124. package/dist/organizations/events.js.map +1 -1
  125. package/dist/organizations/index.d.ts +8 -7
  126. package/dist/organizations/index.js +4 -3
  127. package/dist/organizations/members.d.ts +8 -5
  128. package/dist/organizations/members.js.map +1 -1
  129. package/dist/organizations/ports.d.ts +20 -21
  130. package/dist/organizations/roles.d.ts +6 -10
  131. package/dist/organizations/roles.js +19 -7
  132. package/dist/organizations/roles.js.map +1 -1
  133. package/dist/organizations/service.d.ts +28 -14
  134. package/dist/organizations/service.js +274 -105
  135. package/dist/organizations/service.js.map +1 -1
  136. package/dist/organizations/types.d.ts +2 -11
  137. package/dist/progress/events.d.ts +34 -2
  138. package/dist/progress/events.js +15 -2
  139. package/dist/progress/events.js.map +1 -1
  140. package/dist/progress/index.d.ts +2 -2
  141. package/dist/progress/ports.d.ts +6 -1
  142. package/dist/progress/service.d.ts +5 -3
  143. package/dist/progress/service.js +9 -3
  144. package/dist/progress/service.js.map +1 -1
  145. package/dist/reporting/admin/index.d.ts +4 -0
  146. package/dist/reporting/admin/index.js +2 -0
  147. package/dist/reporting/admin/model.d.ts +108 -0
  148. package/dist/reporting/admin/ports.d.ts +27 -0
  149. package/dist/reporting/admin/service.d.ts +22 -0
  150. package/dist/reporting/admin/service.js +38 -0
  151. package/dist/reporting/admin/service.js.map +1 -0
  152. package/dist/reporting/member/index.d.ts +5 -0
  153. package/dist/reporting/member/index.js +2 -0
  154. package/dist/reporting/member/model.d.ts +124 -0
  155. package/dist/reporting/member/ports.d.ts +45 -0
  156. package/dist/reporting/member/service.d.ts +73 -0
  157. package/dist/reporting/member/service.js +286 -0
  158. package/dist/reporting/member/service.js.map +1 -0
  159. package/dist/shared/actor.d.ts +9 -0
  160. package/dist/shared/errors.d.ts +7 -1
  161. package/dist/shared/errors.js +10 -1
  162. package/dist/shared/errors.js.map +1 -1
  163. package/dist/shared/events.d.ts +5 -3
  164. package/dist/shared/events.js +1 -0
  165. package/dist/shared/events.js.map +1 -1
  166. package/dist/shared/id.d.ts +12 -0
  167. package/dist/shared/id.js +13 -1
  168. package/dist/shared/id.js.map +1 -1
  169. package/dist/shared/mailer.d.ts +6 -1
  170. package/dist/shared/mailer.js.map +1 -1
  171. package/dist/shared/network.d.ts +10 -0
  172. package/dist/shared/network.js +211 -0
  173. package/dist/shared/network.js.map +1 -0
  174. package/dist/shared/settings.js.map +1 -1
  175. package/dist/types/assets.d.ts +2 -2
  176. package/dist/types/automations.d.ts +21 -2
  177. package/dist/types/content.d.ts +2 -2
  178. package/dist/types/discussion.d.ts +12 -3
  179. package/dist/types/email-templates.d.ts +124 -1
  180. package/dist/types/entitlements.d.ts +2 -2
  181. package/dist/types/events.d.ts +3 -3
  182. package/dist/types/identity.d.ts +48 -2
  183. package/dist/types/index.d.ts +10 -10
  184. package/dist/types/organizations.d.ts +2 -2
  185. package/dist/types/ports.d.ts +7 -1
  186. package/dist/types/schemas/assets.d.ts +67 -36
  187. package/dist/types/schemas/assets.js +19 -13
  188. package/dist/types/schemas/assets.js.map +1 -1
  189. package/dist/types/schemas/automations.d.ts +88 -4
  190. package/dist/types/schemas/automations.js +46 -6
  191. package/dist/types/schemas/automations.js.map +1 -1
  192. package/dist/types/schemas/content.d.ts +385 -20
  193. package/dist/types/schemas/content.js +178 -15
  194. package/dist/types/schemas/content.js.map +1 -1
  195. package/dist/types/schemas/credentials.js.map +1 -1
  196. package/dist/types/schemas/discussion.d.ts +379 -4
  197. package/dist/types/schemas/discussion.js +209 -7
  198. package/dist/types/schemas/discussion.js.map +1 -1
  199. package/dist/types/schemas/entitlements.d.ts +44 -4
  200. package/dist/types/schemas/entitlements.js +32 -4
  201. package/dist/types/schemas/entitlements.js.map +1 -1
  202. package/dist/types/schemas/identity.d.ts +115 -5
  203. package/dist/types/schemas/identity.js +59 -6
  204. package/dist/types/schemas/identity.js.map +1 -1
  205. package/dist/types/schemas/index.d.ts +9 -9
  206. package/dist/types/schemas/index.js +9 -9
  207. package/dist/types/schemas/integrations.js.map +1 -1
  208. package/dist/types/schemas/organizations.d.ts +27 -5
  209. package/dist/types/schemas/organizations.js +14 -7
  210. package/dist/types/schemas/organizations.js.map +1 -1
  211. package/dist/types/schemas/outbox.d.ts +1 -0
  212. package/dist/types/schemas/outbox.js +1 -0
  213. package/dist/types/schemas/outbox.js.map +1 -1
  214. package/dist/types/schemas/progress.d.ts +1 -0
  215. package/dist/types/schemas/progress.js +1 -0
  216. package/dist/types/schemas/progress.js.map +1 -1
  217. package/dist/types/schemas/settings.js.map +1 -1
  218. package/dist/types/schemas/shared.d.ts +4 -1
  219. package/dist/types/schemas/shared.js +7 -1
  220. package/dist/types/schemas/shared.js.map +1 -1
  221. package/dist/types/shared.d.ts +1 -1
  222. package/package.json +2 -2
  223. package/dist/reporting/dashboard/index.d.ts +0 -4
  224. package/dist/reporting/dashboard/index.js +0 -2
  225. package/dist/reporting/dashboard/model.d.ts +0 -14
  226. package/dist/reporting/dashboard/ports.d.ts +0 -13
  227. package/dist/reporting/dashboard/service.d.ts +0 -19
  228. package/dist/reporting/dashboard/service.js +0 -20
  229. package/dist/reporting/dashboard/service.js.map +0 -1
  230. package/dist/reporting/learn/index.d.ts +0 -5
  231. package/dist/reporting/learn/index.js +0 -2
  232. package/dist/reporting/learn/model.d.ts +0 -23
  233. package/dist/reporting/learn/ports.d.ts +0 -42
  234. package/dist/reporting/learn/service.d.ts +0 -48
  235. package/dist/reporting/learn/service.js +0 -111
  236. package/dist/reporting/learn/service.js.map +0 -1
  237. package/dist/reporting/students/index.d.ts +0 -4
  238. package/dist/reporting/students/index.js +0 -2
  239. package/dist/reporting/students/model.d.ts +0 -31
  240. package/dist/reporting/students/ports.d.ts +0 -13
  241. package/dist/reporting/students/ports.js +0 -1
  242. package/dist/reporting/students/service.d.ts +0 -19
  243. package/dist/reporting/students/service.js +0 -20
  244. package/dist/reporting/students/service.js.map +0 -1
  245. /package/dist/reporting/{dashboard → admin}/model.js +0 -0
  246. /package/dist/reporting/{dashboard → admin}/ports.js +0 -0
  247. /package/dist/reporting/{learn → member}/model.js +0 -0
  248. /package/dist/reporting/{learn → member}/ports.js +0 -0
  249. /package/dist/{reporting/students/model.js → shared/actor.js} +0 -0
@@ -1 +1 @@
1
- {"version":3,"file":"service.js","names":[],"sources":["../../src/discussion/service.ts"],"sourcesContent":["import { genId } from '../shared/id.js';\nimport { ForbiddenError, NotFoundError } from '../shared/errors.js';\nimport type { Logger } from '../shared/ports.js';\nimport { noopLogger } from '../shared/logger.js';\nimport type {\n ActivityCommentsRule,\n Comment,\n CommentAuthor,\n CommentReport,\n CommentSettings,\n} from './model.js';\nimport type {\n CommentListItem,\n CommentsConfig,\n CommentView,\n ListCommentsQuery,\n Page,\n ReactionEmoji,\n} from './types.js';\nimport { SettingsNamespace, type SettingsService } from '../shared/settings.js';\nimport type {\n ActivityComments,\n ActivityGetter,\n Actor,\n AuthorRecord,\n CommentReactions,\n CourseAccessReader,\n DiscussionRepository,\n DiscussionService,\n DiscussionUnitOfWork,\n PostCommentInput,\n} from './ports.js';\nimport { discussionEvents } from './events.js';\n\n/** A course with no stored settings. Discussion is opt-in, so the common case\n * persists no row at all and every existing course stays silent. */\nexport const DEFAULT_SETTINGS = {\n enabled: false,\n threaded: true,\n requireReview: false,\n reactions: true,\n} as const;\n\n/** The slice of the course's stored settings this context reads. Absent for a\n * course that has never been configured, so the defaults apply. */\ninterface StoredCourseSettings {\n comments?: Partial<CommentSettings>;\n}\n\n/** The slice of an activity's opaque settings blob this context reads. Absent\n * or \"inherit\" = no override, so the course setting stands. */\ninterface StoredActivitySettings {\n comments?: ActivityCommentsRule;\n}\n\nconst EMPTY_REACTIONS: CommentReactions = { reactions: {} };\n\nexport type DiscussionServiceParams = {\n repo: DiscussionRepository;\n access: CourseAccessReader;\n content: ActivityGetter;\n uow: DiscussionUnitOfWork;\n settings: SettingsService;\n logger?: Logger;\n};\n\nexport class DiscussionServiceImpl implements DiscussionService {\n private readonly repo: DiscussionRepository;\n private readonly access: CourseAccessReader;\n private readonly content: ActivityGetter;\n private readonly uow: DiscussionUnitOfWork;\n private readonly settings: SettingsService;\n private readonly logger: Logger;\n\n constructor(params: DiscussionServiceParams) {\n this.repo = params.repo;\n this.access = params.access;\n this.content = params.content;\n this.uow = params.uow;\n this.settings = params.settings;\n this.logger = params.logger ?? noopLogger;\n }\n\n async resolveConfig(orgId: string, activityId: string): Promise<CommentsConfig> {\n const activity = await this.content.getActivity(orgId, activityId);\n if (!activity) {\n throw new NotFoundError('Activity', activityId);\n }\n\n // The `content` namespace row is the whole CourseSettings; comment settings\n // are one key inside it, written there by content.patchSettings.\n const stored = await this.settings.get<StoredCourseSettings>(\n orgId,\n SettingsNamespace.content,\n activity.courseId,\n );\n const settings = { ...DEFAULT_SETTINGS, ...stored?.comments };\n // Comments off for the course cannot be overridden back on by an activity:\n // the course switch is the master.\n const rule = ((activity?.settings ?? {}) as StoredActivitySettings).comments ?? 'inherit';\n return {\n enabled: settings.enabled && rule !== 'never',\n threaded: settings.threaded,\n requireReview: settings.requireReview,\n reactions: settings.reactions,\n };\n }\n\n /** Any org user that is not a learner moderates. The role arrives from\n * the edge already resolved; core never looks one up to authorise. */\n private isStaff(actor: Actor): boolean {\n return actor.role !== 'student';\n }\n\n private toAuthor(record: AuthorRecord): CommentAuthor {\n return {\n id: record.id,\n firstName: record.firstName,\n lastName: record.lastName,\n image: record.image,\n role: record.role,\n };\n }\n\n private toView(\n comment: Comment,\n authors: Record<string, AuthorRecord>,\n reactions: CommentReactions,\n ): CommentView {\n const author = authors[comment.orgUserId];\n if (!author) {\n throw new NotFoundError('OrgUser', comment.orgUserId);\n }\n if (comment.removedBy && !authors[comment.removedBy]) {\n throw new NotFoundError('OrgUser', comment.removedBy);\n }\n const remover = comment.removedBy ? authors[comment.removedBy] : undefined;\n return {\n id: comment.id,\n activityId: comment.activityId,\n parentId: comment.parentId,\n author: this.toAuthor(author),\n body: comment.status === 'removed' ? null : comment.body,\n status: comment.status,\n removedBy: remover ? this.toAuthor(remover) : null,\n reactions: reactions.reactions,\n viewerReaction: reactions.viewerReaction,\n createdAt: comment.createdAt,\n updatedAt: comment.updatedAt,\n };\n }\n\n private peopleIn(comments: Comment[]): string[] {\n const ids = new Set<string>();\n for (const c of comments) {\n ids.add(c.orgUserId);\n if (c.removedBy) {\n ids.add(c.removedBy);\n }\n }\n return [...ids];\n }\n\n private async renderOne(orgId: string, comment: Comment, actor: Actor): Promise<CommentView> {\n const authors = await this.repo.authorsOf(orgId, this.peopleIn([comment]));\n const stored =\n comment.status === 'removed'\n ? EMPTY_REACTIONS\n : ((await this.repo.reactionsOf(orgId, [comment.id], actor.id))[comment.id] ??\n EMPTY_REACTIONS);\n return this.toView(comment, authors, stored);\n }\n\n private async hasAccess(orgId: string, activityId: string, actor: Actor): Promise<boolean> {\n return (\n this.isStaff(actor) ||\n (await (async () => {\n const activity = await this.content.getActivity(orgId, activityId);\n return activity ? this.access.hasCourseAccess(orgId, actor.id, activity.courseId) : false;\n })())\n );\n }\n\n async postComment(\n orgId: string,\n { activityId, actor, parentId, body }: PostCommentInput,\n ): Promise<CommentView> {\n await this.hasAccess(orgId, activityId, actor);\n const config = await this.resolveConfig(orgId, activityId);\n\n if (!config.enabled) {\n throw new ForbiddenError('discussion is not open on this activity');\n }\n\n if (parentId !== null) {\n if (!config.threaded) {\n throw new ForbiddenError('replies are disabled on this course');\n }\n const parent = await this.repo.findComment(orgId, parentId);\n if (!parent || parent.activityId !== activityId) {\n throw new NotFoundError('Comment', parentId);\n }\n\n if (parent.parentId !== null) {\n throw new ForbiddenError('replies nest one level');\n }\n\n if (parent.status !== 'published') {\n throw new ForbiddenError('cannot reply to a comment that is not published');\n }\n }\n\n const status: Comment['status'] =\n config.requireReview && !this.isStaff(actor) ? 'pending' : 'published';\n\n const comment: Comment = {\n id: genId('comment'),\n orgId,\n activityId: activityId,\n parentId: parentId,\n orgUserId: actor.id,\n body: body,\n status,\n removedBy: null,\n createdAt: new Date(),\n updatedAt: new Date(),\n };\n const saved = await this.uow.run(async (scope) => {\n const row = await scope.discussion.insertComment(orgId, comment);\n await scope.outbox.append([\n discussionEvents.commentCreated.make({ orgId, data: row }),\n ]);\n this.logger.info('comment created', { orgId, commentId: row.id, status });\n return row;\n });\n return this.renderOne(orgId, saved, actor);\n }\n\n async activityComments(\n orgId: string,\n activityId: string,\n actor: Actor,\n ): Promise<ActivityComments> {\n const config = await this.resolveConfig(orgId, activityId);\n if (!config.enabled) {\n return { config, comments: [] };\n }\n const rows = await this.repo.listByActivity(orgId, activityId);\n // A pending comment is visible to its own author and to staff, nobody else.\n const readable = rows.filter((c) => {\n if (c.status === 'published' || c.status === 'removed') {\n return true;\n }\n return this.isStaff(actor) || c.orgUserId === actor.id;\n });\n // Replies nest one level, so a reply has nothing hanging off it — a removed\n // reply is simply dropped rather than held open as a placeholder.\n const replies = readable.filter((c) => c.parentId !== null && c.status !== 'removed');\n const heldOpen = new Set(replies.map((r) => r.parentId));\n // A removed root survives only to hold replies THIS reader can see. Judging\n // it against every reply would show a marker with nothing beneath it.\n const roots = readable.filter(\n (c) => c.parentId === null && (c.status !== 'removed' || heldOpen.has(c.id)),\n );\n const rootIds = new Set(roots.map((r) => r.id));\n const served = [...roots, ...replies.filter((r) => rootIds.has(r.parentId!))].sort(\n (a, b) => a.createdAt.getTime() - b.createdAt.getTime(),\n );\n\n const [authors, reactions] = await Promise.all([\n this.repo.authorsOf(orgId, this.peopleIn(served)),\n config.reactions\n ? this.repo.reactionsOf(\n orgId,\n served.map((c) => c.id),\n actor.id,\n )\n : Promise.resolve<Record<string, CommentReactions>>({}),\n ]);\n\n return {\n config,\n comments: served.map((c) => this.toView(c, authors, reactions[c.id] ?? EMPTY_REACTIONS)),\n };\n }\n\n async edit(orgId: string, commentId: string, actor: Actor, body: string): Promise<CommentView> {\n const { comment, config } = await this.loadWithConfig(orgId, commentId, actor);\n if (!config.enabled) {\n throw new ForbiddenError('discussion is not open on this activity');\n }\n // Editing is the author's alone — moderators remove, they do not rewrite.\n if (comment.orgUserId !== actor.id) {\n throw new ForbiddenError('only the author may edit a comment');\n }\n if (comment.status === 'removed') {\n throw new ForbiddenError('a removed comment cannot be edited');\n }\n const updated = await this.repo.updateComment(orgId, commentId, {\n body,\n });\n if (!updated) {\n // getComment() proved it existed; a null here means it vanished mid-write.\n // Returning the pre-edit comment would report success and hand back\n // the old body.\n throw new NotFoundError('Comment', commentId);\n }\n return this.renderOne(orgId, updated, actor);\n }\n\n async remove(orgId: string, commentId: string, actor: Actor): Promise<Comment> {\n const comment = await this.getComment(orgId, commentId);\n if (!comment) {\n throw new NotFoundError('Comment', commentId);\n }\n\n await this.hasAccess(orgId, comment.activityId, actor);\n if (comment.orgUserId !== actor.id && !this.isStaff(actor)) {\n throw new ForbiddenError('only the author or a moderator may remove a comment');\n }\n\n if (comment.status === 'removed') {\n return comment;\n }\n\n return this.uow.run(async (scope) => {\n const updated = await scope.discussion.updateComment(orgId, commentId, {\n status: 'removed',\n removedBy: actor.id,\n });\n if (!updated) {\n // getComment() proved it existed; a null here means it vanished mid-operation.\n // Roll back rather than emit an event for a transition that didn't happen.\n throw new NotFoundError('Comment', commentId);\n }\n await scope.outbox.append([\n discussionEvents.commentRemoved.make({ orgId, data: updated }),\n ]);\n this.logger.info('comment removed', { orgId, commentId, by: actor.id });\n return updated;\n });\n }\n\n async restore(orgId: string, commentId: string, actor: Actor): Promise<Comment> {\n if (!this.isStaff(actor)) {\n throw new ForbiddenError('only a moderator may restore a comment');\n }\n const comment = await this.getComment(orgId, commentId);\n if (!comment) {\n throw new NotFoundError('Comment', commentId);\n }\n if (comment.status !== 'removed') {\n return comment;\n }\n const updated = await this.repo.updateComment(orgId, commentId, {\n status: 'published',\n removedBy: null,\n });\n if (!updated) {\n // getComment() proved it existed; a null here means it vanished mid-write.\n // Returning the still-removed comment would report success while\n // handing back the removed row.\n throw new NotFoundError('Comment', commentId);\n }\n return updated;\n }\n\n /** Load a comment together with its activity's resolved comments config,\n * having proved the actor may reach that activity at all. Every path that\n * acts on one comment needs the pair, and none should read the row twice. */\n private async loadWithConfig(\n orgId: string,\n commentId: string,\n actor: Actor,\n ): Promise<{ comment: Comment; config: CommentsConfig }> {\n const comment = await this.getComment(orgId, commentId);\n if (!comment) {\n throw new NotFoundError('Comment', commentId);\n }\n await this.hasAccess(orgId, comment.activityId, actor);\n const config = await this.resolveConfig(orgId, comment.activityId);\n return { comment, config };\n }\n\n async setReaction(\n orgId: string,\n commentId: string,\n actor: Actor,\n emoji: ReactionEmoji | null,\n ): Promise<CommentReactions> {\n const { comment, config } = await this.loadWithConfig(orgId, commentId, actor);\n // Writing to comments — locked and hidden both refuse.\n if (!config.enabled) {\n throw new ForbiddenError('discussion is not open on this activity');\n }\n if (!config.reactions && emoji !== null) {\n throw new ForbiddenError('reactions are disabled on this course');\n }\n if (comment.status === 'removed') {\n throw new ForbiddenError('a removed comment cannot be reacted to');\n }\n await this.repo.setReaction(orgId, commentId, actor.id, emoji);\n const stored = await this.repo.reactionsOf(orgId, [commentId], actor.id);\n return stored[commentId] ?? EMPTY_REACTIONS;\n }\n\n async approve(orgId: string, commentId: string, actor: Actor): Promise<Comment> {\n if (!this.isStaff(actor)) {\n throw new ForbiddenError('only a moderator may approve a comment');\n }\n const comment = await this.getComment(orgId, commentId);\n if (!comment) {\n throw new NotFoundError('Comment', commentId);\n }\n if (comment.status !== 'pending') {\n throw new ForbiddenError('only a pending comment can be approved');\n }\n return this.uow.run(async (scope) => {\n const updated = await scope.discussion.updateComment(orgId, commentId, {\n status: 'published',\n });\n if (!updated) {\n // getComment() proved it existed; a null here means it vanished mid-operation.\n // Roll back rather than emit an event for a transition that didn't happen.\n throw new NotFoundError('Comment', commentId);\n }\n await scope.outbox.append([\n discussionEvents.commentPublished.make({ orgId, data: updated }),\n ]);\n this.logger.info('comment published', { orgId, commentId });\n return updated;\n });\n }\n\n /** Loads the comment and dispatches to whichever transition applies — the\n * caller must not decide that itself. `approve` and `restore` still enforce\n * their own staff check; the check here covers the third path, where a\n * comment is already published and neither of them would run. */\n async publish(orgId: string, commentId: string, actor: Actor): Promise<Comment> {\n if (!this.isStaff(actor)) {\n throw new ForbiddenError('only a moderator may publish a comment');\n }\n const comment = await this.getComment(orgId, commentId);\n if (!comment) {\n throw new NotFoundError('Comment', commentId);\n }\n if (comment.status === 'pending') {\n return this.approve(orgId, commentId, actor);\n }\n if (comment.status === 'removed') {\n return this.restore(orgId, commentId, actor);\n }\n return comment;\n }\n\n async resolveReports(orgId: string, commentId: string, actor: Actor): Promise<void> {\n if (!this.isStaff(actor)) {\n throw new ForbiddenError('only a moderator may resolve a report');\n }\n const comment = await this.getComment(orgId, commentId);\n if (!comment) {\n throw new NotFoundError('Comment', commentId);\n }\n await this.repo.resolveReportsFor(orgId, commentId, new Date());\n this.logger.info('comment reports resolved', { orgId, commentId });\n }\n\n async reportComment(\n orgId: string,\n commentId: string,\n actor: Actor,\n reason: string,\n ): Promise<CommentReport> {\n const { config } = await this.loadWithConfig(orgId, commentId, actor);\n // Locked accepts reports — a locked activity can still hold something a\n // moderator needs to see. Hidden does not: nothing in it is being served,\n // so nobody is looking at a comment to flag.\n if (!config.enabled) {\n throw new ForbiddenError('discussion is not open on this activity');\n }\n const report: CommentReport = {\n id: genId('commentReport'),\n orgId,\n commentId,\n orgUserId: actor.id,\n reason,\n resolvedAt: null,\n createdAt: new Date(),\n };\n return this.uow.run(async (scope) => {\n const saved = await scope.discussion.insertReport(orgId, report);\n if (!saved) {\n // Already reported by this person. Return the report that actually\n // exists — a fabricated id resolves to nothing — and emit no second\n // event, which would double-count any threshold automation.\n const open = await scope.discussion.listOpenReports(orgId, [commentId]);\n const mine = open.find((r) => r.orgUserId === actor.id);\n if (mine) {\n return mine;\n }\n return report;\n }\n await scope.outbox.append([\n discussionEvents.commentReported.make({ orgId, data: saved }),\n ]);\n this.logger.info('comment reported', { orgId, commentId });\n return saved;\n });\n }\n\n async listComments(orgId: string, query: ListCommentsQuery): Promise<Page<CommentListItem>> {\n const page = await this.repo.listComments(orgId, query);\n const rows = page.rows.map((r) => r.comment);\n if (rows.length === 0) {\n return { ...page, rows: [] };\n }\n const reports = await this.repo.listOpenReports(\n orgId,\n rows.map((c) => c.id),\n );\n const authors = await this.repo.authorsOf(orgId, [\n ...new Set([...this.peopleIn(rows), ...reports.map((r) => r.orgUserId)]),\n ]);\n\n const byComment = new Map<string, CommentReport[]>();\n for (const report of reports) {\n byComment.set(report.commentId, [...(byComment.get(report.commentId) ?? []), report]);\n }\n\n return {\n ...page,\n rows: page.rows.map(({ comment, courseId, activityTitle }) => {\n const author = authors[comment.orgUserId];\n if (!author) {\n throw new NotFoundError('OrgUser', comment.orgUserId);\n }\n return {\n id: comment.id,\n parentId: comment.parentId,\n activityId: comment.activityId,\n activityTitle,\n courseId,\n body: comment.body,\n status: comment.status,\n author: this.toAuthor(author),\n authorEmail: author.email,\n removedBy: comment.removedBy,\n reports: (byComment.get(comment.id) ?? []).map((r) => {\n const reporter = authors[r.orgUserId];\n if (!reporter) {\n throw new NotFoundError('OrgUser', r.orgUserId);\n }\n return {\n reporter: this.toAuthor(reporter),\n reason: r.reason,\n createdAt: r.createdAt,\n };\n }),\n createdAt: comment.createdAt,\n updatedAt: comment.updatedAt,\n };\n }),\n };\n }\n\n getComment(orgId: string, commentId: string): Promise<Comment | null> {\n return this.repo.findComment(orgId, commentId);\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"service.js","names":[],"sources":["../../src/discussion/service.ts"],"sourcesContent":["import { genId } from '../shared/id.js';\nimport { ConflictError, ForbiddenError, NotFoundError } from '../shared/errors.js';\nimport { canModerate, isStaffRole } from '../organizations/index.js';\nimport type { Logger } from '../shared/ports.js';\nimport { noopLogger } from '../shared/logger.js';\nimport type { Comment, CommentReport, CommentSettings } from './model.js';\nimport type { ActivitySettings, CourseSettings, Post, SpaceDetail } from '../types/index.js';\nimport type {\n CommentListItem,\n CommentsConfig,\n CommentView,\n ListCommentsQuery,\n Page,\n ReactionEmoji,\n} from './types.js';\nimport { SettingsNamespace, type SettingsService } from '../shared/settings.js';\nimport type {\n ActivityComments,\n ActivityGetter,\n Actor,\n AuthorRecord,\n CommentReactions,\n ContentAccessReader,\n DiscussionRepository,\n DiscussionService,\n DiscussionUnitOfWork,\n PageQuery,\n PostCommentInput,\n PostService,\n} from './ports.js';\nimport { discussionEvents } from './events.js';\nimport { authorView } from './authors.js';\nimport type { FileUrls } from '../assets/index.js';\n\n/** A course with no stored settings. Discussion is opt-in, so the common case\n * persists no row at all and every existing course stays silent. */\nexport const DEFAULT_SETTINGS = {\n enabled: false,\n threaded: true,\n requireReview: false,\n reactions: true,\n} as const satisfies CommentSettings;\n\nconst EMPTY_REACTIONS: CommentReactions = { reactions: {} };\n\nexport type DiscussionServiceParams = {\n repo: DiscussionRepository;\n access: ContentAccessReader;\n content: ActivityGetter;\n posts: Pick<PostService, 'visible'>;\n uow: DiscussionUnitOfWork;\n settings: SettingsService;\n communities: { listIds(orgId: string): Promise<string[]> };\n fileUrls: FileUrls;\n logger?: Logger;\n};\n\nexport class DiscussionServiceImpl implements DiscussionService {\n private readonly repo: DiscussionRepository;\n private readonly access: ContentAccessReader;\n private readonly content: ActivityGetter;\n private readonly posts: Pick<PostService, 'visible'>;\n private readonly uow: DiscussionUnitOfWork;\n private readonly settings: SettingsService;\n private readonly communities: { listIds(orgId: string): Promise<string[]> };\n private readonly fileUrls: FileUrls;\n private readonly logger: Logger;\n\n constructor(params: DiscussionServiceParams) {\n this.repo = params.repo;\n this.access = params.access;\n this.content = params.content;\n this.posts = params.posts;\n this.uow = params.uow;\n this.settings = params.settings;\n this.communities = params.communities;\n this.fileUrls = params.fileUrls;\n this.logger = params.logger ?? noopLogger;\n }\n\n async resolveConfig(orgId: string, activityId: string): Promise<CommentsConfig> {\n const activity = await this.content.getActivity(orgId, activityId);\n if (!activity) {\n throw new NotFoundError('Activity', activityId);\n }\n\n // The `content` namespace row is the whole CourseSettings, owned and written\n // by content; this context only reads its `comments` key.\n const stored = await this.settings.get<Partial<CourseSettings>>(\n orgId,\n SettingsNamespace.content,\n activity.courseId,\n );\n const settings = { ...DEFAULT_SETTINGS, ...stored?.comments };\n // Comments off for the course cannot be overridden back on by an activity:\n // the course switch is the master.\n const rule = ((activity?.settings ?? {}) as ActivitySettings).comments ?? 'inherit';\n return {\n enabled: settings.enabled && rule !== 'never',\n threaded: settings.threaded,\n requireReview: settings.requireReview,\n reactions: settings.reactions,\n };\n }\n\n private isStaff(actor: Actor): boolean {\n return isStaffRole(actor.role);\n }\n\n private toView(\n orgId: string,\n comment: Comment,\n authors: Record<string, AuthorRecord>,\n urls: FileUrls,\n reactions: CommentReactions,\n ): CommentView {\n const author = authors[comment.orgUserId];\n if (!author) {\n throw new NotFoundError('Member', comment.orgUserId);\n }\n if (comment.removedBy && !authors[comment.removedBy]) {\n throw new NotFoundError('Member', comment.removedBy);\n }\n const remover = comment.removedBy ? authors[comment.removedBy] : undefined;\n return {\n id: comment.id,\n activityId: comment.activityId,\n postId: comment.postId,\n communityId: comment.communityId,\n parentId: comment.parentId,\n author: authorView(orgId, author, urls),\n body: comment.status === 'removed' ? null : comment.body,\n status: comment.status,\n removedBy: remover ? authorView(orgId, remover, urls) : null,\n reactions: reactions.reactions,\n viewerReaction: reactions.viewerReaction,\n createdAt: comment.createdAt,\n updatedAt: comment.updatedAt,\n };\n }\n\n private peopleIn(comments: Comment[]): string[] {\n const ids = new Set<string>();\n for (const c of comments) {\n ids.add(c.orgUserId);\n if (c.removedBy) {\n ids.add(c.removedBy);\n }\n }\n return [...ids];\n }\n\n private async renderOne(orgId: string, comment: Comment, actor: Actor): Promise<CommentView> {\n const authors = await this.repo.authorsOf(orgId, this.peopleIn([comment]));\n const stored =\n comment.status === 'removed'\n ? EMPTY_REACTIONS\n : ((await this.repo.reactionsOf(orgId, [comment.id], actor.id))[comment.id] ??\n EMPTY_REACTIONS);\n return this.toView(orgId, comment, authors, this.fileUrls, stored);\n }\n\n private activityIdOf(comment: Comment): string {\n if (comment.activityId === null) {\n throw new NotFoundError('Activity', comment.id);\n }\n return comment.activityId;\n }\n\n private async hasAccess(orgId: string, activityId: string, actor: Actor): Promise<boolean> {\n if (this.isStaff(actor)) {\n return true;\n }\n const activity = await this.content.getActivity(orgId, activityId);\n return activity ? this.access.hasAccess(orgId, actor.id, activity.courseId) : false;\n }\n\n private async requireActivityOpen(\n orgId: string,\n activityId: string,\n actor: Actor,\n ): Promise<void> {\n const activity = await this.content.getActivity(orgId, activityId);\n if (!activity) {\n throw new NotFoundError('Activity', activityId);\n }\n if (\n !this.isStaff(actor) &&\n !(await this.access.hasAccess(orgId, actor.id, activity.courseId))\n ) {\n throw new ForbiddenError('You cannot open this activity');\n }\n }\n\n async postComment(orgId: string, input: PostCommentInput): Promise<CommentView> {\n if (input.postId) {\n return this.postOnPost(orgId, input.postId, input);\n }\n const activityId = input.activityId;\n if (!activityId) {\n throw new NotFoundError('Activity', activityId ?? '');\n }\n return this.postOnActivity(orgId, activityId, input);\n }\n\n private async postOnActivity(\n orgId: string,\n activityId: string,\n { actor, parentId, body }: PostCommentInput,\n ): Promise<CommentView> {\n await this.requireActivityOpen(orgId, activityId, actor);\n const config = await this.resolveConfig(orgId, activityId);\n\n if (!config.enabled) {\n throw new ForbiddenError('Discussion is not open on this activity');\n }\n\n if (parentId !== null) {\n if (!config.threaded) {\n throw new ForbiddenError('Replies are off on this course');\n }\n const parent = await this.repo.findComment(orgId, parentId);\n if (!parent || parent.activityId !== activityId) {\n throw new NotFoundError('Comment', parentId);\n }\n\n if (parent.parentId !== null) {\n throw new ForbiddenError('A reply cannot have replies of its own');\n }\n\n if (parent.status !== 'published') {\n throw new ForbiddenError('You can reply only to a published comment');\n }\n }\n\n const status: Comment['status'] =\n config.requireReview && !this.isStaff(actor) ? 'pending' : 'published';\n\n const comment: Comment = {\n id: genId('comment'),\n orgId,\n activityId,\n postId: null,\n communityId: null,\n parentId,\n orgUserId: actor.id,\n body,\n status,\n removedBy: null,\n createdAt: new Date(),\n updatedAt: new Date(),\n };\n const saved = await this.uow.run(async (scope) => {\n const row = await scope.discussion.insertComment(orgId, comment);\n await scope.outbox.append([discussionEvents.commentCreated.make({ orgId, data: row })]);\n this.logger.info('comment created', { orgId, commentId: row.id, status, actorId: actor.id });\n return row;\n });\n return this.renderOne(orgId, saved, actor);\n }\n\n private async postOnPost(\n orgId: string,\n postId: string,\n { actor, parentId, body }: PostCommentInput,\n ): Promise<CommentView> {\n const { post, space } = await this.visiblePost(orgId, actor, postId);\n if (post.status !== 'published') {\n throw new ConflictError('This post is not published');\n }\n if (!this.commentsOpen(post, space)) {\n throw new ForbiddenError('Comments are closed on this post');\n }\n if (parentId !== null) {\n const parent = await this.repo.findComment(orgId, parentId);\n if (!parent || parent.postId !== postId) {\n throw new NotFoundError('Comment', parentId);\n }\n if (parent.parentId !== null) {\n throw new ForbiddenError('A reply cannot have replies of its own');\n }\n if (parent.status !== 'published') {\n throw new ForbiddenError('You can reply only to a published comment');\n }\n }\n const now = new Date();\n const comment: Comment = {\n id: genId('comment'),\n orgId,\n activityId: null,\n postId,\n communityId: post.communityId,\n parentId,\n orgUserId: actor.id,\n body,\n status: 'published',\n removedBy: null,\n createdAt: now,\n updatedAt: now,\n };\n const saved = await this.uow.run(async (scope) => {\n const row = await scope.discussion.insertComment(orgId, comment);\n await scope.outbox.append([\n discussionEvents.commentCreated.make({\n orgId,\n data: row,\n metadata: { actorOrgUserId: actor.id },\n }),\n ]);\n this.logger.info('comment created', {\n orgId,\n commentId: row.id,\n postId,\n status: 'published',\n actorId: actor.id,\n });\n return row;\n });\n return this.renderOne(orgId, saved, actor);\n }\n\n async postComments(\n orgId: string,\n postId: string,\n actor: Actor,\n query: PageQuery,\n ): Promise<Page<CommentView>> {\n const { post, space } = await this.visiblePost(orgId, actor, postId);\n const seesHeld = (person: Actor) => this.canModerate(person);\n const page = await this.repo.listThreadsByPost(orgId, postId, {\n ...query,\n viewerId: actor.id,\n seesHeld: seesHeld(actor),\n });\n const comments = await this.thread(\n orgId,\n page.rows,\n actor,\n post.reactionsEnabled ?? space.settings.reactions,\n seesHeld,\n );\n this.logger.debug('post comments read', { orgId, postId, count: comments.length });\n return { ...page, rows: comments };\n }\n\n private async thread(\n orgId: string,\n rows: Comment[],\n actor: Actor,\n reactionsOn: boolean,\n seesHeld: (actor: Actor) => boolean,\n ): Promise<CommentView[]> {\n const readable = rows.filter((comment) => {\n if (comment.status === 'published' || comment.status === 'removed') {\n return true;\n }\n return seesHeld(actor) || comment.orgUserId === actor.id;\n });\n const replies = readable.filter(\n (comment) => comment.parentId !== null && comment.status !== 'removed',\n );\n const heldOpen = new Set(replies.map((reply) => reply.parentId));\n const roots = readable.filter(\n (comment) =>\n comment.parentId === null && (comment.status !== 'removed' || heldOpen.has(comment.id)),\n );\n const rootIds = new Set(roots.map((root) => root.id));\n const served = [...roots, ...replies.filter((reply) => rootIds.has(reply.parentId!))].sort(\n (a, b) => a.createdAt.getTime() - b.createdAt.getTime(),\n );\n const [authors, reactions] = await Promise.all([\n this.repo.authorsOf(orgId, this.peopleIn(served)),\n reactionsOn\n ? this.repo.reactionsOf(\n orgId,\n served.map((comment) => comment.id),\n actor.id,\n )\n : Promise.resolve<Record<string, CommentReactions>>({}),\n ]);\n const urls = this.fileUrls;\n return served.map((comment) =>\n this.toView(orgId, comment, authors, urls, reactions[comment.id] ?? EMPTY_REACTIONS),\n );\n }\n\n private async visiblePost(\n orgId: string,\n actor: Actor,\n postId: string,\n ): Promise<{ post: Post; space: SpaceDetail }> {\n return this.posts.visible(orgId, actor, postId);\n }\n\n private async postOfComment(\n orgId: string,\n actor: Actor,\n commentId: string,\n postId: string,\n ): Promise<{ post: Post; space: SpaceDetail }> {\n try {\n return await this.visiblePost(orgId, actor, postId);\n } catch (error) {\n if (error instanceof NotFoundError) {\n throw new NotFoundError('Comment', commentId);\n }\n throw error;\n }\n }\n\n private canModerate(actor: Actor): boolean {\n return canModerate(actor.role);\n }\n\n private inCommunity(comment: Comment, communityId: string | null | undefined): boolean {\n return communityId === undefined || comment.communityId === communityId;\n }\n\n private commentsOpen(post: Post, space: SpaceDetail): boolean {\n return post.commentsClosed === null ? space.settings.comments : !post.commentsClosed;\n }\n\n async activityComments(\n orgId: string,\n activityId: string,\n actor: Actor,\n ): Promise<ActivityComments> {\n await this.requireActivityOpen(orgId, activityId, actor);\n const config = await this.resolveConfig(orgId, activityId);\n if (!config.enabled) {\n return { config, comments: [] };\n }\n const rows = await this.repo.listByActivity(orgId, activityId);\n return {\n config,\n comments: await this.thread(orgId, rows, actor, config.reactions, (person) =>\n this.isStaff(person),\n ),\n };\n }\n\n async edit(orgId: string, commentId: string, actor: Actor, body: string): Promise<CommentView> {\n const { comment, config } = await this.loadWithConfig(orgId, commentId, actor);\n if (!config.enabled) {\n throw new ForbiddenError(\n comment.postId\n ? 'Comments are closed on this post'\n : 'Discussion is not open on this activity',\n );\n }\n // Editing is the author's alone — moderators remove, they do not rewrite.\n if (comment.orgUserId !== actor.id) {\n throw new ForbiddenError('You can edit only your own comment');\n }\n if (comment.status === 'removed') {\n throw new ForbiddenError('A removed comment cannot be edited');\n }\n const updated = await this.repo.updateComment(orgId, commentId, {\n body,\n });\n if (!updated) {\n // getComment() proved it existed; a null here means it vanished mid-write.\n // Returning the pre-edit comment would report success and hand back\n // the old body.\n throw new NotFoundError('Comment', commentId);\n }\n return this.renderOne(orgId, updated, actor);\n }\n\n async remove(\n orgId: string,\n commentId: string,\n actor: Actor,\n communityId?: string | null,\n ): Promise<CommentView> {\n return this.renderOne(\n orgId,\n await this.removeComment(orgId, commentId, actor, communityId),\n actor,\n );\n }\n\n private async removeComment(\n orgId: string,\n commentId: string,\n actor: Actor,\n communityId?: string | null,\n ): Promise<Comment> {\n const comment = await this.getComment(orgId, commentId);\n if (!comment || !this.inCommunity(comment, communityId)) {\n this.logger.warn('comment remove rejected — not found', {\n orgId,\n commentId,\n actorId: actor.id,\n });\n throw new NotFoundError('Comment', commentId);\n }\n\n if (comment.postId) {\n await this.postOfComment(orgId, actor, commentId, comment.postId);\n if (comment.orgUserId !== actor.id && !this.canModerate(actor)) {\n throw new ForbiddenError('You cannot remove this comment');\n }\n } else {\n if (!(await this.hasAccess(orgId, this.activityIdOf(comment), actor))) {\n throw new ForbiddenError('You cannot open this activity');\n }\n if (comment.orgUserId !== actor.id && !this.isStaff(actor)) {\n throw new ForbiddenError('You cannot remove this comment');\n }\n }\n\n if (comment.status === 'removed') {\n return comment;\n }\n\n return this.uow.run(async (scope) => {\n const updated = await scope.discussion.updateComment(orgId, commentId, {\n status: 'removed',\n removedBy: actor.id,\n });\n if (!updated) {\n // getComment() proved it existed; a null here means it vanished mid-operation.\n // Roll back rather than emit an event for a transition that didn't happen.\n throw new NotFoundError('Comment', commentId);\n }\n if (comment.orgUserId !== actor.id) {\n await scope.discussion.resolveReportsFor(orgId, commentId, {\n resolvedAt: new Date(),\n resolvedBy: actor.id,\n resolution: 'removed',\n });\n }\n await scope.outbox.append([\n discussionEvents.commentRemoved.make({\n orgId,\n data: updated,\n metadata: updated.postId ? { actorOrgUserId: actor.id } : undefined,\n }),\n ]);\n this.logger.info('comment removed', { orgId, commentId, actorId: actor.id });\n return updated;\n });\n }\n\n async restore(\n orgId: string,\n commentId: string,\n actor: Actor,\n communityId?: string | null,\n ): Promise<CommentView> {\n return this.renderOne(\n orgId,\n await this.restoreComment(orgId, commentId, actor, communityId),\n actor,\n );\n }\n\n private async restoreComment(\n orgId: string,\n commentId: string,\n actor: Actor,\n communityId?: string | null,\n ): Promise<Comment> {\n if (!this.isStaff(actor)) {\n throw new ForbiddenError('You cannot restore this comment');\n }\n const comment = await this.getComment(orgId, commentId);\n if (!comment || !this.inCommunity(comment, communityId)) {\n this.logger.warn('comment restore rejected — not found', {\n orgId,\n commentId,\n actorId: actor.id,\n });\n throw new NotFoundError('Comment', commentId);\n }\n if (comment.status !== 'removed') {\n return comment;\n }\n const updated = await this.repo.updateComment(orgId, commentId, {\n status: 'published',\n removedBy: null,\n });\n if (!updated) {\n // getComment() proved it existed; a null here means it vanished mid-write.\n // Returning the still-removed comment would report success while\n // handing back the removed row.\n throw new NotFoundError('Comment', commentId);\n }\n this.logger.info('comment restored', { orgId, commentId, actorId: actor.id });\n return updated;\n }\n\n /** Load a comment together with its activity's resolved comments config,\n * having proved the actor may reach that activity at all. Every path that\n * acts on one comment needs the pair, and none should read the row twice. */\n private async loadWithConfig(\n orgId: string,\n commentId: string,\n actor: Actor,\n ): Promise<{ comment: Comment; config: CommentsConfig }> {\n const comment = await this.getComment(orgId, commentId);\n if (!comment) {\n throw new NotFoundError('Comment', commentId);\n }\n if (comment.postId) {\n const { post, space } = await this.postOfComment(orgId, actor, commentId, comment.postId);\n const config: CommentsConfig = {\n enabled: this.commentsOpen(post, space),\n threaded: true,\n requireReview: false,\n reactions: post.reactionsEnabled ?? space.settings.reactions,\n };\n return { comment, config };\n }\n const activityId = this.activityIdOf(comment);\n if (!(await this.hasAccess(orgId, activityId, actor))) {\n throw new ForbiddenError('You cannot open this activity');\n }\n const config = await this.resolveConfig(orgId, activityId);\n return { comment, config };\n }\n\n async setReaction(\n orgId: string,\n commentId: string,\n actor: Actor,\n emoji: ReactionEmoji | null,\n ): Promise<CommentReactions> {\n const { comment, config } = await this.loadWithConfig(orgId, commentId, actor);\n if (!config.enabled && !comment.postId) {\n throw new ForbiddenError('Discussion is not open on this activity');\n }\n if (!config.reactions && emoji !== null) {\n throw new ForbiddenError(\n comment.postId ? 'Reactions are off on this post' : 'Reactions are off on this course',\n );\n }\n if (comment.status === 'removed') {\n throw new ForbiddenError('A removed comment cannot be reacted to');\n }\n await this.repo.setReaction(orgId, commentId, actor.id, emoji);\n const stored = await this.repo.reactionsOf(orgId, [commentId], actor.id);\n return stored[commentId] ?? EMPTY_REACTIONS;\n }\n\n async approve(orgId: string, commentId: string, actor: Actor): Promise<Comment> {\n if (!this.isStaff(actor)) {\n throw new ForbiddenError('You cannot approve this comment');\n }\n const comment = await this.getComment(orgId, commentId);\n if (!comment) {\n this.logger.warn('comment approve rejected — not found', {\n orgId,\n commentId,\n actorId: actor.id,\n });\n throw new NotFoundError('Comment', commentId);\n }\n if (comment.status !== 'pending') {\n throw new ForbiddenError('Only a comment awaiting review can be approved');\n }\n return this.uow.run(async (scope) => {\n const updated = await scope.discussion.updateComment(orgId, commentId, {\n status: 'published',\n });\n if (!updated) {\n // getComment() proved it existed; a null here means it vanished mid-operation.\n // Roll back rather than emit an event for a transition that didn't happen.\n throw new NotFoundError('Comment', commentId);\n }\n await scope.outbox.append([discussionEvents.commentPublished.make({ orgId, data: updated })]);\n this.logger.info('comment published', { orgId, commentId, actorId: actor.id });\n return updated;\n });\n }\n\n /** Loads the comment and dispatches to whichever transition applies — the\n * caller must not decide that itself. `approve` and `restore` still enforce\n * their own staff check; the check here covers the third path, where a\n * comment is already published and neither of them would run. */\n async publish(\n orgId: string,\n commentId: string,\n actor: Actor,\n communityId?: string | null,\n ): Promise<Comment> {\n if (!this.isStaff(actor)) {\n throw new ForbiddenError('You cannot publish this comment');\n }\n const comment = await this.getComment(orgId, commentId);\n if (!comment || !this.inCommunity(comment, communityId)) {\n this.logger.warn('comment publish rejected — not found', {\n orgId,\n commentId,\n actorId: actor.id,\n });\n throw new NotFoundError('Comment', commentId);\n }\n if (comment.status === 'pending') {\n return this.approve(orgId, commentId, actor);\n }\n if (comment.status === 'removed') {\n return this.restoreComment(orgId, commentId, actor);\n }\n return comment;\n }\n\n async resolveReports(\n orgId: string,\n commentId: string,\n actor: Actor,\n communityId?: string | null,\n ): Promise<void> {\n const comment = await this.getComment(orgId, commentId);\n if (!comment || !this.inCommunity(comment, communityId)) {\n this.logger.warn('comment reports resolve rejected — not found', {\n orgId,\n commentId,\n actorId: actor.id,\n });\n throw new NotFoundError('Comment', commentId);\n }\n if (comment.postId) {\n await this.postOfComment(orgId, actor, commentId, comment.postId);\n if (!this.canModerate(actor)) {\n throw new ForbiddenError('You cannot work this queue');\n }\n } else if (!this.isStaff(actor)) {\n throw new ForbiddenError('You cannot resolve this report');\n }\n await this.repo.resolveReportsFor(orgId, commentId, {\n resolvedAt: new Date(),\n resolvedBy: actor.id,\n resolution: 'dismissed',\n });\n this.logger.info('comment reports resolved', { orgId, commentId, actorId: actor.id });\n }\n\n async reportComment(\n orgId: string,\n commentId: string,\n actor: Actor,\n reason: string,\n ): Promise<CommentReport> {\n const { comment, config } = await this.loadWithConfig(orgId, commentId, actor);\n // Locked accepts reports — a locked activity can still hold something a\n // moderator needs to see. Hidden does not: nothing in it is being served,\n // so nobody is looking at a comment to flag.\n if (!config.enabled && !comment.postId) {\n throw new ForbiddenError('Discussion is not open on this activity');\n }\n const now = new Date();\n const report: CommentReport = {\n id: genId('commentReport'),\n orgId,\n commentId,\n orgUserId: actor.id,\n reason,\n resolvedAt: null,\n resolution: null,\n resolvedBy: null,\n createdAt: now,\n updatedAt: now,\n };\n return this.uow.run(async (scope) => {\n const saved = await scope.discussion.insertReport(orgId, report);\n if (!saved) {\n // Already reported by this person. Return the report that actually\n // exists — a fabricated id resolves to nothing — and emit no second\n // event, which would double-count any threshold automation.\n const open = await scope.discussion.listOpenReports(orgId, [commentId]);\n const mine = open.find((r) => r.orgUserId === actor.id);\n if (mine) {\n return mine;\n }\n return report;\n }\n await scope.outbox.append([\n discussionEvents.commentReported.make({\n orgId,\n data: { ...saved, communityId: comment.communityId },\n metadata: comment.postId ? { actorOrgUserId: actor.id } : undefined,\n }),\n ]);\n this.logger.info('comment reported', { orgId, commentId, actorId: actor.id });\n return saved;\n });\n }\n\n async listComments(orgId: string, query: ListCommentsQuery): Promise<Page<CommentListItem>> {\n const page = await this.repo.listComments(orgId, query);\n const rows = page.rows.map((r) => r.comment);\n if (rows.length === 0) {\n return { ...page, rows: [] };\n }\n const reports = await this.repo.listOpenReports(\n orgId,\n rows.map((c) => c.id),\n );\n const authors = await this.repo.authorsOf(orgId, [\n ...new Set([...this.peopleIn(rows), ...reports.map((r) => r.orgUserId)]),\n ]);\n const urls = this.fileUrls;\n\n const byComment = new Map<string, CommentReport[]>();\n for (const report of reports) {\n byComment.set(report.commentId, [...(byComment.get(report.commentId) ?? []), report]);\n }\n\n return {\n ...page,\n rows: page.rows.map(({ comment, courseId, activityTitle }) => {\n const author = authors[comment.orgUserId];\n if (!author) {\n throw new NotFoundError('Member', comment.orgUserId);\n }\n return {\n id: comment.id,\n parentId: comment.parentId,\n activityId: this.activityIdOf(comment),\n activityTitle,\n courseId,\n body: comment.body,\n status: comment.status,\n author: authorView(orgId, author, urls),\n authorEmail: author.email,\n removedBy: comment.removedBy,\n reports: (byComment.get(comment.id) ?? []).map((r) => {\n const reporter = authors[r.orgUserId];\n if (!reporter) {\n throw new NotFoundError('Member', r.orgUserId);\n }\n return {\n reporter: authorView(orgId, reporter, urls),\n reason: r.reason,\n createdAt: r.createdAt,\n };\n }),\n createdAt: comment.createdAt,\n updatedAt: comment.updatedAt,\n };\n }),\n };\n }\n\n getComment(orgId: string, commentId: string): Promise<Comment | null> {\n return this.repo.findComment(orgId, commentId);\n }\n\n async sendDigest(orgId: string, now: Date): Promise<void> {\n const communityIds = await this.communities.listIds(orgId);\n if (communityIds.length === 0) {\n this.logger.debug('no communities for digest', { orgId });\n return;\n }\n const period = now.toISOString().slice(0, 10);\n await this.uow.run(async ({ outbox }) => {\n await outbox.append(\n communityIds.map((communityId) =>\n discussionEvents.digestDue.make({\n orgId,\n data: { communityId, period },\n }),\n ),\n );\n });\n this.logger.info('digest due', { orgId, communities: communityIds.length, period });\n }\n}\n"],"mappings":";;;;;;;;;;AAoCA,MAAa,mBAAmB;CAC9B,SAAS;CACT,UAAU;CACV,eAAe;CACf,WAAW;AACb;AAEA,MAAM,kBAAoC,EAAE,WAAW,CAAC,EAAE;AAc1D,IAAa,wBAAb,MAAgE;CAC9D;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA,YAAY,QAAiC;EAC3C,KAAK,OAAO,OAAO;EACnB,KAAK,SAAS,OAAO;EACrB,KAAK,UAAU,OAAO;EACtB,KAAK,QAAQ,OAAO;EACpB,KAAK,MAAM,OAAO;EAClB,KAAK,WAAW,OAAO;EACvB,KAAK,cAAc,OAAO;EAC1B,KAAK,WAAW,OAAO;EACvB,KAAK,SAAS,OAAO,UAAU;CACjC;CAEA,MAAM,cAAc,OAAe,YAA6C;EAC9E,MAAM,WAAW,MAAM,KAAK,QAAQ,YAAY,OAAO,UAAU;EACjE,IAAI,CAAC,UACH,MAAM,IAAI,cAAc,YAAY,UAAU;EAKhD,MAAM,SAAS,MAAM,KAAK,SAAS,IACjC,OAAA,WAEA,SAAS,QACX;EACA,MAAM,WAAW;GAAE,GAAG;GAAkB,GAAG,QAAQ;EAAS;EAG5D,MAAM,QAAS,UAAU,YAAY,CAAC,EAAA,CAAwB,YAAY;EAC1E,OAAO;GACL,SAAS,SAAS,WAAW,SAAS;GACtC,UAAU,SAAS;GACnB,eAAe,SAAS;GACxB,WAAW,SAAS;EACtB;CACF;CAEA,QAAgB,OAAuB;EACrC,OAAO,YAAY,MAAM,IAAI;CAC/B;CAEA,OACE,OACA,SACA,SACA,MACA,WACa;EACb,MAAM,SAAS,QAAQ,QAAQ;EAC/B,IAAI,CAAC,QACH,MAAM,IAAI,cAAc,UAAU,QAAQ,SAAS;EAErD,IAAI,QAAQ,aAAa,CAAC,QAAQ,QAAQ,YACxC,MAAM,IAAI,cAAc,UAAU,QAAQ,SAAS;EAErD,MAAM,UAAU,QAAQ,YAAY,QAAQ,QAAQ,aAAa,KAAA;EACjE,OAAO;GACL,IAAI,QAAQ;GACZ,YAAY,QAAQ;GACpB,QAAQ,QAAQ;GAChB,aAAa,QAAQ;GACrB,UAAU,QAAQ;GAClB,QAAQ,WAAW,OAAO,QAAQ,IAAI;GACtC,MAAM,QAAQ,WAAW,YAAY,OAAO,QAAQ;GACpD,QAAQ,QAAQ;GAChB,WAAW,UAAU,WAAW,OAAO,SAAS,IAAI,IAAI;GACxD,WAAW,UAAU;GACrB,gBAAgB,UAAU;GAC1B,WAAW,QAAQ;GACnB,WAAW,QAAQ;EACrB;CACF;CAEA,SAAiB,UAA+B;EAC9C,MAAM,sBAAM,IAAI,IAAY;EAC5B,KAAK,MAAM,KAAK,UAAU;GACxB,IAAI,IAAI,EAAE,SAAS;GACnB,IAAI,EAAE,WACJ,IAAI,IAAI,EAAE,SAAS;EAEvB;EACA,OAAO,CAAC,GAAG,GAAG;CAChB;CAEA,MAAc,UAAU,OAAe,SAAkB,OAAoC;EAC3F,MAAM,UAAU,MAAM,KAAK,KAAK,UAAU,OAAO,KAAK,SAAS,CAAC,OAAO,CAAC,CAAC;EACzE,MAAM,SACJ,QAAQ,WAAW,YACf,mBACE,MAAM,KAAK,KAAK,YAAY,OAAO,CAAC,QAAQ,EAAE,GAAG,MAAM,EAAE,EAAA,CAAG,QAAQ,OACtE;EACN,OAAO,KAAK,OAAO,OAAO,SAAS,SAAS,KAAK,UAAU,MAAM;CACnE;CAEA,aAAqB,SAA0B;EAC7C,IAAI,QAAQ,eAAe,MACzB,MAAM,IAAI,cAAc,YAAY,QAAQ,EAAE;EAEhD,OAAO,QAAQ;CACjB;CAEA,MAAc,UAAU,OAAe,YAAoB,OAAgC;EACzF,IAAI,KAAK,QAAQ,KAAK,GACpB,OAAO;EAET,MAAM,WAAW,MAAM,KAAK,QAAQ,YAAY,OAAO,UAAU;EACjE,OAAO,WAAW,KAAK,OAAO,UAAU,OAAO,MAAM,IAAI,SAAS,QAAQ,IAAI;CAChF;CAEA,MAAc,oBACZ,OACA,YACA,OACe;EACf,MAAM,WAAW,MAAM,KAAK,QAAQ,YAAY,OAAO,UAAU;EACjE,IAAI,CAAC,UACH,MAAM,IAAI,cAAc,YAAY,UAAU;EAEhD,IACE,CAAC,KAAK,QAAQ,KAAK,KACnB,CAAE,MAAM,KAAK,OAAO,UAAU,OAAO,MAAM,IAAI,SAAS,QAAQ,GAEhE,MAAM,IAAI,eAAe,+BAA+B;CAE5D;CAEA,MAAM,YAAY,OAAe,OAA+C;EAC9E,IAAI,MAAM,QACR,OAAO,KAAK,WAAW,OAAO,MAAM,QAAQ,KAAK;EAEnD,MAAM,aAAa,MAAM;EACzB,IAAI,CAAC,YACH,MAAM,IAAI,cAAc,YAAY,cAAc,EAAE;EAEtD,OAAO,KAAK,eAAe,OAAO,YAAY,KAAK;CACrD;CAEA,MAAc,eACZ,OACA,YACA,EAAE,OAAO,UAAU,QACG;EACtB,MAAM,KAAK,oBAAoB,OAAO,YAAY,KAAK;EACvD,MAAM,SAAS,MAAM,KAAK,cAAc,OAAO,UAAU;EAEzD,IAAI,CAAC,OAAO,SACV,MAAM,IAAI,eAAe,yCAAyC;EAGpE,IAAI,aAAa,MAAM;GACrB,IAAI,CAAC,OAAO,UACV,MAAM,IAAI,eAAe,gCAAgC;GAE3D,MAAM,SAAS,MAAM,KAAK,KAAK,YAAY,OAAO,QAAQ;GAC1D,IAAI,CAAC,UAAU,OAAO,eAAe,YACnC,MAAM,IAAI,cAAc,WAAW,QAAQ;GAG7C,IAAI,OAAO,aAAa,MACtB,MAAM,IAAI,eAAe,wCAAwC;GAGnE,IAAI,OAAO,WAAW,aACpB,MAAM,IAAI,eAAe,2CAA2C;EAExE;EAEA,MAAM,SACJ,OAAO,iBAAiB,CAAC,KAAK,QAAQ,KAAK,IAAI,YAAY;EAE7D,MAAM,UAAmB;GACvB,IAAI,MAAM,SAAS;GACnB;GACA;GACA,QAAQ;GACR,aAAa;GACb;GACA,WAAW,MAAM;GACjB;GACA;GACA,WAAW;GACX,2BAAW,IAAI,KAAK;GACpB,2BAAW,IAAI,KAAK;EACtB;EACA,MAAM,QAAQ,MAAM,KAAK,IAAI,IAAI,OAAO,UAAU;GAChD,MAAM,MAAM,MAAM,MAAM,WAAW,cAAc,OAAO,OAAO;GAC/D,MAAM,MAAM,OAAO,OAAO,CAAC,iBAAiB,eAAe,KAAK;IAAE;IAAO,MAAM;GAAI,CAAC,CAAC,CAAC;GACtF,KAAK,OAAO,KAAK,mBAAmB;IAAE;IAAO,WAAW,IAAI;IAAI;IAAQ,SAAS,MAAM;GAAG,CAAC;GAC3F,OAAO;EACT,CAAC;EACD,OAAO,KAAK,UAAU,OAAO,OAAO,KAAK;CAC3C;CAEA,MAAc,WACZ,OACA,QACA,EAAE,OAAO,UAAU,QACG;EACtB,MAAM,EAAE,MAAM,UAAU,MAAM,KAAK,YAAY,OAAO,OAAO,MAAM;EACnE,IAAI,KAAK,WAAW,aAClB,MAAM,IAAI,cAAc,4BAA4B;EAEtD,IAAI,CAAC,KAAK,aAAa,MAAM,KAAK,GAChC,MAAM,IAAI,eAAe,kCAAkC;EAE7D,IAAI,aAAa,MAAM;GACrB,MAAM,SAAS,MAAM,KAAK,KAAK,YAAY,OAAO,QAAQ;GAC1D,IAAI,CAAC,UAAU,OAAO,WAAW,QAC/B,MAAM,IAAI,cAAc,WAAW,QAAQ;GAE7C,IAAI,OAAO,aAAa,MACtB,MAAM,IAAI,eAAe,wCAAwC;GAEnE,IAAI,OAAO,WAAW,aACpB,MAAM,IAAI,eAAe,2CAA2C;EAExE;EACA,MAAM,sBAAM,IAAI,KAAK;EACrB,MAAM,UAAmB;GACvB,IAAI,MAAM,SAAS;GACnB;GACA,YAAY;GACZ;GACA,aAAa,KAAK;GAClB;GACA,WAAW,MAAM;GACjB;GACA,QAAQ;GACR,WAAW;GACX,WAAW;GACX,WAAW;EACb;EACA,MAAM,QAAQ,MAAM,KAAK,IAAI,IAAI,OAAO,UAAU;GAChD,MAAM,MAAM,MAAM,MAAM,WAAW,cAAc,OAAO,OAAO;GAC/D,MAAM,MAAM,OAAO,OAAO,CACxB,iBAAiB,eAAe,KAAK;IACnC;IACA,MAAM;IACN,UAAU,EAAE,gBAAgB,MAAM,GAAG;GACvC,CAAC,CACH,CAAC;GACD,KAAK,OAAO,KAAK,mBAAmB;IAClC;IACA,WAAW,IAAI;IACf;IACA,QAAQ;IACR,SAAS,MAAM;GACjB,CAAC;GACD,OAAO;EACT,CAAC;EACD,OAAO,KAAK,UAAU,OAAO,OAAO,KAAK;CAC3C;CAEA,MAAM,aACJ,OACA,QACA,OACA,OAC4B;EAC5B,MAAM,EAAE,MAAM,UAAU,MAAM,KAAK,YAAY,OAAO,OAAO,MAAM;EACnE,MAAM,YAAY,WAAkB,KAAK,YAAY,MAAM;EAC3D,MAAM,OAAO,MAAM,KAAK,KAAK,kBAAkB,OAAO,QAAQ;GAC5D,GAAG;GACH,UAAU,MAAM;GAChB,UAAU,SAAS,KAAK;EAC1B,CAAC;EACD,MAAM,WAAW,MAAM,KAAK,OAC1B,OACA,KAAK,MACL,OACA,KAAK,oBAAoB,MAAM,SAAS,WACxC,QACF;EACA,KAAK,OAAO,MAAM,sBAAsB;GAAE;GAAO;GAAQ,OAAO,SAAS;EAAO,CAAC;EACjF,OAAO;GAAE,GAAG;GAAM,MAAM;EAAS;CACnC;CAEA,MAAc,OACZ,OACA,MACA,OACA,aACA,UACwB;EACxB,MAAM,WAAW,KAAK,QAAQ,YAAY;GACxC,IAAI,QAAQ,WAAW,eAAe,QAAQ,WAAW,WACvD,OAAO;GAET,OAAO,SAAS,KAAK,KAAK,QAAQ,cAAc,MAAM;EACxD,CAAC;EACD,MAAM,UAAU,SAAS,QACtB,YAAY,QAAQ,aAAa,QAAQ,QAAQ,WAAW,SAC/D;EACA,MAAM,WAAW,IAAI,IAAI,QAAQ,KAAK,UAAU,MAAM,QAAQ,CAAC;EAC/D,MAAM,QAAQ,SAAS,QACpB,YACC,QAAQ,aAAa,SAAS,QAAQ,WAAW,aAAa,SAAS,IAAI,QAAQ,EAAE,EACzF;EACA,MAAM,UAAU,IAAI,IAAI,MAAM,KAAK,SAAS,KAAK,EAAE,CAAC;EACpD,MAAM,SAAS,CAAC,GAAG,OAAO,GAAG,QAAQ,QAAQ,UAAU,QAAQ,IAAI,MAAM,QAAS,CAAC,CAAC,CAAC,CAAC,MACnF,GAAG,MAAM,EAAE,UAAU,QAAQ,IAAI,EAAE,UAAU,QAAQ,CACxD;EACA,MAAM,CAAC,SAAS,aAAa,MAAM,QAAQ,IAAI,CAC7C,KAAK,KAAK,UAAU,OAAO,KAAK,SAAS,MAAM,CAAC,GAChD,cACI,KAAK,KAAK,YACR,OACA,OAAO,KAAK,YAAY,QAAQ,EAAE,GAClC,MAAM,EACR,IACA,QAAQ,QAA0C,CAAC,CAAC,CAC1D,CAAC;EACD,MAAM,OAAO,KAAK;EAClB,OAAO,OAAO,KAAK,YACjB,KAAK,OAAO,OAAO,SAAS,SAAS,MAAM,UAAU,QAAQ,OAAO,eAAe,CACrF;CACF;CAEA,MAAc,YACZ,OACA,OACA,QAC6C;EAC7C,OAAO,KAAK,MAAM,QAAQ,OAAO,OAAO,MAAM;CAChD;CAEA,MAAc,cACZ,OACA,OACA,WACA,QAC6C;EAC7C,IAAI;GACF,OAAO,MAAM,KAAK,YAAY,OAAO,OAAO,MAAM;EACpD,SAAS,OAAO;GACd,IAAI,iBAAiB,eACnB,MAAM,IAAI,cAAc,WAAW,SAAS;GAE9C,MAAM;EACR;CACF;CAEA,YAAoB,OAAuB;EACzC,OAAO,YAAY,MAAM,IAAI;CAC/B;CAEA,YAAoB,SAAkB,aAAiD;EACrF,OAAO,gBAAgB,KAAA,KAAa,QAAQ,gBAAgB;CAC9D;CAEA,aAAqB,MAAY,OAA6B;EAC5D,OAAO,KAAK,mBAAmB,OAAO,MAAM,SAAS,WAAW,CAAC,KAAK;CACxE;CAEA,MAAM,iBACJ,OACA,YACA,OAC2B;EAC3B,MAAM,KAAK,oBAAoB,OAAO,YAAY,KAAK;EACvD,MAAM,SAAS,MAAM,KAAK,cAAc,OAAO,UAAU;EACzD,IAAI,CAAC,OAAO,SACV,OAAO;GAAE;GAAQ,UAAU,CAAC;EAAE;EAEhC,MAAM,OAAO,MAAM,KAAK,KAAK,eAAe,OAAO,UAAU;EAC7D,OAAO;GACL;GACA,UAAU,MAAM,KAAK,OAAO,OAAO,MAAM,OAAO,OAAO,YAAY,WACjE,KAAK,QAAQ,MAAM,CACrB;EACF;CACF;CAEA,MAAM,KAAK,OAAe,WAAmB,OAAc,MAAoC;EAC7F,MAAM,EAAE,SAAS,WAAW,MAAM,KAAK,eAAe,OAAO,WAAW,KAAK;EAC7E,IAAI,CAAC,OAAO,SACV,MAAM,IAAI,eACR,QAAQ,SACJ,qCACA,yCACN;EAGF,IAAI,QAAQ,cAAc,MAAM,IAC9B,MAAM,IAAI,eAAe,oCAAoC;EAE/D,IAAI,QAAQ,WAAW,WACrB,MAAM,IAAI,eAAe,oCAAoC;EAE/D,MAAM,UAAU,MAAM,KAAK,KAAK,cAAc,OAAO,WAAW,EAC9D,KACF,CAAC;EACD,IAAI,CAAC,SAIH,MAAM,IAAI,cAAc,WAAW,SAAS;EAE9C,OAAO,KAAK,UAAU,OAAO,SAAS,KAAK;CAC7C;CAEA,MAAM,OACJ,OACA,WACA,OACA,aACsB;EACtB,OAAO,KAAK,UACV,OACA,MAAM,KAAK,cAAc,OAAO,WAAW,OAAO,WAAW,GAC7D,KACF;CACF;CAEA,MAAc,cACZ,OACA,WACA,OACA,aACkB;EAClB,MAAM,UAAU,MAAM,KAAK,WAAW,OAAO,SAAS;EACtD,IAAI,CAAC,WAAW,CAAC,KAAK,YAAY,SAAS,WAAW,GAAG;GACvD,KAAK,OAAO,KAAK,uCAAuC;IACtD;IACA;IACA,SAAS,MAAM;GACjB,CAAC;GACD,MAAM,IAAI,cAAc,WAAW,SAAS;EAC9C;EAEA,IAAI,QAAQ,QAAQ;GAClB,MAAM,KAAK,cAAc,OAAO,OAAO,WAAW,QAAQ,MAAM;GAChE,IAAI,QAAQ,cAAc,MAAM,MAAM,CAAC,KAAK,YAAY,KAAK,GAC3D,MAAM,IAAI,eAAe,gCAAgC;EAE7D,OAAO;GACL,IAAI,CAAE,MAAM,KAAK,UAAU,OAAO,KAAK,aAAa,OAAO,GAAG,KAAK,GACjE,MAAM,IAAI,eAAe,+BAA+B;GAE1D,IAAI,QAAQ,cAAc,MAAM,MAAM,CAAC,KAAK,QAAQ,KAAK,GACvD,MAAM,IAAI,eAAe,gCAAgC;EAE7D;EAEA,IAAI,QAAQ,WAAW,WACrB,OAAO;EAGT,OAAO,KAAK,IAAI,IAAI,OAAO,UAAU;GACnC,MAAM,UAAU,MAAM,MAAM,WAAW,cAAc,OAAO,WAAW;IACrE,QAAQ;IACR,WAAW,MAAM;GACnB,CAAC;GACD,IAAI,CAAC,SAGH,MAAM,IAAI,cAAc,WAAW,SAAS;GAE9C,IAAI,QAAQ,cAAc,MAAM,IAC9B,MAAM,MAAM,WAAW,kBAAkB,OAAO,WAAW;IACzD,4BAAY,IAAI,KAAK;IACrB,YAAY,MAAM;IAClB,YAAY;GACd,CAAC;GAEH,MAAM,MAAM,OAAO,OAAO,CACxB,iBAAiB,eAAe,KAAK;IACnC;IACA,MAAM;IACN,UAAU,QAAQ,SAAS,EAAE,gBAAgB,MAAM,GAAG,IAAI,KAAA;GAC5D,CAAC,CACH,CAAC;GACD,KAAK,OAAO,KAAK,mBAAmB;IAAE;IAAO;IAAW,SAAS,MAAM;GAAG,CAAC;GAC3E,OAAO;EACT,CAAC;CACH;CAEA,MAAM,QACJ,OACA,WACA,OACA,aACsB;EACtB,OAAO,KAAK,UACV,OACA,MAAM,KAAK,eAAe,OAAO,WAAW,OAAO,WAAW,GAC9D,KACF;CACF;CAEA,MAAc,eACZ,OACA,WACA,OACA,aACkB;EAClB,IAAI,CAAC,KAAK,QAAQ,KAAK,GACrB,MAAM,IAAI,eAAe,iCAAiC;EAE5D,MAAM,UAAU,MAAM,KAAK,WAAW,OAAO,SAAS;EACtD,IAAI,CAAC,WAAW,CAAC,KAAK,YAAY,SAAS,WAAW,GAAG;GACvD,KAAK,OAAO,KAAK,wCAAwC;IACvD;IACA;IACA,SAAS,MAAM;GACjB,CAAC;GACD,MAAM,IAAI,cAAc,WAAW,SAAS;EAC9C;EACA,IAAI,QAAQ,WAAW,WACrB,OAAO;EAET,MAAM,UAAU,MAAM,KAAK,KAAK,cAAc,OAAO,WAAW;GAC9D,QAAQ;GACR,WAAW;EACb,CAAC;EACD,IAAI,CAAC,SAIH,MAAM,IAAI,cAAc,WAAW,SAAS;EAE9C,KAAK,OAAO,KAAK,oBAAoB;GAAE;GAAO;GAAW,SAAS,MAAM;EAAG,CAAC;EAC5E,OAAO;CACT;;;;CAKA,MAAc,eACZ,OACA,WACA,OACuD;EACvD,MAAM,UAAU,MAAM,KAAK,WAAW,OAAO,SAAS;EACtD,IAAI,CAAC,SACH,MAAM,IAAI,cAAc,WAAW,SAAS;EAE9C,IAAI,QAAQ,QAAQ;GAClB,MAAM,EAAE,MAAM,UAAU,MAAM,KAAK,cAAc,OAAO,OAAO,WAAW,QAAQ,MAAM;GAOxF,OAAO;IAAE;IAAS,QAAA;KALhB,SAAS,KAAK,aAAa,MAAM,KAAK;KACtC,UAAU;KACV,eAAe;KACf,WAAW,KAAK,oBAAoB,MAAM,SAAS;IAE9B;GAAE;EAC3B;EACA,MAAM,aAAa,KAAK,aAAa,OAAO;EAC5C,IAAI,CAAE,MAAM,KAAK,UAAU,OAAO,YAAY,KAAK,GACjD,MAAM,IAAI,eAAe,+BAA+B;EAG1D,OAAO;GAAE;GAAS,QAAA,MADG,KAAK,cAAc,OAAO,UAAU;EAChC;CAC3B;CAEA,MAAM,YACJ,OACA,WACA,OACA,OAC2B;EAC3B,MAAM,EAAE,SAAS,WAAW,MAAM,KAAK,eAAe,OAAO,WAAW,KAAK;EAC7E,IAAI,CAAC,OAAO,WAAW,CAAC,QAAQ,QAC9B,MAAM,IAAI,eAAe,yCAAyC;EAEpE,IAAI,CAAC,OAAO,aAAa,UAAU,MACjC,MAAM,IAAI,eACR,QAAQ,SAAS,mCAAmC,kCACtD;EAEF,IAAI,QAAQ,WAAW,WACrB,MAAM,IAAI,eAAe,wCAAwC;EAEnE,MAAM,KAAK,KAAK,YAAY,OAAO,WAAW,MAAM,IAAI,KAAK;EAE7D,QAAO,MADc,KAAK,KAAK,YAAY,OAAO,CAAC,SAAS,GAAG,MAAM,EAAE,EAAA,CACzD,cAAc;CAC9B;CAEA,MAAM,QAAQ,OAAe,WAAmB,OAAgC;EAC9E,IAAI,CAAC,KAAK,QAAQ,KAAK,GACrB,MAAM,IAAI,eAAe,iCAAiC;EAE5D,MAAM,UAAU,MAAM,KAAK,WAAW,OAAO,SAAS;EACtD,IAAI,CAAC,SAAS;GACZ,KAAK,OAAO,KAAK,wCAAwC;IACvD;IACA;IACA,SAAS,MAAM;GACjB,CAAC;GACD,MAAM,IAAI,cAAc,WAAW,SAAS;EAC9C;EACA,IAAI,QAAQ,WAAW,WACrB,MAAM,IAAI,eAAe,gDAAgD;EAE3E,OAAO,KAAK,IAAI,IAAI,OAAO,UAAU;GACnC,MAAM,UAAU,MAAM,MAAM,WAAW,cAAc,OAAO,WAAW,EACrE,QAAQ,YACV,CAAC;GACD,IAAI,CAAC,SAGH,MAAM,IAAI,cAAc,WAAW,SAAS;GAE9C,MAAM,MAAM,OAAO,OAAO,CAAC,iBAAiB,iBAAiB,KAAK;IAAE;IAAO,MAAM;GAAQ,CAAC,CAAC,CAAC;GAC5F,KAAK,OAAO,KAAK,qBAAqB;IAAE;IAAO;IAAW,SAAS,MAAM;GAAG,CAAC;GAC7E,OAAO;EACT,CAAC;CACH;;;;;CAMA,MAAM,QACJ,OACA,WACA,OACA,aACkB;EAClB,IAAI,CAAC,KAAK,QAAQ,KAAK,GACrB,MAAM,IAAI,eAAe,iCAAiC;EAE5D,MAAM,UAAU,MAAM,KAAK,WAAW,OAAO,SAAS;EACtD,IAAI,CAAC,WAAW,CAAC,KAAK,YAAY,SAAS,WAAW,GAAG;GACvD,KAAK,OAAO,KAAK,wCAAwC;IACvD;IACA;IACA,SAAS,MAAM;GACjB,CAAC;GACD,MAAM,IAAI,cAAc,WAAW,SAAS;EAC9C;EACA,IAAI,QAAQ,WAAW,WACrB,OAAO,KAAK,QAAQ,OAAO,WAAW,KAAK;EAE7C,IAAI,QAAQ,WAAW,WACrB,OAAO,KAAK,eAAe,OAAO,WAAW,KAAK;EAEpD,OAAO;CACT;CAEA,MAAM,eACJ,OACA,WACA,OACA,aACe;EACf,MAAM,UAAU,MAAM,KAAK,WAAW,OAAO,SAAS;EACtD,IAAI,CAAC,WAAW,CAAC,KAAK,YAAY,SAAS,WAAW,GAAG;GACvD,KAAK,OAAO,KAAK,gDAAgD;IAC/D;IACA;IACA,SAAS,MAAM;GACjB,CAAC;GACD,MAAM,IAAI,cAAc,WAAW,SAAS;EAC9C;EACA,IAAI,QAAQ,QAAQ;GAClB,MAAM,KAAK,cAAc,OAAO,OAAO,WAAW,QAAQ,MAAM;GAChE,IAAI,CAAC,KAAK,YAAY,KAAK,GACzB,MAAM,IAAI,eAAe,4BAA4B;EAEzD,OAAO,IAAI,CAAC,KAAK,QAAQ,KAAK,GAC5B,MAAM,IAAI,eAAe,gCAAgC;EAE3D,MAAM,KAAK,KAAK,kBAAkB,OAAO,WAAW;GAClD,4BAAY,IAAI,KAAK;GACrB,YAAY,MAAM;GAClB,YAAY;EACd,CAAC;EACD,KAAK,OAAO,KAAK,4BAA4B;GAAE;GAAO;GAAW,SAAS,MAAM;EAAG,CAAC;CACtF;CAEA,MAAM,cACJ,OACA,WACA,OACA,QACwB;EACxB,MAAM,EAAE,SAAS,WAAW,MAAM,KAAK,eAAe,OAAO,WAAW,KAAK;EAI7E,IAAI,CAAC,OAAO,WAAW,CAAC,QAAQ,QAC9B,MAAM,IAAI,eAAe,yCAAyC;EAEpE,MAAM,sBAAM,IAAI,KAAK;EACrB,MAAM,SAAwB;GAC5B,IAAI,MAAM,eAAe;GACzB;GACA;GACA,WAAW,MAAM;GACjB;GACA,YAAY;GACZ,YAAY;GACZ,YAAY;GACZ,WAAW;GACX,WAAW;EACb;EACA,OAAO,KAAK,IAAI,IAAI,OAAO,UAAU;GACnC,MAAM,QAAQ,MAAM,MAAM,WAAW,aAAa,OAAO,MAAM;GAC/D,IAAI,CAAC,OAAO;IAKV,MAAM,QAAO,MADM,MAAM,WAAW,gBAAgB,OAAO,CAAC,SAAS,CAAC,EAAA,CACpD,MAAM,MAAM,EAAE,cAAc,MAAM,EAAE;IACtD,IAAI,MACF,OAAO;IAET,OAAO;GACT;GACA,MAAM,MAAM,OAAO,OAAO,CACxB,iBAAiB,gBAAgB,KAAK;IACpC;IACA,MAAM;KAAE,GAAG;KAAO,aAAa,QAAQ;IAAY;IACnD,UAAU,QAAQ,SAAS,EAAE,gBAAgB,MAAM,GAAG,IAAI,KAAA;GAC5D,CAAC,CACH,CAAC;GACD,KAAK,OAAO,KAAK,oBAAoB;IAAE;IAAO;IAAW,SAAS,MAAM;GAAG,CAAC;GAC5E,OAAO;EACT,CAAC;CACH;CAEA,MAAM,aAAa,OAAe,OAA0D;EAC1F,MAAM,OAAO,MAAM,KAAK,KAAK,aAAa,OAAO,KAAK;EACtD,MAAM,OAAO,KAAK,KAAK,KAAK,MAAM,EAAE,OAAO;EAC3C,IAAI,KAAK,WAAW,GAClB,OAAO;GAAE,GAAG;GAAM,MAAM,CAAC;EAAE;EAE7B,MAAM,UAAU,MAAM,KAAK,KAAK,gBAC9B,OACA,KAAK,KAAK,MAAM,EAAE,EAAE,CACtB;EACA,MAAM,UAAU,MAAM,KAAK,KAAK,UAAU,OAAO,CAC/C,mBAAG,IAAI,IAAI,CAAC,GAAG,KAAK,SAAS,IAAI,GAAG,GAAG,QAAQ,KAAK,MAAM,EAAE,SAAS,CAAC,CAAC,CACzE,CAAC;EACD,MAAM,OAAO,KAAK;EAElB,MAAM,4BAAY,IAAI,IAA6B;EACnD,KAAK,MAAM,UAAU,SACnB,UAAU,IAAI,OAAO,WAAW,CAAC,GAAI,UAAU,IAAI,OAAO,SAAS,KAAK,CAAC,GAAI,MAAM,CAAC;EAGtF,OAAO;GACL,GAAG;GACH,MAAM,KAAK,KAAK,KAAK,EAAE,SAAS,UAAU,oBAAoB;IAC5D,MAAM,SAAS,QAAQ,QAAQ;IAC/B,IAAI,CAAC,QACH,MAAM,IAAI,cAAc,UAAU,QAAQ,SAAS;IAErD,OAAO;KACL,IAAI,QAAQ;KACZ,UAAU,QAAQ;KAClB,YAAY,KAAK,aAAa,OAAO;KACrC;KACA;KACA,MAAM,QAAQ;KACd,QAAQ,QAAQ;KAChB,QAAQ,WAAW,OAAO,QAAQ,IAAI;KACtC,aAAa,OAAO;KACpB,WAAW,QAAQ;KACnB,UAAU,UAAU,IAAI,QAAQ,EAAE,KAAK,CAAC,EAAA,CAAG,KAAK,MAAM;MACpD,MAAM,WAAW,QAAQ,EAAE;MAC3B,IAAI,CAAC,UACH,MAAM,IAAI,cAAc,UAAU,EAAE,SAAS;MAE/C,OAAO;OACL,UAAU,WAAW,OAAO,UAAU,IAAI;OAC1C,QAAQ,EAAE;OACV,WAAW,EAAE;MACf;KACF,CAAC;KACD,WAAW,QAAQ;KACnB,WAAW,QAAQ;IACrB;GACF,CAAC;EACH;CACF;CAEA,WAAW,OAAe,WAA4C;EACpE,OAAO,KAAK,KAAK,YAAY,OAAO,SAAS;CAC/C;CAEA,MAAM,WAAW,OAAe,KAA0B;EACxD,MAAM,eAAe,MAAM,KAAK,YAAY,QAAQ,KAAK;EACzD,IAAI,aAAa,WAAW,GAAG;GAC7B,KAAK,OAAO,MAAM,6BAA6B,EAAE,MAAM,CAAC;GACxD;EACF;EACA,MAAM,SAAS,IAAI,YAAY,CAAC,CAAC,MAAM,GAAG,EAAE;EAC5C,MAAM,KAAK,IAAI,IAAI,OAAO,EAAE,aAAa;GACvC,MAAM,OAAO,OACX,aAAa,KAAK,gBAChB,iBAAiB,UAAU,KAAK;IAC9B;IACA,MAAM;KAAE;KAAa;IAAO;GAC9B,CAAC,CACH,CACF;EACF,CAAC;EACD,KAAK,OAAO,KAAK,cAAc;GAAE;GAAO,aAAa,aAAa;GAAQ;EAAO,CAAC;CACpF;AACF"}
@@ -6,84 +6,335 @@ declare const entitlementEvents: {
6
6
  orgId: string;
7
7
  id: string;
8
8
  orgUserId: string;
9
- contentId: string;
9
+ bundleId: string | null;
10
+ contentId: string | null;
10
11
  status: "active" | "revoked";
11
12
  source: string;
12
13
  grantedAt: Date;
13
14
  expiresAt: Date | null;
15
+ createdAt: Date;
16
+ updatedAt: Date;
14
17
  }, "entitlement.created", 1>, {
15
18
  orgId: string;
16
19
  id: string;
17
20
  orgUserId: string;
18
- contentId: string;
21
+ bundleId: string | null;
22
+ contentId: string | null;
19
23
  status: "active" | "revoked";
20
24
  source: string;
21
25
  grantedAt: unknown;
22
26
  expiresAt: unknown;
23
- }>;
27
+ createdAt: unknown;
28
+ updatedAt: unknown;
29
+ }, import("zod").ZodObject<{
30
+ orgId: import("zod").ZodString;
31
+ id: import("zod").ZodString;
32
+ source: import("zod").ZodString;
33
+ status: import("zod").ZodEnum<{
34
+ active: "active";
35
+ revoked: "revoked";
36
+ }>;
37
+ createdAt: import("zod").ZodCoercedDate<unknown>;
38
+ updatedAt: import("zod").ZodCoercedDate<unknown>;
39
+ bundleId: import("zod").ZodNullable<import("zod").ZodString>;
40
+ contentId: import("zod").ZodNullable<import("zod").ZodString>;
41
+ orgUserId: import("zod").ZodString;
42
+ grantedAt: import("zod").ZodCoercedDate<unknown>;
43
+ expiresAt: import("zod").ZodNullable<import("zod").ZodCoercedDate<unknown>>;
44
+ }, import("zod/v4/core").$strip>>;
24
45
  entitlementUpdated: RuntimeEventDefinition<VersionedDomainEvent<{
25
46
  orgId: string;
26
47
  id: string;
27
48
  orgUserId: string;
28
- contentId: string;
49
+ bundleId: string | null;
50
+ contentId: string | null;
29
51
  status: "active" | "revoked";
30
52
  source: string;
31
53
  grantedAt: Date;
32
54
  expiresAt: Date | null;
55
+ createdAt: Date;
56
+ updatedAt: Date;
33
57
  }, "entitlement.updated", 1>, {
34
58
  orgId: string;
35
59
  id: string;
36
60
  orgUserId: string;
37
- contentId: string;
61
+ bundleId: string | null;
62
+ contentId: string | null;
38
63
  status: "active" | "revoked";
39
64
  source: string;
40
65
  grantedAt: unknown;
41
66
  expiresAt: unknown;
42
- }>;
67
+ createdAt: unknown;
68
+ updatedAt: unknown;
69
+ }, import("zod").ZodObject<{
70
+ orgId: import("zod").ZodString;
71
+ id: import("zod").ZodString;
72
+ source: import("zod").ZodString;
73
+ status: import("zod").ZodEnum<{
74
+ active: "active";
75
+ revoked: "revoked";
76
+ }>;
77
+ createdAt: import("zod").ZodCoercedDate<unknown>;
78
+ updatedAt: import("zod").ZodCoercedDate<unknown>;
79
+ bundleId: import("zod").ZodNullable<import("zod").ZodString>;
80
+ contentId: import("zod").ZodNullable<import("zod").ZodString>;
81
+ orgUserId: import("zod").ZodString;
82
+ grantedAt: import("zod").ZodCoercedDate<unknown>;
83
+ expiresAt: import("zod").ZodNullable<import("zod").ZodCoercedDate<unknown>>;
84
+ }, import("zod/v4/core").$strip>>;
43
85
  entitlementDeleted: RuntimeEventDefinition<VersionedDomainEvent<{
44
86
  orgId: string;
45
87
  id: string;
46
88
  orgUserId: string;
47
- contentId: string;
89
+ bundleId: string | null;
90
+ contentId: string | null;
48
91
  status: "active" | "revoked";
49
92
  source: string;
50
93
  grantedAt: Date;
51
94
  expiresAt: Date | null;
95
+ createdAt: Date;
96
+ updatedAt: Date;
52
97
  }, "entitlement.deleted", 1>, {
53
98
  orgId: string;
54
99
  id: string;
55
100
  orgUserId: string;
56
- contentId: string;
101
+ bundleId: string | null;
102
+ contentId: string | null;
57
103
  status: "active" | "revoked";
58
104
  source: string;
59
105
  grantedAt: unknown;
60
106
  expiresAt: unknown;
61
- }>;
107
+ createdAt: unknown;
108
+ updatedAt: unknown;
109
+ }, import("zod").ZodObject<{
110
+ orgId: import("zod").ZodString;
111
+ id: import("zod").ZodString;
112
+ source: import("zod").ZodString;
113
+ status: import("zod").ZodEnum<{
114
+ active: "active";
115
+ revoked: "revoked";
116
+ }>;
117
+ createdAt: import("zod").ZodCoercedDate<unknown>;
118
+ updatedAt: import("zod").ZodCoercedDate<unknown>;
119
+ bundleId: import("zod").ZodNullable<import("zod").ZodString>;
120
+ contentId: import("zod").ZodNullable<import("zod").ZodString>;
121
+ orgUserId: import("zod").ZodString;
122
+ grantedAt: import("zod").ZodCoercedDate<unknown>;
123
+ expiresAt: import("zod").ZodNullable<import("zod").ZodCoercedDate<unknown>>;
124
+ }, import("zod/v4/core").$strip>>;
62
125
  entitlementExpired: RuntimeEventDefinition<VersionedDomainEvent<{
63
126
  orgId: string;
64
127
  id: string;
65
128
  orgUserId: string;
66
- contentId: string;
129
+ bundleId: string | null;
130
+ contentId: string | null;
67
131
  status: "active" | "revoked";
68
132
  source: string;
69
133
  grantedAt: Date;
70
134
  expiresAt: Date | null;
135
+ createdAt: Date;
136
+ updatedAt: Date;
71
137
  }, "entitlement.expired", 1>, {
72
138
  orgId: string;
73
139
  id: string;
74
140
  orgUserId: string;
75
- contentId: string;
141
+ bundleId: string | null;
142
+ contentId: string | null;
76
143
  status: "active" | "revoked";
77
144
  source: string;
78
145
  grantedAt: unknown;
79
146
  expiresAt: unknown;
80
- }>;
147
+ createdAt: unknown;
148
+ updatedAt: unknown;
149
+ }, import("zod").ZodObject<{
150
+ orgId: import("zod").ZodString;
151
+ id: import("zod").ZodString;
152
+ source: import("zod").ZodString;
153
+ status: import("zod").ZodEnum<{
154
+ active: "active";
155
+ revoked: "revoked";
156
+ }>;
157
+ createdAt: import("zod").ZodCoercedDate<unknown>;
158
+ updatedAt: import("zod").ZodCoercedDate<unknown>;
159
+ bundleId: import("zod").ZodNullable<import("zod").ZodString>;
160
+ contentId: import("zod").ZodNullable<import("zod").ZodString>;
161
+ orgUserId: import("zod").ZodString;
162
+ grantedAt: import("zod").ZodCoercedDate<unknown>;
163
+ expiresAt: import("zod").ZodNullable<import("zod").ZodCoercedDate<unknown>>;
164
+ }, import("zod/v4/core").$strip>>;
165
+ entitlementEndingSoon: RuntimeEventDefinition<VersionedDomainEvent<{
166
+ orgId: string;
167
+ id: string;
168
+ orgUserId: string;
169
+ bundleId: string | null;
170
+ contentId: string | null;
171
+ status: "active" | "revoked";
172
+ source: string;
173
+ grantedAt: Date;
174
+ expiresAt: Date | null;
175
+ createdAt: Date;
176
+ updatedAt: Date;
177
+ communityId: string;
178
+ }, "entitlement.ending_soon", 1>, {
179
+ orgId: string;
180
+ id: string;
181
+ orgUserId: string;
182
+ bundleId: string | null;
183
+ contentId: string | null;
184
+ status: "active" | "revoked";
185
+ source: string;
186
+ grantedAt: unknown;
187
+ expiresAt: unknown;
188
+ createdAt: unknown;
189
+ updatedAt: unknown;
190
+ communityId: string;
191
+ }, import("zod").ZodObject<{
192
+ orgId: import("zod").ZodString;
193
+ id: import("zod").ZodString;
194
+ source: import("zod").ZodString;
195
+ status: import("zod").ZodEnum<{
196
+ active: "active";
197
+ revoked: "revoked";
198
+ }>;
199
+ createdAt: import("zod").ZodCoercedDate<unknown>;
200
+ updatedAt: import("zod").ZodCoercedDate<unknown>;
201
+ bundleId: import("zod").ZodNullable<import("zod").ZodString>;
202
+ contentId: import("zod").ZodNullable<import("zod").ZodString>;
203
+ orgUserId: import("zod").ZodString;
204
+ grantedAt: import("zod").ZodCoercedDate<unknown>;
205
+ expiresAt: import("zod").ZodNullable<import("zod").ZodCoercedDate<unknown>>;
206
+ communityId: import("zod").ZodString;
207
+ }, import("zod/v4/core").$strip>>;
208
+ spaceMemberCreated: RuntimeEventDefinition<VersionedDomainEvent<{
209
+ orgId: string;
210
+ id: string;
211
+ communityId: string;
212
+ spaceId: string;
213
+ orgUserId: string;
214
+ status: "active" | "revoked" | "requested" | "invited";
215
+ source: "request" | "invite" | "manual";
216
+ createdAt: Date;
217
+ updatedAt: Date;
218
+ }, "entitlement.space_member.created", 1>, {
219
+ orgId: string;
220
+ id: string;
221
+ communityId: string;
222
+ spaceId: string;
223
+ orgUserId: string;
224
+ status: "active" | "revoked" | "requested" | "invited";
225
+ source: "request" | "invite" | "manual";
226
+ createdAt: unknown;
227
+ updatedAt: unknown;
228
+ }, import("zod").ZodObject<{
229
+ orgId: import("zod").ZodString;
230
+ id: import("zod").ZodString;
231
+ communityId: import("zod").ZodString;
232
+ spaceId: import("zod").ZodString;
233
+ orgUserId: import("zod").ZodString;
234
+ status: import("zod").ZodEnum<{
235
+ active: "active";
236
+ revoked: "revoked";
237
+ requested: "requested";
238
+ invited: "invited";
239
+ }>;
240
+ source: import("zod").ZodEnum<{
241
+ request: "request";
242
+ invite: "invite";
243
+ manual: "manual";
244
+ }>;
245
+ createdAt: import("zod").ZodCoercedDate<unknown>;
246
+ updatedAt: import("zod").ZodCoercedDate<unknown>;
247
+ }, import("zod/v4/core").$strict>>;
248
+ spaceMemberUpdated: RuntimeEventDefinition<VersionedDomainEvent<{
249
+ orgId: string;
250
+ id: string;
251
+ communityId: string;
252
+ spaceId: string;
253
+ orgUserId: string;
254
+ status: "active" | "revoked" | "requested" | "invited";
255
+ source: "request" | "invite" | "manual";
256
+ createdAt: Date;
257
+ updatedAt: Date;
258
+ }, "entitlement.space_member.updated", 1>, {
259
+ orgId: string;
260
+ id: string;
261
+ communityId: string;
262
+ spaceId: string;
263
+ orgUserId: string;
264
+ status: "active" | "revoked" | "requested" | "invited";
265
+ source: "request" | "invite" | "manual";
266
+ createdAt: unknown;
267
+ updatedAt: unknown;
268
+ }, import("zod").ZodObject<{
269
+ orgId: import("zod").ZodString;
270
+ id: import("zod").ZodString;
271
+ communityId: import("zod").ZodString;
272
+ spaceId: import("zod").ZodString;
273
+ orgUserId: import("zod").ZodString;
274
+ status: import("zod").ZodEnum<{
275
+ active: "active";
276
+ revoked: "revoked";
277
+ requested: "requested";
278
+ invited: "invited";
279
+ }>;
280
+ source: import("zod").ZodEnum<{
281
+ request: "request";
282
+ invite: "invite";
283
+ manual: "manual";
284
+ }>;
285
+ createdAt: import("zod").ZodCoercedDate<unknown>;
286
+ updatedAt: import("zod").ZodCoercedDate<unknown>;
287
+ }, import("zod/v4/core").$strict>>;
288
+ spaceMemberDeleted: RuntimeEventDefinition<VersionedDomainEvent<{
289
+ orgId: string;
290
+ id: string;
291
+ communityId: string;
292
+ spaceId: string;
293
+ orgUserId: string;
294
+ status: "active" | "revoked" | "requested" | "invited";
295
+ source: "request" | "invite" | "manual";
296
+ createdAt: Date;
297
+ updatedAt: Date;
298
+ }, "entitlement.space_member.deleted", 1>, {
299
+ orgId: string;
300
+ id: string;
301
+ communityId: string;
302
+ spaceId: string;
303
+ orgUserId: string;
304
+ status: "active" | "revoked" | "requested" | "invited";
305
+ source: "request" | "invite" | "manual";
306
+ createdAt: unknown;
307
+ updatedAt: unknown;
308
+ }, import("zod").ZodObject<{
309
+ orgId: import("zod").ZodString;
310
+ id: import("zod").ZodString;
311
+ communityId: import("zod").ZodString;
312
+ spaceId: import("zod").ZodString;
313
+ orgUserId: import("zod").ZodString;
314
+ status: import("zod").ZodEnum<{
315
+ active: "active";
316
+ revoked: "revoked";
317
+ requested: "requested";
318
+ invited: "invited";
319
+ }>;
320
+ source: import("zod").ZodEnum<{
321
+ request: "request";
322
+ invite: "invite";
323
+ manual: "manual";
324
+ }>;
325
+ createdAt: import("zod").ZodCoercedDate<unknown>;
326
+ updatedAt: import("zod").ZodCoercedDate<unknown>;
327
+ }, import("zod/v4/core").$strict>>;
81
328
  };
82
329
  type EntitlementCreated = EventOf<typeof entitlementEvents.entitlementCreated>;
83
330
  type EntitlementUpdated = EventOf<typeof entitlementEvents.entitlementUpdated>;
84
331
  type EntitlementDeleted = EventOf<typeof entitlementEvents.entitlementDeleted>;
85
332
  type EntitlementExpired = EventOf<typeof entitlementEvents.entitlementExpired>;
333
+ type EntitlementEndingSoon = EventOf<typeof entitlementEvents.entitlementEndingSoon>;
334
+ type SpaceMemberCreated = EventOf<typeof entitlementEvents.spaceMemberCreated>;
335
+ type SpaceMemberUpdated = EventOf<typeof entitlementEvents.spaceMemberUpdated>;
336
+ type SpaceMemberDeleted = EventOf<typeof entitlementEvents.spaceMemberDeleted>;
86
337
  type EntitlementEvent = EventOfValues<typeof entitlementEvents>;
87
338
  //#endregion
88
- export { EntitlementCreated, EntitlementDeleted, EntitlementEvent, EntitlementExpired, EntitlementUpdated, entitlementEvents };
339
+ export { EntitlementCreated, EntitlementDeleted, EntitlementEndingSoon, EntitlementEvent, EntitlementExpired, EntitlementUpdated, SpaceMemberCreated, SpaceMemberDeleted, SpaceMemberUpdated, entitlementEvents };
89
340
  //# sourceMappingURL=events.d.ts.map
@@ -1,26 +1,70 @@
1
- import { entitlementSchema } from "../types/schemas/entitlements.js";
1
+ import { idSchema } from "../types/schemas/shared.js";
2
+ import { entitlementSchema, spaceMemberSchema } from "../types/schemas/entitlements.js";
2
3
  import { defineEvent } from "../shared/events.js";
3
4
  //#region src/entitlements/events.ts
5
+ const publicEntitlement = entitlementSchema.pick({
6
+ orgId: true,
7
+ id: true,
8
+ orgUserId: true,
9
+ bundleId: true,
10
+ contentId: true,
11
+ status: true,
12
+ source: true,
13
+ grantedAt: true,
14
+ expiresAt: true,
15
+ createdAt: true,
16
+ updatedAt: true
17
+ }).strip();
18
+ const endingSoonSchema = entitlementSchema.extend({ communityId: idSchema }).strict();
19
+ const publicEndingSoon = publicEntitlement.extend({ communityId: idSchema }).strip();
4
20
  const entitlementEvents = {
5
21
  entitlementCreated: defineEvent({
6
22
  type: "entitlement.created",
7
23
  version: 1,
8
- data: entitlementSchema
24
+ data: entitlementSchema,
25
+ publicData: publicEntitlement
9
26
  }),
10
27
  entitlementUpdated: defineEvent({
11
28
  type: "entitlement.updated",
12
29
  version: 1,
13
- data: entitlementSchema
30
+ data: entitlementSchema,
31
+ publicData: publicEntitlement
14
32
  }),
15
33
  entitlementDeleted: defineEvent({
16
34
  type: "entitlement.deleted",
17
35
  version: 1,
18
- data: entitlementSchema
36
+ data: entitlementSchema,
37
+ publicData: publicEntitlement
19
38
  }),
20
39
  entitlementExpired: defineEvent({
21
40
  type: "entitlement.expired",
22
41
  version: 1,
23
- data: entitlementSchema
42
+ data: entitlementSchema,
43
+ publicData: publicEntitlement
44
+ }),
45
+ entitlementEndingSoon: defineEvent({
46
+ type: "entitlement.ending_soon",
47
+ version: 1,
48
+ data: endingSoonSchema,
49
+ publicData: publicEndingSoon
50
+ }),
51
+ spaceMemberCreated: defineEvent({
52
+ type: "entitlement.space_member.created",
53
+ version: 1,
54
+ data: spaceMemberSchema,
55
+ publicData: spaceMemberSchema
56
+ }),
57
+ spaceMemberUpdated: defineEvent({
58
+ type: "entitlement.space_member.updated",
59
+ version: 1,
60
+ data: spaceMemberSchema,
61
+ publicData: spaceMemberSchema
62
+ }),
63
+ spaceMemberDeleted: defineEvent({
64
+ type: "entitlement.space_member.deleted",
65
+ version: 1,
66
+ data: spaceMemberSchema,
67
+ publicData: spaceMemberSchema
24
68
  })
25
69
  };
26
70
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"events.js","names":[],"sources":["../../src/entitlements/events.ts"],"sourcesContent":["import { entitlementSchema } from '../types/schemas/index.js';\nimport { defineEvent, type EventOf, type EventOfValues } from '../shared/ports.js';\n\nexport const entitlementEvents = {\n entitlementCreated: defineEvent({\n type: 'entitlement.created',\n version: 1,\n data: entitlementSchema,\n }),\n entitlementUpdated: defineEvent({\n type: 'entitlement.updated',\n version: 1,\n data: entitlementSchema,\n }),\n entitlementDeleted: defineEvent({\n type: 'entitlement.deleted',\n version: 1,\n data: entitlementSchema,\n }),\n entitlementExpired: defineEvent({\n type: 'entitlement.expired',\n version: 1,\n data: entitlementSchema,\n }),\n};\n\nexport type EntitlementCreated = EventOf<typeof entitlementEvents.entitlementCreated>;\nexport type EntitlementUpdated = EventOf<typeof entitlementEvents.entitlementUpdated>;\nexport type EntitlementDeleted = EventOf<typeof entitlementEvents.entitlementDeleted>;\nexport type EntitlementExpired = EventOf<typeof entitlementEvents.entitlementExpired>;\nexport type EntitlementEvent = EventOfValues<typeof entitlementEvents>;\n"],"mappings":";;;AAGA,MAAa,oBAAoB;CAC/B,oBAAoB,YAAY;EAC9B,MAAM;EACN,SAAS;EACT,MAAM;CACR,CAAC;CACD,oBAAoB,YAAY;EAC9B,MAAM;EACN,SAAS;EACT,MAAM;CACR,CAAC;CACD,oBAAoB,YAAY;EAC9B,MAAM;EACN,SAAS;EACT,MAAM;CACR,CAAC;CACD,oBAAoB,YAAY;EAC9B,MAAM;EACN,SAAS;EACT,MAAM;CACR,CAAC;AACH"}
1
+ {"version":3,"file":"events.js","names":[],"sources":["../../src/entitlements/events.ts"],"sourcesContent":["import { entitlementSchema, idSchema, spaceMemberSchema } from '../types/schemas/index.js';\nimport { defineEvent, type EventOf, type EventOfValues } from '../shared/ports.js';\n\nconst publicEntitlement = entitlementSchema\n .pick({\n orgId: true,\n id: true,\n orgUserId: true,\n bundleId: true,\n contentId: true,\n status: true,\n source: true,\n grantedAt: true,\n expiresAt: true,\n createdAt: true,\n updatedAt: true,\n })\n .strip();\n\nconst endingSoonSchema = entitlementSchema.extend({ communityId: idSchema }).strict();\nconst publicEndingSoon = publicEntitlement.extend({ communityId: idSchema }).strip();\n\nexport const entitlementEvents = {\n entitlementCreated: defineEvent({\n type: 'entitlement.created',\n version: 1,\n data: entitlementSchema,\n publicData: publicEntitlement,\n }),\n entitlementUpdated: defineEvent({\n type: 'entitlement.updated',\n version: 1,\n data: entitlementSchema,\n publicData: publicEntitlement,\n }),\n entitlementDeleted: defineEvent({\n type: 'entitlement.deleted',\n version: 1,\n data: entitlementSchema,\n publicData: publicEntitlement,\n }),\n entitlementExpired: defineEvent({\n type: 'entitlement.expired',\n version: 1,\n data: entitlementSchema,\n publicData: publicEntitlement,\n }),\n entitlementEndingSoon: defineEvent({\n type: 'entitlement.ending_soon',\n version: 1,\n data: endingSoonSchema,\n publicData: publicEndingSoon,\n }),\n spaceMemberCreated: defineEvent({\n type: 'entitlement.space_member.created',\n version: 1,\n data: spaceMemberSchema,\n publicData: spaceMemberSchema,\n }),\n spaceMemberUpdated: defineEvent({\n type: 'entitlement.space_member.updated',\n version: 1,\n data: spaceMemberSchema,\n publicData: spaceMemberSchema,\n }),\n spaceMemberDeleted: defineEvent({\n type: 'entitlement.space_member.deleted',\n version: 1,\n data: spaceMemberSchema,\n publicData: spaceMemberSchema,\n }),\n};\n\nexport type EntitlementCreated = EventOf<typeof entitlementEvents.entitlementCreated>;\nexport type EntitlementUpdated = EventOf<typeof entitlementEvents.entitlementUpdated>;\nexport type EntitlementDeleted = EventOf<typeof entitlementEvents.entitlementDeleted>;\nexport type EntitlementExpired = EventOf<typeof entitlementEvents.entitlementExpired>;\nexport type EntitlementEndingSoon = EventOf<typeof entitlementEvents.entitlementEndingSoon>;\nexport type SpaceMemberCreated = EventOf<typeof entitlementEvents.spaceMemberCreated>;\nexport type SpaceMemberUpdated = EventOf<typeof entitlementEvents.spaceMemberUpdated>;\nexport type SpaceMemberDeleted = EventOf<typeof entitlementEvents.spaceMemberDeleted>;\nexport type EntitlementEvent = EventOfValues<typeof entitlementEvents>;\n"],"mappings":";;;;AAGA,MAAM,oBAAoB,kBACvB,KAAK;CACJ,OAAO;CACP,IAAI;CACJ,WAAW;CACX,UAAU;CACV,WAAW;CACX,QAAQ;CACR,QAAQ;CACR,WAAW;CACX,WAAW;CACX,WAAW;CACX,WAAW;AACb,CAAC,CAAC,CACD,MAAM;AAET,MAAM,mBAAmB,kBAAkB,OAAO,EAAE,aAAa,SAAS,CAAC,CAAC,CAAC,OAAO;AACpF,MAAM,mBAAmB,kBAAkB,OAAO,EAAE,aAAa,SAAS,CAAC,CAAC,CAAC,MAAM;AAEnF,MAAa,oBAAoB;CAC/B,oBAAoB,YAAY;EAC9B,MAAM;EACN,SAAS;EACT,MAAM;EACN,YAAY;CACd,CAAC;CACD,oBAAoB,YAAY;EAC9B,MAAM;EACN,SAAS;EACT,MAAM;EACN,YAAY;CACd,CAAC;CACD,oBAAoB,YAAY;EAC9B,MAAM;EACN,SAAS;EACT,MAAM;EACN,YAAY;CACd,CAAC;CACD,oBAAoB,YAAY;EAC9B,MAAM;EACN,SAAS;EACT,MAAM;EACN,YAAY;CACd,CAAC;CACD,uBAAuB,YAAY;EACjC,MAAM;EACN,SAAS;EACT,MAAM;EACN,YAAY;CACd,CAAC;CACD,oBAAoB,YAAY;EAC9B,MAAM;EACN,SAAS;EACT,MAAM;EACN,YAAY;CACd,CAAC;CACD,oBAAoB,YAAY;EAC9B,MAAM;EACN,SAAS;EACT,MAAM;EACN,YAAY;CACd,CAAC;CACD,oBAAoB,YAAY;EAC9B,MAAM;EACN,SAAS;EACT,MAAM;EACN,YAAY;CACd,CAAC;AACH"}
@@ -1,7 +1,8 @@
1
1
  import { Page } from "../types/shared.js";
2
- import { Entitlement, EntitlementStatus, EntitlementsQuery, GrantEntitlementInput } from "../types/schemas/entitlements.js";
3
- import { EntitlementCreated, EntitlementDeleted, EntitlementEvent, EntitlementExpired, EntitlementUpdated, entitlementEvents } from "./events.js";
2
+ import { Entitlement, EntitlementStatus, EntitlementsQuery, GrantEntitlementInput, SpaceMember, SpaceMemberSource, SpaceMemberStatus } from "../types/schemas/entitlements.js";
4
3
  import "./model.js";
5
- import { EntitlementsRepository, EntitlementsService } from "./ports.js";
4
+ import { AccessFacts, CommunityAccessReader, EndingGrant, EndingGrantKey, EntitlementNotice, EntitlementsRepository, EntitlementsService, GrantKey, GrantTarget, InsertSpaceMemberInput, OpenableSpace, SpaceAccessRule, SpaceMembersQuery } from "./ports.js";
6
5
  import { EntitlementsServiceImpl } from "./service.js";
7
- export { type Entitlement, type EntitlementCreated, type EntitlementDeleted, type EntitlementEvent, type EntitlementExpired, type EntitlementStatus, type EntitlementUpdated, type EntitlementsQuery, type EntitlementsRepository, type EntitlementsService, EntitlementsServiceImpl, type GrantEntitlementInput, type Page, entitlementEvents };
6
+ import { EntitlementCreated, EntitlementDeleted, EntitlementEndingSoon, EntitlementEvent, EntitlementExpired, EntitlementUpdated, SpaceMemberCreated, SpaceMemberDeleted, SpaceMemberUpdated, entitlementEvents } from "./events.js";
7
+ import { seesSpace } from "./rules.js";
8
+ export { type AccessFacts, type CommunityAccessReader, type EndingGrant, type EndingGrantKey, type Entitlement, type EntitlementCreated, type EntitlementDeleted, type EntitlementEndingSoon, type EntitlementEvent, type EntitlementExpired, type EntitlementNotice, type EntitlementStatus, type EntitlementUpdated, type EntitlementsQuery, type EntitlementsRepository, type EntitlementsService, EntitlementsServiceImpl, type GrantEntitlementInput, type GrantKey, type GrantTarget, type InsertSpaceMemberInput, type OpenableSpace, type Page, type SpaceAccessRule, type SpaceMember, type SpaceMemberCreated, type SpaceMemberDeleted, type SpaceMemberSource, type SpaceMemberStatus, type SpaceMemberUpdated, type SpaceMembersQuery, entitlementEvents, seesSpace };
@@ -1,3 +1,4 @@
1
1
  import { entitlementEvents } from "./events.js";
2
+ import { seesSpace } from "./rules.js";
2
3
  import { EntitlementsServiceImpl } from "./service.js";
3
- export { EntitlementsServiceImpl, entitlementEvents };
4
+ export { EntitlementsServiceImpl, entitlementEvents, seesSpace };
@@ -1,4 +1,5 @@
1
1
  import { Page } from "../types/shared.js";
2
- import { Entitlement, EntitlementStatus, EntitlementsQuery, GrantEntitlementInput } from "../types/schemas/entitlements.js";
2
+ import { ContentType } from "../types/schemas/content.js";
3
+ import { Entitlement, EntitlementStatus, EntitlementsQuery, GrantEntitlementInput, SpaceMember, SpaceMemberSource, SpaceMemberStatus } from "../types/schemas/entitlements.js";
3
4
  import "../types/index.js";
4
- export type { Entitlement, EntitlementStatus, EntitlementsQuery, GrantEntitlementInput, Page };
5
+ export type { ContentType, Entitlement, EntitlementStatus, EntitlementsQuery, GrantEntitlementInput, Page, SpaceMember, SpaceMemberSource, SpaceMemberStatus };