@headless-lms/core 0.2.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 (225) hide show
  1. package/dist/assets/events.d.ts +39 -21
  2. package/dist/assets/events.js +1 -1
  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 +291 -38
  13. package/dist/assets/service.js.map +1 -1
  14. package/dist/automations/actions.d.ts +13 -24
  15. package/dist/automations/actions.js +168 -43
  16. package/dist/automations/actions.js.map +1 -1
  17. package/dist/automations/catalog.d.ts +2 -1
  18. package/dist/automations/catalog.js +229 -13
  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 +28 -0
  30. package/dist/automations/index.d.ts +4 -2
  31. package/dist/automations/index.js +3 -2
  32. package/dist/automations/model.d.ts +10 -3
  33. package/dist/automations/model.js +15 -2
  34. package/dist/automations/model.js.map +1 -1
  35. package/dist/automations/ports.d.ts +5 -1
  36. package/dist/automations/schedule.d.ts +6 -0
  37. package/dist/automations/schedule.js +41 -0
  38. package/dist/automations/schedule.js.map +1 -0
  39. package/dist/automations/seeds.d.ts +17 -0
  40. package/dist/automations/seeds.js +235 -0
  41. package/dist/automations/seeds.js.map +1 -0
  42. package/dist/automations/service.d.ts +22 -3
  43. package/dist/automations/service.js +306 -43
  44. package/dist/automations/service.js.map +1 -1
  45. package/dist/content/events.d.ts +631 -19
  46. package/dist/content/events.js +44 -1
  47. package/dist/content/events.js.map +1 -1
  48. package/dist/content/index.d.ts +4 -4
  49. package/dist/content/model.d.ts +2 -2
  50. package/dist/content/ports.d.ts +71 -18
  51. package/dist/content/service.d.ts +57 -18
  52. package/dist/content/service.js +502 -23
  53. package/dist/content/service.js.map +1 -1
  54. package/dist/content/types.d.ts +2 -2
  55. package/dist/discussion/authors.d.ts +10 -0
  56. package/dist/discussion/authors.js +23 -0
  57. package/dist/discussion/authors.js.map +1 -0
  58. package/dist/discussion/chats.d.ts +104 -0
  59. package/dist/discussion/chats.js +798 -0
  60. package/dist/discussion/chats.js.map +1 -0
  61. package/dist/discussion/events.d.ts +619 -56
  62. package/dist/discussion/events.js +106 -3
  63. package/dist/discussion/events.js.map +1 -1
  64. package/dist/discussion/index.d.ts +7 -3
  65. package/dist/discussion/index.js +4 -1
  66. package/dist/discussion/ports.d.ts +288 -27
  67. package/dist/discussion/ports.js +6 -1
  68. package/dist/discussion/ports.js.map +1 -0
  69. package/dist/discussion/posts.d.ts +103 -0
  70. package/dist/discussion/posts.js +723 -0
  71. package/dist/discussion/posts.js.map +1 -0
  72. package/dist/discussion/service.d.ts +32 -10
  73. package/dist/discussion/service.js +293 -78
  74. package/dist/discussion/service.js.map +1 -1
  75. package/dist/entitlements/events.d.ts +178 -11
  76. package/dist/entitlements/events.js +28 -1
  77. package/dist/entitlements/events.js.map +1 -1
  78. package/dist/entitlements/index.d.ts +5 -4
  79. package/dist/entitlements/index.js +2 -1
  80. package/dist/entitlements/model.d.ts +3 -2
  81. package/dist/entitlements/ports.d.ts +124 -11
  82. package/dist/entitlements/rules.d.ts +12 -0
  83. package/dist/entitlements/rules.js +17 -0
  84. package/dist/entitlements/rules.js.map +1 -0
  85. package/dist/entitlements/service.d.ts +54 -3
  86. package/dist/entitlements/service.js +472 -2
  87. package/dist/entitlements/service.js.map +1 -1
  88. package/dist/identity/access.d.ts +6 -0
  89. package/dist/identity/access.js +7 -0
  90. package/dist/identity/access.js.map +1 -0
  91. package/dist/identity/index.d.ts +6 -4
  92. package/dist/identity/index.js +3 -1
  93. package/dist/identity/model.d.ts +2 -1
  94. package/dist/identity/notifications.d.ts +27 -0
  95. package/dist/identity/notifications.js +101 -0
  96. package/dist/identity/notifications.js.map +1 -0
  97. package/dist/identity/ports.d.ts +73 -3
  98. package/dist/identity/service.d.ts +14 -3
  99. package/dist/identity/service.js +62 -5
  100. package/dist/identity/service.js.map +1 -1
  101. package/dist/integrations/model.js +1 -1
  102. package/dist/integrations/model.js.map +1 -1
  103. package/dist/integrations/service.js +2 -2
  104. package/dist/integrations/service.js.map +1 -1
  105. package/dist/organizations/access-scopes.d.ts +19 -0
  106. package/dist/organizations/access-scopes.js +89 -0
  107. package/dist/organizations/access-scopes.js.map +1 -0
  108. package/dist/organizations/events.d.ts +74 -32
  109. package/dist/organizations/events.js +6 -0
  110. package/dist/organizations/events.js.map +1 -1
  111. package/dist/organizations/index.d.ts +7 -6
  112. package/dist/organizations/index.js +4 -3
  113. package/dist/organizations/members.d.ts +8 -5
  114. package/dist/organizations/members.js.map +1 -1
  115. package/dist/organizations/ports.d.ts +14 -11
  116. package/dist/organizations/roles.d.ts +6 -10
  117. package/dist/organizations/roles.js +19 -7
  118. package/dist/organizations/roles.js.map +1 -1
  119. package/dist/organizations/service.d.ts +20 -11
  120. package/dist/organizations/service.js +157 -47
  121. package/dist/organizations/service.js.map +1 -1
  122. package/dist/organizations/types.d.ts +1 -10
  123. package/dist/progress/index.d.ts +2 -2
  124. package/dist/progress/ports.d.ts +6 -1
  125. package/dist/progress/service.d.ts +3 -1
  126. package/dist/progress/service.js +6 -2
  127. package/dist/progress/service.js.map +1 -1
  128. package/dist/reporting/admin/index.d.ts +4 -0
  129. package/dist/reporting/admin/index.js +2 -0
  130. package/dist/reporting/admin/model.d.ts +108 -0
  131. package/dist/reporting/admin/ports.d.ts +27 -0
  132. package/dist/reporting/admin/service.d.ts +22 -0
  133. package/dist/reporting/admin/service.js +38 -0
  134. package/dist/reporting/admin/service.js.map +1 -0
  135. package/dist/reporting/member/index.d.ts +5 -0
  136. package/dist/reporting/member/index.js +2 -0
  137. package/dist/reporting/member/model.d.ts +124 -0
  138. package/dist/reporting/member/ports.d.ts +45 -0
  139. package/dist/reporting/member/service.d.ts +73 -0
  140. package/dist/reporting/member/service.js +286 -0
  141. package/dist/reporting/member/service.js.map +1 -0
  142. package/dist/shared/actor.d.ts +9 -0
  143. package/dist/shared/errors.d.ts +7 -1
  144. package/dist/shared/errors.js +10 -1
  145. package/dist/shared/errors.js.map +1 -1
  146. package/dist/shared/id.d.ts +11 -0
  147. package/dist/shared/id.js +12 -1
  148. package/dist/shared/id.js.map +1 -1
  149. package/dist/shared/mailer.d.ts +4 -1
  150. package/dist/shared/mailer.js.map +1 -1
  151. package/dist/shared/network.d.ts +10 -0
  152. package/dist/shared/network.js +211 -0
  153. package/dist/shared/network.js.map +1 -0
  154. package/dist/types/assets.d.ts +2 -2
  155. package/dist/types/automations.d.ts +10 -2
  156. package/dist/types/content.d.ts +2 -2
  157. package/dist/types/discussion.d.ts +11 -3
  158. package/dist/types/email-templates.d.ts +121 -0
  159. package/dist/types/entitlements.d.ts +2 -2
  160. package/dist/types/identity.d.ts +48 -2
  161. package/dist/types/index.d.ts +9 -9
  162. package/dist/types/ports.d.ts +6 -0
  163. package/dist/types/schemas/assets.d.ts +65 -37
  164. package/dist/types/schemas/assets.js +17 -12
  165. package/dist/types/schemas/assets.js.map +1 -1
  166. package/dist/types/schemas/automations.d.ts +18 -1
  167. package/dist/types/schemas/automations.js +15 -6
  168. package/dist/types/schemas/automations.js.map +1 -1
  169. package/dist/types/schemas/content.d.ts +335 -3
  170. package/dist/types/schemas/content.js +139 -3
  171. package/dist/types/schemas/content.js.map +1 -1
  172. package/dist/types/schemas/discussion.d.ts +370 -3
  173. package/dist/types/schemas/discussion.js +199 -4
  174. package/dist/types/schemas/discussion.js.map +1 -1
  175. package/dist/types/schemas/entitlements.d.ts +36 -2
  176. package/dist/types/schemas/entitlements.js +23 -1
  177. package/dist/types/schemas/entitlements.js.map +1 -1
  178. package/dist/types/schemas/identity.d.ts +111 -1
  179. package/dist/types/schemas/identity.js +55 -2
  180. package/dist/types/schemas/identity.js.map +1 -1
  181. package/dist/types/schemas/index.d.ts +8 -8
  182. package/dist/types/schemas/index.js +8 -8
  183. package/dist/types/schemas/organizations.d.ts +8 -0
  184. package/dist/types/schemas/organizations.js +2 -0
  185. package/dist/types/schemas/organizations.js.map +1 -1
  186. package/dist/types/schemas/shared.d.ts +3 -1
  187. package/dist/types/schemas/shared.js +5 -1
  188. package/dist/types/schemas/shared.js.map +1 -1
  189. package/package.json +2 -2
  190. package/dist/reporting/courses/index.d.ts +0 -4
  191. package/dist/reporting/courses/index.js +0 -2
  192. package/dist/reporting/courses/model.d.ts +0 -24
  193. package/dist/reporting/courses/ports.d.ts +0 -13
  194. package/dist/reporting/courses/service.d.ts +0 -19
  195. package/dist/reporting/courses/service.js +0 -20
  196. package/dist/reporting/courses/service.js.map +0 -1
  197. package/dist/reporting/dashboard/index.d.ts +0 -4
  198. package/dist/reporting/dashboard/index.js +0 -2
  199. package/dist/reporting/dashboard/model.d.ts +0 -14
  200. package/dist/reporting/dashboard/ports.d.ts +0 -13
  201. package/dist/reporting/dashboard/service.d.ts +0 -19
  202. package/dist/reporting/dashboard/service.js +0 -20
  203. package/dist/reporting/dashboard/service.js.map +0 -1
  204. package/dist/reporting/learn/index.d.ts +0 -5
  205. package/dist/reporting/learn/index.js +0 -2
  206. package/dist/reporting/learn/model.d.ts +0 -23
  207. package/dist/reporting/learn/ports.d.ts +0 -43
  208. package/dist/reporting/learn/ports.js +0 -1
  209. package/dist/reporting/learn/service.d.ts +0 -49
  210. package/dist/reporting/learn/service.js +0 -116
  211. package/dist/reporting/learn/service.js.map +0 -1
  212. package/dist/reporting/students/index.d.ts +0 -4
  213. package/dist/reporting/students/index.js +0 -2
  214. package/dist/reporting/students/model.d.ts +0 -51
  215. package/dist/reporting/students/model.js +0 -1
  216. package/dist/reporting/students/ports.d.ts +0 -15
  217. package/dist/reporting/students/ports.js +0 -1
  218. package/dist/reporting/students/service.d.ts +0 -20
  219. package/dist/reporting/students/service.js +0 -23
  220. package/dist/reporting/students/service.js.map +0 -1
  221. /package/dist/reporting/{courses → admin}/model.js +0 -0
  222. /package/dist/reporting/{courses → admin}/ports.js +0 -0
  223. /package/dist/reporting/{dashboard → member}/model.js +0 -0
  224. /package/dist/reporting/{dashboard → member}/ports.js +0 -0
  225. /package/dist/{reporting/learn/model.js → shared/actor.js} +0 -0
@@ -0,0 +1,27 @@
1
+ import { Logger } from "../types/ports.js";
2
+ import { Notification, NotificationKind, NotificationPreference } from "../types/schemas/identity.js";
3
+ import "../shared/ports.js";
4
+ import "./model.js";
5
+ import { NewNotification, NotificationPage, NotificationService, NotificationsQuery, NotificationsRepository } from "./ports.js";
6
+ //#region src/identity/notifications.d.ts
7
+ declare function defaultPreference(kind: NotificationKind): NotificationPreference;
8
+ declare class NotificationServiceImpl implements NotificationService {
9
+ private readonly repo;
10
+ private readonly logger;
11
+ constructor(params: {
12
+ repo: NotificationsRepository;
13
+ logger?: Logger;
14
+ });
15
+ notify(orgId: string, notifications: NewNotification[]): Promise<void>;
16
+ preferencesFor(orgId: string, userIds: string[], kind: NotificationKind): Promise<Record<string, NotificationPreference>>;
17
+ listNotifications(orgId: string, userId: string, query: NotificationsQuery): Promise<NotificationPage>;
18
+ markNotificationRead(orgId: string, userId: string, notificationId: string): Promise<Notification>;
19
+ markAllNotificationsRead(orgId: string, userId: string): Promise<{
20
+ updated: number;
21
+ }>;
22
+ notificationPreferences(orgId: string, userId: string): Promise<NotificationPreference[]>;
23
+ setNotificationPreferences(orgId: string, userId: string, preferences: NotificationPreference[]): Promise<NotificationPreference[]>;
24
+ }
25
+ //#endregion
26
+ export { NotificationServiceImpl, defaultPreference };
27
+ //# sourceMappingURL=notifications.d.ts.map
@@ -0,0 +1,101 @@
1
+ import { notificationKindSchema } from "../types/schemas/identity.js";
2
+ import { genId } from "../shared/id.js";
3
+ import { NotFoundError } from "../shared/errors.js";
4
+ import { noopLogger } from "../shared/logger.js";
5
+ //#region src/identity/notifications.ts
6
+ function defaultPreference(kind) {
7
+ return {
8
+ kind,
9
+ inApp: true,
10
+ email: true
11
+ };
12
+ }
13
+ var NotificationServiceImpl = class {
14
+ repo;
15
+ logger;
16
+ constructor(params) {
17
+ this.repo = params.repo;
18
+ this.logger = params.logger ?? noopLogger;
19
+ }
20
+ async notify(orgId, notifications) {
21
+ if (notifications.length === 0) return;
22
+ const now = /* @__PURE__ */ new Date();
23
+ const rows = notifications.map((notification) => ({
24
+ ...notification,
25
+ orgId,
26
+ id: genId("notification"),
27
+ createdAt: now,
28
+ readAt: null
29
+ }));
30
+ await this.repo.insert(orgId, rows);
31
+ this.logger.info("notifications added", {
32
+ orgId,
33
+ kinds: [...new Set(rows.map((row) => row.kind))],
34
+ count: rows.length
35
+ });
36
+ }
37
+ async preferencesFor(orgId, userIds, kind) {
38
+ const unique = [...new Set(userIds)];
39
+ const stored = await this.repo.preferences(orgId, unique);
40
+ const result = {};
41
+ for (const userId of unique) result[userId] = defaultPreference(kind);
42
+ for (const row of stored) if (row.kind === kind) result[row.userId] = {
43
+ kind,
44
+ inApp: row.inApp,
45
+ email: row.email
46
+ };
47
+ return result;
48
+ }
49
+ async listNotifications(orgId, userId, query) {
50
+ const page = await this.repo.page(orgId, userId, query);
51
+ this.logger.debug("notifications listed", {
52
+ orgId,
53
+ userId,
54
+ count: page.rows.length
55
+ });
56
+ return {
57
+ ...page,
58
+ page: query.page,
59
+ pageSize: query.pageSize
60
+ };
61
+ }
62
+ async markNotificationRead(orgId, userId, notificationId) {
63
+ const row = await this.repo.markRead(orgId, userId, notificationId, /* @__PURE__ */ new Date());
64
+ if (!row) throw new NotFoundError("Notification", notificationId);
65
+ return row;
66
+ }
67
+ async markAllNotificationsRead(orgId, userId) {
68
+ const updated = await this.repo.markAllRead(orgId, userId, /* @__PURE__ */ new Date());
69
+ this.logger.debug("notifications marked read", {
70
+ orgId,
71
+ userId,
72
+ updated
73
+ });
74
+ return { updated };
75
+ }
76
+ async notificationPreferences(orgId, userId) {
77
+ const stored = await this.repo.preferences(orgId, [userId]);
78
+ const byKind = new Map(stored.map((row) => [row.kind, row]));
79
+ return notificationKindSchema.options.map((kind) => {
80
+ const row = byKind.get(kind);
81
+ return row ? {
82
+ kind,
83
+ inApp: row.inApp,
84
+ email: row.email
85
+ } : defaultPreference(kind);
86
+ });
87
+ }
88
+ async setNotificationPreferences(orgId, userId, preferences) {
89
+ await this.repo.upsertPreferences(orgId, userId, preferences);
90
+ this.logger.info("notification preferences updated", {
91
+ orgId,
92
+ userId,
93
+ kinds: preferences.map((pref) => pref.kind)
94
+ });
95
+ return this.notificationPreferences(orgId, userId);
96
+ }
97
+ };
98
+ //#endregion
99
+ export { NotificationServiceImpl, defaultPreference };
100
+
101
+ //# sourceMappingURL=notifications.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notifications.js","names":[],"sources":["../../src/identity/notifications.ts"],"sourcesContent":["import { NotFoundError } from '../shared/errors.js';\nimport { genId } from '../shared/id.js';\nimport { noopLogger } from '../shared/logger.js';\nimport type { Logger } from '../shared/ports.js';\nimport { notificationKindSchema } from '../types/schemas/identity.js';\nimport type { Notification, NotificationKind, NotificationPreference } from './model.js';\nimport type {\n NewNotification,\n NotificationPage,\n NotificationService,\n NotificationsQuery,\n NotificationsRepository,\n} from './ports.js';\n\nexport function defaultPreference(kind: NotificationKind): NotificationPreference {\n return { kind, inApp: true, email: true };\n}\n\nexport class NotificationServiceImpl implements NotificationService {\n private readonly repo: NotificationsRepository;\n private readonly logger: Logger;\n\n constructor(params: { repo: NotificationsRepository; logger?: Logger }) {\n this.repo = params.repo;\n this.logger = params.logger ?? noopLogger;\n }\n\n async notify(orgId: string, notifications: NewNotification[]): Promise<void> {\n if (notifications.length === 0) {\n return;\n }\n const now = new Date();\n const rows: Notification[] = notifications.map((notification) => ({\n ...notification,\n orgId,\n id: genId('notification'),\n createdAt: now,\n readAt: null,\n }));\n await this.repo.insert(orgId, rows);\n this.logger.info('notifications added', {\n orgId,\n kinds: [...new Set(rows.map((row) => row.kind))],\n count: rows.length,\n });\n }\n\n async preferencesFor(\n orgId: string,\n userIds: string[],\n kind: NotificationKind,\n ): Promise<Record<string, NotificationPreference>> {\n const unique = [...new Set(userIds)];\n const stored = await this.repo.preferences(orgId, unique);\n const result: Record<string, NotificationPreference> = {};\n for (const userId of unique) {\n result[userId] = defaultPreference(kind);\n }\n for (const row of stored) {\n if (row.kind === kind) {\n result[row.userId] = { kind, inApp: row.inApp, email: row.email };\n }\n }\n return result;\n }\n\n async listNotifications(\n orgId: string,\n userId: string,\n query: NotificationsQuery,\n ): Promise<NotificationPage> {\n const page = await this.repo.page(orgId, userId, query);\n this.logger.debug('notifications listed', { orgId, userId, count: page.rows.length });\n return { ...page, page: query.page, pageSize: query.pageSize };\n }\n\n async markNotificationRead(\n orgId: string,\n userId: string,\n notificationId: string,\n ): Promise<Notification> {\n const row = await this.repo.markRead(orgId, userId, notificationId, new Date());\n if (!row) {\n throw new NotFoundError('Notification', notificationId);\n }\n return row;\n }\n\n async markAllNotificationsRead(orgId: string, userId: string): Promise<{ updated: number }> {\n const updated = await this.repo.markAllRead(orgId, userId, new Date());\n this.logger.debug('notifications marked read', { orgId, userId, updated });\n return { updated };\n }\n\n async notificationPreferences(orgId: string, userId: string): Promise<NotificationPreference[]> {\n const stored = await this.repo.preferences(orgId, [userId]);\n const byKind = new Map(stored.map((row) => [row.kind, row]));\n return notificationKindSchema.options.map((kind) => {\n const row = byKind.get(kind);\n return row ? { kind, inApp: row.inApp, email: row.email } : defaultPreference(kind);\n });\n }\n\n async setNotificationPreferences(\n orgId: string,\n userId: string,\n preferences: NotificationPreference[],\n ): Promise<NotificationPreference[]> {\n await this.repo.upsertPreferences(orgId, userId, preferences);\n this.logger.info('notification preferences updated', {\n orgId,\n userId,\n kinds: preferences.map((pref) => pref.kind),\n });\n return this.notificationPreferences(orgId, userId);\n }\n}\n"],"mappings":";;;;;AAcA,SAAgB,kBAAkB,MAAgD;CAChF,OAAO;EAAE;EAAM,OAAO;EAAM,OAAO;CAAK;AAC1C;AAEA,IAAa,0BAAb,MAAoE;CAClE;CACA;CAEA,YAAY,QAA4D;EACtE,KAAK,OAAO,OAAO;EACnB,KAAK,SAAS,OAAO,UAAU;CACjC;CAEA,MAAM,OAAO,OAAe,eAAiD;EAC3E,IAAI,cAAc,WAAW,GAC3B;EAEF,MAAM,sBAAM,IAAI,KAAK;EACrB,MAAM,OAAuB,cAAc,KAAK,kBAAkB;GAChE,GAAG;GACH;GACA,IAAI,MAAM,cAAc;GACxB,WAAW;GACX,QAAQ;EACV,EAAE;EACF,MAAM,KAAK,KAAK,OAAO,OAAO,IAAI;EAClC,KAAK,OAAO,KAAK,uBAAuB;GACtC;GACA,OAAO,CAAC,GAAG,IAAI,IAAI,KAAK,KAAK,QAAQ,IAAI,IAAI,CAAC,CAAC;GAC/C,OAAO,KAAK;EACd,CAAC;CACH;CAEA,MAAM,eACJ,OACA,SACA,MACiD;EACjD,MAAM,SAAS,CAAC,GAAG,IAAI,IAAI,OAAO,CAAC;EACnC,MAAM,SAAS,MAAM,KAAK,KAAK,YAAY,OAAO,MAAM;EACxD,MAAM,SAAiD,CAAC;EACxD,KAAK,MAAM,UAAU,QACnB,OAAO,UAAU,kBAAkB,IAAI;EAEzC,KAAK,MAAM,OAAO,QAChB,IAAI,IAAI,SAAS,MACf,OAAO,IAAI,UAAU;GAAE;GAAM,OAAO,IAAI;GAAO,OAAO,IAAI;EAAM;EAGpE,OAAO;CACT;CAEA,MAAM,kBACJ,OACA,QACA,OAC2B;EAC3B,MAAM,OAAO,MAAM,KAAK,KAAK,KAAK,OAAO,QAAQ,KAAK;EACtD,KAAK,OAAO,MAAM,wBAAwB;GAAE;GAAO;GAAQ,OAAO,KAAK,KAAK;EAAO,CAAC;EACpF,OAAO;GAAE,GAAG;GAAM,MAAM,MAAM;GAAM,UAAU,MAAM;EAAS;CAC/D;CAEA,MAAM,qBACJ,OACA,QACA,gBACuB;EACvB,MAAM,MAAM,MAAM,KAAK,KAAK,SAAS,OAAO,QAAQ,gCAAgB,IAAI,KAAK,CAAC;EAC9E,IAAI,CAAC,KACH,MAAM,IAAI,cAAc,gBAAgB,cAAc;EAExD,OAAO;CACT;CAEA,MAAM,yBAAyB,OAAe,QAA8C;EAC1F,MAAM,UAAU,MAAM,KAAK,KAAK,YAAY,OAAO,wBAAQ,IAAI,KAAK,CAAC;EACrE,KAAK,OAAO,MAAM,6BAA6B;GAAE;GAAO;GAAQ;EAAQ,CAAC;EACzE,OAAO,EAAE,QAAQ;CACnB;CAEA,MAAM,wBAAwB,OAAe,QAAmD;EAC9F,MAAM,SAAS,MAAM,KAAK,KAAK,YAAY,OAAO,CAAC,MAAM,CAAC;EAC1D,MAAM,SAAS,IAAI,IAAI,OAAO,KAAK,QAAQ,CAAC,IAAI,MAAM,GAAG,CAAC,CAAC;EAC3D,OAAO,uBAAuB,QAAQ,KAAK,SAAS;GAClD,MAAM,MAAM,OAAO,IAAI,IAAI;GAC3B,OAAO,MAAM;IAAE;IAAM,OAAO,IAAI;IAAO,OAAO,IAAI;GAAM,IAAI,kBAAkB,IAAI;EACpF,CAAC;CACH;CAEA,MAAM,2BACJ,OACA,QACA,aACmC;EACnC,MAAM,KAAK,KAAK,kBAAkB,OAAO,QAAQ,WAAW;EAC5D,KAAK,OAAO,KAAK,oCAAoC;GACnD;GACA;GACA,OAAO,YAAY,KAAK,SAAS,KAAK,IAAI;EAC5C,CAAC;EACD,OAAO,KAAK,wBAAwB,OAAO,MAAM;CACnD;AACF"}
@@ -1,9 +1,21 @@
1
- import { CreateUserInput, UpdateUserInput } from "../types/schemas/identity.js";
1
+ import { JsonValue } from "../types/shared.js";
2
+ import { CreateUserInput, Notification, NotificationKind, NotificationPreference, UpdateUserInput, UserProfile, UserProfileView } from "../types/schemas/identity.js";
2
3
  import { User } from "../types/identity.js";
4
+ import "../types/index.js";
3
5
  import { OutboxAppender, UnitOfWork } from "../shared/ports.js";
4
6
  import "./model.js";
5
7
  import "./types.js";
6
8
  //#region src/identity/ports.d.ts
9
+ interface UpdateProfileInput {
10
+ displayName?: string | null;
11
+ headline?: string | null;
12
+ bio?: string | null;
13
+ imageAssetId?: string | null;
14
+ extra?: JsonValue;
15
+ }
16
+ interface ProfilePatch extends UpdateProfileInput {
17
+ imageAssetOrgId?: string | null;
18
+ }
7
19
  interface UserProvisioner {
8
20
  createUser(input: CreateUserInput): Promise<User>;
9
21
  /** Auth signup entry point. Claims a person provisioned at invite time by
@@ -43,13 +55,71 @@ interface SessionAdmin {
43
55
  /** Points the caller's current session at `orgId`. */
44
56
  stampActiveOrganization(headers: AuthHeaders, orgId: string): Promise<boolean>;
45
57
  }
46
- interface IdentityService extends UserProvisioner, UserResolver, UserEditor {}
58
+ interface NewNotification {
59
+ userId: string;
60
+ kind: NotificationKind;
61
+ actorOrgUserId: string | null;
62
+ communityId: string | null;
63
+ spaceId: string | null;
64
+ postId: string | null;
65
+ commentId: string | null;
66
+ chatId: string | null;
67
+ messageId: string | null;
68
+ }
69
+ interface NotificationsQuery {
70
+ page: number;
71
+ pageSize: number;
72
+ }
73
+ interface NotificationPage {
74
+ rows: Notification[];
75
+ total: number;
76
+ page: number;
77
+ pageSize: number;
78
+ unreadCount: number;
79
+ }
80
+ /** What automations calls to tell people. */
81
+ interface NotificationDesk {
82
+ notify(orgId: string, notifications: NewNotification[]): Promise<void>;
83
+ preferencesFor(orgId: string, userIds: string[], kind: NotificationKind): Promise<Record<string, NotificationPreference>>;
84
+ }
85
+ /** A person's own notifications and preferences. */
86
+ interface NotificationInbox {
87
+ listNotifications(orgId: string, userId: string, query: NotificationsQuery): Promise<NotificationPage>;
88
+ markNotificationRead(orgId: string, userId: string, notificationId: string): Promise<Notification>;
89
+ markAllNotificationsRead(orgId: string, userId: string): Promise<{
90
+ updated: number;
91
+ }>;
92
+ notificationPreferences(orgId: string, userId: string): Promise<NotificationPreference[]>;
93
+ setNotificationPreferences(orgId: string, userId: string, preferences: NotificationPreference[]): Promise<NotificationPreference[]>;
94
+ }
95
+ interface NotificationService extends NotificationDesk, NotificationInbox {}
96
+ interface IdentityService extends UserProvisioner, UserResolver, UserEditor {
97
+ profileOf(orgId: string, userId: string): Promise<UserProfileView>;
98
+ displayName(userId: string | null): Promise<string>;
99
+ updateProfile(orgId: string, userId: string, actorUserId: string, orgUserId: string, patch: UpdateProfileInput): Promise<UserProfileView>;
100
+ }
47
101
  interface IdentityRepository {
48
102
  insertUser(input: CreateUserInput): Promise<User>;
49
103
  updateUser(id: string, input: UpdateUserInput): Promise<User | null>;
50
104
  findUserById(id: string): Promise<User | null>;
51
105
  findUserByExternalId(externalId: string): Promise<User | null>;
52
106
  findUserByEmail(email: string): Promise<User | null>;
107
+ findProfile(userId: string): Promise<UserProfile | null>;
108
+ upsertProfile(userId: string, patch: ProfilePatch): Promise<UserProfile>;
109
+ }
110
+ interface NotificationsRepository {
111
+ insert(orgId: string, rows: Notification[]): Promise<void>;
112
+ page(orgId: string, userId: string, query: NotificationsQuery): Promise<{
113
+ rows: Notification[];
114
+ total: number;
115
+ unreadCount: number;
116
+ }>;
117
+ markRead(orgId: string, userId: string, notificationId: string, at: Date): Promise<Notification | null>;
118
+ markAllRead(orgId: string, userId: string, at: Date): Promise<number>;
119
+ preferences(orgId: string, userIds: string[]): Promise<(NotificationPreference & {
120
+ userId: string;
121
+ })[]>;
122
+ upsertPreferences(orgId: string, userId: string, preferences: NotificationPreference[]): Promise<void>;
53
123
  }
54
124
  interface IdentityTxScope {
55
125
  identity: IdentityRepository;
@@ -57,5 +127,5 @@ interface IdentityTxScope {
57
127
  }
58
128
  type IdentityUnitOfWork = UnitOfWork<IdentityTxScope>;
59
129
  //#endregion
60
- export { AuthHeaders, IdentityRepository, IdentityService, IdentityTxScope, IdentityUnitOfWork, SessionAdmin, UserEditor, UserProvisioner, UserResolver };
130
+ export { AuthHeaders, IdentityRepository, IdentityService, IdentityTxScope, IdentityUnitOfWork, NewNotification, NotificationDesk, NotificationInbox, NotificationPage, NotificationService, NotificationsQuery, NotificationsRepository, ProfilePatch, SessionAdmin, UpdateProfileInput, UserEditor, UserProvisioner, UserResolver };
61
131
  //# sourceMappingURL=ports.d.ts.map
@@ -1,22 +1,29 @@
1
1
  import { Logger } from "../types/ports.js";
2
- import { CreateUserInput, UpdateUserInput } from "../types/schemas/identity.js";
2
+ import { CreateUserInput, UpdateUserInput, UserProfileView } from "../types/schemas/identity.js";
3
3
  import { User } from "../types/identity.js";
4
4
  import "../shared/ports.js";
5
- import { Mailer } from "../shared/mailer.js";
5
+ import { FileUrls } from "../assets/file-urls.js";
6
+ import { MemberImageReader } from "../assets/ports.js";
7
+ import "../assets/index.js";
6
8
  import "./model.js";
7
9
  import "./types.js";
8
- import { IdentityRepository, IdentityService, IdentityUnitOfWork } from "./ports.js";
10
+ import { IdentityRepository, IdentityService, IdentityUnitOfWork, UpdateProfileInput } from "./ports.js";
11
+ import { Mailer } from "../shared/mailer.js";
9
12
  //#region src/identity/service.d.ts
10
13
  declare class IdentityServiceImpl implements IdentityService {
11
14
  private readonly repo;
12
15
  private readonly logger;
13
16
  private readonly uow;
14
17
  private readonly mailer;
18
+ private readonly images;
19
+ private readonly fileUrls;
15
20
  constructor(input: {
16
21
  repo: IdentityRepository;
17
22
  logger?: Logger;
18
23
  mailer: Mailer;
19
24
  uow: IdentityUnitOfWork;
25
+ images: MemberImageReader;
26
+ fileUrls: FileUrls;
20
27
  });
21
28
  createUser(input: CreateUserInput): Promise<User>;
22
29
  linkOrCreateUser(input: CreateUserInput): Promise<User>;
@@ -35,6 +42,10 @@ declare class IdentityServiceImpl implements IdentityService {
35
42
  email: string;
36
43
  url: string;
37
44
  }): Promise<void>;
45
+ profileOf(orgId: string, userId: string): Promise<UserProfileView>;
46
+ private view;
47
+ displayName(userId: string | null): Promise<string>;
48
+ updateProfile(orgId: string, userId: string, actorUserId: string, orgUserId: string, patch: UpdateProfileInput): Promise<UserProfileView>;
38
49
  }
39
50
  //#endregion
40
51
  export { IdentityServiceImpl };
@@ -1,18 +1,23 @@
1
- import { noopLogger } from "../shared/logger.js";
2
1
  import { genId } from "../shared/id.js";
3
- import { ConflictError, NotFoundError } from "../shared/errors.js";
2
+ import { ConflictError, ForbiddenError, NotFoundError } from "../shared/errors.js";
3
+ import { noopLogger } from "../shared/logger.js";
4
4
  import { identityEvents } from "./events.js";
5
5
  //#region src/identity/service.ts
6
+ const UNNAMED = "A member";
6
7
  var IdentityServiceImpl = class {
7
8
  repo;
8
9
  logger;
9
10
  uow;
10
11
  mailer;
12
+ images;
13
+ fileUrls;
11
14
  constructor(input) {
12
15
  this.repo = input.repo;
13
16
  this.mailer = input.mailer;
14
17
  this.logger = input.logger ?? noopLogger;
15
18
  this.uow = input.uow;
19
+ this.images = input.images;
20
+ this.fileUrls = input.fileUrls;
16
21
  }
17
22
  async createUser(input) {
18
23
  const created = await this.uow.run(async ({ identity, outbox }) => {
@@ -23,7 +28,7 @@ var IdentityServiceImpl = class {
23
28
  })]);
24
29
  return user;
25
30
  });
26
- this.logger.info("user created", { user: created });
31
+ this.logger.info("user created", { userId: created.id });
27
32
  return created;
28
33
  }
29
34
  async linkOrCreateUser(input) {
@@ -49,7 +54,7 @@ var IdentityServiceImpl = class {
49
54
  })]);
50
55
  return user;
51
56
  });
52
- this.logger.info("user linked to auth account", { user: linked });
57
+ this.logger.info("user linked to auth account", { userId: linked.id });
53
58
  return linked;
54
59
  }
55
60
  async updateUser(id, input) {
@@ -62,7 +67,7 @@ var IdentityServiceImpl = class {
62
67
  })]);
63
68
  return user;
64
69
  });
65
- this.logger.info("user updated", { user: updated });
70
+ this.logger.info("user updated", { userId: updated.id });
66
71
  return updated;
67
72
  }
68
73
  async getUserByExternalId(externalId) {
@@ -83,6 +88,58 @@ var IdentityServiceImpl = class {
83
88
  async sendMagicLink(input) {
84
89
  await this.mailer.send(input.email, "magicLink", { url: input.url });
85
90
  }
91
+ async profileOf(orgId, userId) {
92
+ const stored = await this.repo.findProfile(userId);
93
+ if (stored) return this.view(orgId, stored);
94
+ const never = /* @__PURE__ */ new Date(0);
95
+ return {
96
+ userId,
97
+ displayName: null,
98
+ headline: null,
99
+ bio: null,
100
+ imageAssetOrgId: null,
101
+ imageAssetId: null,
102
+ imageUrl: null,
103
+ extra: {},
104
+ createdAt: never,
105
+ updatedAt: never
106
+ };
107
+ }
108
+ view(orgId, profile) {
109
+ const imageAssetId = profile.imageAssetOrgId === orgId ? profile.imageAssetId : null;
110
+ return {
111
+ ...profile,
112
+ imageUrl: imageAssetId ? this.fileUrls.memberUpload(imageAssetId) : null
113
+ };
114
+ }
115
+ async displayName(userId) {
116
+ if (userId === null) return UNNAMED;
117
+ const [profile, user] = await Promise.all([this.repo.findProfile(userId), this.repo.findUserById(userId)]);
118
+ const named = profile?.displayName?.trim();
119
+ if (named) return named;
120
+ return [user?.firstName, user?.lastName].filter(Boolean).join(" ").trim() || UNNAMED;
121
+ }
122
+ async updateProfile(orgId, userId, actorUserId, orgUserId, patch) {
123
+ if (actorUserId !== userId) throw new ForbiddenError("You can only update your own profile");
124
+ if (!await this.repo.findUserById(userId)) throw new NotFoundError("User", userId);
125
+ if (patch.imageAssetId != null) {
126
+ if (!await this.images.ownsImage(orgId, patch.imageAssetId, orgUserId)) {
127
+ this.logger.debug("profile photo refused", {
128
+ userId,
129
+ orgUserId,
130
+ assetId: patch.imageAssetId
131
+ });
132
+ throw new NotFoundError("Asset", patch.imageAssetId);
133
+ }
134
+ }
135
+ const imageAssetOrgId = patch.imageAssetId === void 0 ? void 0 : patch.imageAssetId === null ? null : orgId;
136
+ const profile = await this.repo.upsertProfile(userId, {
137
+ ...patch,
138
+ imageAssetOrgId
139
+ });
140
+ this.logger.info("profile updated", { userId });
141
+ return this.view(orgId, profile);
142
+ }
86
143
  };
87
144
  //#endregion
88
145
  export { IdentityServiceImpl };
@@ -1 +1 @@
1
- {"version":3,"file":"service.js","names":[],"sources":["../../src/identity/service.ts"],"sourcesContent":["import type { IdentityRepository, IdentityService, IdentityUnitOfWork } from './ports.js';\nimport type { User } from './model.js';\nimport type { CreateUserInput, UpdateUserInput } from './types.js';\nimport type { Logger } from '../shared/ports.js';\nimport { noopLogger } from '../shared/logger.js';\nimport { genId } from '../shared/id.js';\nimport { ConflictError, NotFoundError } from '../shared/errors.js';\nimport type { Mailer } from '../shared/mailer.js';\nimport { identityEvents } from './events.js';\n\nexport class IdentityServiceImpl implements IdentityService {\n private readonly repo: IdentityRepository;\n private readonly logger: Logger;\n private readonly uow: IdentityUnitOfWork;\n private readonly mailer: Pick<Mailer, 'send'>;\n\n constructor(input: {\n repo: IdentityRepository;\n logger?: Logger;\n mailer: Mailer;\n uow: IdentityUnitOfWork;\n }) {\n this.repo = input.repo;\n this.mailer = input.mailer;\n this.logger = input.logger ?? noopLogger;\n this.uow = input.uow;\n }\n\n async createUser(input: CreateUserInput): Promise<User> {\n const created = await this.uow.run(async ({ identity, outbox }) => {\n const user = await identity.insertUser(input);\n await outbox.append([identityEvents.userCreated.make({ orgId: '-', data: user })]);\n return user;\n });\n this.logger.info('user created', { user: created });\n return created;\n }\n\n async linkOrCreateUser(input: CreateUserInput): Promise<User> {\n const existing = await this.repo.findUserByEmail(input.email);\n if (!existing) {\n const id = input.id ?? genId('user');\n return this.createUser({ ...input, id, externalId: id });\n }\n if (existing.externalId !== null) {\n throw new ConflictError('That email already has an account');\n }\n // Existing user without external link, so invited. update the user with the\n // id.\n const linked = await this.uow.run(async ({ identity, outbox }) => {\n const user = await identity.updateUser(existing.id, {\n externalId: existing.id,\n ...(input.firstName !== undefined && { firstName: input.firstName }),\n ...(input.lastName !== undefined && { lastName: input.lastName }),\n });\n await outbox.append([identityEvents.userUpdated.make({ orgId: '-', data: user! })]);\n return user!;\n });\n this.logger.info('user linked to auth account', { user: linked });\n return linked;\n }\n\n async updateUser(id: string, input: UpdateUserInput): Promise<User> {\n const person = await this.repo.findUserById(id);\n if (!person) {\n throw new NotFoundError('User', id);\n }\n\n const updated = await this.uow.run(async ({ identity, outbox }) => {\n const user = await identity.updateUser(id, input);\n await outbox.append([identityEvents.userUpdated.make({ orgId: '-', data: user! })]);\n return user!;\n });\n this.logger.info('user updated', { user: updated });\n return updated;\n }\n\n async getUserByExternalId(externalId: string): Promise<User | null> {\n return this.repo.findUserByExternalId(externalId);\n }\n\n async getUserByEmail(email: string): Promise<User | null> {\n return this.repo.findUserByEmail(email);\n }\n\n async getUserById(id: string): Promise<User | null> {\n return this.repo.findUserById(id);\n }\n\n async sendPasswordReset(input: { email: string; url: string }): Promise<void> {\n await this.mailer.send(input.email, 'passwordReset', { resetUrl: input.url });\n }\n\n async sendPasswordChanged(input: { email: string }): Promise<void> {\n await this.mailer.send(input.email, 'passwordChanged', {});\n }\n\n async sendMagicLink(input: { email: string; url: string }): Promise<void> {\n await this.mailer.send(input.email, 'magicLink', { url: input.url });\n }\n}\n"],"mappings":";;;;;AAUA,IAAa,sBAAb,MAA4D;CAC1D;CACA;CACA;CACA;CAEA,YAAY,OAKT;EACD,KAAK,OAAO,MAAM;EAClB,KAAK,SAAS,MAAM;EACpB,KAAK,SAAS,MAAM,UAAU;EAC9B,KAAK,MAAM,MAAM;CACnB;CAEA,MAAM,WAAW,OAAuC;EACtD,MAAM,UAAU,MAAM,KAAK,IAAI,IAAI,OAAO,EAAE,UAAU,aAAa;GACjE,MAAM,OAAO,MAAM,SAAS,WAAW,KAAK;GAC5C,MAAM,OAAO,OAAO,CAAC,eAAe,YAAY,KAAK;IAAE,OAAO;IAAK,MAAM;GAAK,CAAC,CAAC,CAAC;GACjF,OAAO;EACT,CAAC;EACD,KAAK,OAAO,KAAK,gBAAgB,EAAE,MAAM,QAAQ,CAAC;EAClD,OAAO;CACT;CAEA,MAAM,iBAAiB,OAAuC;EAC5D,MAAM,WAAW,MAAM,KAAK,KAAK,gBAAgB,MAAM,KAAK;EAC5D,IAAI,CAAC,UAAU;GACb,MAAM,KAAK,MAAM,MAAM,MAAM,MAAM;GACnC,OAAO,KAAK,WAAW;IAAE,GAAG;IAAO;IAAI,YAAY;GAAG,CAAC;EACzD;EACA,IAAI,SAAS,eAAe,MAC1B,MAAM,IAAI,cAAc,mCAAmC;EAI7D,MAAM,SAAS,MAAM,KAAK,IAAI,IAAI,OAAO,EAAE,UAAU,aAAa;GAChE,MAAM,OAAO,MAAM,SAAS,WAAW,SAAS,IAAI;IAClD,YAAY,SAAS;IACrB,GAAI,MAAM,cAAc,KAAA,KAAa,EAAE,WAAW,MAAM,UAAU;IAClE,GAAI,MAAM,aAAa,KAAA,KAAa,EAAE,UAAU,MAAM,SAAS;GACjE,CAAC;GACD,MAAM,OAAO,OAAO,CAAC,eAAe,YAAY,KAAK;IAAE,OAAO;IAAK,MAAM;GAAM,CAAC,CAAC,CAAC;GAClF,OAAO;EACT,CAAC;EACD,KAAK,OAAO,KAAK,+BAA+B,EAAE,MAAM,OAAO,CAAC;EAChE,OAAO;CACT;CAEA,MAAM,WAAW,IAAY,OAAuC;EAElE,IAAI,CAAC,MADgB,KAAK,KAAK,aAAa,EAAE,GAE5C,MAAM,IAAI,cAAc,QAAQ,EAAE;EAGpC,MAAM,UAAU,MAAM,KAAK,IAAI,IAAI,OAAO,EAAE,UAAU,aAAa;GACjE,MAAM,OAAO,MAAM,SAAS,WAAW,IAAI,KAAK;GAChD,MAAM,OAAO,OAAO,CAAC,eAAe,YAAY,KAAK;IAAE,OAAO;IAAK,MAAM;GAAM,CAAC,CAAC,CAAC;GAClF,OAAO;EACT,CAAC;EACD,KAAK,OAAO,KAAK,gBAAgB,EAAE,MAAM,QAAQ,CAAC;EAClD,OAAO;CACT;CAEA,MAAM,oBAAoB,YAA0C;EAClE,OAAO,KAAK,KAAK,qBAAqB,UAAU;CAClD;CAEA,MAAM,eAAe,OAAqC;EACxD,OAAO,KAAK,KAAK,gBAAgB,KAAK;CACxC;CAEA,MAAM,YAAY,IAAkC;EAClD,OAAO,KAAK,KAAK,aAAa,EAAE;CAClC;CAEA,MAAM,kBAAkB,OAAsD;EAC5E,MAAM,KAAK,OAAO,KAAK,MAAM,OAAO,iBAAiB,EAAE,UAAU,MAAM,IAAI,CAAC;CAC9E;CAEA,MAAM,oBAAoB,OAAyC;EACjE,MAAM,KAAK,OAAO,KAAK,MAAM,OAAO,mBAAmB,CAAC,CAAC;CAC3D;CAEA,MAAM,cAAc,OAAsD;EACxE,MAAM,KAAK,OAAO,KAAK,MAAM,OAAO,aAAa,EAAE,KAAK,MAAM,IAAI,CAAC;CACrE;AACF"}
1
+ {"version":3,"file":"service.js","names":[],"sources":["../../src/identity/service.ts"],"sourcesContent":["import type { IdentityRepository, IdentityService, IdentityUnitOfWork } from './ports.js';\nimport type { User, UserProfile, UserProfileView } from './model.js';\nimport type { UpdateProfileInput } from './ports.js';\nimport type { CreateUserInput, UpdateUserInput } from './types.js';\nimport type { Logger } from '../shared/ports.js';\nimport { noopLogger } from '../shared/logger.js';\nimport { genId } from '../shared/id.js';\nimport { ConflictError, ForbiddenError, NotFoundError } from '../shared/errors.js';\nimport type { Mailer } from '../shared/mailer.js';\nimport type { FileUrls, MemberImageReader } from '../assets/index.js';\nimport { identityEvents } from './events.js';\n\nconst UNNAMED = 'A member';\n\nexport class IdentityServiceImpl implements IdentityService {\n private readonly repo: IdentityRepository;\n private readonly logger: Logger;\n private readonly uow: IdentityUnitOfWork;\n private readonly mailer: Pick<Mailer, 'send'>;\n private readonly images: MemberImageReader;\n private readonly fileUrls: FileUrls;\n\n constructor(input: {\n repo: IdentityRepository;\n logger?: Logger;\n mailer: Mailer;\n uow: IdentityUnitOfWork;\n images: MemberImageReader;\n fileUrls: FileUrls;\n }) {\n this.repo = input.repo;\n this.mailer = input.mailer;\n this.logger = input.logger ?? noopLogger;\n this.uow = input.uow;\n this.images = input.images;\n this.fileUrls = input.fileUrls;\n }\n\n async createUser(input: CreateUserInput): Promise<User> {\n const created = await this.uow.run(async ({ identity, outbox }) => {\n const user = await identity.insertUser(input);\n await outbox.append([identityEvents.userCreated.make({ orgId: '-', data: user })]);\n return user;\n });\n this.logger.info('user created', { userId: created.id });\n return created;\n }\n\n async linkOrCreateUser(input: CreateUserInput): Promise<User> {\n const existing = await this.repo.findUserByEmail(input.email);\n if (!existing) {\n const id = input.id ?? genId('user');\n return this.createUser({ ...input, id, externalId: id });\n }\n if (existing.externalId !== null) {\n throw new ConflictError('That email already has an account');\n }\n // Existing user without external link, so invited. update the user with the\n // id.\n const linked = await this.uow.run(async ({ identity, outbox }) => {\n const user = await identity.updateUser(existing.id, {\n externalId: existing.id,\n ...(input.firstName !== undefined && { firstName: input.firstName }),\n ...(input.lastName !== undefined && { lastName: input.lastName }),\n });\n await outbox.append([identityEvents.userUpdated.make({ orgId: '-', data: user! })]);\n return user!;\n });\n this.logger.info('user linked to auth account', { userId: linked.id });\n return linked;\n }\n\n async updateUser(id: string, input: UpdateUserInput): Promise<User> {\n const person = await this.repo.findUserById(id);\n if (!person) {\n throw new NotFoundError('User', id);\n }\n\n const updated = await this.uow.run(async ({ identity, outbox }) => {\n const user = await identity.updateUser(id, input);\n await outbox.append([identityEvents.userUpdated.make({ orgId: '-', data: user! })]);\n return user!;\n });\n this.logger.info('user updated', { userId: updated.id });\n return updated;\n }\n\n async getUserByExternalId(externalId: string): Promise<User | null> {\n return this.repo.findUserByExternalId(externalId);\n }\n\n async getUserByEmail(email: string): Promise<User | null> {\n return this.repo.findUserByEmail(email);\n }\n\n async getUserById(id: string): Promise<User | null> {\n return this.repo.findUserById(id);\n }\n\n async sendPasswordReset(input: { email: string; url: string }): Promise<void> {\n await this.mailer.send(input.email, 'passwordReset', { resetUrl: input.url });\n }\n\n async sendPasswordChanged(input: { email: string }): Promise<void> {\n await this.mailer.send(input.email, 'passwordChanged', {});\n }\n\n async sendMagicLink(input: { email: string; url: string }): Promise<void> {\n await this.mailer.send(input.email, 'magicLink', { url: input.url });\n }\n\n async profileOf(orgId: string, userId: string): Promise<UserProfileView> {\n const stored = await this.repo.findProfile(userId);\n if (stored) {\n return this.view(orgId, stored);\n }\n const never = new Date(0);\n return {\n userId,\n displayName: null,\n headline: null,\n bio: null,\n imageAssetOrgId: null,\n imageAssetId: null,\n imageUrl: null,\n extra: {},\n createdAt: never,\n updatedAt: never,\n };\n }\n\n private view(orgId: string, profile: UserProfile): UserProfileView {\n const imageAssetId = profile.imageAssetOrgId === orgId ? profile.imageAssetId : null;\n return { ...profile, imageUrl: imageAssetId ? this.fileUrls.memberUpload(imageAssetId) : null };\n }\n\n async displayName(userId: string | null): Promise<string> {\n if (userId === null) {\n return UNNAMED;\n }\n const [profile, user] = await Promise.all([\n this.repo.findProfile(userId),\n this.repo.findUserById(userId),\n ]);\n const named = profile?.displayName?.trim();\n if (named) {\n return named;\n }\n const composed = [user?.firstName, user?.lastName].filter(Boolean).join(' ').trim();\n return composed || UNNAMED;\n }\n\n async updateProfile(\n orgId: string,\n userId: string,\n actorUserId: string,\n orgUserId: string,\n patch: UpdateProfileInput,\n ): Promise<UserProfileView> {\n if (actorUserId !== userId) {\n throw new ForbiddenError('You can only update your own profile');\n }\n const user = await this.repo.findUserById(userId);\n if (!user) {\n throw new NotFoundError('User', userId);\n }\n if (patch.imageAssetId != null) {\n const owned = await this.images.ownsImage(orgId, patch.imageAssetId, orgUserId);\n if (!owned) {\n this.logger.debug('profile photo refused', {\n userId,\n orgUserId,\n assetId: patch.imageAssetId,\n });\n throw new NotFoundError('Asset', patch.imageAssetId);\n }\n }\n const imageAssetOrgId =\n patch.imageAssetId === undefined ? undefined : patch.imageAssetId === null ? null : orgId;\n const profile = await this.repo.upsertProfile(userId, { ...patch, imageAssetOrgId });\n this.logger.info('profile updated', { userId });\n return this.view(orgId, profile);\n }\n}\n"],"mappings":"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"}
@@ -22,7 +22,7 @@ var ActionInvocationError = class extends Error {
22
22
  integrationId;
23
23
  actionId;
24
24
  constructor(integrationId, actionId, cause) {
25
- super(`action "${integrationId}.${actionId}" failed: ${cause instanceof Error ? cause.message : String(cause)}`);
25
+ super(`action "${integrationId}.${actionId}" failed: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
26
26
  this.integrationId = integrationId;
27
27
  this.actionId = actionId;
28
28
  this.name = "ActionInvocationError";
@@ -1 +1 @@
1
- {"version":3,"file":"model.js","names":[],"sources":["../../src/integrations/model.ts"],"sourcesContent":["// integrations context — entities & DTOs are owned by @headless-lms/core/types;\n// the domain errors below are runtime code and stay in core.\nexport type { Connection, ConnectInput, ConfigureInput } from '../types/index.js';\n\n/** An org already has a connection for this integration (one per integration per org). */\nexport class AlreadyConnectedError extends Error {\n constructor(readonly integrationId: string) {\n super(`a connection for \"${integrationId}\" already exists`);\n this.name = 'AlreadyConnectedError';\n }\n}\n\n/** The named integration is not declared in the registry. */\nexport class UnknownIntegrationError extends Error {\n constructor(readonly integrationId: string) {\n super(`unknown integration \"${integrationId}\"`);\n this.name = 'UnknownIntegrationError';\n }\n}\n\n/** An action's external call failed (or the action rejected its input). */\nexport class ActionInvocationError extends Error {\n constructor(\n readonly integrationId: string,\n readonly actionId: string,\n cause: unknown,\n ) {\n super(\n `action \"${integrationId}.${actionId}\" failed: ${\n cause instanceof Error ? cause.message : String(cause)\n }`,\n );\n this.name = 'ActionInvocationError';\n }\n}\n\n/** The config was rejected by the integration's validator. */\nexport class InvalidConfigError extends Error {\n constructor(\n readonly integrationId: string,\n readonly errors: string[],\n ) {\n super(`invalid config for \"${integrationId}\": ${errors.join('; ')}`);\n this.name = 'InvalidConfigError';\n }\n}\n"],"mappings":";;AAKA,IAAa,wBAAb,cAA2C,MAAM;CAC1B;CAArB,YAAY,eAAgC;EAC1C,MAAM,qBAAqB,cAAc,iBAAiB;EADvC,KAAA,gBAAA;EAEnB,KAAK,OAAO;CACd;AACF;;AAGA,IAAa,0BAAb,cAA6C,MAAM;CAC5B;CAArB,YAAY,eAAgC;EAC1C,MAAM,wBAAwB,cAAc,EAAE;EAD3B,KAAA,gBAAA;EAEnB,KAAK,OAAO;CACd;AACF;;AAGA,IAAa,wBAAb,cAA2C,MAAM;CAEpC;CACA;CAFX,YACE,eACA,UACA,OACA;EACA,MACE,WAAW,cAAc,GAAG,SAAS,YACnC,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,GAEzD;EARS,KAAA,gBAAA;EACA,KAAA,WAAA;EAQT,KAAK,OAAO;CACd;AACF;;AAGA,IAAa,qBAAb,cAAwC,MAAM;CAEjC;CACA;CAFX,YACE,eACA,QACA;EACA,MAAM,uBAAuB,cAAc,KAAK,OAAO,KAAK,IAAI,GAAG;EAH1D,KAAA,gBAAA;EACA,KAAA,SAAA;EAGT,KAAK,OAAO;CACd;AACF"}
1
+ {"version":3,"file":"model.js","names":[],"sources":["../../src/integrations/model.ts"],"sourcesContent":["// integrations context — entities & DTOs are owned by @headless-lms/core/types;\n// the domain errors below are runtime code and stay in core.\nexport type { Connection, ConnectInput, ConfigureInput } from '../types/index.js';\n\n/** An org already has a connection for this integration (one per integration per org). */\nexport class AlreadyConnectedError extends Error {\n constructor(readonly integrationId: string) {\n super(`a connection for \"${integrationId}\" already exists`);\n this.name = 'AlreadyConnectedError';\n }\n}\n\n/** The named integration is not declared in the registry. */\nexport class UnknownIntegrationError extends Error {\n constructor(readonly integrationId: string) {\n super(`unknown integration \"${integrationId}\"`);\n this.name = 'UnknownIntegrationError';\n }\n}\n\n/** An action's external call failed (or the action rejected its input). */\nexport class ActionInvocationError extends Error {\n constructor(\n readonly integrationId: string,\n readonly actionId: string,\n cause: unknown,\n ) {\n super(\n `action \"${integrationId}.${actionId}\" failed: ${\n cause instanceof Error ? cause.message : String(cause)\n }`,\n { cause },\n );\n this.name = 'ActionInvocationError';\n }\n}\n\n/** The config was rejected by the integration's validator. */\nexport class InvalidConfigError extends Error {\n constructor(\n readonly integrationId: string,\n readonly errors: string[],\n ) {\n super(`invalid config for \"${integrationId}\": ${errors.join('; ')}`);\n this.name = 'InvalidConfigError';\n }\n}\n"],"mappings":";;AAKA,IAAa,wBAAb,cAA2C,MAAM;CAC1B;CAArB,YAAY,eAAgC;EAC1C,MAAM,qBAAqB,cAAc,iBAAiB;EADvC,KAAA,gBAAA;EAEnB,KAAK,OAAO;CACd;AACF;;AAGA,IAAa,0BAAb,cAA6C,MAAM;CAC5B;CAArB,YAAY,eAAgC;EAC1C,MAAM,wBAAwB,cAAc,EAAE;EAD3B,KAAA,gBAAA;EAEnB,KAAK,OAAO;CACd;AACF;;AAGA,IAAa,wBAAb,cAA2C,MAAM;CAEpC;CACA;CAFX,YACE,eACA,UACA,OACA;EACA,MACE,WAAW,cAAc,GAAG,SAAS,YACnC,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,KAEvD,EAAE,MAAM,CACV;EATS,KAAA,gBAAA;EACA,KAAA,WAAA;EAST,KAAK,OAAO;CACd;AACF;;AAGA,IAAa,qBAAb,cAAwC,MAAM;CAEjC;CACA;CAFX,YACE,eACA,QACA;EACA,MAAM,uBAAuB,cAAc,KAAK,OAAO,KAAK,IAAI,GAAG;EAH1D,KAAA,gBAAA;EACA,KAAA,SAAA;EAGT,KAAK,OAAO;CACd;AACF"}
@@ -1,6 +1,6 @@
1
- import { noopLogger } from "../shared/logger.js";
2
1
  import { genId } from "../shared/id.js";
3
2
  import { ConflictError, NotFoundError } from "../shared/errors.js";
3
+ import { noopLogger } from "../shared/logger.js";
4
4
  import { ActionInvocationError, AlreadyConnectedError, InvalidConfigError, UnknownIntegrationError } from "./model.js";
5
5
  import { integrationEvents } from "./events.js";
6
6
  //#region src/integrations/service.ts
@@ -172,7 +172,7 @@ var IntegrationsServiceImpl = class {
172
172
  connectionId,
173
173
  integrationId: connection.integrationId,
174
174
  actionId,
175
- error: err instanceof Error ? err.message : String(err)
175
+ err
176
176
  });
177
177
  throw new ActionInvocationError(connection.integrationId, actionId, err);
178
178
  }
@@ -1 +1 @@
1
- {"version":3,"file":"service.js","names":[],"sources":["../../src/integrations/service.ts"],"sourcesContent":["// integrations context — service implementation (inbound port). Owns the\n// connection lifecycle; credentials live in the shared secure credential store\n// (this service holds only the ref). The integrations the system supports are\n// declared at startup via the IntegrationsRegistry: connect/configure reject\n// unknown integration ids and validate config with the integration's own\n// validator. The domain never calls the external service — consumers take the\n// connection, reveal the credential at point of use, and build their own adapter.\n//\n// Mutations run inside the context's UnitOfWork: credential writes, connection\n// writes, and the outbox append commit in ONE transaction (transactional\n// outbox; this also closed the historical orphan-credential window). The\n// outbox relay — not this service — publishes to the EventBus.\nimport { genId } from '../shared/id.js';\nimport {\n ActionInvocationError,\n AlreadyConnectedError,\n InvalidConfigError,\n UnknownIntegrationError,\n type ConfigureInput,\n type ConnectInput,\n type Connection,\n} from './model.js';\nimport type {\n ConnectionsRepository,\n IntegrationsRegistry,\n IntegrationsService,\n IntegrationsUnitOfWork,\n} from './ports.js';\nimport type { CredentialStore, Logger } from '../shared/ports.js';\nimport { ConflictError, NotFoundError } from '../shared/errors.js';\nimport { noopLogger } from '../shared/logger.js';\nimport { integrationEvents } from './events.js';\n\nexport type IntegrationsServiceParams = {\n registry: IntegrationsRegistry;\n /** Read-only access (find/list) — runs outside any transaction. */\n repo: ConnectionsRepository;\n /** Atomic write scope: tx-bound connections repo + credential store + outbox. */\n uow: IntegrationsUnitOfWork;\n /** Point-of-use credential reveal for action invocation — reads only. */\n credentials: CredentialStore;\n logger?: Logger;\n};\n\nexport class IntegrationsServiceImpl implements IntegrationsService {\n private readonly registry: IntegrationsRegistry;\n private readonly repo: ConnectionsRepository;\n private readonly uow: IntegrationsUnitOfWork;\n private readonly credentials: CredentialStore;\n private readonly logger: Logger;\n\n constructor(params: IntegrationsServiceParams) {\n this.registry = params.registry;\n this.repo = params.repo;\n this.uow = params.uow;\n this.credentials = params.credentials;\n this.logger = params.logger ?? noopLogger;\n }\n\n available() {\n return this.registry.list().map((integration) => ({\n id: integration.id,\n configSchema: integration.configSchema(),\n secretsSchema: integration.secretsSchema(),\n actions: integration.actions.map((action) => ({\n id: action.id,\n description: action.description,\n inputSchema: action.inputSchema(),\n outputSchema: action.outputSchema(),\n })),\n }));\n }\n\n private validate(integrationId: string, config: Record<string, unknown>): void {\n const integration = this.registry.get(integrationId);\n if (!integration) {\n this.logger.warn('unknown integration rejected', { integrationId });\n throw new UnknownIntegrationError(integrationId);\n }\n const result = integration.validateConfig(config);\n if (!result.ok) {\n this.logger.warn('invalid integration config rejected', {\n integrationId,\n errors: result.errors,\n });\n throw new InvalidConfigError(integrationId, result.errors);\n }\n }\n\n async connect(orgId: string, input: ConnectInput): Promise<Connection> {\n const config = input.config ?? {};\n this.validate(input.integrationId, config);\n const existing = await this.repo.findByIntegration(orgId, input.integrationId);\n if (existing) {\n this.logger.warn('duplicate connection rejected', {\n orgId,\n integrationId: input.integrationId,\n });\n throw new AlreadyConnectedError(input.integrationId);\n }\n const connection = await this.uow.run(async ({ connections, credentials, outbox }) => {\n const credentialRef = await credentials.store(orgId, input.secrets);\n const at = new Date();\n const created = await connections.insert(orgId, {\n orgId,\n id: genId('connection'),\n integrationId: input.integrationId,\n config,\n active: true,\n credentialRef,\n createdAt: at,\n updatedAt: at,\n });\n await outbox.append([\n integrationEvents.connectionCreated.make({\n orgId,\n data: created,\n }),\n ]);\n return created;\n });\n this.logger.info('integration connected', {\n orgId,\n integrationId: connection.integrationId,\n connectionId: connection.id,\n });\n return connection;\n }\n\n async reconnect(\n orgId: string,\n id: string,\n secrets: Record<string, unknown>,\n ): Promise<Connection | null> {\n const connection = await this.repo.findById(orgId, id);\n if (!connection) {\n return null;\n }\n const updated = await this.uow.run(async ({ connections, credentials, outbox }) => {\n await credentials.update(orgId, connection.credentialRef, secrets);\n const result = await connections.update(orgId, id, { updatedAt: new Date() });\n if (result) {\n await outbox.append([integrationEvents.connectionUpdated.make({ orgId, data: result })]);\n }\n return result;\n });\n this.logger.info('integration credentials rotated', {\n orgId,\n connectionId: id,\n integrationId: connection.integrationId,\n });\n return updated;\n }\n\n async configure(orgId: string, id: string, input: ConfigureInput): Promise<Connection | null> {\n const connection = await this.repo.findById(orgId, id);\n if (!connection) {\n return null;\n }\n if (input.config !== undefined) {\n this.validate(connection.integrationId, input.config);\n }\n const updated = await this.uow.run(async ({ connections, outbox }) => {\n const result = await connections.update(orgId, id, {\n ...(input.config !== undefined ? { config: input.config } : {}),\n ...(input.active !== undefined ? { active: input.active } : {}),\n updatedAt: new Date(),\n });\n if (result) {\n await outbox.append([integrationEvents.connectionUpdated.make({ orgId, data: result })]);\n }\n return result;\n });\n this.logger.info('integration configured', {\n orgId,\n connectionId: id,\n integrationId: connection.integrationId,\n });\n return updated;\n }\n\n async disconnect(orgId: string, id: string): Promise<boolean> {\n const connection = await this.repo.findById(orgId, id);\n if (!connection) {\n return false;\n }\n const deleted = await this.uow.run(async ({ connections, credentials, outbox }) => {\n // Connection first: it holds the FK onto the credential row.\n const ok = await connections.delete(orgId, id);\n await credentials.destroy(orgId, connection.credentialRef);\n await outbox.append([integrationEvents.connectionRemoved.make({ orgId, data: connection })]);\n return ok;\n });\n this.logger.info('integration disconnected', {\n orgId,\n connectionId: id,\n integrationId: connection.integrationId,\n });\n return deleted;\n }\n\n list(orgId: string): Promise<Connection[]> {\n return this.repo.list(orgId);\n }\n\n get(orgId: string, id: string): Promise<Connection | null> {\n return this.repo.findById(orgId, id);\n }\n\n getByIntegration(orgId: string, integrationId: string): Promise<Connection | null> {\n return this.repo.findByIntegration(orgId, integrationId);\n }\n\n async invoke(\n orgId: string,\n connectionId: string,\n actionId: string,\n input: Record<string, unknown>,\n ): Promise<unknown> {\n const connection = await this.repo.findById(orgId, connectionId);\n if (!connection) {\n throw new NotFoundError('Connection', connectionId);\n }\n if (!connection.active) {\n throw new ConflictError(`connection \"${connectionId}\" is inactive`);\n }\n const integration = this.registry.get(connection.integrationId);\n if (!integration) {\n throw new UnknownIntegrationError(connection.integrationId);\n }\n const action = integration.actions.find((a) => a.id === actionId);\n if (!action) {\n throw new NotFoundError('Action', actionId);\n }\n const secrets = await this.credentials.reveal(orgId, connection.credentialRef);\n if (!secrets) {\n throw new NotFoundError('Credential', connectionId);\n }\n try {\n return await action.invoke({ secrets, config: connection.config }, input);\n } catch (err) {\n this.logger.warn('integration action failed', {\n orgId,\n connectionId,\n integrationId: connection.integrationId,\n actionId,\n error: err instanceof Error ? err.message : String(err),\n });\n throw new ActionInvocationError(connection.integrationId, actionId, err);\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"service.js","names":[],"sources":["../../src/integrations/service.ts"],"sourcesContent":["// integrations context — service implementation (inbound port). Owns the\n// connection lifecycle; credentials live in the shared secure credential store\n// (this service holds only the ref). The integrations the system supports are\n// declared at startup via the IntegrationsRegistry: connect/configure reject\n// unknown integration ids and validate config with the integration's own\n// validator. The domain never calls the external service — consumers take the\n// connection, reveal the credential at point of use, and build their own adapter.\n//\n// Mutations run inside the context's UnitOfWork: credential writes, connection\n// writes, and the outbox append commit in ONE transaction (transactional\n// outbox; this also closed the historical orphan-credential window). The\n// outbox relay — not this service — publishes to the EventBus.\nimport { genId } from '../shared/id.js';\nimport {\n ActionInvocationError,\n AlreadyConnectedError,\n InvalidConfigError,\n UnknownIntegrationError,\n type ConfigureInput,\n type ConnectInput,\n type Connection,\n} from './model.js';\nimport type {\n ConnectionsRepository,\n IntegrationsRegistry,\n IntegrationsService,\n IntegrationsUnitOfWork,\n} from './ports.js';\nimport type { CredentialStore, Logger } from '../shared/ports.js';\nimport { ConflictError, NotFoundError } from '../shared/errors.js';\nimport { noopLogger } from '../shared/logger.js';\nimport { integrationEvents } from './events.js';\n\nexport type IntegrationsServiceParams = {\n registry: IntegrationsRegistry;\n /** Read-only access (find/list) — runs outside any transaction. */\n repo: ConnectionsRepository;\n /** Atomic write scope: tx-bound connections repo + credential store + outbox. */\n uow: IntegrationsUnitOfWork;\n /** Point-of-use credential reveal for action invocation — reads only. */\n credentials: CredentialStore;\n logger?: Logger;\n};\n\nexport class IntegrationsServiceImpl implements IntegrationsService {\n private readonly registry: IntegrationsRegistry;\n private readonly repo: ConnectionsRepository;\n private readonly uow: IntegrationsUnitOfWork;\n private readonly credentials: CredentialStore;\n private readonly logger: Logger;\n\n constructor(params: IntegrationsServiceParams) {\n this.registry = params.registry;\n this.repo = params.repo;\n this.uow = params.uow;\n this.credentials = params.credentials;\n this.logger = params.logger ?? noopLogger;\n }\n\n available() {\n return this.registry.list().map((integration) => ({\n id: integration.id,\n configSchema: integration.configSchema(),\n secretsSchema: integration.secretsSchema(),\n actions: integration.actions.map((action) => ({\n id: action.id,\n description: action.description,\n inputSchema: action.inputSchema(),\n outputSchema: action.outputSchema(),\n })),\n }));\n }\n\n private validate(integrationId: string, config: Record<string, unknown>): void {\n const integration = this.registry.get(integrationId);\n if (!integration) {\n this.logger.warn('unknown integration rejected', { integrationId });\n throw new UnknownIntegrationError(integrationId);\n }\n const result = integration.validateConfig(config);\n if (!result.ok) {\n this.logger.warn('invalid integration config rejected', {\n integrationId,\n errors: result.errors,\n });\n throw new InvalidConfigError(integrationId, result.errors);\n }\n }\n\n async connect(orgId: string, input: ConnectInput): Promise<Connection> {\n const config = input.config ?? {};\n this.validate(input.integrationId, config);\n const existing = await this.repo.findByIntegration(orgId, input.integrationId);\n if (existing) {\n this.logger.warn('duplicate connection rejected', {\n orgId,\n integrationId: input.integrationId,\n });\n throw new AlreadyConnectedError(input.integrationId);\n }\n const connection = await this.uow.run(async ({ connections, credentials, outbox }) => {\n const credentialRef = await credentials.store(orgId, input.secrets);\n const at = new Date();\n const created = await connections.insert(orgId, {\n orgId,\n id: genId('connection'),\n integrationId: input.integrationId,\n config,\n active: true,\n credentialRef,\n createdAt: at,\n updatedAt: at,\n });\n await outbox.append([\n integrationEvents.connectionCreated.make({\n orgId,\n data: created,\n }),\n ]);\n return created;\n });\n this.logger.info('integration connected', {\n orgId,\n integrationId: connection.integrationId,\n connectionId: connection.id,\n });\n return connection;\n }\n\n async reconnect(\n orgId: string,\n id: string,\n secrets: Record<string, unknown>,\n ): Promise<Connection | null> {\n const connection = await this.repo.findById(orgId, id);\n if (!connection) {\n return null;\n }\n const updated = await this.uow.run(async ({ connections, credentials, outbox }) => {\n await credentials.update(orgId, connection.credentialRef, secrets);\n const result = await connections.update(orgId, id, { updatedAt: new Date() });\n if (result) {\n await outbox.append([integrationEvents.connectionUpdated.make({ orgId, data: result })]);\n }\n return result;\n });\n this.logger.info('integration credentials rotated', {\n orgId,\n connectionId: id,\n integrationId: connection.integrationId,\n });\n return updated;\n }\n\n async configure(orgId: string, id: string, input: ConfigureInput): Promise<Connection | null> {\n const connection = await this.repo.findById(orgId, id);\n if (!connection) {\n return null;\n }\n if (input.config !== undefined) {\n this.validate(connection.integrationId, input.config);\n }\n const updated = await this.uow.run(async ({ connections, outbox }) => {\n const result = await connections.update(orgId, id, {\n ...(input.config !== undefined ? { config: input.config } : {}),\n ...(input.active !== undefined ? { active: input.active } : {}),\n updatedAt: new Date(),\n });\n if (result) {\n await outbox.append([integrationEvents.connectionUpdated.make({ orgId, data: result })]);\n }\n return result;\n });\n this.logger.info('integration configured', {\n orgId,\n connectionId: id,\n integrationId: connection.integrationId,\n });\n return updated;\n }\n\n async disconnect(orgId: string, id: string): Promise<boolean> {\n const connection = await this.repo.findById(orgId, id);\n if (!connection) {\n return false;\n }\n const deleted = await this.uow.run(async ({ connections, credentials, outbox }) => {\n // Connection first: it holds the FK onto the credential row.\n const ok = await connections.delete(orgId, id);\n await credentials.destroy(orgId, connection.credentialRef);\n await outbox.append([integrationEvents.connectionRemoved.make({ orgId, data: connection })]);\n return ok;\n });\n this.logger.info('integration disconnected', {\n orgId,\n connectionId: id,\n integrationId: connection.integrationId,\n });\n return deleted;\n }\n\n list(orgId: string): Promise<Connection[]> {\n return this.repo.list(orgId);\n }\n\n get(orgId: string, id: string): Promise<Connection | null> {\n return this.repo.findById(orgId, id);\n }\n\n getByIntegration(orgId: string, integrationId: string): Promise<Connection | null> {\n return this.repo.findByIntegration(orgId, integrationId);\n }\n\n async invoke(\n orgId: string,\n connectionId: string,\n actionId: string,\n input: Record<string, unknown>,\n ): Promise<unknown> {\n const connection = await this.repo.findById(orgId, connectionId);\n if (!connection) {\n throw new NotFoundError('Connection', connectionId);\n }\n if (!connection.active) {\n throw new ConflictError(`connection \"${connectionId}\" is inactive`);\n }\n const integration = this.registry.get(connection.integrationId);\n if (!integration) {\n throw new UnknownIntegrationError(connection.integrationId);\n }\n const action = integration.actions.find((a) => a.id === actionId);\n if (!action) {\n throw new NotFoundError('Action', actionId);\n }\n const secrets = await this.credentials.reveal(orgId, connection.credentialRef);\n if (!secrets) {\n throw new NotFoundError('Credential', connectionId);\n }\n try {\n return await action.invoke({ secrets, config: connection.config }, input);\n } catch (err) {\n this.logger.warn('integration action failed', {\n orgId,\n connectionId,\n integrationId: connection.integrationId,\n actionId,\n err,\n });\n throw new ActionInvocationError(connection.integrationId, actionId, err);\n }\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ import { Role } from "../types/schemas/organizations.js";
2
+ import { Permission } from "./roles.js";
3
+ //#region src/organizations/access-scopes.d.ts
4
+ declare const ACCESS_SCOPES: readonly ["courses:read", "courses:write", "courses:delete", "downloads:read", "downloads:write", "downloads:delete", "bundles:read", "bundles:write", "bundles:delete", "media:read", "students:read", "students:invite", "access:read", "access:write", "analytics:read", "automations:read", "automations:write", "automations:delete", "comments:read", "comments:moderate", "communities:read", "communities:write", "communities:delete", "community_members:read", "community_members:write", "community_moderation:read", "community_moderation:write"];
5
+ type AccessScope = (typeof ACCESS_SCOPES)[number];
6
+ declare const SCOPE_PERMISSION: Record<AccessScope, Permission | null>;
7
+ type ScopeOption = {
8
+ scope: AccessScope;
9
+ allowed: boolean;
10
+ };
11
+ declare function isAccessScope(value: string): value is AccessScope;
12
+ declare function isDeleteScope(scope: AccessScope): boolean;
13
+ declare function roleHoldsScope(role: Role, scope: AccessScope): boolean;
14
+ declare function scopesForRole(role: Role): AccessScope[];
15
+ declare function scopeOptions(role: Role): ScopeOption[];
16
+ declare function assertRoleHoldsScopes(role: Role, scopes: readonly AccessScope[]): void;
17
+ //#endregion
18
+ export { ACCESS_SCOPES, AccessScope, SCOPE_PERMISSION, ScopeOption, assertRoleHoldsScopes, isAccessScope, isDeleteScope, roleHoldsScope, scopeOptions, scopesForRole };
19
+ //# sourceMappingURL=access-scopes.d.ts.map