@headless-lms/server 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/app/container.d.ts +7 -3
- package/dist/app/container.js +57 -26
- package/dist/app/container.js.map +1 -1
- package/dist/app/notifications.js +4 -1
- package/dist/app/notifications.js.map +1 -1
- package/dist/http/plugins/error-handler.d.ts +1 -1
- package/dist/http/plugins/error-handler.js +15 -4
- package/dist/http/plugins/error-handler.js.map +1 -1
- package/dist/http/plugins/openapi.js +25 -22
- package/dist/http/plugins/openapi.js.map +1 -1
- package/dist/http/routes/activities.js +34 -2
- package/dist/http/routes/activities.js.map +1 -1
- package/dist/http/routes/assets.js +4 -0
- package/dist/http/routes/assets.js.map +1 -1
- package/dist/http/routes/automations.js +32 -8
- package/dist/http/routes/automations.js.map +1 -1
- package/dist/http/routes/bundles.d.ts +7 -0
- package/dist/http/routes/bundles.js +170 -0
- package/dist/http/routes/bundles.js.map +1 -0
- package/dist/http/routes/course-analytics.d.ts +7 -0
- package/dist/http/routes/course-analytics.js +58 -0
- package/dist/http/routes/course-analytics.js.map +1 -0
- package/dist/http/routes/courses.js +8 -8
- package/dist/http/routes/courses.js.map +1 -1
- package/dist/http/routes/dashboard.js +2 -2
- package/dist/http/routes/dashboard.js.map +1 -1
- package/dist/http/routes/discussion.js +1 -1
- package/dist/http/routes/discussion.js.map +1 -1
- package/dist/http/routes/downloads.js +10 -10
- package/dist/http/routes/downloads.js.map +1 -1
- package/dist/http/routes/integrations.js.map +1 -1
- package/dist/http/routes/learn/assets.js +1 -1
- package/dist/http/routes/learn/assets.js.map +1 -1
- package/dist/http/routes/learn/comments.js +7 -7
- package/dist/http/routes/learn/comments.js.map +1 -1
- package/dist/http/routes/learn/courses.js +26 -3
- package/dist/http/routes/learn/courses.js.map +1 -1
- package/dist/http/routes/learn/downloads.js +3 -3
- package/dist/http/routes/learn/downloads.js.map +1 -1
- package/dist/http/routes/learn/org.js +2 -2
- package/dist/http/routes/learn/org.js.map +1 -1
- package/dist/http/routes/learn/progress.js +2 -2
- package/dist/http/routes/learn/progress.js.map +1 -1
- package/dist/http/routes/organizations.js.map +1 -1
- package/dist/http/routes/students.js +27 -6
- package/dist/http/routes/students.js.map +1 -1
- package/dist/http/routes/webhooks.d.ts +7 -0
- package/dist/http/routes/webhooks.js +159 -0
- package/dist/http/routes/webhooks.js.map +1 -0
- package/dist/http/routes.js +6 -0
- package/dist/http/routes.js.map +1 -1
- package/dist/http/schemas/activities.d.ts +34 -1
- package/dist/http/schemas/activities.js +7 -2
- package/dist/http/schemas/activities.js.map +1 -1
- package/dist/http/schemas/assets.d.ts +11 -7
- package/dist/http/schemas/assets.js.map +1 -1
- package/dist/http/schemas/automations.d.ts +75 -8
- package/dist/http/schemas/automations.js +13 -2
- package/dist/http/schemas/automations.js.map +1 -1
- package/dist/http/schemas/bundles.d.ts +63 -0
- package/dist/http/schemas/bundles.js +23 -0
- package/dist/http/schemas/bundles.js.map +1 -0
- package/dist/http/schemas/content.d.ts +18 -11
- package/dist/http/schemas/content.js +1 -2
- package/dist/http/schemas/content.js.map +1 -1
- package/dist/http/schemas/course-analytics.d.ts +29 -0
- package/dist/http/schemas/course-analytics.js +21 -0
- package/dist/http/schemas/course-analytics.js.map +1 -0
- package/dist/http/schemas/dashboard.js.map +1 -1
- package/dist/http/schemas/discussion.d.ts +9 -9
- package/dist/http/schemas/discussion.js.map +1 -1
- package/dist/http/schemas/downloads.d.ts +2 -0
- package/dist/http/schemas/downloads.js.map +1 -1
- package/dist/http/schemas/entitlements.d.ts +14 -6
- package/dist/http/schemas/entitlements.js +4 -2
- package/dist/http/schemas/entitlements.js.map +1 -1
- package/dist/http/schemas/index.d.ts +6 -4
- package/dist/http/schemas/index.js +6 -4
- package/dist/http/schemas/integrations.d.ts +4 -4
- package/dist/http/schemas/integrations.js.map +1 -1
- package/dist/http/schemas/invites.js +2 -1
- package/dist/http/schemas/invites.js.map +1 -1
- package/dist/http/schemas/learn.d.ts +11 -1
- package/dist/http/schemas/learn.js.map +1 -1
- package/dist/http/schemas/members.js.map +1 -1
- package/dist/http/schemas/organizations.js.map +1 -1
- package/dist/http/schemas/settings.js.map +1 -1
- package/dist/http/schemas/shared.d.ts +1 -0
- package/dist/http/schemas/shared.js +3 -1
- package/dist/http/schemas/shared.js.map +1 -1
- package/dist/http/schemas/students.d.ts +30 -2
- package/dist/http/schemas/students.js +21 -2
- package/dist/http/schemas/students.js.map +1 -1
- package/dist/http/server.js +4 -2
- package/dist/http/server.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js.map +1 -1
- package/dist/package.js +6 -0
- package/dist/package.js.map +1 -0
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@ Public surface (`src/index.ts`): `createContainer`, `buildServer`,
|
|
|
23
23
|
`@headless-lms/adapter-db`, which ships the `drizzle/` migration assets;
|
|
24
24
|
`@headless-lms/cli` wraps it as `headless-lms migrate`), and the types
|
|
25
25
|
installations need (`ServerConfig`, `Container`, `AdapterOverrides`, shared ports
|
|
26
|
-
like `EmailSender` / `ObjectStorage`). Everything else is internal.
|
|
26
|
+
like `EmailSender` / `ObjectStorage` / `WebhookSender`). Everything else is internal.
|
|
27
27
|
|
|
28
28
|
## Layout
|
|
29
29
|
|
package/dist/app/container.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { PollingOutboxRelayConfig } from "@headless-lms/adapter-defaults/events/outbox-relay";
|
|
2
2
|
import { LogLevel, PinoInstance, RequestLogContext } from "@headless-lms/adapter-defaults/logging";
|
|
3
3
|
import { BetterAuth } from "@headless-lms/adapter-auth";
|
|
4
|
-
import {
|
|
4
|
+
import { ContentService } from "@headless-lms/core/content";
|
|
5
5
|
import { EntitlementsServiceImpl } from "@headless-lms/core/entitlements";
|
|
6
6
|
import { ProgressServiceImpl } from "@headless-lms/core/progress";
|
|
7
7
|
import { DiscussionServiceImpl } from "@headless-lms/core/discussion";
|
|
@@ -12,10 +12,11 @@ import { IntegrationsServiceImpl } from "@headless-lms/core/integrations";
|
|
|
12
12
|
import { AutomationsServiceImpl } from "@headless-lms/core/automations";
|
|
13
13
|
import { StudentsReportServiceImpl } from "@headless-lms/core/reporting/students";
|
|
14
14
|
import { DashboardReportServiceImpl } from "@headless-lms/core/reporting/dashboard";
|
|
15
|
+
import { CoursesReportServiceImpl } from "@headless-lms/core/reporting/courses";
|
|
15
16
|
import { LearnReportServiceImpl } from "@headless-lms/core/reporting/learn";
|
|
16
17
|
import { Mailer } from "@headless-lms/core/shared/mailer";
|
|
17
18
|
import { SettingsService } from "@headless-lms/core/shared/settings";
|
|
18
|
-
import { AutomationEngine } from "@headless-lms/core/types";
|
|
19
|
+
import { AutomationEngine, WebhookSender } from "@headless-lms/core/types";
|
|
19
20
|
import { CredentialStore, EmailSender, Logger, ObjectStorage, OutboxRelay, TemplateContext, TemplateRenderer } from "@headless-lms/core/shared/ports";
|
|
20
21
|
//#region src/app/container.d.ts
|
|
21
22
|
/** Installation-supplied ports; an absent slot falls back to a fail-loudly stub. */
|
|
@@ -26,6 +27,8 @@ interface AdapterOverrides {
|
|
|
26
27
|
templates?: TemplateRenderer;
|
|
27
28
|
/** Durable automation engine. Absent → InlineAutomationEngine (in-process, one attempt per action). */
|
|
28
29
|
workflows?: AutomationEngine;
|
|
30
|
+
/** Outbound HTTP for webhook deliveries. Absent → FetchWebhookSender (fetch, 10s timeout). */
|
|
31
|
+
webhooks?: WebhookSender;
|
|
29
32
|
}
|
|
30
33
|
interface BuildContainerOptions {
|
|
31
34
|
/** Installation's plugins folder, scanned by loadIntegrations. Absent → no integrations. */
|
|
@@ -82,7 +85,7 @@ interface Container {
|
|
|
82
85
|
orgProvider: OrgAdmin;
|
|
83
86
|
identity: IdentityServiceImpl;
|
|
84
87
|
organizations: OrganizationServiceImpl;
|
|
85
|
-
content:
|
|
88
|
+
content: ContentService;
|
|
86
89
|
entitlements: EntitlementsServiceImpl;
|
|
87
90
|
progress: ProgressServiceImpl;
|
|
88
91
|
discussion: DiscussionServiceImpl;
|
|
@@ -95,6 +98,7 @@ interface Container {
|
|
|
95
98
|
reporting: {
|
|
96
99
|
students: StudentsReportServiceImpl;
|
|
97
100
|
dashboard: DashboardReportServiceImpl;
|
|
101
|
+
courses: CoursesReportServiceImpl;
|
|
98
102
|
learn: LearnReportServiceImpl;
|
|
99
103
|
};
|
|
100
104
|
storage: ObjectStorage;
|
package/dist/app/container.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { loadIntegrations } from "./integrations.js";
|
|
2
2
|
import { registerNotificationSubscribers } from "./notifications.js";
|
|
3
|
-
import { DrizzleAssetsRepository, DrizzleAutomationRunsRepository, DrizzleAutomationsRepository, DrizzleConnectionsRepository, DrizzleContentRepository, DrizzleCredentialStore, DrizzleDashboardRepository, DrizzleDiscussionRepository, DrizzleEntitlementsRepository, DrizzleIdentityRepository, DrizzleLearnRepository, DrizzleMembersRepository, DrizzleOrganizationsRepository, DrizzleOutboxAppender, DrizzleOutboxStore, DrizzleProgressRepository, DrizzleSettingsRepository, DrizzleStudentsRepository, DrizzleUnitOfWork, createDb } from "@headless-lms/adapter-db";
|
|
3
|
+
import { DrizzleAssetsRepository, DrizzleAutomationRunsRepository, DrizzleAutomationsRepository, DrizzleConnectionsRepository, DrizzleContentRepository, DrizzleCourseAnalyticsRepository, DrizzleCredentialStore, DrizzleDashboardRepository, DrizzleDiscussionRepository, DrizzleEntitlementsRepository, DrizzleIdentityRepository, DrizzleLearnRepository, DrizzleMembersRepository, DrizzleOrganizationsRepository, DrizzleOutboxAppender, DrizzleOutboxStore, DrizzleProgressRepository, DrizzleSettingsRepository, DrizzleStudentsRepository, DrizzleUnitOfWork, createDb } from "@headless-lms/adapter-db";
|
|
4
4
|
import { InMemoryEventBus } from "@headless-lms/adapter-defaults/events";
|
|
5
5
|
import { PollingOutboxRelay } from "@headless-lms/adapter-defaults/events/outbox-relay";
|
|
6
6
|
import { InlineAutomationEngine } from "@headless-lms/adapter-defaults/workflows";
|
|
7
|
+
import { FetchWebhookSender } from "@headless-lms/adapter-defaults/webhooks";
|
|
7
8
|
import { EmailAdapter, StubTemplateRenderer } from "@headless-lms/adapter-defaults/email";
|
|
8
9
|
import { createRootLogger, requestLogContext } from "@headless-lms/adapter-defaults/logging";
|
|
9
10
|
import { StorageAdapter } from "@headless-lms/adapter-defaults/storage";
|
|
10
11
|
import { BetterAuth } from "@headless-lms/adapter-auth";
|
|
11
|
-
import {
|
|
12
|
+
import { ContentService } from "@headless-lms/core/content";
|
|
12
13
|
import { EntitlementsServiceImpl } from "@headless-lms/core/entitlements";
|
|
13
14
|
import { ProgressServiceImpl } from "@headless-lms/core/progress";
|
|
14
15
|
import { DiscussionServiceImpl } from "@headless-lms/core/discussion";
|
|
@@ -19,6 +20,7 @@ import { IntegrationsServiceImpl } from "@headless-lms/core/integrations";
|
|
|
19
20
|
import { AutomationsServiceImpl } from "@headless-lms/core/automations";
|
|
20
21
|
import { StudentsReportServiceImpl } from "@headless-lms/core/reporting/students";
|
|
21
22
|
import { DashboardReportServiceImpl } from "@headless-lms/core/reporting/dashboard";
|
|
23
|
+
import { CoursesReportServiceImpl } from "@headless-lms/core/reporting/courses";
|
|
22
24
|
import { LearnReportServiceImpl } from "@headless-lms/core/reporting/learn";
|
|
23
25
|
import { Mailer } from "@headless-lms/core/shared/mailer";
|
|
24
26
|
import { SettingsService } from "@headless-lms/core/shared/settings";
|
|
@@ -76,11 +78,16 @@ async function buildContainer(config, options) {
|
|
|
76
78
|
organizations: new DrizzleOrganizationsRepository(tx, logger.child({ name: "org" })),
|
|
77
79
|
outbox: new DrizzleOutboxAppender(tx, logger.child({ name: "org" }))
|
|
78
80
|
}));
|
|
81
|
+
const orgAdminRef = { current: void 0 };
|
|
79
82
|
const organizations = new OrganizationServiceImpl({
|
|
80
83
|
repo: new DrizzleOrganizationsRepository(db, logger.child({ name: "org" })),
|
|
81
84
|
membersRepo: new DrizzleMembersRepository(db, logger.child({ name: "org" })),
|
|
82
85
|
people: identity,
|
|
83
86
|
uow: organizationsUow,
|
|
87
|
+
orgAdmin: () => {
|
|
88
|
+
if (!orgAdminRef.current) throw new Error("orgAdmin not initialised");
|
|
89
|
+
return orgAdminRef.current;
|
|
90
|
+
},
|
|
84
91
|
logger: logger.child({ name: "org" }),
|
|
85
92
|
mailer,
|
|
86
93
|
inviteUrls: {
|
|
@@ -93,10 +100,9 @@ async function buildContainer(config, options) {
|
|
|
93
100
|
content: new DrizzleContentRepository(tx, contentLogger),
|
|
94
101
|
outbox: new DrizzleOutboxAppender(tx, outboxLogger)
|
|
95
102
|
}));
|
|
96
|
-
const content = new
|
|
103
|
+
const content = new ContentService({
|
|
97
104
|
repo: new DrizzleContentRepository(db, contentLogger),
|
|
98
105
|
uow: contentUow,
|
|
99
|
-
settings,
|
|
100
106
|
logger: contentLogger
|
|
101
107
|
});
|
|
102
108
|
const entitlementsUow = new DrizzleUnitOfWork(db, (tx) => ({
|
|
@@ -147,6 +153,10 @@ async function buildContainer(config, options) {
|
|
|
147
153
|
repo: new DrizzleDashboardRepository(db, reportingLogger),
|
|
148
154
|
logger: reportingLogger
|
|
149
155
|
}),
|
|
156
|
+
courses: new CoursesReportServiceImpl({
|
|
157
|
+
repo: new DrizzleCourseAnalyticsRepository(db, reportingLogger),
|
|
158
|
+
logger: reportingLogger
|
|
159
|
+
}),
|
|
150
160
|
learn: new LearnReportServiceImpl({
|
|
151
161
|
reader: new DrizzleLearnRepository(db, reportingLogger),
|
|
152
162
|
content,
|
|
@@ -173,6 +183,7 @@ async function buildContainer(config, options) {
|
|
|
173
183
|
const automationsUow = new DrizzleUnitOfWork(db, (tx) => ({
|
|
174
184
|
automations: new DrizzleAutomationsRepository(tx, automationsLogger),
|
|
175
185
|
runs: new DrizzleAutomationRunsRepository(tx, automationsLogger),
|
|
186
|
+
credentials: new DrizzleCredentialStore(tx, config.credentialStoreKey, automationsLogger),
|
|
176
187
|
outbox: new DrizzleOutboxAppender(tx, automationsLogger)
|
|
177
188
|
}));
|
|
178
189
|
const mailerLookups = {
|
|
@@ -181,12 +192,14 @@ async function buildContainer(config, options) {
|
|
|
181
192
|
const course = await content.getCourse(orgId, contentId);
|
|
182
193
|
if (course) return {
|
|
183
194
|
id: course.id,
|
|
184
|
-
title: course.title
|
|
195
|
+
title: course.title,
|
|
196
|
+
type: course.type
|
|
185
197
|
};
|
|
186
198
|
const download = await content.getDownload(orgId, contentId);
|
|
187
199
|
return download ? {
|
|
188
200
|
id: download.id,
|
|
189
|
-
title: download.title
|
|
201
|
+
title: download.title,
|
|
202
|
+
type: download.type
|
|
190
203
|
} : null;
|
|
191
204
|
}
|
|
192
205
|
};
|
|
@@ -198,6 +211,8 @@ async function buildContainer(config, options) {
|
|
|
198
211
|
mailer,
|
|
199
212
|
lookups: mailerLookups,
|
|
200
213
|
integrations,
|
|
214
|
+
credentials: credentialStore,
|
|
215
|
+
webhooks: options?.adapters?.webhooks ?? new FetchWebhookSender(),
|
|
201
216
|
logger: automationsLogger
|
|
202
217
|
});
|
|
203
218
|
automationEngine.register(automations);
|
|
@@ -206,25 +221,37 @@ async function buildContainer(config, options) {
|
|
|
206
221
|
eventBus.subscribeAll((event) => automations.handle(event));
|
|
207
222
|
const outboxConfig = resolveOutboxConfig(config.outbox);
|
|
208
223
|
const outboxRelay = new PollingOutboxRelay(new DrizzleOutboxStore(db, outboxLogger), eventBus, outboxConfig, outboxLogger);
|
|
224
|
+
const authLogger = logger.child({ name: "auth" });
|
|
225
|
+
const authHook = (name, fn) => async (...args) => {
|
|
226
|
+
try {
|
|
227
|
+
return await fn(...args);
|
|
228
|
+
} catch (err) {
|
|
229
|
+
authLogger.error(`auth hook ${name} failed`, { err });
|
|
230
|
+
throw err;
|
|
231
|
+
}
|
|
232
|
+
};
|
|
209
233
|
const auth = new BetterAuth({
|
|
210
234
|
db,
|
|
211
235
|
baseUrl: config.authBaseURL,
|
|
212
236
|
secret: config.authSecret,
|
|
213
237
|
trustedOrigins: config.trustedOrigins,
|
|
214
238
|
hooks: {
|
|
215
|
-
sendResetPassword: async (
|
|
239
|
+
sendResetPassword: async ({ user, url }) => {
|
|
216
240
|
await identity.sendPasswordReset({
|
|
217
|
-
email:
|
|
218
|
-
url
|
|
241
|
+
email: user.email,
|
|
242
|
+
url
|
|
219
243
|
});
|
|
220
244
|
},
|
|
221
|
-
|
|
245
|
+
onPasswordReset: async ({ user }) => {
|
|
246
|
+
await identity.sendPasswordChanged({ email: user.email });
|
|
247
|
+
},
|
|
248
|
+
sendMagicLink: authHook("sendMagicLink", async ({ email, url }) => {
|
|
222
249
|
await identity.sendMagicLink({
|
|
223
250
|
email,
|
|
224
251
|
url
|
|
225
252
|
});
|
|
226
|
-
},
|
|
227
|
-
beforeUserCreate: async (user) => {
|
|
253
|
+
}),
|
|
254
|
+
beforeUserCreate: authHook("beforeUserCreate", async (user) => {
|
|
228
255
|
const { email, name } = user;
|
|
229
256
|
const [firstName, lastName] = name.split(" ");
|
|
230
257
|
const domainUser = await identity.linkOrCreateUser({
|
|
@@ -236,8 +263,8 @@ async function buildContainer(config, options) {
|
|
|
236
263
|
...user,
|
|
237
264
|
id: domainUser.id
|
|
238
265
|
} };
|
|
239
|
-
},
|
|
240
|
-
beforeCreateSession: async (session) => {
|
|
266
|
+
}),
|
|
267
|
+
beforeCreateSession: authHook("beforeCreateSession", async (session) => {
|
|
241
268
|
const person = await identity.getUserByExternalId(session.userId);
|
|
242
269
|
if (!person) return;
|
|
243
270
|
const orgs = await organizations.getOrgUsersForUser(person.id);
|
|
@@ -248,8 +275,8 @@ async function buildContainer(config, options) {
|
|
|
248
275
|
...session,
|
|
249
276
|
activeOrganizationId: org.id
|
|
250
277
|
} };
|
|
251
|
-
},
|
|
252
|
-
beforeCreateOrganization: async ({ organization: org, user: baUser }) => {
|
|
278
|
+
}),
|
|
279
|
+
beforeCreateOrganization: authHook("beforeCreateOrganization", async ({ organization: org, user: baUser }) => {
|
|
253
280
|
const domainOrg = await organizations.createOrganization({
|
|
254
281
|
ownerId: baUser.id,
|
|
255
282
|
...org,
|
|
@@ -259,28 +286,32 @@ async function buildContainer(config, options) {
|
|
|
259
286
|
...org,
|
|
260
287
|
id: domainOrg.id
|
|
261
288
|
} };
|
|
262
|
-
},
|
|
263
|
-
beforeUpdateOrganization: async ({ organization }) => {
|
|
289
|
+
}),
|
|
290
|
+
beforeUpdateOrganization: authHook("beforeUpdateOrganization", async ({ organization }) => {
|
|
264
291
|
await organizations.updateOrganization(organization.id, organization);
|
|
265
|
-
},
|
|
266
|
-
beforeDeleteOrganization: async ({ organization }) => {
|
|
292
|
+
}),
|
|
293
|
+
beforeDeleteOrganization: authHook("beforeDeleteOrganization", async ({ organization }) => {
|
|
267
294
|
await organizations.deleteOrganization(organization.id);
|
|
268
|
-
},
|
|
269
|
-
beforeAddMember: async ({ member }) => {
|
|
295
|
+
}),
|
|
296
|
+
beforeAddMember: authHook("beforeAddMember", async ({ member }) => {
|
|
270
297
|
await organizations.addOrgUser({
|
|
271
298
|
orgId: member.organizationId,
|
|
272
299
|
userId: member.userId,
|
|
273
300
|
role: parseRole(member.role)
|
|
274
301
|
});
|
|
275
|
-
},
|
|
276
|
-
|
|
302
|
+
}),
|
|
303
|
+
beforeUpdateMemberRole: authHook("beforeUpdateMemberRole", async ({ member, newRole }) => {
|
|
304
|
+
await organizations.updateOrgUser(member.organizationId, member.userId, { role: parseRole(newRole) });
|
|
305
|
+
}),
|
|
306
|
+
beforeRemoveMember: authHook("beforeRemoveMember", async ({ member }) => {
|
|
277
307
|
await organizations.removeOrgUser(member.organizationId, member.userId);
|
|
278
|
-
}
|
|
308
|
+
})
|
|
279
309
|
},
|
|
280
|
-
logger:
|
|
310
|
+
logger: authLogger,
|
|
281
311
|
cookieDomain: config.cookieDomain,
|
|
282
312
|
secureCookies: config.secureCookies
|
|
283
313
|
});
|
|
314
|
+
orgAdminRef.current = auth;
|
|
284
315
|
return {
|
|
285
316
|
auth,
|
|
286
317
|
identity,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"container.js","names":[],"sources":["../../src/app/container.ts"],"sourcesContent":["// Wires adapters + services in dependency order. Starts nothing.\nimport {\n createDb,\n DrizzleAssetsRepository,\n DrizzleAutomationRunsRepository,\n DrizzleAutomationsRepository,\n DrizzleConnectionsRepository,\n DrizzleContentRepository,\n DrizzleCredentialStore,\n DrizzleDashboardRepository,\n DrizzleDiscussionRepository,\n DrizzleEntitlementsRepository,\n DrizzleIdentityRepository,\n DrizzleLearnRepository,\n DrizzleMembersRepository,\n DrizzleOrganizationsRepository,\n DrizzleOutboxAppender,\n DrizzleOutboxStore,\n DrizzleProgressRepository,\n DrizzleSettingsRepository,\n DrizzleStudentsRepository,\n DrizzleUnitOfWork,\n} from '@headless-lms/adapter-db';\nimport { InMemoryEventBus } from '@headless-lms/adapter-defaults/events';\nimport {\n PollingOutboxRelay,\n type PollingOutboxRelayConfig,\n} from '@headless-lms/adapter-defaults/events/outbox-relay';\nimport { InlineAutomationEngine } from '@headless-lms/adapter-defaults/workflows';\nimport { EmailAdapter, StubTemplateRenderer } from '@headless-lms/adapter-defaults/email';\nimport {\n createRootLogger,\n type LogLevel,\n type PinoInstance,\n requestLogContext,\n type RequestLogContext,\n} from '@headless-lms/adapter-defaults/logging';\nimport { StorageAdapter } from '@headless-lms/adapter-defaults/storage';\nimport { BetterAuth } from '@headless-lms/adapter-auth';\n\nimport { ContentServiceImpl } from '@headless-lms/core/content';\nimport { EntitlementsServiceImpl } from '@headless-lms/core/entitlements';\nimport { ProgressServiceImpl } from '@headless-lms/core/progress';\nimport { DiscussionServiceImpl } from '@headless-lms/core/discussion';\nimport { IdentityServiceImpl, type SessionAdmin } from '@headless-lms/core/identity';\nimport { type OrgAdmin, OrganizationServiceImpl, parseRole } from '@headless-lms/core/organizations';\nimport { AssetsServiceImpl } from '@headless-lms/core/assets';\nimport { IntegrationsServiceImpl } from '@headless-lms/core/integrations';\nimport { AutomationsServiceImpl } from '@headless-lms/core/automations';\nimport { loadIntegrations } from './integrations.js';\nimport { registerNotificationSubscribers } from './notifications.js';\nimport { StudentsReportServiceImpl } from '@headless-lms/core/reporting/students';\nimport { DashboardReportServiceImpl } from '@headless-lms/core/reporting/dashboard';\nimport { LearnReportServiceImpl } from '@headless-lms/core/reporting/learn';\nimport { Mailer, type MailerLookups } from '@headless-lms/core/shared/mailer';\nimport { SettingsService } from '@headless-lms/core/shared/settings';\n\nimport type {\n CredentialStore,\n EmailSender,\n Logger,\n ObjectStorage,\n OutboxRelay,\n TemplateContext,\n TemplateRenderer,\n} from '@headless-lms/core/shared/ports';\nimport type { AutomationEngine } from '@headless-lms/core/types';\n\n/** Installation-supplied ports; an absent slot falls back to a fail-loudly stub. */\nexport interface AdapterOverrides {\n email?: EmailSender;\n storage?: ObjectStorage;\n /** Resolves email templates to rendered content. Absent → fail-loudly stub. */\n templates?: TemplateRenderer;\n /** Durable automation engine. Absent → InlineAutomationEngine (in-process, one attempt per action). */\n workflows?: AutomationEngine;\n}\n\nexport interface BuildContainerOptions {\n /** Installation's plugins folder, scanned by loadIntegrations. Absent → no integrations. */\n pluginsDir?: string;\n adapters?: AdapterOverrides;\n}\n\nexport interface Config {\n databaseUrl: string;\n authBaseURL: string;\n authSecret: string;\n trustedOrigins: string[];\n /** Branding threaded into every email template. Default: brandName \"Headless LMS\", baseUrl = adminAppUrl.\n * (studentPortalUrl is composed in from the top-level config field.) */\n emailBranding?: Omit<TemplateContext, 'studentPortalUrl'>;\n /** base64-encoded 32-byte key for the credential store (CREDENTIAL_STORE_KEY). */\n credentialStoreKey: string;\n /** Parent domain for cross-subdomain session cookies (e.g. \".example.com\"); undefined → host-only cookie. */\n cookieDomain?: string;\n /** Mark session cookies Secure (set behind HTTPS / in production). */\n secureCookies?: boolean;\n /** Student portal origin — invite links for students, and the origin whose signups are invite-gated. */\n studentPortalUrl: string;\n /** Admin app origin — invite links for staff. */\n adminAppUrl: string;\n /** Transactional-outbox relay tuning. All optional — see OUTBOX_DEFAULTS. */\n outbox?: OutboxConfig;\n /** Log level for the process-wide logger (HTTP + domain + relay). Default \"info\". */\n logging?: LoggingConfig;\n /** Presigned URL lifetime for entitled download delivery, seconds. Mirrors\n * ServerConfig.deliveryExpirySeconds — createContainer threads it through. */\n deliveryExpirySeconds: number;\n}\n\n/** Tuning for the transactional-outbox relay. Every field is optional; the\n * container resolves against OUTBOX_DEFAULTS. */\nexport interface OutboxConfig {\n /** Master switch for the same-process relay. Default true. */\n enabled?: boolean;\n /** Idle delay between polls. Default 1000. */\n pollIntervalMs?: number;\n /** Max rows fetched/dispatched per tick. Default 100. */\n batchSize?: number;\n}\n\nexport const OUTBOX_DEFAULTS: PollingOutboxRelayConfig = {\n enabled: true,\n pollIntervalMs: 1000,\n batchSize: 100,\n};\n\nexport function resolveOutboxConfig(config: OutboxConfig = {}): PollingOutboxRelayConfig {\n const overrides = Object.fromEntries(\n Object.entries(config).filter(([, value]) => value !== undefined),\n );\n return { ...OUTBOX_DEFAULTS, ...overrides };\n}\n\n/** Logging tuning. Optional; resolved against LOGGING_DEFAULTS. */\nexport interface LoggingConfig {\n /** Minimum level emitted. Default \"info\". */\n level?: LogLevel;\n}\n\nexport const LOGGING_DEFAULTS: Required<LoggingConfig> = { level: 'info' };\n\nexport function resolveLoggingConfig(config: LoggingConfig = {}): Required<LoggingConfig> {\n return { level: config.level ?? LOGGING_DEFAULTS.level };\n}\n\nexport interface Container {\n auth: BetterAuth;\n // Org Provider\n orgProvider: OrgAdmin;\n\n // Domains\n identity: IdentityServiceImpl;\n organizations: OrganizationServiceImpl;\n content: ContentServiceImpl;\n entitlements: EntitlementsServiceImpl;\n progress: ProgressServiceImpl;\n discussion: DiscussionServiceImpl;\n assets: AssetsServiceImpl;\n integrations: IntegrationsServiceImpl;\n automations: AutomationsServiceImpl;\n /** Cross-cutting settings store. Not a context — a leaf every context may use;\n * inject `settings.for('<namespace>')` into a service, never the service itself. */\n settings: SettingsService;\n // Reporting read layer (composed cross-context reads; owns no domain rules).\n reporting: {\n students: StudentsReportServiceImpl;\n dashboard: DashboardReportServiceImpl;\n learn: LearnReportServiceImpl;\n };\n storage: ObjectStorage;\n mailer: Mailer;\n /** Shared secure credential store — encrypted at rest, org-scoped, decrypt at point of use. */\n credentials: CredentialStore;\n /** The outbox relay — constructed but NEVER started by the container; the\n * installation's entry point starts it after listen (gen-openapi must not\n * poll). buildServer stops it onClose. */\n outboxRelay: OutboxRelay;\n /** The automation engine — constructed but NEVER started by the container;\n * the installation's entry point starts it after listen (gen-openapi must\n * not run a worker). buildServer stops it onClose. */\n automationEngine: AutomationEngine;\n /** Root logger port — components receive children bound with { name }. */\n logger: Logger;\n /** The raw pino root; buildServer hands it to Fastify so HTTP shares the stream. */\n loggerInstance: PinoInstance;\n /** Request-scoped log correlation; the HTTP layer enters it per request. */\n requestContext: RequestLogContext;\n /** Identity's session-write port; the auth adapter fulfils it. */\n sessions: SessionAdmin;\n}\n\nexport async function buildContainer(\n config: Config,\n options?: BuildContainerOptions,\n): Promise<Container> {\n const { instance: loggerInstance, logger } = createRootLogger(\n resolveLoggingConfig(config.logging).level,\n );\n // One child per domain — a context's service and repositories share it.\n\n const contentLogger = logger.child({ name: 'content' });\n const entitlementsLogger = logger.child({ name: 'entitlements' });\n const progressLogger = logger.child({ name: 'progress' });\n const discussionLogger = logger.child({ name: 'discussion' });\n const assetsLogger = logger.child({ name: 'assets' });\n const integrationsLogger = logger.child({ name: 'integrations' });\n const automationsLogger = logger.child({ name: 'automations' });\n const reportingLogger = logger.child({ name: 'reporting' });\n const outboxLogger = logger.child({ name: 'outbox' });\n\n // Outbound adapters\n const db = createDb(config.databaseUrl);\n const email = options?.adapters?.email ?? new EmailAdapter(logger.child({ name: 'email' }));\n const storage: ObjectStorage =\n options?.adapters?.storage ?? new StorageAdapter(logger.child({ name: 'storage' }));\n const templates =\n options?.adapters?.templates ?? new StubTemplateRenderer(logger.child({ name: 'email' }));\n const automationEngine: AutomationEngine =\n options?.adapters?.workflows ?? new InlineAutomationEngine();\n const mailer = new Mailer(templates, email, {\n ...(config.emailBranding ?? { brandName: 'Headless LMS', baseUrl: config.adminAppUrl }),\n studentPortalUrl: config.studentPortalUrl,\n });\n\n /*\n * Identity\n */\n const identityUoW = new DrizzleUnitOfWork(db, (tx) => ({\n identity: new DrizzleIdentityRepository(tx, logger.child({ name: 'identity' })),\n outbox: new DrizzleOutboxAppender(tx, logger.child({ name: 'identity' })),\n }));\n const identity = new IdentityServiceImpl({\n repo: new DrizzleIdentityRepository(db, logger.child({ name: 'identity_repo' })),\n logger: logger.child({ name: 'identity' }),\n uow: identityUoW,\n mailer,\n });\n\n /*\n * Organization\n */\n const organizationsUow = new DrizzleUnitOfWork(db, (tx) => ({\n organizations: new DrizzleOrganizationsRepository(tx, logger.child({ name: 'org' })),\n outbox: new DrizzleOutboxAppender(tx, logger.child({ name: 'org' })),\n }));\n const organizations = new OrganizationServiceImpl({\n repo: new DrizzleOrganizationsRepository(db, logger.child({ name: 'org' })),\n membersRepo: new DrizzleMembersRepository(db, logger.child({ name: 'org' })),\n people: identity,\n uow: organizationsUow,\n logger: logger.child({ name: 'org' }),\n mailer,\n inviteUrls: { studentPortalUrl: config.studentPortalUrl, adminAppUrl: config.adminAppUrl },\n });\n\n /*\n * Settings\n */\n const settings = new SettingsService(\n new DrizzleSettingsRepository(db, logger.child({ name: 'settings' })),\n );\n // Content: reads on the root db; course writes + outbox append in one tx.\n const contentUow = new DrizzleUnitOfWork(db, (tx) => ({\n content: new DrizzleContentRepository(tx, contentLogger),\n outbox: new DrizzleOutboxAppender(tx, outboxLogger),\n }));\n const content = new ContentServiceImpl({\n repo: new DrizzleContentRepository(db, contentLogger),\n uow: contentUow,\n settings,\n logger: contentLogger,\n });\n // Entitlements: reads on the root db; writes + outbox append in one tx.\n const entitlementsUow = new DrizzleUnitOfWork(db, (tx) => ({\n entitlements: new DrizzleEntitlementsRepository(tx, entitlementsLogger),\n outbox: new DrizzleOutboxAppender(tx, outboxLogger),\n }));\n const entitlements = new EntitlementsServiceImpl({\n repo: new DrizzleEntitlementsRepository(db, entitlementsLogger),\n uow: entitlementsUow,\n logger: entitlementsLogger,\n });\n // Progress: report writes + outbox append in one tx; content supplies the\n // structure and completion rules the service evaluates against.\n const progressUow = new DrizzleUnitOfWork(db, (tx) => ({\n progress: new DrizzleProgressRepository(tx, progressLogger),\n outbox: new DrizzleOutboxAppender(tx, outboxLogger),\n }));\n const progress = new ProgressServiceImpl({\n repo: new DrizzleProgressRepository(db, progressLogger),\n content,\n uow: progressUow,\n logger: progressLogger,\n });\n\n const discussion = new DiscussionServiceImpl({\n repo: new DrizzleDiscussionRepository(db, discussionLogger),\n access: entitlements,\n content: content,\n uow: new DrizzleUnitOfWork(db, (tx) => ({\n discussion: new DrizzleDiscussionRepository(tx, discussionLogger),\n outbox: new DrizzleOutboxAppender(tx, outboxLogger),\n })),\n settings,\n logger: discussionLogger,\n });\n const assets = new AssetsServiceImpl({\n storage,\n repo: new DrizzleAssetsRepository(db, assetsLogger),\n uow: new DrizzleUnitOfWork(db, (tx) => ({\n assets: new DrizzleAssetsRepository(tx, assetsLogger),\n outbox: new DrizzleOutboxAppender(tx, outboxLogger),\n })),\n logger: assetsLogger,\n });\n\n const reporting = {\n students: new StudentsReportServiceImpl({\n repo: new DrizzleStudentsRepository(db, reportingLogger),\n logger: reportingLogger,\n }),\n dashboard: new DashboardReportServiceImpl({\n repo: new DrizzleDashboardRepository(db, reportingLogger),\n logger: reportingLogger,\n }),\n learn: new LearnReportServiceImpl({\n reader: new DrizzleLearnRepository(db, reportingLogger),\n content,\n progress,\n assets,\n deliveryExpirySeconds: config.deliveryExpirySeconds,\n logger: reportingLogger,\n }),\n };\n\n const credentialStore = new DrizzleCredentialStore(\n db,\n config.credentialStoreKey,\n integrationsLogger,\n );\n // The integrations this deployment supports: the installation's declared\n // plugins folder (directory name = integration id), loaded at startup.\n // Connect/configure reject undeclared ids and validate config with the\n // integration's own schema. No folder → no integrations.\n const integrationsRegistry = await loadIntegrations(options?.pluginsDir, integrationsLogger);\n // Integrations: credential + connection writes + outbox append in one tx\n // (a tx-bound credential store instance shares the scope's transaction).\n const integrationsUow = new DrizzleUnitOfWork(db, (tx) => ({\n connections: new DrizzleConnectionsRepository(tx, integrationsLogger),\n credentials: new DrizzleCredentialStore(tx, config.credentialStoreKey, integrationsLogger),\n outbox: new DrizzleOutboxAppender(tx, outboxLogger),\n }));\n const integrations = new IntegrationsServiceImpl({\n registry: integrationsRegistry,\n repo: new DrizzleConnectionsRepository(db, integrationsLogger),\n uow: integrationsUow,\n credentials: credentialStore,\n logger: integrationsLogger,\n });\n\n // Automations: run writes + outbox append in one tx; the engine drives\n // execution (register below), integrations supplies the loaded\n // integrations' actions for `available`.\n const automationsUow = new DrizzleUnitOfWork(db, (tx) => ({\n automations: new DrizzleAutomationsRepository(tx, automationsLogger),\n runs: new DrizzleAutomationRunsRepository(tx, automationsLogger),\n outbox: new DrizzleOutboxAppender(tx, automationsLogger),\n }));\n // Emails triggered by row-shaped events resolve their recipient and content\n // details at send time — the event carries ids only.\n const mailerLookups: MailerLookups = {\n orgUserEmail: async (orgId, orgUserId) =>\n (await reporting.students.get(orgId, orgUserId))?.email ?? null,\n contentInfo: async (orgId, contentId) => {\n const course = await content.getCourse(orgId, contentId);\n if (course) {\n return { id: course.id, title: course.title };\n }\n const download = await content.getDownload(orgId, contentId);\n return download ? { id: download.id, title: download.title } : null;\n },\n };\n\n const automations = new AutomationsServiceImpl({\n repo: new DrizzleAutomationsRepository(db, automationsLogger),\n runsRepo: new DrizzleAutomationRunsRepository(db, automationsLogger),\n uow: automationsUow,\n engine: automationEngine,\n mailer,\n lookups: mailerLookups,\n integrations,\n logger: automationsLogger,\n });\n automationEngine.register(automations);\n\n const eventBus = new InMemoryEventBus();\n registerNotificationSubscribers(eventBus, mailer, mailerLookups);\n // handle() never throws — the EventBus fans out to every subscriber\n // sequentially, and a rejection here would break sibling subscribers.\n eventBus.subscribeAll((event) => automations.handle(event));\n const outboxConfig = resolveOutboxConfig(config.outbox);\n const outboxRelay = new PollingOutboxRelay(\n new DrizzleOutboxStore(db, outboxLogger),\n eventBus,\n outboxConfig,\n outboxLogger,\n );\n\n /*\n * Auth Adapter\n */\n const auth = new BetterAuth({\n db,\n baseUrl: config.authBaseURL,\n secret: config.authSecret,\n trustedOrigins: config.trustedOrigins,\n hooks: {\n sendResetPassword: async (data) => {\n await identity.sendPasswordReset({\n email: data.user.email,\n url: '',\n });\n },\n sendMagicLink: async ({ email, url }) => {\n await identity.sendMagicLink({\n email,\n url,\n });\n },\n beforeUserCreate: async (user) => {\n const { email, name } = user;\n const [firstName, lastName] = name.split(' ');\n // Better Auth has not minted an id yet — the id returned below becomes\n // its account id. A person provisioned at invite time is linked instead of re-created.\n const domainUser = await identity.linkOrCreateUser({\n email,\n ...(firstName !== undefined && { firstName }),\n ...(lastName !== undefined && { lastName }),\n });\n\n return {\n data: {\n ...user,\n id: domainUser.id,\n },\n };\n },\n beforeCreateSession: async (session) => {\n const person = await identity.getUserByExternalId(session.userId);\n if (!person) {\n return;\n }\n const orgs = await organizations.getOrgUsersForUser(person.id);\n if (orgs.length !== 1) {\n return;\n }\n // TODO - fix, why default to the first?\n const org = await organizations.getById(orgs[0]!.orgId);\n if (!org) {\n return;\n }\n\n return { data: { ...session, activeOrganizationId: org.id } };\n },\n /*\n * We're making sure the domain org creates and using the same ID for better-auth org ID.\n */\n beforeCreateOrganization: async ({ organization: org, user: baUser }) => {\n const domainOrg = await organizations.createOrganization({\n ownerId: baUser.id,\n ...org,\n logo: org.logo ?? undefined,\n });\n return { data: { ...org, id: domainOrg.id } };\n },\n beforeUpdateOrganization: async ({ organization }) => {\n await organizations.updateOrganization(organization.id, organization);\n },\n beforeDeleteOrganization: async ({ organization }) => {\n await organizations.deleteOrganization(organization.id);\n },\n beforeAddMember: async ({ member }) => {\n await organizations.addOrgUser({\n orgId: member.organizationId,\n userId: member.userId,\n role: parseRole(member.role),\n });\n },\n beforeRemoveMember: async ({ member }) => {\n await organizations.removeOrgUser(member.organizationId, member.userId);\n },\n },\n logger: logger.child({ name: 'auth' }),\n cookieDomain: config.cookieDomain,\n secureCookies: config.secureCookies,\n });\n\n return {\n auth,\n identity,\n organizations,\n // Better Auth owns org writes; the same instance fulfils the OrgAdmin port.\n orgProvider: auth,\n content,\n entitlements,\n progress,\n discussion,\n assets,\n integrations,\n automations,\n settings,\n reporting,\n storage,\n mailer,\n credentials: credentialStore,\n outboxRelay,\n automationEngine,\n logger,\n loggerInstance,\n requestContext: requestLogContext,\n // Better Auth owns the session store; the same instance fulfils SessionAdmin.\n sessions: auth,\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"container.js","names":[],"sources":["../../src/app/container.ts"],"sourcesContent":["// Wires adapters + services in dependency order. Starts nothing.\nimport {\n createDb,\n DrizzleAssetsRepository,\n DrizzleAutomationRunsRepository,\n DrizzleAutomationsRepository,\n DrizzleConnectionsRepository,\n DrizzleContentRepository,\n DrizzleCredentialStore,\n DrizzleCourseAnalyticsRepository,\n DrizzleDashboardRepository,\n DrizzleDiscussionRepository,\n DrizzleEntitlementsRepository,\n DrizzleIdentityRepository,\n DrizzleLearnRepository,\n DrizzleMembersRepository,\n DrizzleOrganizationsRepository,\n DrizzleOutboxAppender,\n DrizzleOutboxStore,\n DrizzleProgressRepository,\n DrizzleSettingsRepository,\n DrizzleStudentsRepository,\n DrizzleUnitOfWork,\n} from '@headless-lms/adapter-db';\nimport { InMemoryEventBus } from '@headless-lms/adapter-defaults/events';\nimport {\n PollingOutboxRelay,\n type PollingOutboxRelayConfig,\n} from '@headless-lms/adapter-defaults/events/outbox-relay';\nimport { InlineAutomationEngine } from '@headless-lms/adapter-defaults/workflows';\nimport { FetchWebhookSender } from '@headless-lms/adapter-defaults/webhooks';\nimport { EmailAdapter, StubTemplateRenderer } from '@headless-lms/adapter-defaults/email';\nimport {\n createRootLogger,\n type LogLevel,\n type PinoInstance,\n requestLogContext,\n type RequestLogContext,\n} from '@headless-lms/adapter-defaults/logging';\nimport { StorageAdapter } from '@headless-lms/adapter-defaults/storage';\nimport { BetterAuth } from '@headless-lms/adapter-auth';\n\nimport { ContentService } from '@headless-lms/core/content';\nimport { EntitlementsServiceImpl } from '@headless-lms/core/entitlements';\nimport { ProgressServiceImpl } from '@headless-lms/core/progress';\nimport { DiscussionServiceImpl } from '@headless-lms/core/discussion';\nimport { IdentityServiceImpl, type SessionAdmin } from '@headless-lms/core/identity';\nimport {\n type OrgAdmin,\n OrganizationServiceImpl,\n parseRole,\n} from '@headless-lms/core/organizations';\nimport { AssetsServiceImpl } from '@headless-lms/core/assets';\nimport { IntegrationsServiceImpl } from '@headless-lms/core/integrations';\nimport { AutomationsServiceImpl } from '@headless-lms/core/automations';\nimport { loadIntegrations } from './integrations.js';\nimport { registerNotificationSubscribers } from './notifications.js';\nimport { StudentsReportServiceImpl } from '@headless-lms/core/reporting/students';\nimport { DashboardReportServiceImpl } from '@headless-lms/core/reporting/dashboard';\nimport { CoursesReportServiceImpl } from '@headless-lms/core/reporting/courses';\nimport { LearnReportServiceImpl } from '@headless-lms/core/reporting/learn';\nimport { Mailer, type MailerLookups } from '@headless-lms/core/shared/mailer';\nimport { SettingsService } from '@headless-lms/core/shared/settings';\n\nimport type {\n CredentialStore,\n EmailSender,\n Logger,\n ObjectStorage,\n OutboxRelay,\n TemplateContext,\n TemplateRenderer,\n} from '@headless-lms/core/shared/ports';\nimport type { AutomationEngine, WebhookSender } from '@headless-lms/core/types';\n\n/** Installation-supplied ports; an absent slot falls back to a fail-loudly stub. */\nexport interface AdapterOverrides {\n email?: EmailSender;\n storage?: ObjectStorage;\n /** Resolves email templates to rendered content. Absent → fail-loudly stub. */\n templates?: TemplateRenderer;\n /** Durable automation engine. Absent → InlineAutomationEngine (in-process, one attempt per action). */\n workflows?: AutomationEngine;\n /** Outbound HTTP for webhook deliveries. Absent → FetchWebhookSender (fetch, 10s timeout). */\n webhooks?: WebhookSender;\n}\n\nexport interface BuildContainerOptions {\n /** Installation's plugins folder, scanned by loadIntegrations. Absent → no integrations. */\n pluginsDir?: string;\n adapters?: AdapterOverrides;\n}\n\nexport interface Config {\n databaseUrl: string;\n authBaseURL: string;\n authSecret: string;\n trustedOrigins: string[];\n /** Branding threaded into every email template. Default: brandName \"Headless LMS\", baseUrl = adminAppUrl.\n * (studentPortalUrl is composed in from the top-level config field.) */\n emailBranding?: Omit<TemplateContext, 'studentPortalUrl'>;\n /** base64-encoded 32-byte key for the credential store (CREDENTIAL_STORE_KEY). */\n credentialStoreKey: string;\n /** Parent domain for cross-subdomain session cookies (e.g. \".example.com\"); undefined → host-only cookie. */\n cookieDomain?: string;\n /** Mark session cookies Secure (set behind HTTPS / in production). */\n secureCookies?: boolean;\n /** Student portal origin — invite links for students, and the origin whose signups are invite-gated. */\n studentPortalUrl: string;\n /** Admin app origin — invite links for staff. */\n adminAppUrl: string;\n /** Transactional-outbox relay tuning. All optional — see OUTBOX_DEFAULTS. */\n outbox?: OutboxConfig;\n /** Log level for the process-wide logger (HTTP + domain + relay). Default \"info\". */\n logging?: LoggingConfig;\n /** Presigned URL lifetime for entitled download delivery, seconds. Mirrors\n * ServerConfig.deliveryExpirySeconds — createContainer threads it through. */\n deliveryExpirySeconds: number;\n}\n\n/** Tuning for the transactional-outbox relay. Every field is optional; the\n * container resolves against OUTBOX_DEFAULTS. */\nexport interface OutboxConfig {\n /** Master switch for the same-process relay. Default true. */\n enabled?: boolean;\n /** Idle delay between polls. Default 1000. */\n pollIntervalMs?: number;\n /** Max rows fetched/dispatched per tick. Default 100. */\n batchSize?: number;\n}\n\nexport const OUTBOX_DEFAULTS: PollingOutboxRelayConfig = {\n enabled: true,\n pollIntervalMs: 1000,\n batchSize: 100,\n};\n\nexport function resolveOutboxConfig(config: OutboxConfig = {}): PollingOutboxRelayConfig {\n const overrides = Object.fromEntries(\n Object.entries(config).filter(([, value]) => value !== undefined),\n );\n return { ...OUTBOX_DEFAULTS, ...overrides };\n}\n\n/** Logging tuning. Optional; resolved against LOGGING_DEFAULTS. */\nexport interface LoggingConfig {\n /** Minimum level emitted. Default \"info\". */\n level?: LogLevel;\n}\n\nexport const LOGGING_DEFAULTS: Required<LoggingConfig> = { level: 'info' };\n\nexport function resolveLoggingConfig(config: LoggingConfig = {}): Required<LoggingConfig> {\n return { level: config.level ?? LOGGING_DEFAULTS.level };\n}\n\nexport interface Container {\n auth: BetterAuth;\n // Org Provider\n orgProvider: OrgAdmin;\n\n // Domains\n identity: IdentityServiceImpl;\n organizations: OrganizationServiceImpl;\n content: ContentService;\n entitlements: EntitlementsServiceImpl;\n progress: ProgressServiceImpl;\n discussion: DiscussionServiceImpl;\n assets: AssetsServiceImpl;\n integrations: IntegrationsServiceImpl;\n automations: AutomationsServiceImpl;\n /** Cross-cutting settings store. Not a context — a leaf every context may use;\n * inject `settings.for('<namespace>')` into a service, never the service itself. */\n settings: SettingsService;\n // Reporting read layer (composed cross-context reads; owns no domain rules).\n reporting: {\n students: StudentsReportServiceImpl;\n dashboard: DashboardReportServiceImpl;\n courses: CoursesReportServiceImpl;\n learn: LearnReportServiceImpl;\n };\n storage: ObjectStorage;\n mailer: Mailer;\n /** Shared secure credential store — encrypted at rest, org-scoped, decrypt at point of use. */\n credentials: CredentialStore;\n /** The outbox relay — constructed but NEVER started by the container; the\n * installation's entry point starts it after listen (gen-openapi must not\n * poll). buildServer stops it onClose. */\n outboxRelay: OutboxRelay;\n /** The automation engine — constructed but NEVER started by the container;\n * the installation's entry point starts it after listen (gen-openapi must\n * not run a worker). buildServer stops it onClose. */\n automationEngine: AutomationEngine;\n /** Root logger port — components receive children bound with { name }. */\n logger: Logger;\n /** The raw pino root; buildServer hands it to Fastify so HTTP shares the stream. */\n loggerInstance: PinoInstance;\n /** Request-scoped log correlation; the HTTP layer enters it per request. */\n requestContext: RequestLogContext;\n /** Identity's session-write port; the auth adapter fulfils it. */\n sessions: SessionAdmin;\n}\n\nexport async function buildContainer(\n config: Config,\n options?: BuildContainerOptions,\n): Promise<Container> {\n const { instance: loggerInstance, logger } = createRootLogger(\n resolveLoggingConfig(config.logging).level,\n );\n // One child per domain — a context's service and repositories share it.\n\n const contentLogger = logger.child({ name: 'content' });\n const entitlementsLogger = logger.child({ name: 'entitlements' });\n const progressLogger = logger.child({ name: 'progress' });\n const discussionLogger = logger.child({ name: 'discussion' });\n const assetsLogger = logger.child({ name: 'assets' });\n const integrationsLogger = logger.child({ name: 'integrations' });\n const automationsLogger = logger.child({ name: 'automations' });\n const reportingLogger = logger.child({ name: 'reporting' });\n const outboxLogger = logger.child({ name: 'outbox' });\n\n // Outbound adapters\n const db = createDb(config.databaseUrl);\n const email = options?.adapters?.email ?? new EmailAdapter(logger.child({ name: 'email' }));\n const storage: ObjectStorage =\n options?.adapters?.storage ?? new StorageAdapter(logger.child({ name: 'storage' }));\n const templates =\n options?.adapters?.templates ?? new StubTemplateRenderer(logger.child({ name: 'email' }));\n const automationEngine: AutomationEngine =\n options?.adapters?.workflows ?? new InlineAutomationEngine();\n const mailer = new Mailer(templates, email, {\n ...(config.emailBranding ?? { brandName: 'Headless LMS', baseUrl: config.adminAppUrl }),\n studentPortalUrl: config.studentPortalUrl,\n });\n\n /*\n * Identity\n */\n const identityUoW = new DrizzleUnitOfWork(db, (tx) => ({\n identity: new DrizzleIdentityRepository(tx, logger.child({ name: 'identity' })),\n outbox: new DrizzleOutboxAppender(tx, logger.child({ name: 'identity' })),\n }));\n const identity = new IdentityServiceImpl({\n repo: new DrizzleIdentityRepository(db, logger.child({ name: 'identity_repo' })),\n logger: logger.child({ name: 'identity' }),\n uow: identityUoW,\n mailer,\n });\n\n /*\n * Organization\n */\n const organizationsUow = new DrizzleUnitOfWork(db, (tx) => ({\n organizations: new DrizzleOrganizationsRepository(tx, logger.child({ name: 'org' })),\n outbox: new DrizzleOutboxAppender(tx, logger.child({ name: 'org' })),\n }));\n const orgAdminRef: { current: OrgAdmin | undefined } = { current: undefined };\n const organizations = new OrganizationServiceImpl({\n repo: new DrizzleOrganizationsRepository(db, logger.child({ name: 'org' })),\n membersRepo: new DrizzleMembersRepository(db, logger.child({ name: 'org' })),\n people: identity,\n uow: organizationsUow,\n orgAdmin: () => {\n if (!orgAdminRef.current) {\n throw new Error('orgAdmin not initialised');\n }\n return orgAdminRef.current;\n },\n logger: logger.child({ name: 'org' }),\n mailer,\n inviteUrls: { studentPortalUrl: config.studentPortalUrl, adminAppUrl: config.adminAppUrl },\n });\n\n /*\n * Settings\n */\n const settings = new SettingsService(\n new DrizzleSettingsRepository(db, logger.child({ name: 'settings' })),\n );\n // Content: reads on the root db; course writes + outbox append in one tx.\n const contentUow = new DrizzleUnitOfWork(db, (tx) => ({\n content: new DrizzleContentRepository(tx, contentLogger),\n outbox: new DrizzleOutboxAppender(tx, outboxLogger),\n }));\n const content = new ContentService({\n repo: new DrizzleContentRepository(db, contentLogger),\n uow: contentUow,\n logger: contentLogger,\n });\n // Entitlements: reads on the root db; writes + outbox append in one tx.\n const entitlementsUow = new DrizzleUnitOfWork(db, (tx) => ({\n entitlements: new DrizzleEntitlementsRepository(tx, entitlementsLogger),\n outbox: new DrizzleOutboxAppender(tx, outboxLogger),\n }));\n const entitlements = new EntitlementsServiceImpl({\n repo: new DrizzleEntitlementsRepository(db, entitlementsLogger),\n uow: entitlementsUow,\n logger: entitlementsLogger,\n });\n // Progress: report writes + outbox append in one tx; content supplies the\n // structure and completion rules the service evaluates against.\n const progressUow = new DrizzleUnitOfWork(db, (tx) => ({\n progress: new DrizzleProgressRepository(tx, progressLogger),\n outbox: new DrizzleOutboxAppender(tx, outboxLogger),\n }));\n const progress = new ProgressServiceImpl({\n repo: new DrizzleProgressRepository(db, progressLogger),\n content,\n uow: progressUow,\n logger: progressLogger,\n });\n\n const discussion = new DiscussionServiceImpl({\n repo: new DrizzleDiscussionRepository(db, discussionLogger),\n access: entitlements,\n content: content,\n uow: new DrizzleUnitOfWork(db, (tx) => ({\n discussion: new DrizzleDiscussionRepository(tx, discussionLogger),\n outbox: new DrizzleOutboxAppender(tx, outboxLogger),\n })),\n settings,\n logger: discussionLogger,\n });\n const assets = new AssetsServiceImpl({\n storage,\n repo: new DrizzleAssetsRepository(db, assetsLogger),\n uow: new DrizzleUnitOfWork(db, (tx) => ({\n assets: new DrizzleAssetsRepository(tx, assetsLogger),\n outbox: new DrizzleOutboxAppender(tx, outboxLogger),\n })),\n logger: assetsLogger,\n });\n\n const reporting = {\n students: new StudentsReportServiceImpl({\n repo: new DrizzleStudentsRepository(db, reportingLogger),\n logger: reportingLogger,\n }),\n dashboard: new DashboardReportServiceImpl({\n repo: new DrizzleDashboardRepository(db, reportingLogger),\n logger: reportingLogger,\n }),\n courses: new CoursesReportServiceImpl({\n repo: new DrizzleCourseAnalyticsRepository(db, reportingLogger),\n logger: reportingLogger,\n }),\n learn: new LearnReportServiceImpl({\n reader: new DrizzleLearnRepository(db, reportingLogger),\n content,\n progress,\n assets,\n deliveryExpirySeconds: config.deliveryExpirySeconds,\n logger: reportingLogger,\n }),\n };\n\n const credentialStore = new DrizzleCredentialStore(\n db,\n config.credentialStoreKey,\n integrationsLogger,\n );\n // The integrations this deployment supports: the installation's declared\n // plugins folder (directory name = integration id), loaded at startup.\n // Connect/configure reject undeclared ids and validate config with the\n // integration's own schema. No folder → no integrations.\n const integrationsRegistry = await loadIntegrations(options?.pluginsDir, integrationsLogger);\n // Integrations: credential + connection writes + outbox append in one tx\n // (a tx-bound credential store instance shares the scope's transaction).\n const integrationsUow = new DrizzleUnitOfWork(db, (tx) => ({\n connections: new DrizzleConnectionsRepository(tx, integrationsLogger),\n credentials: new DrizzleCredentialStore(tx, config.credentialStoreKey, integrationsLogger),\n outbox: new DrizzleOutboxAppender(tx, outboxLogger),\n }));\n const integrations = new IntegrationsServiceImpl({\n registry: integrationsRegistry,\n repo: new DrizzleConnectionsRepository(db, integrationsLogger),\n uow: integrationsUow,\n credentials: credentialStore,\n logger: integrationsLogger,\n });\n\n // Automations: run writes + outbox append in one tx; the engine drives\n // execution (register below), integrations supplies the loaded\n // integrations' actions for `available`.\n const automationsUow = new DrizzleUnitOfWork(db, (tx) => ({\n automations: new DrizzleAutomationsRepository(tx, automationsLogger),\n runs: new DrizzleAutomationRunsRepository(tx, automationsLogger),\n credentials: new DrizzleCredentialStore(tx, config.credentialStoreKey, automationsLogger),\n outbox: new DrizzleOutboxAppender(tx, automationsLogger),\n }));\n // Emails triggered by row-shaped events resolve their recipient and content\n // details at send time — the event carries ids only.\n const mailerLookups: MailerLookups = {\n orgUserEmail: async (orgId, orgUserId) =>\n (await reporting.students.get(orgId, orgUserId))?.email ?? null,\n contentInfo: async (orgId, contentId) => {\n const course = await content.getCourse(orgId, contentId);\n if (course) {\n return { id: course.id, title: course.title, type: course.type };\n }\n const download = await content.getDownload(orgId, contentId);\n return download ? { id: download.id, title: download.title, type: download.type } : null;\n },\n };\n\n const automations = new AutomationsServiceImpl({\n repo: new DrizzleAutomationsRepository(db, automationsLogger),\n runsRepo: new DrizzleAutomationRunsRepository(db, automationsLogger),\n uow: automationsUow,\n engine: automationEngine,\n mailer,\n lookups: mailerLookups,\n integrations,\n credentials: credentialStore,\n webhooks: options?.adapters?.webhooks ?? new FetchWebhookSender(),\n logger: automationsLogger,\n });\n automationEngine.register(automations);\n\n const eventBus = new InMemoryEventBus();\n registerNotificationSubscribers(eventBus, mailer, mailerLookups);\n // handle() never throws — the EventBus fans out to every subscriber\n // sequentially, and a rejection here would break sibling subscribers.\n eventBus.subscribeAll((event) => automations.handle(event));\n const outboxConfig = resolveOutboxConfig(config.outbox);\n const outboxRelay = new PollingOutboxRelay(\n new DrizzleOutboxStore(db, outboxLogger),\n eventBus,\n outboxConfig,\n outboxLogger,\n );\n\n /*\n * Auth Adapter\n */\n const authLogger = logger.child({ name: 'auth' });\n // Hook failures otherwise surface through Better Auth's route as an opaque\n // 500 — log which hook died before rethrowing.\n const authHook =\n <A extends unknown[], R>(name: string, fn: (...args: A) => Promise<R>) =>\n async (...args: A): Promise<R> => {\n try {\n return await fn(...args);\n } catch (err) {\n authLogger.error(`auth hook ${name} failed`, { err });\n throw err;\n }\n };\n const auth = new BetterAuth({\n db,\n baseUrl: config.authBaseURL,\n secret: config.authSecret,\n trustedOrigins: config.trustedOrigins,\n hooks: {\n sendResetPassword: async ({ user, url }) => {\n await identity.sendPasswordReset({ email: user.email, url });\n },\n onPasswordReset: async ({ user }) => {\n await identity.sendPasswordChanged({ email: user.email });\n },\n sendMagicLink: authHook('sendMagicLink', async ({ email, url }) => {\n await identity.sendMagicLink({\n email,\n url,\n });\n }),\n beforeUserCreate: authHook('beforeUserCreate', async (user) => {\n const { email, name } = user;\n const [firstName, lastName] = name.split(' ');\n // Better Auth has not minted an id yet — the id returned below becomes\n // its account id. A person provisioned at invite time is linked instead of re-created.\n const domainUser = await identity.linkOrCreateUser({\n email,\n ...(firstName !== undefined && { firstName }),\n ...(lastName !== undefined && { lastName }),\n });\n\n return {\n data: {\n ...user,\n id: domainUser.id,\n },\n };\n }),\n beforeCreateSession: authHook('beforeCreateSession', async (session) => {\n const person = await identity.getUserByExternalId(session.userId);\n if (!person) {\n return;\n }\n const orgs = await organizations.getOrgUsersForUser(person.id);\n if (orgs.length !== 1) {\n return;\n }\n // TODO - fix, why default to the first?\n const org = await organizations.getById(orgs[0]!.orgId);\n if (!org) {\n return;\n }\n\n return { data: { ...session, activeOrganizationId: org.id } };\n }),\n /*\n * We're making sure the domain org creates and using the same ID for better-auth org ID.\n */\n beforeCreateOrganization: authHook(\n 'beforeCreateOrganization',\n async ({ organization: org, user: baUser }) => {\n const domainOrg = await organizations.createOrganization({\n ownerId: baUser.id,\n ...org,\n logo: org.logo ?? undefined,\n });\n return { data: { ...org, id: domainOrg.id } };\n },\n ),\n beforeUpdateOrganization: authHook('beforeUpdateOrganization', async ({ organization }) => {\n await organizations.updateOrganization(organization.id, organization);\n }),\n beforeDeleteOrganization: authHook('beforeDeleteOrganization', async ({ organization }) => {\n await organizations.deleteOrganization(organization.id);\n }),\n beforeAddMember: authHook('beforeAddMember', async ({ member }) => {\n await organizations.addOrgUser({\n orgId: member.organizationId,\n userId: member.userId,\n role: parseRole(member.role),\n });\n }),\n beforeUpdateMemberRole: authHook('beforeUpdateMemberRole', async ({ member, newRole }) => {\n await organizations.updateOrgUser(member.organizationId, member.userId, {\n role: parseRole(newRole),\n });\n }),\n beforeRemoveMember: authHook('beforeRemoveMember', async ({ member }) => {\n await organizations.removeOrgUser(member.organizationId, member.userId);\n }),\n },\n logger: authLogger,\n cookieDomain: config.cookieDomain,\n secureCookies: config.secureCookies,\n });\n orgAdminRef.current = auth;\n\n return {\n auth,\n identity,\n organizations,\n // Better Auth owns org writes; the same instance fulfils the OrgAdmin port.\n orgProvider: auth,\n content,\n entitlements,\n progress,\n discussion,\n assets,\n integrations,\n automations,\n settings,\n reporting,\n storage,\n mailer,\n credentials: credentialStore,\n outboxRelay,\n automationEngine,\n logger,\n loggerInstance,\n requestContext: requestLogContext,\n // Better Auth owns the session store; the same instance fulfils SessionAdmin.\n sessions: auth,\n };\n}\n"],"mappings":"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"}
|
|
@@ -3,15 +3,18 @@ import { entitlementEvents } from "@headless-lms/core/entitlements";
|
|
|
3
3
|
function registerNotificationSubscribers(bus, mailer, lookups) {
|
|
4
4
|
bus.subscribe(entitlementEvents.entitlementCreated, async (event) => {
|
|
5
5
|
const { orgUserId, contentId } = event.data;
|
|
6
|
+
if (!contentId) return;
|
|
6
7
|
const [to, content] = await Promise.all([lookups.orgUserEmail(event.orgId, orgUserId), lookups.contentInfo(event.orgId, contentId)]);
|
|
7
8
|
if (!to || !content) throw new Error(`entitlement notification lookups failed: ${event.data.id}`);
|
|
8
9
|
await mailer.send(to, "accessGranted", {
|
|
9
10
|
contentTitle: content.title,
|
|
10
|
-
contentId: content.id
|
|
11
|
+
contentId: content.id,
|
|
12
|
+
contentType: content.type
|
|
11
13
|
});
|
|
12
14
|
});
|
|
13
15
|
bus.subscribe(entitlementEvents.entitlementDeleted, async (event) => {
|
|
14
16
|
const { orgUserId, contentId } = event.data;
|
|
17
|
+
if (!contentId) return;
|
|
15
18
|
const [to, content] = await Promise.all([lookups.orgUserEmail(event.orgId, orgUserId), lookups.contentInfo(event.orgId, contentId)]);
|
|
16
19
|
if (!to || !content) throw new Error(`entitlement notification lookups failed: ${event.data.id}`);
|
|
17
20
|
await mailer.send(to, "accessRevoked", { contentTitle: content.title });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"notifications.js","names":[],"sources":["../../src/app/notifications.ts"],"sourcesContent":["// Email side-effects of domain events, subscribed on the EventBus. Events\n// reach the bus via the outbox relay at-least-once: a mailer failure throws\n// through publish, and the relay retries with backoff — no email is silently\n// dropped.\nimport type { EventBus } from '@headless-lms/core/shared/ports';\nimport type { Mailer, MailerLookups } from '@headless-lms/core/shared/mailer';\nimport { entitlementEvents } from '@headless-lms/core/entitlements';\n\nexport function registerNotificationSubscribers(\n bus: EventBus,\n mailer: Pick<Mailer, 'send'>,\n lookups: MailerLookups,\n): void {\n bus.subscribe(entitlementEvents.entitlementCreated, async (event) => {\n const { orgUserId, contentId } = event.data;\n const [to, content] = await Promise.all([\n lookups.orgUserEmail(event.orgId, orgUserId),\n lookups.contentInfo(event.orgId, contentId),\n ]);\n if (!to || !content) {\n throw new Error(`entitlement notification lookups failed: ${event.data.id}`);\n }\n await mailer.send(to, 'accessGranted', {\n contentTitle: content.title,\n contentId: content.id,\n });\n });\n\n bus.subscribe(entitlementEvents.entitlementDeleted, async (event) => {\n const { orgUserId, contentId } = event.data;\n const [to, content] = await Promise.all([\n lookups.orgUserEmail(event.orgId, orgUserId),\n lookups.contentInfo(event.orgId, contentId),\n ]);\n if (!to || !content) {\n throw new Error(`entitlement notification lookups failed: ${event.data.id}`);\n }\n await mailer.send(to, 'accessRevoked', {\n contentTitle: content.title,\n });\n });\n}\n"],"mappings":";;AAQA,SAAgB,gCACd,KACA,QACA,SACM;CACN,IAAI,UAAU,kBAAkB,oBAAoB,OAAO,UAAU;EACnE,MAAM,EAAE,WAAW,cAAc,MAAM;
|
|
1
|
+
{"version":3,"file":"notifications.js","names":[],"sources":["../../src/app/notifications.ts"],"sourcesContent":["// Email side-effects of domain events, subscribed on the EventBus. Events\n// reach the bus via the outbox relay at-least-once: a mailer failure throws\n// through publish, and the relay retries with backoff — no email is silently\n// dropped.\nimport type { EventBus } from '@headless-lms/core/shared/ports';\nimport type { Mailer, MailerLookups } from '@headless-lms/core/shared/mailer';\nimport { entitlementEvents } from '@headless-lms/core/entitlements';\n\nexport function registerNotificationSubscribers(\n bus: EventBus,\n mailer: Pick<Mailer, 'send'>,\n lookups: MailerLookups,\n): void {\n bus.subscribe(entitlementEvents.entitlementCreated, async (event) => {\n const { orgUserId, contentId } = event.data;\n // Bundle grants carry no content id — these emails are per content item.\n if (!contentId) {\n return;\n }\n const [to, content] = await Promise.all([\n lookups.orgUserEmail(event.orgId, orgUserId),\n lookups.contentInfo(event.orgId, contentId),\n ]);\n if (!to || !content) {\n throw new Error(`entitlement notification lookups failed: ${event.data.id}`);\n }\n await mailer.send(to, 'accessGranted', {\n contentTitle: content.title,\n contentId: content.id,\n contentType: content.type,\n });\n });\n\n bus.subscribe(entitlementEvents.entitlementDeleted, async (event) => {\n const { orgUserId, contentId } = event.data;\n // Bundle grants carry no content id — these emails are per content item.\n if (!contentId) {\n return;\n }\n const [to, content] = await Promise.all([\n lookups.orgUserEmail(event.orgId, orgUserId),\n lookups.contentInfo(event.orgId, contentId),\n ]);\n if (!to || !content) {\n throw new Error(`entitlement notification lookups failed: ${event.data.id}`);\n }\n await mailer.send(to, 'accessRevoked', {\n contentTitle: content.title,\n });\n });\n}\n"],"mappings":";;AAQA,SAAgB,gCACd,KACA,QACA,SACM;CACN,IAAI,UAAU,kBAAkB,oBAAoB,OAAO,UAAU;EACnE,MAAM,EAAE,WAAW,cAAc,MAAM;EAEvC,IAAI,CAAC,WACH;EAEF,MAAM,CAAC,IAAI,WAAW,MAAM,QAAQ,IAAI,CACtC,QAAQ,aAAa,MAAM,OAAO,SAAS,GAC3C,QAAQ,YAAY,MAAM,OAAO,SAAS,CAC5C,CAAC;EACD,IAAI,CAAC,MAAM,CAAC,SACV,MAAM,IAAI,MAAM,4CAA4C,MAAM,KAAK,IAAI;EAE7E,MAAM,OAAO,KAAK,IAAI,iBAAiB;GACrC,cAAc,QAAQ;GACtB,WAAW,QAAQ;GACnB,aAAa,QAAQ;EACvB,CAAC;CACH,CAAC;CAED,IAAI,UAAU,kBAAkB,oBAAoB,OAAO,UAAU;EACnE,MAAM,EAAE,WAAW,cAAc,MAAM;EAEvC,IAAI,CAAC,WACH;EAEF,MAAM,CAAC,IAAI,WAAW,MAAM,QAAQ,IAAI,CACtC,QAAQ,aAAa,MAAM,OAAO,SAAS,GAC3C,QAAQ,YAAY,MAAM,OAAO,SAAS,CAC5C,CAAC;EACD,IAAI,CAAC,MAAM,CAAC,SACV,MAAM,IAAI,MAAM,4CAA4C,MAAM,KAAK,IAAI;EAE7E,MAAM,OAAO,KAAK,IAAI,iBAAiB,EACrC,cAAc,QAAQ,MACxB,CAAC;CACH,CAAC;AACH"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { FastifyReply, FastifyRequest } from "fastify";
|
|
2
2
|
//#region src/http/plugins/error-handler.d.ts
|
|
3
|
-
declare function errorHandler(error: unknown,
|
|
3
|
+
declare function errorHandler(error: unknown, request: FastifyRequest, reply: FastifyReply): FastifyReply<import("fastify").RouteGenericInterface, import("fastify").RawServerDefault, import("http").IncomingMessage, import("http").ServerResponse<import("http").IncomingMessage>, unknown, import("fastify").FastifySchema, import("fastify").FastifyTypeProviderDefault, unknown>;
|
|
4
4
|
//#endregion
|
|
5
5
|
export { errorHandler };
|
|
6
6
|
//# sourceMappingURL=error-handler.d.ts.map
|
|
@@ -2,10 +2,10 @@ import { UnauthorizedError } from "./auth.js";
|
|
|
2
2
|
import { NoActiveOrgError } from "../scope.js";
|
|
3
3
|
import { OrganizationRuleError } from "@headless-lms/core/organizations";
|
|
4
4
|
import { ActionInvocationError, AlreadyConnectedError, InvalidConfigError, UnknownIntegrationError } from "@headless-lms/core/integrations";
|
|
5
|
-
import { InvalidTriggerError } from "@headless-lms/core/automations";
|
|
5
|
+
import { AutomationKindMismatchError, InvalidTriggerError, ReservedActionError } from "@headless-lms/core/automations";
|
|
6
6
|
import { ConflictError, ForbiddenError, NotFoundError } from "@headless-lms/core/shared/errors";
|
|
7
7
|
//#region src/http/plugins/error-handler.ts
|
|
8
|
-
function errorHandler(error,
|
|
8
|
+
function errorHandler(error, request, reply) {
|
|
9
9
|
if (error instanceof NotFoundError) return reply.status(404).send({
|
|
10
10
|
error: "not_found",
|
|
11
11
|
message: error.message
|
|
@@ -42,6 +42,14 @@ function errorHandler(error, _request, reply) {
|
|
|
42
42
|
error: "invalid_trigger",
|
|
43
43
|
message: error.message
|
|
44
44
|
});
|
|
45
|
+
if (error instanceof ReservedActionError) return reply.status(400).send({
|
|
46
|
+
error: "reserved_action",
|
|
47
|
+
message: error.message
|
|
48
|
+
});
|
|
49
|
+
if (error instanceof AutomationKindMismatchError) return reply.status(409).send({
|
|
50
|
+
error: "kind_mismatch",
|
|
51
|
+
message: error.message
|
|
52
|
+
});
|
|
45
53
|
if (error instanceof NoActiveOrgError) return reply.status(403).send({
|
|
46
54
|
error: "forbidden",
|
|
47
55
|
message: error.message
|
|
@@ -55,8 +63,11 @@ function errorHandler(error, _request, reply) {
|
|
|
55
63
|
error: err.code ?? "bad_request",
|
|
56
64
|
message: err.message ?? "Bad request"
|
|
57
65
|
});
|
|
58
|
-
|
|
59
|
-
return reply.status(500).send({
|
|
66
|
+
request.log.error({ err: error }, `unhandled error in ${request.method} ${request.url}`);
|
|
67
|
+
return reply.status(500).send({
|
|
68
|
+
error: "internal_error",
|
|
69
|
+
requestId: request.id
|
|
70
|
+
});
|
|
60
71
|
}
|
|
61
72
|
//#endregion
|
|
62
73
|
export { errorHandler };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"error-handler.js","names":[],"sources":["../../../src/http/plugins/error-handler.ts"],"sourcesContent":["import { ConflictError, ForbiddenError, NotFoundError } from '@headless-lms/core/shared/errors';\nimport { OrganizationRuleError } from '@headless-lms/core/organizations';\nimport {\n ActionInvocationError,\n AlreadyConnectedError,\n InvalidConfigError,\n UnknownIntegrationError,\n} from '@headless-lms/core/integrations';\nimport {
|
|
1
|
+
{"version":3,"file":"error-handler.js","names":[],"sources":["../../../src/http/plugins/error-handler.ts"],"sourcesContent":["import { ConflictError, ForbiddenError, NotFoundError } from '@headless-lms/core/shared/errors';\nimport { OrganizationRuleError } from '@headless-lms/core/organizations';\nimport {\n ActionInvocationError,\n AlreadyConnectedError,\n InvalidConfigError,\n UnknownIntegrationError,\n} from '@headless-lms/core/integrations';\nimport {\n AutomationKindMismatchError,\n InvalidTriggerError,\n ReservedActionError,\n} from '@headless-lms/core/automations';\nimport { NoActiveOrgError } from '../scope.js';\nimport { UnauthorizedError } from './auth.js';\nimport type { FastifyReply, FastifyRequest } from 'fastify';\n\nexport function errorHandler(error: unknown, request: FastifyRequest, reply: FastifyReply) {\n if (error instanceof NotFoundError) {\n return reply.status(404).send({ error: 'not_found', message: error.message });\n }\n if (error instanceof ConflictError) {\n return reply.status(409).send({ error: 'conflict', message: error.message });\n }\n if (error instanceof ForbiddenError) {\n return reply.status(403).send({ error: 'forbidden', message: error.message });\n }\n if (error instanceof OrganizationRuleError) {\n return reply.status(409).send({ error: 'conflict', message: error.message });\n }\n if (error instanceof AlreadyConnectedError) {\n return reply.status(409).send({ error: 'already_connected', message: error.message });\n }\n if (error instanceof UnknownIntegrationError) {\n return reply.status(400).send({ error: 'unknown_integration', message: error.message });\n }\n if (error instanceof InvalidConfigError) {\n return reply.status(400).send({ error: 'invalid_config', message: error.message });\n }\n if (error instanceof ActionInvocationError) {\n return reply.status(502).send({ error: 'action_failed', message: error.message });\n }\n if (error instanceof InvalidTriggerError) {\n return reply.status(400).send({ error: 'invalid_trigger', message: error.message });\n }\n if (error instanceof ReservedActionError) {\n return reply.status(400).send({ error: 'reserved_action', message: error.message });\n }\n if (error instanceof AutomationKindMismatchError) {\n return reply.status(409).send({ error: 'kind_mismatch', message: error.message });\n }\n if (error instanceof NoActiveOrgError) {\n return reply.status(403).send({ error: 'forbidden', message: error.message });\n }\n // No session at all — 401 so the client bounces to login, not a generic 403.\n if (error instanceof UnauthorizedError) {\n return reply.status(401).send({ error: 'unauthorized', message: error.message });\n }\n const err = error as { statusCode?: number; code?: string; message?: string };\n if (typeof err.statusCode === 'number' && err.statusCode >= 400 && err.statusCode < 500) {\n return reply\n .status(err.statusCode)\n .send({ error: err.code ?? 'bad_request', message: err.message ?? 'Bad request' });\n }\n request.log.error({ err: error }, `unhandled error in ${request.method} ${request.url}`);\n return reply.status(500).send({ error: 'internal_error', requestId: request.id });\n}\n"],"mappings":";;;;;;;AAiBA,SAAgB,aAAa,OAAgB,SAAyB,OAAqB;CACzF,IAAI,iBAAiB,eACnB,OAAO,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK;EAAE,OAAO;EAAa,SAAS,MAAM;CAAQ,CAAC;CAE9E,IAAI,iBAAiB,eACnB,OAAO,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK;EAAE,OAAO;EAAY,SAAS,MAAM;CAAQ,CAAC;CAE7E,IAAI,iBAAiB,gBACnB,OAAO,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK;EAAE,OAAO;EAAa,SAAS,MAAM;CAAQ,CAAC;CAE9E,IAAI,iBAAiB,uBACnB,OAAO,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK;EAAE,OAAO;EAAY,SAAS,MAAM;CAAQ,CAAC;CAE7E,IAAI,iBAAiB,uBACnB,OAAO,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK;EAAE,OAAO;EAAqB,SAAS,MAAM;CAAQ,CAAC;CAEtF,IAAI,iBAAiB,yBACnB,OAAO,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK;EAAE,OAAO;EAAuB,SAAS,MAAM;CAAQ,CAAC;CAExF,IAAI,iBAAiB,oBACnB,OAAO,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK;EAAE,OAAO;EAAkB,SAAS,MAAM;CAAQ,CAAC;CAEnF,IAAI,iBAAiB,uBACnB,OAAO,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK;EAAE,OAAO;EAAiB,SAAS,MAAM;CAAQ,CAAC;CAElF,IAAI,iBAAiB,qBACnB,OAAO,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK;EAAE,OAAO;EAAmB,SAAS,MAAM;CAAQ,CAAC;CAEpF,IAAI,iBAAiB,qBACnB,OAAO,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK;EAAE,OAAO;EAAmB,SAAS,MAAM;CAAQ,CAAC;CAEpF,IAAI,iBAAiB,6BACnB,OAAO,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK;EAAE,OAAO;EAAiB,SAAS,MAAM;CAAQ,CAAC;CAElF,IAAI,iBAAiB,kBACnB,OAAO,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK;EAAE,OAAO;EAAa,SAAS,MAAM;CAAQ,CAAC;CAG9E,IAAI,iBAAiB,mBACnB,OAAO,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK;EAAE,OAAO;EAAgB,SAAS,MAAM;CAAQ,CAAC;CAEjF,MAAM,MAAM;CACZ,IAAI,OAAO,IAAI,eAAe,YAAY,IAAI,cAAc,OAAO,IAAI,aAAa,KAClF,OAAO,MACJ,OAAO,IAAI,UAAU,CAAC,CACtB,KAAK;EAAE,OAAO,IAAI,QAAQ;EAAe,SAAS,IAAI,WAAW;CAAc,CAAC;CAErF,QAAQ,IAAI,MAAM,EAAE,KAAK,MAAM,GAAG,sBAAsB,QAAQ,OAAO,GAAG,QAAQ,KAAK;CACvF,OAAO,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK;EAAE,OAAO;EAAkB,WAAW,QAAQ;CAAG,CAAC;AAClF"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { version } from "../../package.js";
|
|
2
|
+
import { jsonSchemaTransform, jsonSchemaTransformObject } from "fastify-type-provider-zod";
|
|
2
3
|
import swagger from "@fastify/swagger";
|
|
3
4
|
import swaggerUi from "@fastify/swagger-ui";
|
|
4
5
|
//#region src/http/plugins/openapi.ts
|
|
@@ -7,25 +8,22 @@ function registerOpenApi(app, config) {
|
|
|
7
8
|
openapi: {
|
|
8
9
|
info: {
|
|
9
10
|
title: "Headless LMS API",
|
|
10
|
-
version
|
|
11
|
+
version,
|
|
12
|
+
description: "API documentation for the Headless LMS platform",
|
|
13
|
+
license: {
|
|
14
|
+
name: "MIT",
|
|
15
|
+
url: "https://opensource.org/licenses/MIT"
|
|
16
|
+
}
|
|
11
17
|
},
|
|
12
18
|
servers: [{ url: config.publicUrl }],
|
|
13
19
|
tags: [
|
|
14
20
|
{
|
|
15
21
|
name: "Organizations",
|
|
16
|
-
description: "
|
|
17
|
-
},
|
|
18
|
-
{
|
|
19
|
-
name: "Courses",
|
|
20
|
-
description: "Course authoring: courses, modules, activities"
|
|
22
|
+
description: "The tenant: its members, roles, invites and students"
|
|
21
23
|
},
|
|
22
24
|
{
|
|
23
|
-
name: "
|
|
24
|
-
description: "
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
name: "Students",
|
|
28
|
-
description: "Students and their enrollments"
|
|
25
|
+
name: "Content",
|
|
26
|
+
description: "Authored content: courses and downloads, their structure and delivery"
|
|
29
27
|
},
|
|
30
28
|
{
|
|
31
29
|
name: "Entitlements",
|
|
@@ -33,27 +31,32 @@ function registerOpenApi(app, config) {
|
|
|
33
31
|
},
|
|
34
32
|
{
|
|
35
33
|
name: "Progress",
|
|
36
|
-
description: "
|
|
34
|
+
description: "Per-student progress and completion through course content"
|
|
37
35
|
},
|
|
38
36
|
{
|
|
39
|
-
name: "
|
|
40
|
-
description: "
|
|
37
|
+
name: "Discussion",
|
|
38
|
+
description: "Learner comments, replies, reactions and moderation"
|
|
41
39
|
},
|
|
42
40
|
{
|
|
43
|
-
name: "
|
|
44
|
-
description: "
|
|
41
|
+
name: "Assets",
|
|
42
|
+
description: "The org media library: uploads, serving and lifecycle"
|
|
45
43
|
},
|
|
46
44
|
{
|
|
47
|
-
name: "
|
|
48
|
-
description: "
|
|
45
|
+
name: "Automations",
|
|
46
|
+
description: "Workflows and webhooks that react to system events"
|
|
49
47
|
},
|
|
50
48
|
{
|
|
51
49
|
name: "Integrations",
|
|
52
|
-
description: "
|
|
50
|
+
description: "Connections to external services"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: "Reporting",
|
|
54
|
+
description: "Reporting and analytics"
|
|
53
55
|
}
|
|
54
56
|
]
|
|
55
57
|
},
|
|
56
|
-
transform: jsonSchemaTransform
|
|
58
|
+
transform: jsonSchemaTransform,
|
|
59
|
+
transformObject: jsonSchemaTransformObject
|
|
57
60
|
});
|
|
58
61
|
app.register(swaggerUi, { routePrefix: "/docs" });
|
|
59
62
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"openapi.js","names":[],"sources":["../../../src/http/plugins/openapi.ts"],"sourcesContent":["// OpenAPI documentation. @fastify/swagger reads the Zod route schemas (via the\n// transform) to build the spec the frontend SDK is generated from; it must be\n// registered before the routes so its onRoute hook captures them. UI + JSON are\n// served under /docs.\nimport type { FastifyInstance } from 'fastify';\nimport swagger from '@fastify/swagger';\nimport swaggerUi from '@fastify/swagger-ui';\nimport { jsonSchemaTransform } from 'fastify-type-provider-zod';\nimport type { ServerConfig } from '../config.js';\n\nexport function registerOpenApi(app: FastifyInstance, config: ServerConfig): void {\n app.register(swagger, {\n openapi: {\n info: {
|
|
1
|
+
{"version":3,"file":"openapi.js","names":["pkg.version"],"sources":["../../../src/http/plugins/openapi.ts"],"sourcesContent":["// OpenAPI documentation. @fastify/swagger reads the Zod route schemas (via the\n// transform) to build the spec the frontend SDK is generated from; it must be\n// registered before the routes so its onRoute hook captures them. UI + JSON are\n// served under /docs.\nimport type { FastifyInstance } from 'fastify';\nimport swagger from '@fastify/swagger';\nimport swaggerUi from '@fastify/swagger-ui';\nimport { jsonSchemaTransform, jsonSchemaTransformObject } from 'fastify-type-provider-zod';\nimport type { ServerConfig } from '../config.js';\nimport pkg from '../../../package.json' with { type: 'json' };\n\nexport function registerOpenApi(app: FastifyInstance, config: ServerConfig): void {\n app.register(swagger, {\n openapi: {\n info: {\n title: 'Headless LMS API',\n version: pkg.version,\n description: 'API documentation for the Headless LMS platform',\n license: {\n name: 'MIT',\n url: 'https://opensource.org/licenses/MIT',\n },\n },\n servers: [{ url: config.publicUrl }],\n tags: [\n {\n name: 'Organizations',\n description: 'The tenant: its members, roles, invites and students',\n },\n {\n name: 'Content',\n description: 'Authored content: courses and downloads, their structure and delivery',\n },\n { name: 'Entitlements', description: 'Access grants to content' },\n {\n name: 'Progress',\n description: 'Per-student progress and completion through course content',\n },\n { name: 'Discussion', description: 'Learner comments, replies, reactions and moderation' },\n { name: 'Assets', description: 'The org media library: uploads, serving and lifecycle' },\n { name: 'Automations', description: 'Workflows and webhooks that react to system events' },\n { name: 'Integrations', description: 'Connections to external services' },\n { name: 'Reporting', description: 'Reporting and analytics' },\n ],\n },\n transform: jsonSchemaTransform,\n transformObject: jsonSchemaTransformObject,\n });\n app.register(swaggerUi, { routePrefix: '/docs' });\n}\n"],"mappings":";;;;;AAWA,SAAgB,gBAAgB,KAAsB,QAA4B;CAChF,IAAI,SAAS,SAAS;EACpB,SAAS;GACP,MAAM;IACJ,OAAO;IACEA;IACT,aAAa;IACb,SAAS;KACP,MAAM;KACN,KAAK;IACP;GACF;GACA,SAAS,CAAC,EAAE,KAAK,OAAO,UAAU,CAAC;GACnC,MAAM;IACJ;KACE,MAAM;KACN,aAAa;IACf;IACA;KACE,MAAM;KACN,aAAa;IACf;IACA;KAAE,MAAM;KAAgB,aAAa;IAA2B;IAChE;KACE,MAAM;KACN,aAAa;IACf;IACA;KAAE,MAAM;KAAc,aAAa;IAAsD;IACzF;KAAE,MAAM;KAAU,aAAa;IAAwD;IACvF;KAAE,MAAM;KAAe,aAAa;IAAqD;IACzF;KAAE,MAAM;KAAgB,aAAa;IAAmC;IACxE;KAAE,MAAM;KAAa,aAAa;IAA0B;GAC9D;EACF;EACA,WAAW;EACX,iBAAiB;CACnB,CAAC;CACD,IAAI,SAAS,WAAW,EAAE,aAAa,QAAQ,CAAC;AAClD"}
|