@headless-lms/core 0.7.0 → 0.8.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 (35) hide show
  1. package/dist/assets/file-urls.d.ts +5 -19
  2. package/dist/assets/file-urls.js +2 -23
  3. package/dist/assets/file-urls.js.map +1 -1
  4. package/dist/assets/index.d.ts +3 -3
  5. package/dist/assets/ports.d.ts +9 -10
  6. package/dist/assets/service.d.ts +10 -13
  7. package/dist/assets/service.js +41 -62
  8. package/dist/assets/service.js.map +1 -1
  9. package/dist/content/events.d.ts +13 -13
  10. package/dist/content/service.d.ts +4 -1
  11. package/dist/content/service.js +52 -29
  12. package/dist/content/service.js.map +1 -1
  13. package/dist/discussion/chats.d.ts +1 -2
  14. package/dist/discussion/chats.js +8 -17
  15. package/dist/discussion/chats.js.map +1 -1
  16. package/dist/discussion/ports.d.ts +0 -6
  17. package/dist/discussion/ports.js.map +1 -1
  18. package/dist/discussion/posts.d.ts +1 -4
  19. package/dist/discussion/posts.js +8 -21
  20. package/dist/discussion/posts.js.map +1 -1
  21. package/dist/entitlements/events.d.ts +33 -33
  22. package/dist/identity/events.d.ts +2 -2
  23. package/dist/integrations/events.d.ts +3 -3
  24. package/dist/organizations/events.d.ts +2 -2
  25. package/dist/reporting/member/ports.d.ts +1 -10
  26. package/dist/reporting/member/service.d.ts +2 -11
  27. package/dist/reporting/member/service.js +12 -62
  28. package/dist/reporting/member/service.js.map +1 -1
  29. package/dist/types/ports.d.ts +4 -2
  30. package/dist/types/ports.js.map +1 -1
  31. package/dist/types/schemas/content.d.ts +4 -4
  32. package/dist/types/schemas/discussion.d.ts +14 -14
  33. package/dist/types/schemas/entitlements.d.ts +8 -8
  34. package/dist/types/schemas/notifications.d.ts +3 -3
  35. package/package.json +1 -1
@@ -22,9 +22,9 @@ declare const identityEvents: {
22
22
  id: import("zod").ZodString;
23
23
  createdAt: import("zod").ZodCoercedDate<unknown>;
24
24
  updatedAt: import("zod").ZodCoercedDate<unknown>;
25
+ email: import("zod").ZodEmail;
25
26
  firstName: import("zod").ZodNullable<import("zod").ZodString>;
26
27
  lastName: import("zod").ZodNullable<import("zod").ZodString>;
27
- email: import("zod").ZodEmail;
28
28
  }, import("zod/v4/core").$strip>>;
29
29
  userUpdated: RuntimeEventDefinition<VersionedDomainEvent<{
30
30
  id: string;
@@ -46,9 +46,9 @@ declare const identityEvents: {
46
46
  id: import("zod").ZodString;
47
47
  createdAt: import("zod").ZodCoercedDate<unknown>;
48
48
  updatedAt: import("zod").ZodCoercedDate<unknown>;
49
+ email: import("zod").ZodEmail;
49
50
  firstName: import("zod").ZodNullable<import("zod").ZodString>;
50
51
  lastName: import("zod").ZodNullable<import("zod").ZodString>;
51
- email: import("zod").ZodEmail;
52
52
  }, import("zod/v4/core").$strip>>;
53
53
  userDeleted: RuntimeEventDefinition<VersionedDomainEvent<{
54
54
  id: string;
@@ -26,9 +26,9 @@ declare const integrationEvents: {
26
26
  id: import("zod").ZodString;
27
27
  createdAt: import("zod").ZodCoercedDate<unknown>;
28
28
  updatedAt: import("zod").ZodCoercedDate<unknown>;
29
- active: import("zod").ZodBoolean;
30
29
  integrationId: import("zod").ZodString;
31
30
  config: import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodType<JsonValue, unknown, import("zod/v4/core").$ZodTypeInternals<JsonValue, unknown>>>;
31
+ active: import("zod").ZodBoolean;
32
32
  }, import("zod/v4/core").$strip>>;
33
33
  connectionUpdated: RuntimeEventDefinition<VersionedDomainEvent<{
34
34
  orgId: string;
@@ -53,9 +53,9 @@ declare const integrationEvents: {
53
53
  id: import("zod").ZodString;
54
54
  createdAt: import("zod").ZodCoercedDate<unknown>;
55
55
  updatedAt: import("zod").ZodCoercedDate<unknown>;
56
- active: import("zod").ZodBoolean;
57
56
  integrationId: import("zod").ZodString;
58
57
  config: import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodType<JsonValue, unknown, import("zod/v4/core").$ZodTypeInternals<JsonValue, unknown>>>;
58
+ active: import("zod").ZodBoolean;
59
59
  }, import("zod/v4/core").$strip>>;
60
60
  connectionRemoved: RuntimeEventDefinition<VersionedDomainEvent<{
61
61
  orgId: string;
@@ -80,9 +80,9 @@ declare const integrationEvents: {
80
80
  id: import("zod").ZodString;
81
81
  createdAt: import("zod").ZodCoercedDate<unknown>;
82
82
  updatedAt: import("zod").ZodCoercedDate<unknown>;
83
- active: import("zod").ZodBoolean;
84
83
  integrationId: import("zod").ZodString;
85
84
  config: import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodType<JsonValue, unknown, import("zod/v4/core").$ZodTypeInternals<JsonValue, unknown>>>;
85
+ active: import("zod").ZodBoolean;
86
86
  }, import("zod/v4/core").$strip>>;
87
87
  };
88
88
  type ConnectionCreated = EventOf<typeof integrationEvents.connectionCreated>;
@@ -218,8 +218,8 @@ declare const organizationEvents: {
218
218
  updatedAt: z.ZodCoercedDate<unknown>;
219
219
  email: z.ZodEmail;
220
220
  expiresAt: z.ZodNullable<z.ZodCoercedDate<unknown>>;
221
- roleKey: z.ZodString;
222
221
  invitedBy: z.ZodString;
222
+ roleKey: z.ZodString;
223
223
  }, z.core.$strip>>;
224
224
  inviteAccepted: RuntimeEventDefinition<VersionedDomainEvent<{
225
225
  id: string;
@@ -256,8 +256,8 @@ declare const organizationEvents: {
256
256
  updatedAt: z.ZodCoercedDate<unknown>;
257
257
  email: z.ZodEmail;
258
258
  expiresAt: z.ZodNullable<z.ZodCoercedDate<unknown>>;
259
- roleKey: z.ZodString;
260
259
  invitedBy: z.ZodString;
260
+ roleKey: z.ZodString;
261
261
  }, z.core.$strip>>;
262
262
  inviteCanceled: RuntimeEventDefinition<VersionedDomainEvent<{
263
263
  id: string;
@@ -1,5 +1,5 @@
1
1
  import { Page } from "../../types/shared.js";
2
- import { ActivityView, CourseView, DownloadFile, DownloadView, EventAttendee, MemberEvent, MemberEventSummary, MemberEventsQuery, Module, SpaceImage } from "../../types/schemas/content.js";
2
+ import { ActivityView, CourseView, DownloadFile, DownloadView, EventAttendee, MemberEvent, MemberEventSummary, MemberEventsQuery, Module } from "../../types/schemas/content.js";
3
3
  import { NotificationFilter } from "../../types/notifications.js";
4
4
  import "../../types/index.js";
5
5
  import { Actor } from "../../shared/actor.js";
@@ -21,10 +21,6 @@ interface MemberReportService {
21
21
  download: DownloadView;
22
22
  assets: DownloadFile[];
23
23
  }>;
24
- /** Signed URL of a course, download or community thumbnail: published, or the item's read
25
- * permission. An event's when the caller can read the event. */
26
- thumbnailFile(owner: 'course' | 'download' | 'community' | 'event', id: string, actor: Actor): Promise<string>;
27
- spaceImageFile(spaceId: string, image: SpaceImage, actor: Actor): Promise<string>;
28
24
  getEvent(eventId: string, actor: Actor): Promise<MemberEvent>;
29
25
  listEvents(query: MemberEventsQuery, actor: Actor): Promise<Page<MemberEventSummary>>;
30
26
  eventCalendar(eventId: string, actor: Actor): Promise<string>;
@@ -32,11 +28,6 @@ interface MemberReportService {
32
28
  page: number;
33
29
  pageSize: number;
34
30
  }, actor: Actor): Promise<Page<EventAttendee>>;
35
- eventFile(eventId: string, assetId: string, actor: Actor): Promise<string>;
36
- /** Signed URL of an asset the activity links, for a person who can open its course. */
37
- activityFile(activityId: string, assetId: string, actor: Actor): Promise<string>;
38
- /** Signed download URL of a file in a download the person can open. */
39
- downloadFile(downloadId: string, assetId: string, actor: Actor): Promise<string>;
40
31
  directory(communityId: string, query: DirectoryQuery, actor: Actor): Promise<Page<DirectoryEntry>>;
41
32
  communityHome(communityId: string, actor: Actor): Promise<CommunityHome>;
42
33
  communitySpace(communityId: string, spaceId: string, actor: Actor): Promise<CommunityHomeSpace>;
@@ -1,11 +1,10 @@
1
1
  import { Page } from "../../types/shared.js";
2
- import { ActivityView, CourseView, DownloadFile, DownloadView, EventAttendee, MemberEvent, MemberEventSummary, MemberEventsQuery, Module, SpaceImage } from "../../types/schemas/content.js";
2
+ import { ActivityView, CourseView, DownloadFile, DownloadView, EventAttendee, MemberEvent, MemberEventSummary, MemberEventsQuery, Module } from "../../types/schemas/content.js";
3
3
  import { NotificationFilter } from "../../types/notifications.js";
4
4
  import "../../types/index.js";
5
5
  import { Actor } from "../../shared/actor.js";
6
6
  import { ChannelRegistry, NotificationCategoryService } from "../../shared/ports.js";
7
7
  import { FileUrls } from "../../assets/file-urls.js";
8
- import { AssetFiles } from "../../assets/ports.js";
9
8
  import "../../assets/index.js";
10
9
  import { ContentService, ItemTitles } from "../../content/ports.js";
11
10
  import "../../content/index.js";
@@ -25,14 +24,12 @@ import { CommunityHome, CommunityHomeSpace, CommunityNotificationSettingsView, C
25
24
  import { MemberReportRepository, MemberReportService } from "./ports.js";
26
25
  //#region src/reporting/member/service.d.ts
27
26
  type MemberAccess = Pick<EntitlementsService, 'isCommunityMember' | 'openSpaceIds' | 'spaceStandings' | 'hasAccess' | 'listAccessibleContent' | 'openCommunityIds' | 'opensEverySpace' | 'everySpaceOpeners'>;
28
- type MemberContent = Pick<ContentService, 'findCourse' | 'listCourseModules' | 'listCourseActivityViews' | 'listCourseActivities' | 'listCourseActivityAssets' | 'getActivity' | 'findDownload' | 'listDownloadAssets' | 'findCommunity' | 'listSpaces' | 'getSpace' | 'findEvent' | 'findEvents' | 'eventFilesFor'>;
27
+ type MemberContent = Pick<ContentService, 'findCourse' | 'listCourseModules' | 'listCourseActivityViews' | 'listCourseActivities' | 'findDownload' | 'listDownloadAssets' | 'findCommunity' | 'listSpaces' | 'getSpace' | 'findEvent' | 'findEvents' | 'eventFilesFor'>;
29
28
  type MemberPeople = Pick<OrganizationService, 'systemActor' | 'permissionsOfMany' | 'holders'>;
30
- type Owner = 'course' | 'download' | 'community' | 'event';
31
29
  type MemberReportServiceParams = {
32
30
  repo: MemberReportRepository;
33
31
  content: MemberContent;
34
32
  progress: Pick<ProgressService, 'get' | 'listByTargets'>;
35
- files: Pick<AssetFiles, 'signFile'>;
36
33
  fileUrls: FileUrls;
37
34
  access: MemberAccess;
38
35
  people: MemberPeople;
@@ -55,7 +52,6 @@ declare class MemberReportServiceImpl implements MemberReportService {
55
52
  private readonly repo;
56
53
  private readonly content;
57
54
  private readonly progress;
58
- private readonly files;
59
55
  private readonly fileUrls;
60
56
  private readonly access;
61
57
  private readonly people;
@@ -93,10 +89,6 @@ declare class MemberReportServiceImpl implements MemberReportService {
93
89
  download: DownloadView;
94
90
  assets: DownloadFile[];
95
91
  }>;
96
- thumbnailFile(owner: Owner, id: string, actor: Actor): Promise<string>;
97
- spaceImageFile(spaceId: string, image: SpaceImage, actor: Actor): Promise<string>;
98
- activityFile(activityId: string, assetId: string, actor: Actor): Promise<string>;
99
- downloadFile(downloadId: string, assetId: string, actor: Actor): Promise<string>;
100
92
  private seeEvents;
101
93
  private requireEvent;
102
94
  private eventSummaries;
@@ -108,7 +100,6 @@ declare class MemberReportServiceImpl implements MemberReportService {
108
100
  page: number;
109
101
  pageSize: number;
110
102
  }, actor: Actor): Promise<Page<EventAttendee>>;
111
- eventFile(eventId: string, assetId: string, actor: Actor): Promise<string>;
112
103
  directory(communityId: string, query: DirectoryQuery, actor: Actor): Promise<Page<DirectoryEntry>>;
113
104
  communityHome(communityId: string, actor: Actor): Promise<CommunityHome>;
114
105
  communitySpace(communityId: string, spaceId: string, actor: Actor): Promise<CommunityHomeSpace>;
@@ -33,23 +33,11 @@ const READ_OF = {
33
33
  community: "communities:read",
34
34
  event: "events:read"
35
35
  };
36
- const THUMBNAIL_OF = {
37
- course: "Course thumbnail",
38
- download: "Download thumbnail",
39
- community: "Community thumbnail",
40
- event: "Event thumbnail"
41
- };
42
- const SPACE_IMAGE_ASSET = {
43
- cover: (space) => space.coverAssetId,
44
- background: (space) => space.backgroundAssetId,
45
- icon: (space) => space.iconAssetId
46
- };
47
36
  const EVENT_FACES = 10;
48
37
  var MemberReportServiceImpl = class {
49
38
  repo;
50
39
  content;
51
40
  progress;
52
- files;
53
41
  fileUrls;
54
42
  access;
55
43
  people;
@@ -65,7 +53,6 @@ var MemberReportServiceImpl = class {
65
53
  this.repo = params.repo;
66
54
  this.content = params.content;
67
55
  this.progress = params.progress;
68
- this.files = params.files;
69
56
  this.fileUrls = params.fileUrls;
70
57
  this.access = params.access;
71
58
  this.people = params.people;
@@ -293,36 +280,6 @@ var MemberReportServiceImpl = class {
293
280
  assets: await this.content.listDownloadAssets(downloadId, this.system(actor))
294
281
  };
295
282
  }
296
- async thumbnailFile(owner, id, actor) {
297
- const system = this.system(actor);
298
- const item = owner === "event" ? (await this.seeEvents([id], actor))[0]?.event : owner === "course" ? await this.content.findCourse(id, system) : owner === "download" ? await this.content.findDownload(id, system) : await this.content.findCommunity(id, system);
299
- if (!item?.thumbnailAssetId || owner !== "event" && item.status !== "published" && !actor.permissions.has(READ_OF[owner], id)) throw new NotFoundError(THUMBNAIL_OF[owner], id);
300
- return signed(await this.files.signFile(actor.orgId, item.thumbnailAssetId), THUMBNAIL_OF[owner], id);
301
- }
302
- async spaceImageFile(spaceId, image, actor) {
303
- const system = this.system(actor);
304
- const space = await this.content.getSpace(spaceId, system);
305
- const assetId = space ? SPACE_IMAGE_ASSET[image](space) : null;
306
- const community = space ? await this.content.findCommunity(space.communityId, system) : null;
307
- if (!space || !assetId || !community || community.status !== "published" && !actor.permissions.has("communities:read", space.communityId)) throw new NotFoundError("Space image", spaceId);
308
- return signed(await this.files.signFile(actor.orgId, assetId), "Space image", spaceId);
309
- }
310
- async activityFile(activityId, assetId, actor) {
311
- const system = this.system(actor);
312
- const activity = await this.content.getActivity(activityId, system);
313
- if (!activity) throw new NotFoundError("Lesson file", assetId);
314
- if (!actor.permissions.has("courses:read", activity.courseId)) {
315
- if (!isActivityPublished(activity.settings) || !await this.opensCourse(activity.courseId, actor)) throw new NotFoundError("Lesson file", assetId);
316
- }
317
- if (!(await this.content.listCourseActivityAssets(activity.courseId, system)).some((link) => link.activityId === activityId && link.assetId === assetId)) throw new NotFoundError("Lesson file", assetId);
318
- return signed(await this.files.signFile(actor.orgId, assetId), "Lesson file", assetId);
319
- }
320
- async downloadFile(downloadId, assetId, actor) {
321
- if (!await this.opensDownload(downloadId, actor)) throw new NotFoundError("Download file", assetId);
322
- const file = (await this.content.listDownloadAssets(downloadId, this.system(actor))).find((f) => f.assetId === assetId);
323
- if (!file) throw new NotFoundError("Download file", assetId);
324
- return signed(await this.files.signFile(actor.orgId, assetId, { saveAs: file.displayName ?? file.filename }), "Download file", assetId);
325
- }
326
283
  async seeEvents(ids, actor) {
327
284
  if (ids.length === 0) return [];
328
285
  const system = this.system(actor);
@@ -443,13 +400,6 @@ var MemberReportServiceImpl = class {
443
400
  rows: page.rows.map((row) => attendeeView(actor.orgId, row, this.fileUrls))
444
401
  };
445
402
  }
446
- async eventFile(eventId, assetId, actor) {
447
- const [seen] = await this.seeEvents([eventId], actor);
448
- if (!seen || seen.sight === "public") throw new NotFoundError("Event file", assetId);
449
- const file = (await this.content.eventFilesFor([eventId], this.system(actor)))[eventId]?.find((f) => f.assetId === assetId);
450
- if (!file) throw new NotFoundError("Event file", assetId);
451
- return signed(await this.files.signFile(actor.orgId, assetId, { saveAs: file.displayName ?? file.filename }), "Event file", assetId);
452
- }
453
403
  async directory(communityId, query, actor) {
454
404
  if (!await this.access.isCommunityMember(communityId, actor)) throw new NotFoundError("Community", communityId);
455
405
  const orgId = actor.orgId;
@@ -486,9 +436,9 @@ var MemberReportServiceImpl = class {
486
436
  communityId,
487
437
  title: community.title,
488
438
  description: community.description,
489
- thumbnailUrl: community.thumbnailUrl,
439
+ thumbnailUrl: showsImages(community.status, communityId, actor) ? community.thumbnailUrl : null,
490
440
  dm: community.settings.dm,
491
- spaces: await this.homeSpaces(actor, communityId, spaces, everySpace),
441
+ spaces: await this.homeSpaces(actor, communityId, spaces, everySpace, showsImages(community.status, communityId, actor)),
492
442
  viewer: {
493
443
  permissions: PERMISSION_KEYS.filter((p) => actor.permissions.has(p, communityId)),
494
444
  opensEverySpace: everySpace,
@@ -500,14 +450,15 @@ var MemberReportServiceImpl = class {
500
450
  }
501
451
  async communitySpace(communityId, spaceId, actor) {
502
452
  if (!await this.access.isCommunityMember(communityId, actor)) throw new NotFoundError("Space", spaceId);
503
- const space = await this.content.getSpace(spaceId, this.system(actor));
504
- if (!space || space.communityId !== communityId) throw new NotFoundError("Space", spaceId);
453
+ const system = this.system(actor);
454
+ const [space, community] = await Promise.all([this.content.getSpace(spaceId, system), this.content.findCommunity(communityId, system)]);
455
+ if (!space || space.communityId !== communityId || !community) throw new NotFoundError("Space", spaceId);
505
456
  const everySpace = await this.access.opensEverySpace(actor, communityId);
506
- const [view] = await this.homeSpaces(actor, communityId, [space], everySpace);
457
+ const [view] = await this.homeSpaces(actor, communityId, [space], everySpace, showsImages(community.status, communityId, actor));
507
458
  if (!view) throw new NotFoundError("Space", spaceId);
508
459
  return view;
509
460
  }
510
- async homeSpaces(actor, communityId, spaces, everySpace) {
461
+ async homeSpaces(actor, communityId, spaces, everySpace, images) {
511
462
  const orgId = actor.orgId;
512
463
  const excluding = await this.access.everySpaceOpeners(orgId, communityId);
513
464
  const [openIds, counts, standings, subscribed] = await Promise.all([
@@ -526,9 +477,9 @@ var MemberReportServiceImpl = class {
526
477
  memberCount,
527
478
  accessRule: space.accessRule,
528
479
  standing: standingOf(space, open.has(space.id), memberCount, standings[space.id]),
529
- coverUrl: space.coverUrl,
530
- backgroundUrl: space.backgroundUrl,
531
- iconUrl: space.iconUrl
480
+ coverUrl: images ? space.coverUrl : null,
481
+ backgroundUrl: images ? space.backgroundUrl : null,
482
+ iconUrl: images ? space.iconUrl : null
532
483
  };
533
484
  if (!open.has(space.id)) return {
534
485
  ...base,
@@ -578,9 +529,8 @@ var MemberReportServiceImpl = class {
578
529
  }))).filter((view) => view !== null);
579
530
  }
580
531
  };
581
- function signed(url, what, id) {
582
- if (!url) throw new NotFoundError(what, id);
583
- return url;
532
+ function showsImages(status, communityId, actor) {
533
+ return status === "published" || actor.permissions.has("communities:read", communityId);
584
534
  }
585
535
  function standingOf(space, open, memberCount, status) {
586
536
  if (space.accessRule === "all_members" || open && status !== "active") return "open";
@@ -1 +1 @@
1
- {"version":3,"file":"service.js","names":[],"sources":["../../../src/reporting/member/service.ts"],"sourcesContent":["import {\n eventSight,\n type ContentServicePort,\n type EventSight,\n type EventView,\n type SpaceDetail,\n type SpaceImage,\n type SpaceView,\n} from '../../content/index.js';\nimport { NotFoundError } from '../../shared/errors.js';\nimport { seesSpace, type EntitlementsService, type SpaceMember } from '../../entitlements/index.js';\nimport type { ProgressService } from '../../progress/index.js';\nimport type { AssetFiles, FileUrls } from '../../assets/index.js';\nimport type { OrganizationService } from '../../organizations/index.js';\nimport { PERMISSION_KEYS } from '../../types/schemas/organizations.js';\nimport type {\n ItemTitles as DiscussionItemTitles,\n SubscriptionService,\n} from '../../discussion/index.js';\nimport type { ItemTitles as ContentItemTitles } from '../../content/index.js';\nimport type { MemberActor, NotificationSettingsService } from '../../identity/index.js';\nimport {\n applicableCategories,\n type ListedNotification,\n type NotificationsService,\n} from '../../automations/index.js';\nimport type { ChannelRegistry, NotificationCategoryService } from '../../shared/ports.js';\nimport { resolveChannels } from '../../shared/notification-settings.js';\nimport { channelOrder } from '../../shared/notification-categories.js';\nimport type {\n NotificationCategory,\n NotificationFilter,\n StoredSetting,\n SubjectRef,\n} from '../../types/index.js';\nimport type { Actor } from '../../shared/actor.js';\nimport type { EventFile, Page, Permission } from '../../types/index.js';\nimport type {\n ActivityView,\n CommunityHome,\n CommunityHomeLink,\n CommunityHomeSpace,\n CourseProgressView,\n CourseView,\n DirectoryEntry,\n DirectoryQuery,\n DownloadFile,\n DownloadView,\n EventAttendee,\n MemberEvent,\n MemberEventSummary,\n MemberEventsQuery,\n MemberViewer,\n Module,\n NotificationItemView,\n NotificationView,\n NotificationsView,\n NotificationUnreadCounts,\n CommunityNotificationSettingsView,\n NotificationSettingsCategoryView,\n OrgNotificationSettingsView,\n SettingsChannelView,\n} from './model.js';\nimport type { MemberReportRepository, MemberReportService } from './ports.js';\nimport { eventCalendar } from './calendar.js';\nimport { attendeeFace, attendeeView, type EventFaces } from '../events/index.js';\n\nfunction refKey(ref: SubjectRef): string {\n return `${ref.type}:${ref.id}`;\n}\n\nfunction itemView(\n ref: SubjectRef | null,\n titles: Record<string, { title: string; excerpt?: string }>,\n): NotificationItemView | null {\n const found = ref ? titles[refKey(ref)] : undefined;\n if (!ref || !found) {\n return null;\n }\n return found.excerpt !== undefined\n ? { type: ref.type, id: ref.id, excerpt: found.excerpt }\n : { type: ref.type, id: ref.id, title: found.title };\n}\n\nfunction isActivityPublished(settings: unknown): boolean {\n return (settings as { published?: boolean } | null)?.published !== false;\n}\n\ntype MemberAccess = Pick<\n EntitlementsService,\n | 'isCommunityMember'\n | 'openSpaceIds'\n | 'spaceStandings'\n | 'hasAccess'\n | 'listAccessibleContent'\n | 'openCommunityIds'\n | 'opensEverySpace'\n | 'everySpaceOpeners'\n>;\n\ntype MemberContent = Pick<\n ContentServicePort,\n | 'findCourse'\n | 'listCourseModules'\n | 'listCourseActivityViews'\n | 'listCourseActivities'\n | 'listCourseActivityAssets'\n | 'getActivity'\n | 'findDownload'\n | 'listDownloadAssets'\n | 'findCommunity'\n | 'listSpaces'\n | 'getSpace'\n | 'findEvent'\n | 'findEvents'\n | 'eventFilesFor'\n>;\n\ntype MemberPeople = Pick<OrganizationService, 'systemActor' | 'permissionsOfMany' | 'holders'>;\n\ntype Owner = 'course' | 'download' | 'community' | 'event';\n\nconst READ_OF = {\n course: 'courses:read',\n download: 'downloads:read',\n community: 'communities:read',\n event: 'events:read',\n} as const satisfies Record<Owner, Permission>;\n\nconst THUMBNAIL_OF = {\n course: 'Course thumbnail',\n download: 'Download thumbnail',\n community: 'Community thumbnail',\n event: 'Event thumbnail',\n} as const satisfies Record<Owner, string>;\n\nconst SPACE_IMAGE_ASSET = {\n cover: (space) => space.coverAssetId,\n background: (space) => space.backgroundAssetId,\n icon: (space) => space.iconAssetId,\n} as const satisfies Record<SpaceImage, (space: SpaceView) => string | null>;\n\nconst EVENT_FACES = 10;\n\ninterface SeenEvent {\n event: EventView;\n sight: EventSight;\n attending: boolean;\n}\n\nexport type MemberReportServiceParams = {\n repo: MemberReportRepository;\n content: MemberContent;\n progress: Pick<ProgressService, 'get' | 'listByTargets'>;\n files: Pick<AssetFiles, 'signFile'>;\n fileUrls: FileUrls;\n access: MemberAccess;\n people: MemberPeople;\n notifications: Pick<NotificationsService, 'list' | 'get' | 'unreadCounts'>;\n titles: { discussion: DiscussionItemTitles; content: ContentItemTitles };\n subscriptions: Pick<SubscriptionService, 'viewerSubscriptions'>;\n notificationCategories: {\n community: Pick<NotificationCategoryService, 'get'>;\n org: Pick<NotificationCategoryService, 'get'>;\n };\n notificationSettings: Pick<NotificationSettingsService, 'get'>;\n channels: Pick<ChannelRegistry, 'list'>;\n studentPortalUrl: string;\n now?: () => Date;\n};\n\nexport class MemberReportServiceImpl implements MemberReportService {\n private readonly repo: MemberReportRepository;\n private readonly content: MemberContent;\n private readonly progress: Pick<ProgressService, 'get' | 'listByTargets'>;\n private readonly files: Pick<AssetFiles, 'signFile'>;\n private readonly fileUrls: FileUrls;\n private readonly access: MemberAccess;\n private readonly people: MemberPeople;\n private readonly notificationList: Pick<NotificationsService, 'list' | 'get' | 'unreadCounts'>;\n private readonly titles: MemberReportServiceParams['titles'];\n private readonly subscriptions: Pick<SubscriptionService, 'viewerSubscriptions'>;\n private readonly notificationCategories: MemberReportServiceParams['notificationCategories'];\n private readonly notificationSettings: Pick<NotificationSettingsService, 'get'>;\n private readonly channels: Pick<ChannelRegistry, 'list'>;\n private readonly studentPortalUrl: string;\n private readonly now: () => Date;\n\n constructor(params: MemberReportServiceParams) {\n this.repo = params.repo;\n this.content = params.content;\n this.progress = params.progress;\n this.files = params.files;\n this.fileUrls = params.fileUrls;\n this.access = params.access;\n this.people = params.people;\n this.notificationList = params.notifications;\n this.titles = params.titles;\n this.subscriptions = params.subscriptions;\n this.notificationCategories = params.notificationCategories;\n this.notificationSettings = params.notificationSettings;\n this.channels = params.channels;\n this.studentPortalUrl = params.studentPortalUrl.replace(/\\/+$/, '');\n this.now = params.now ?? (() => new Date());\n }\n\n private system(actor: Actor): Actor {\n return this.people.systemActor(actor.orgId);\n }\n\n viewer(actor: Actor): MemberViewer {\n const role = actor.permissions.roleOn();\n return {\n orgUserId: actor.id,\n orgRole: { key: role.key, name: role.name },\n permissions: PERMISSION_KEYS.filter((p) => actor.permissions.anywhere(p)),\n orgPermissions: PERMISSION_KEYS.filter((p) => actor.permissions.has(p)),\n };\n }\n\n async communityNotificationSettings(\n communityId: string,\n actor: MemberActor,\n ): Promise<CommunityNotificationSettingsView> {\n if (!(await this.access.isCommunityMember(communityId, actor))) {\n throw new NotFoundError('Community', communityId);\n }\n const orgId = actor.orgId;\n const system = this.system(actor);\n const [list, settings, spaceIds, spaces, community] = await Promise.all([\n this.notificationCategories.community.get(communityId, system),\n this.notificationSettings.get(orgId, actor.userId, { communityId }),\n this.access.openSpaceIds(communityId, actor),\n this.content.listSpaces(communityId, system),\n this.content.findCommunity(communityId, system),\n ]);\n const open = new Set(spaceIds);\n return {\n communityId,\n digest: community?.settings.digest ?? null,\n categories: this.settingsView(\n applicableCategories(list.categories, actor.permissions, { communityId }),\n settings,\n spaces\n .filter((space) => open.has(space.id))\n .map((space) => ({ spaceId: space.id, name: space.name })),\n ),\n };\n }\n\n async orgNotificationSettings(actor: MemberActor): Promise<OrgNotificationSettingsView> {\n const orgId = actor.orgId;\n const [list, settings] = await Promise.all([\n this.notificationCategories.org.get(orgId, this.system(actor)),\n this.notificationSettings.get(orgId, actor.userId, 'org'),\n ]);\n return {\n categories: this.settingsView(\n applicableCategories(list.categories, actor.permissions, 'org'),\n settings,\n [],\n ),\n };\n }\n\n private settingsView(\n categories: NotificationCategory[],\n settings: StoredSetting[],\n spaces: { spaceId: string; name: string }[],\n ): NotificationSettingsCategoryView[] {\n const configured = channelOrder(this.channels.list());\n const channelsAt = (\n category: NotificationCategory,\n spaceId: string | null,\n ): SettingsChannelView[] => {\n const withoutLevel = settings.filter(\n (setting) => setting.category !== category.key || setting.spaceId !== spaceId,\n );\n const fallback = new Map(\n resolveChannels(category, withoutLevel, spaceId, configured).map((resolved) => [\n resolved.channel,\n resolved.enabled,\n ]),\n );\n return resolveChannels(category, settings, spaceId, configured).map((resolved) => ({\n ...resolved,\n default: fallback.get(resolved.channel) ?? resolved.enabled,\n }));\n };\n return categories.map((category) => {\n const perSpace = category.perSpace === true;\n const view: NotificationSettingsCategoryView = {\n key: category.key,\n perSpace,\n channels: channelsAt(category, null),\n };\n if (perSpace) {\n view.spaces = spaces.map(({ spaceId, name }) => ({\n spaceId,\n name,\n channels: channelsAt(category, spaceId),\n }));\n }\n return view;\n });\n }\n\n async notifications(\n filter: NotificationFilter,\n page: { page: number; pageSize: number },\n actor: MemberActor,\n ): Promise<NotificationsView> {\n const listed = await this.notificationList.list(filter, page, actor);\n return {\n rows: await this.describeNotifications(actor, listed.items),\n total: listed.total,\n page: page.page,\n pageSize: page.pageSize,\n unreadCount: listed.unreadCount,\n };\n }\n\n async notification(notificationId: string, actor: MemberActor): Promise<NotificationView> {\n const found = await this.notificationList.get(notificationId, actor);\n const [view] = found ? await this.describeNotifications(actor, [found]) : [];\n if (!view) {\n throw new NotFoundError('Notification', notificationId);\n }\n return view;\n }\n\n async notificationUnreadCounts(actor: MemberActor): Promise<NotificationUnreadCounts> {\n const communityIds = await this.access.openCommunityIds(actor);\n return this.notificationList.unreadCounts(communityIds, actor);\n }\n\n private async describeNotifications(\n actor: Actor,\n rows: ListedNotification[],\n ): Promise<NotificationView[]> {\n const orgId = actor.orgId;\n const refs = rows.flatMap((row) => [\n ...(row.subject ? [row.subject] : []),\n ...(row.parent ? [row.parent] : []),\n ...(row.communityId ? [{ type: 'community' as const, id: row.communityId }] : []),\n ...(row.spaceId ? [{ type: 'space' as const, id: row.spaceId }] : []),\n ]);\n const actorIds = [...new Set(rows.flatMap((row) => row.actorId ?? []))];\n const [people, discussionTitles, contentTitles] = await Promise.all([\n this.repo.people(orgId, actorIds),\n this.titles.discussion.titlesFor(refs, actor),\n this.titles.content.titlesFor(refs, actor),\n ]);\n const titles = { ...contentTitles, ...discussionTitles };\n const personById = new Map(people.map((person) => [person.orgUserId, person]));\n return rows.map((row) => {\n const person = row.actorId ? personById.get(row.actorId) : undefined;\n const community = row.communityId ? titles[`community:${row.communityId}`] : undefined;\n const space = row.spaceId ? titles[`space:${row.spaceId}`] : undefined;\n return {\n id: row.id,\n type: row.type,\n scope: row.scope,\n reasons: row.reasons,\n matchedReason: row.matchedReason,\n category: row.category,\n display: row.display,\n community:\n row.communityId && community ? { id: row.communityId, title: community.title } : null,\n space: row.spaceId && space ? { id: row.spaceId, name: space.title } : null,\n actor: person\n ? {\n orgUserId: person.orgUserId,\n name: person.displayName,\n imageUrl: person.imageAssetId\n ? this.fileUrls.publicImage(orgId, person.orgUserId, person.imageAssetId)\n : null,\n }\n : null,\n subject: itemView(row.subject, titles),\n parent: itemView(row.parent, titles),\n createdAt: row.createdAt,\n readAt: row.readAt,\n };\n });\n }\n\n private async opensCourse(courseId: string, actor: Actor): Promise<CourseView | null> {\n const reads = actor.permissions.has('courses:read', courseId);\n if (!reads && !(await this.access.hasAccess(actor.orgId, actor.id, courseId))) {\n return null;\n }\n const course = await this.content.findCourse(courseId, this.system(actor));\n return course && (reads || course.status === 'published') ? course : null;\n }\n\n private async opensDownload(downloadId: string, actor: Actor): Promise<DownloadView | null> {\n const reads = actor.permissions.has('downloads:read', downloadId);\n if (!reads && !(await this.access.hasAccess(actor.orgId, actor.id, downloadId))) {\n return null;\n }\n const download = await this.content.findDownload(downloadId, this.system(actor));\n return download && (reads || download.status === 'published') ? download : null;\n }\n\n async listCourses(actor: Actor): Promise<CourseView[]> {\n const ids = await this.access.listAccessibleContent(actor.orgId, actor.id, 'course');\n const system = this.system(actor);\n const courses = await Promise.all(ids.map((id) => this.content.findCourse(id, system)));\n return courses.filter((c): c is CourseView => c !== null && c.status === 'published');\n }\n\n private async requireCourse(courseId: string, actor: Actor): Promise<CourseView> {\n const course = await this.opensCourse(courseId, actor);\n if (!course) {\n throw new NotFoundError('Course', courseId);\n }\n return course;\n }\n\n getCourse(courseId: string, actor: Actor): Promise<CourseView> {\n return this.requireCourse(courseId, actor);\n }\n\n async listModules(courseId: string, actor: Actor): Promise<Module[]> {\n await this.requireCourse(courseId, actor);\n return this.content.listCourseModules(courseId, this.system(actor));\n }\n\n async listActivities(courseId: string, actor: Actor): Promise<ActivityView[]> {\n await this.requireCourse(courseId, actor);\n const activities = await this.content.listCourseActivityViews(courseId, this.system(actor));\n return activities.filter((a) => isActivityPublished(a.settings));\n }\n\n async courseProgress(courseId: string, actor: Actor): Promise<CourseProgressView> {\n await this.requireCourse(courseId, actor);\n const orgId = actor.orgId;\n const courseActivities = await this.content.listCourseActivities(courseId, this.system(actor));\n const ids = courseActivities.filter((a) => isActivityPublished(a.settings)).map((a) => a.id);\n const records = await this.progress.listByTargets(orgId, actor.id, ids);\n const activities: CourseProgressView['activities'] = {};\n const positions: CourseProgressView['positions'] = {};\n let done = 0;\n for (const r of records) {\n if (r.targetType !== 'activity') {\n continue;\n }\n activities[r.targetId] = r.completedAt ? 'completed' : 'in-progress';\n if (r.completedAt) {\n done += 1;\n }\n if (r.position != null) {\n positions[r.targetId] = r.position;\n }\n }\n const courseRecord = await this.progress.get(orgId, {\n orgUserId: actor.id,\n targetType: 'course',\n targetId: courseId,\n });\n return {\n activities,\n percent: ids.length > 0 ? Math.round((done / ids.length) * 100) : 0,\n completed: courseRecord?.completedAt != null,\n positions,\n };\n }\n\n async listDownloads(actor: Actor): Promise<DownloadView[]> {\n const ids = await this.access.listAccessibleContent(actor.orgId, actor.id, 'download');\n const system = this.system(actor);\n const rows = await Promise.all(ids.map((id) => this.content.findDownload(id, system)));\n return rows.filter((d): d is DownloadView => d !== null && d.status === 'published');\n }\n\n async getDownload(\n downloadId: string,\n actor: Actor,\n ): Promise<{ download: DownloadView; assets: DownloadFile[] }> {\n const download = await this.opensDownload(downloadId, actor);\n if (!download) {\n throw new NotFoundError('Download', downloadId);\n }\n const assets = await this.content.listDownloadAssets(downloadId, this.system(actor));\n return { download, assets };\n }\n\n async thumbnailFile(owner: Owner, id: string, actor: Actor): Promise<string> {\n const system = this.system(actor);\n const item =\n owner === 'event'\n ? (await this.seeEvents([id], actor))[0]?.event\n : owner === 'course'\n ? await this.content.findCourse(id, system)\n : owner === 'download'\n ? await this.content.findDownload(id, system)\n : await this.content.findCommunity(id, system);\n if (\n !item?.thumbnailAssetId ||\n (owner !== 'event' &&\n item.status !== 'published' &&\n !actor.permissions.has(READ_OF[owner], id))\n ) {\n throw new NotFoundError(THUMBNAIL_OF[owner], id);\n }\n return signed(\n await this.files.signFile(actor.orgId, item.thumbnailAssetId),\n THUMBNAIL_OF[owner],\n id,\n );\n }\n\n async spaceImageFile(spaceId: string, image: SpaceImage, actor: Actor): Promise<string> {\n const system = this.system(actor);\n const space = await this.content.getSpace(spaceId, system);\n const assetId = space ? SPACE_IMAGE_ASSET[image](space) : null;\n const community = space ? await this.content.findCommunity(space.communityId, system) : null;\n if (\n !space ||\n !assetId ||\n !community ||\n (community.status !== 'published' &&\n !actor.permissions.has('communities:read', space.communityId))\n ) {\n throw new NotFoundError('Space image', spaceId);\n }\n return signed(await this.files.signFile(actor.orgId, assetId), 'Space image', spaceId);\n }\n\n async activityFile(activityId: string, assetId: string, actor: Actor): Promise<string> {\n const system = this.system(actor);\n const activity = await this.content.getActivity(activityId, system);\n if (!activity) {\n throw new NotFoundError('Lesson file', assetId);\n }\n if (!actor.permissions.has('courses:read', activity.courseId)) {\n if (\n !isActivityPublished(activity.settings) ||\n !(await this.opensCourse(activity.courseId, actor))\n ) {\n throw new NotFoundError('Lesson file', assetId);\n }\n }\n const links = await this.content.listCourseActivityAssets(activity.courseId, system);\n if (!links.some((link) => link.activityId === activityId && link.assetId === assetId)) {\n throw new NotFoundError('Lesson file', assetId);\n }\n return signed(await this.files.signFile(actor.orgId, assetId), 'Lesson file', assetId);\n }\n\n async downloadFile(downloadId: string, assetId: string, actor: Actor): Promise<string> {\n if (!(await this.opensDownload(downloadId, actor))) {\n throw new NotFoundError('Download file', assetId);\n }\n const files = await this.content.listDownloadAssets(downloadId, this.system(actor));\n const file = files.find((f) => f.assetId === assetId);\n if (!file) {\n throw new NotFoundError('Download file', assetId);\n }\n return signed(\n await this.files.signFile(actor.orgId, assetId, {\n saveAs: file.displayName ?? file.filename,\n }),\n 'Download file',\n assetId,\n );\n }\n\n private async seeEvents(ids: string[], actor: Actor): Promise<SeenEvent[]> {\n if (ids.length === 0) {\n return [];\n }\n const system = this.system(actor);\n const [events, attended] = await Promise.all([\n this.content.findEvents(ids, system),\n ids.length === 1\n ? this.access\n .hasAccess(actor.orgId, actor.id, ids[0]!)\n .then((attending) => (attending ? [ids[0]!] : []))\n : this.access.listAccessibleContent(actor.orgId, actor.id, 'event'),\n ]);\n const attending = new Set(attended);\n const byId = new Map(events.map((event) => [event.id, event]));\n return ids.flatMap((id) => {\n const event = byId.get(id);\n if (!event) {\n return [];\n }\n const sight = eventSight(event, {\n canRead: actor.permissions.has('events:read', id),\n attending: attending.has(id),\n });\n return sight ? [{ event, sight, attending: attending.has(id) }] : [];\n });\n }\n\n private async requireEvent(eventId: string, actor: Actor): Promise<SeenEvent> {\n const [seen] = await this.seeEvents([eventId], actor);\n if (!seen) {\n throw new NotFoundError('Event', eventId);\n }\n return seen;\n }\n\n private async eventSummaries(\n seen: SeenEvent[],\n actor: Actor,\n now: Date,\n ): Promise<MemberEventSummary[]> {\n const ids = seen.map((item) => item.event.id);\n const attended = seen.filter((item) => item.attending).map((item) => item.event.id);\n const [faces, direct] = await Promise.all([\n this.repo.eventFaces(actor.orgId, ids, EVENT_FACES),\n attended.length > 0\n ? this.repo.directEventGrants(actor.orgId, actor.id, attended)\n : Promise.resolve([]),\n ]);\n const leaves = new Set(direct);\n return seen.map((item) =>\n this.eventSummary(\n item,\n faces[item.event.id] ?? { total: 0, rows: [] },\n leaves.has(item.event.id),\n actor,\n now,\n ),\n );\n }\n\n private eventSummary(\n seen: SeenEvent,\n faces: EventFaces,\n canLeave: boolean,\n actor: Actor,\n now: Date,\n ): MemberEventSummary {\n const { event, sight, attending } = seen;\n const id = event.id;\n const canOpen = sight !== 'public';\n const capacity = event.settings.capacity;\n const attendeeCount = faces.total;\n const ended = event.endsAt.getTime() <= now.getTime();\n const recording =\n sight === 'read' || (sight === 'attendee' && ended && event.settings.replay)\n ? event.recording\n : null;\n const writes = actor.permissions.has('events:write', id);\n const canManageAttendees = actor.permissions.has('access:write') || writes;\n return {\n orgId: event.orgId,\n id,\n type: event.type,\n title: event.title,\n thumbnailAssetId: event.thumbnailAssetId,\n thumbnailUrl: event.thumbnailUrl,\n status: event.status,\n startsAt: event.startsAt,\n endsAt: event.endsAt,\n timeZone: event.timeZone,\n location: canOpen ? event.location : null,\n recording,\n settings: {\n type: event.settings.type,\n capacity,\n replay: event.settings.replay,\n },\n attendeeCount,\n attendees: faces.rows.map((row) => attendeeFace(actor.orgId, row, this.fileUrls)),\n placesLeft: capacity === null ? null : Math.max(capacity - attendeeCount, 0),\n viewer: {\n attending,\n canRegister:\n event.status === 'published' &&\n !attending &&\n event.settings.type === 'public' &&\n event.startsAt.getTime() > now.getTime() &&\n (capacity === null || attendeeCount < capacity),\n canOpen,\n canLeave,\n canManageAttendees,\n canAddAttendees: canManageAttendees && event.status === 'published' && !ended,\n canEdit: writes,\n canCancel: writes,\n canDelete: actor.permissions.has('events:delete', id),\n },\n };\n }\n\n async getEvent(eventId: string, actor: Actor): Promise<MemberEvent> {\n const seen = await this.requireEvent(eventId, actor);\n const now = this.now();\n const [[summary], files] = await Promise.all([\n this.eventSummaries([seen], actor, now),\n seen.sight === 'public'\n ? Promise.resolve({} as Record<string, EventFile[]>)\n : this.content.eventFilesFor([eventId], this.system(actor)),\n ]);\n return { ...summary!, description: seen.event.description, files: files[eventId] ?? [] };\n }\n\n async listEvents(query: MemberEventsQuery, actor: Actor): Promise<Page<MemberEventSummary>> {\n const now = this.now();\n const reads = actor.permissions.where('events:read');\n const page = await this.repo.events(\n actor.orgId,\n query,\n { orgUserId: actor.id, readAll: reads.org, readIds: reads.content },\n now,\n );\n const rows = await this.eventSummaries(await this.seeEvents(page.rows, actor), actor, now);\n return { ...page, rows };\n }\n\n async eventCalendar(eventId: string, actor: Actor): Promise<string> {\n const { event, sight } = await this.requireEvent(eventId, actor);\n return eventCalendar(event, {\n pageUrl: `${this.studentPortalUrl}/events/${encodeURIComponent(event.id)}`,\n withLocation: sight !== 'public',\n now: this.now(),\n });\n }\n\n async eventAttendees(\n eventId: string,\n query: { page: number; pageSize: number },\n actor: Actor,\n ): Promise<Page<EventAttendee>> {\n actor.permissions.require('events:read', eventId);\n if (!(await this.content.findEvent(eventId, this.system(actor)))) {\n throw new NotFoundError('Event', eventId);\n }\n const page = await this.repo.eventAttendees(actor.orgId, eventId, query);\n return {\n ...page,\n rows: page.rows.map((row) => attendeeView(actor.orgId, row, this.fileUrls)),\n };\n }\n\n async eventFile(eventId: string, assetId: string, actor: Actor): Promise<string> {\n const [seen] = await this.seeEvents([eventId], actor);\n if (!seen || seen.sight === 'public') {\n throw new NotFoundError('Event file', assetId);\n }\n const files = await this.content.eventFilesFor([eventId], this.system(actor));\n const file = files[eventId]?.find((f) => f.assetId === assetId);\n if (!file) {\n throw new NotFoundError('Event file', assetId);\n }\n return signed(\n await this.files.signFile(actor.orgId, assetId, {\n saveAs: file.displayName ?? file.filename,\n }),\n 'Event file',\n assetId,\n );\n }\n\n async directory(\n communityId: string,\n query: DirectoryQuery,\n actor: Actor,\n ): Promise<Page<DirectoryEntry>> {\n if (!(await this.access.isCommunityMember(communityId, actor))) {\n throw new NotFoundError('Community', communityId);\n }\n const orgId = actor.orgId;\n const community = await this.content.findCommunity(communityId, this.system(actor));\n if (!community) {\n throw new NotFoundError('Community', communityId);\n }\n const readers = await this.people.holders(orgId, 'communities:read', communityId);\n const page = await this.repo.directory(orgId, communityId, query, readers);\n const permissions = await this.people.permissionsOfMany(\n orgId,\n page.rows.map((row) => row.orgUserId),\n );\n return {\n ...page,\n rows: page.rows.map((row) => {\n const held = permissions.get(row.orgUserId);\n const item = held?.roleOn(communityId);\n const org = held?.roleOn();\n return {\n ...row,\n imageUrl: row.imageAssetId\n ? this.fileUrls.publicImage(orgId, row.orgUserId, row.imageAssetId)\n : null,\n team: (item?.team ?? false) || (org?.team ?? false),\n roleName: item?.team ? item.name : org?.team ? org.name : null,\n opensEverySpace:\n (held?.has('communities:read', communityId) ||\n held?.has('communities:moderate', communityId)) ??\n false,\n };\n }),\n };\n }\n\n async communityHome(communityId: string, actor: Actor): Promise<CommunityHome> {\n if (!(await this.access.isCommunityMember(communityId, actor))) {\n throw new NotFoundError('Community', communityId);\n }\n const system = this.system(actor);\n const community = await this.content.findCommunity(communityId, system);\n if (!community) {\n throw new NotFoundError('Community', communityId);\n }\n const [spaces, everySpace] = await Promise.all([\n this.content.listSpaces(communityId, system),\n this.access.opensEverySpace(actor, communityId),\n ]);\n const itemRole = actor.permissions.roleOn(communityId);\n const orgRole = actor.permissions.roleOn();\n const role = itemRole.team || !orgRole.team ? itemRole : orgRole;\n return {\n communityId,\n title: community.title,\n description: community.description,\n thumbnailUrl: community.thumbnailUrl,\n dm: community.settings.dm,\n spaces: await this.homeSpaces(actor, communityId, spaces, everySpace),\n viewer: {\n permissions: PERMISSION_KEYS.filter((p) => actor.permissions.has(p, communityId)),\n opensEverySpace: everySpace,\n team: itemRole.team || orgRole.team,\n roleName: role.name,\n canCreateEvents: actor.permissions.has('events:write'),\n },\n };\n }\n\n async communitySpace(\n communityId: string,\n spaceId: string,\n actor: Actor,\n ): Promise<CommunityHomeSpace> {\n if (!(await this.access.isCommunityMember(communityId, actor))) {\n throw new NotFoundError('Space', spaceId);\n }\n const space = await this.content.getSpace(spaceId, this.system(actor));\n if (!space || space.communityId !== communityId) {\n throw new NotFoundError('Space', spaceId);\n }\n const everySpace = await this.access.opensEverySpace(actor, communityId);\n const [view] = await this.homeSpaces(actor, communityId, [space], everySpace);\n if (!view) {\n throw new NotFoundError('Space', spaceId);\n }\n return view;\n }\n\n private async homeSpaces(\n actor: Actor,\n communityId: string,\n spaces: SpaceView[],\n everySpace: boolean,\n ): Promise<CommunityHomeSpace[]> {\n const orgId = actor.orgId;\n const excluding = await this.access.everySpaceOpeners(orgId, communityId);\n const [openIds, counts, standings, subscribed] = await Promise.all([\n this.access.openSpaceIds(communityId, actor),\n this.repo.spaceMemberCounts(orgId, communityId, excluding),\n this.access.spaceStandings(communityId, actor),\n this.subscriptions.viewerSubscriptions(\n orgId,\n actor.id,\n 'space',\n spaces.map((space) => space.id),\n new Date(),\n ),\n ]);\n const open = new Set(openIds);\n const seen = spaces.filter((space) =>\n seesSpace(everySpace, space.accessRule, standings[space.id]),\n );\n return Promise.all(\n seen.map(async (space): Promise<CommunityHomeSpace> => {\n const memberCount = counts[space.id] ?? 0;\n const base = {\n id: space.id,\n name: space.name,\n memberCount,\n accessRule: space.accessRule,\n standing: standingOf(space, open.has(space.id), memberCount, standings[space.id]),\n coverUrl: space.coverUrl,\n backgroundUrl: space.backgroundUrl,\n iconUrl: space.iconUrl,\n };\n if (!open.has(space.id)) {\n return { ...base, locked: true };\n }\n return {\n ...base,\n locked: false,\n description: space.description,\n capacity: space.capacity,\n settings: space.settings,\n topics: space.topics.map((topic) => ({ id: topic.id, name: topic.name })),\n links: await this.linkViews(actor, space.links),\n subscription: subscribed[space.id] ?? null,\n };\n }),\n );\n }\n\n private async linkViews(actor: Actor, links: SpaceDetail['links']): Promise<CommunityHomeLink[]> {\n const system = this.system(actor);\n const shown = (type: keyof typeof READ_OF, id: string, status: string) =>\n status === 'published' || actor.permissions.has(READ_OF[type], id);\n const views = await Promise.all(\n links.map(async (link): Promise<CommunityHomeLink | null> => {\n const course = await this.content.findCourse(link.contentId, system);\n if (course) {\n return shown('course', link.contentId, course.status)\n ? { id: link.id, contentId: link.contentId, type: 'course', title: course.title }\n : null;\n }\n const download = await this.content.findDownload(link.contentId, system);\n if (download) {\n return shown('download', link.contentId, download.status)\n ? { id: link.id, contentId: link.contentId, type: 'download', title: download.title }\n : null;\n }\n const community = await this.content.findCommunity(link.contentId, system);\n if (community) {\n return shown('community', link.contentId, community.status)\n ? { id: link.id, contentId: link.contentId, type: 'community', title: community.title }\n : null;\n }\n return null;\n }),\n );\n return views.filter((view): view is CommunityHomeLink => view !== null);\n }\n}\n\nfunction signed(url: string | null, what: string, id: string): string {\n if (!url) {\n throw new NotFoundError(what, id);\n }\n return url;\n}\n\nfunction standingOf(\n space: SpaceDetail,\n open: boolean,\n memberCount: number,\n status: SpaceMember['status'] | undefined,\n): CommunityHomeSpace['standing'] {\n if (space.accessRule === 'all_members' || (open && status !== 'active')) {\n return 'open';\n }\n if (status === 'active') {\n return 'member';\n }\n if (status === 'requested' || status === 'invited') {\n return status;\n }\n if (space.capacity !== null && memberCount >= space.capacity) {\n return 'full';\n }\n return 'none';\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"service.js","names":[],"sources":["../../../src/reporting/member/service.ts"],"sourcesContent":["import {\n eventSight,\n type ContentServicePort,\n type EventSight,\n type EventView,\n type SpaceDetail,\n type SpaceView,\n} from '../../content/index.js';\nimport { NotFoundError } from '../../shared/errors.js';\nimport { seesSpace, type EntitlementsService, type SpaceMember } from '../../entitlements/index.js';\nimport type { ProgressService } from '../../progress/index.js';\nimport type { FileUrls } from '../../assets/index.js';\nimport type { OrganizationService } from '../../organizations/index.js';\nimport { PERMISSION_KEYS } from '../../types/schemas/organizations.js';\nimport type {\n ItemTitles as DiscussionItemTitles,\n SubscriptionService,\n} from '../../discussion/index.js';\nimport type { ItemTitles as ContentItemTitles } from '../../content/index.js';\nimport type { MemberActor, NotificationSettingsService } from '../../identity/index.js';\nimport {\n applicableCategories,\n type ListedNotification,\n type NotificationsService,\n} from '../../automations/index.js';\nimport type { ChannelRegistry, NotificationCategoryService } from '../../shared/ports.js';\nimport { resolveChannels } from '../../shared/notification-settings.js';\nimport { channelOrder } from '../../shared/notification-categories.js';\nimport type {\n NotificationCategory,\n NotificationFilter,\n StoredSetting,\n SubjectRef,\n} from '../../types/index.js';\nimport type { Actor } from '../../shared/actor.js';\nimport type { EventFile, Page, Permission } from '../../types/index.js';\nimport type {\n ActivityView,\n CommunityHome,\n CommunityHomeLink,\n CommunityHomeSpace,\n CourseProgressView,\n CourseView,\n DirectoryEntry,\n DirectoryQuery,\n DownloadFile,\n DownloadView,\n EventAttendee,\n MemberEvent,\n MemberEventSummary,\n MemberEventsQuery,\n MemberViewer,\n Module,\n NotificationItemView,\n NotificationView,\n NotificationsView,\n NotificationUnreadCounts,\n CommunityNotificationSettingsView,\n NotificationSettingsCategoryView,\n OrgNotificationSettingsView,\n SettingsChannelView,\n} from './model.js';\nimport type { MemberReportRepository, MemberReportService } from './ports.js';\nimport { eventCalendar } from './calendar.js';\nimport { attendeeFace, attendeeView, type EventFaces } from '../events/index.js';\n\nfunction refKey(ref: SubjectRef): string {\n return `${ref.type}:${ref.id}`;\n}\n\nfunction itemView(\n ref: SubjectRef | null,\n titles: Record<string, { title: string; excerpt?: string }>,\n): NotificationItemView | null {\n const found = ref ? titles[refKey(ref)] : undefined;\n if (!ref || !found) {\n return null;\n }\n return found.excerpt !== undefined\n ? { type: ref.type, id: ref.id, excerpt: found.excerpt }\n : { type: ref.type, id: ref.id, title: found.title };\n}\n\nfunction isActivityPublished(settings: unknown): boolean {\n return (settings as { published?: boolean } | null)?.published !== false;\n}\n\ntype MemberAccess = Pick<\n EntitlementsService,\n | 'isCommunityMember'\n | 'openSpaceIds'\n | 'spaceStandings'\n | 'hasAccess'\n | 'listAccessibleContent'\n | 'openCommunityIds'\n | 'opensEverySpace'\n | 'everySpaceOpeners'\n>;\n\ntype MemberContent = Pick<\n ContentServicePort,\n | 'findCourse'\n | 'listCourseModules'\n | 'listCourseActivityViews'\n | 'listCourseActivities'\n | 'findDownload'\n | 'listDownloadAssets'\n | 'findCommunity'\n | 'listSpaces'\n | 'getSpace'\n | 'findEvent'\n | 'findEvents'\n | 'eventFilesFor'\n>;\n\ntype MemberPeople = Pick<OrganizationService, 'systemActor' | 'permissionsOfMany' | 'holders'>;\n\ntype Owner = 'course' | 'download' | 'community' | 'event';\n\nconst READ_OF = {\n course: 'courses:read',\n download: 'downloads:read',\n community: 'communities:read',\n event: 'events:read',\n} as const satisfies Record<Owner, Permission>;\n\nconst EVENT_FACES = 10;\n\ninterface SeenEvent {\n event: EventView;\n sight: EventSight;\n attending: boolean;\n}\n\nexport type MemberReportServiceParams = {\n repo: MemberReportRepository;\n content: MemberContent;\n progress: Pick<ProgressService, 'get' | 'listByTargets'>;\n fileUrls: FileUrls;\n access: MemberAccess;\n people: MemberPeople;\n notifications: Pick<NotificationsService, 'list' | 'get' | 'unreadCounts'>;\n titles: { discussion: DiscussionItemTitles; content: ContentItemTitles };\n subscriptions: Pick<SubscriptionService, 'viewerSubscriptions'>;\n notificationCategories: {\n community: Pick<NotificationCategoryService, 'get'>;\n org: Pick<NotificationCategoryService, 'get'>;\n };\n notificationSettings: Pick<NotificationSettingsService, 'get'>;\n channels: Pick<ChannelRegistry, 'list'>;\n studentPortalUrl: string;\n now?: () => Date;\n};\n\nexport class MemberReportServiceImpl implements MemberReportService {\n private readonly repo: MemberReportRepository;\n private readonly content: MemberContent;\n private readonly progress: Pick<ProgressService, 'get' | 'listByTargets'>;\n private readonly fileUrls: FileUrls;\n private readonly access: MemberAccess;\n private readonly people: MemberPeople;\n private readonly notificationList: Pick<NotificationsService, 'list' | 'get' | 'unreadCounts'>;\n private readonly titles: MemberReportServiceParams['titles'];\n private readonly subscriptions: Pick<SubscriptionService, 'viewerSubscriptions'>;\n private readonly notificationCategories: MemberReportServiceParams['notificationCategories'];\n private readonly notificationSettings: Pick<NotificationSettingsService, 'get'>;\n private readonly channels: Pick<ChannelRegistry, 'list'>;\n private readonly studentPortalUrl: string;\n private readonly now: () => Date;\n\n constructor(params: MemberReportServiceParams) {\n this.repo = params.repo;\n this.content = params.content;\n this.progress = params.progress;\n this.fileUrls = params.fileUrls;\n this.access = params.access;\n this.people = params.people;\n this.notificationList = params.notifications;\n this.titles = params.titles;\n this.subscriptions = params.subscriptions;\n this.notificationCategories = params.notificationCategories;\n this.notificationSettings = params.notificationSettings;\n this.channels = params.channels;\n this.studentPortalUrl = params.studentPortalUrl.replace(/\\/+$/, '');\n this.now = params.now ?? (() => new Date());\n }\n\n private system(actor: Actor): Actor {\n return this.people.systemActor(actor.orgId);\n }\n\n viewer(actor: Actor): MemberViewer {\n const role = actor.permissions.roleOn();\n return {\n orgUserId: actor.id,\n orgRole: { key: role.key, name: role.name },\n permissions: PERMISSION_KEYS.filter((p) => actor.permissions.anywhere(p)),\n orgPermissions: PERMISSION_KEYS.filter((p) => actor.permissions.has(p)),\n };\n }\n\n async communityNotificationSettings(\n communityId: string,\n actor: MemberActor,\n ): Promise<CommunityNotificationSettingsView> {\n if (!(await this.access.isCommunityMember(communityId, actor))) {\n throw new NotFoundError('Community', communityId);\n }\n const orgId = actor.orgId;\n const system = this.system(actor);\n const [list, settings, spaceIds, spaces, community] = await Promise.all([\n this.notificationCategories.community.get(communityId, system),\n this.notificationSettings.get(orgId, actor.userId, { communityId }),\n this.access.openSpaceIds(communityId, actor),\n this.content.listSpaces(communityId, system),\n this.content.findCommunity(communityId, system),\n ]);\n const open = new Set(spaceIds);\n return {\n communityId,\n digest: community?.settings.digest ?? null,\n categories: this.settingsView(\n applicableCategories(list.categories, actor.permissions, { communityId }),\n settings,\n spaces\n .filter((space) => open.has(space.id))\n .map((space) => ({ spaceId: space.id, name: space.name })),\n ),\n };\n }\n\n async orgNotificationSettings(actor: MemberActor): Promise<OrgNotificationSettingsView> {\n const orgId = actor.orgId;\n const [list, settings] = await Promise.all([\n this.notificationCategories.org.get(orgId, this.system(actor)),\n this.notificationSettings.get(orgId, actor.userId, 'org'),\n ]);\n return {\n categories: this.settingsView(\n applicableCategories(list.categories, actor.permissions, 'org'),\n settings,\n [],\n ),\n };\n }\n\n private settingsView(\n categories: NotificationCategory[],\n settings: StoredSetting[],\n spaces: { spaceId: string; name: string }[],\n ): NotificationSettingsCategoryView[] {\n const configured = channelOrder(this.channels.list());\n const channelsAt = (\n category: NotificationCategory,\n spaceId: string | null,\n ): SettingsChannelView[] => {\n const withoutLevel = settings.filter(\n (setting) => setting.category !== category.key || setting.spaceId !== spaceId,\n );\n const fallback = new Map(\n resolveChannels(category, withoutLevel, spaceId, configured).map((resolved) => [\n resolved.channel,\n resolved.enabled,\n ]),\n );\n return resolveChannels(category, settings, spaceId, configured).map((resolved) => ({\n ...resolved,\n default: fallback.get(resolved.channel) ?? resolved.enabled,\n }));\n };\n return categories.map((category) => {\n const perSpace = category.perSpace === true;\n const view: NotificationSettingsCategoryView = {\n key: category.key,\n perSpace,\n channels: channelsAt(category, null),\n };\n if (perSpace) {\n view.spaces = spaces.map(({ spaceId, name }) => ({\n spaceId,\n name,\n channels: channelsAt(category, spaceId),\n }));\n }\n return view;\n });\n }\n\n async notifications(\n filter: NotificationFilter,\n page: { page: number; pageSize: number },\n actor: MemberActor,\n ): Promise<NotificationsView> {\n const listed = await this.notificationList.list(filter, page, actor);\n return {\n rows: await this.describeNotifications(actor, listed.items),\n total: listed.total,\n page: page.page,\n pageSize: page.pageSize,\n unreadCount: listed.unreadCount,\n };\n }\n\n async notification(notificationId: string, actor: MemberActor): Promise<NotificationView> {\n const found = await this.notificationList.get(notificationId, actor);\n const [view] = found ? await this.describeNotifications(actor, [found]) : [];\n if (!view) {\n throw new NotFoundError('Notification', notificationId);\n }\n return view;\n }\n\n async notificationUnreadCounts(actor: MemberActor): Promise<NotificationUnreadCounts> {\n const communityIds = await this.access.openCommunityIds(actor);\n return this.notificationList.unreadCounts(communityIds, actor);\n }\n\n private async describeNotifications(\n actor: Actor,\n rows: ListedNotification[],\n ): Promise<NotificationView[]> {\n const orgId = actor.orgId;\n const refs = rows.flatMap((row) => [\n ...(row.subject ? [row.subject] : []),\n ...(row.parent ? [row.parent] : []),\n ...(row.communityId ? [{ type: 'community' as const, id: row.communityId }] : []),\n ...(row.spaceId ? [{ type: 'space' as const, id: row.spaceId }] : []),\n ]);\n const actorIds = [...new Set(rows.flatMap((row) => row.actorId ?? []))];\n const [people, discussionTitles, contentTitles] = await Promise.all([\n this.repo.people(orgId, actorIds),\n this.titles.discussion.titlesFor(refs, actor),\n this.titles.content.titlesFor(refs, actor),\n ]);\n const titles = { ...contentTitles, ...discussionTitles };\n const personById = new Map(people.map((person) => [person.orgUserId, person]));\n return rows.map((row) => {\n const person = row.actorId ? personById.get(row.actorId) : undefined;\n const community = row.communityId ? titles[`community:${row.communityId}`] : undefined;\n const space = row.spaceId ? titles[`space:${row.spaceId}`] : undefined;\n return {\n id: row.id,\n type: row.type,\n scope: row.scope,\n reasons: row.reasons,\n matchedReason: row.matchedReason,\n category: row.category,\n display: row.display,\n community:\n row.communityId && community ? { id: row.communityId, title: community.title } : null,\n space: row.spaceId && space ? { id: row.spaceId, name: space.title } : null,\n actor: person\n ? {\n orgUserId: person.orgUserId,\n name: person.displayName,\n imageUrl: person.imageAssetId\n ? this.fileUrls.publicImage(orgId, person.orgUserId, person.imageAssetId)\n : null,\n }\n : null,\n subject: itemView(row.subject, titles),\n parent: itemView(row.parent, titles),\n createdAt: row.createdAt,\n readAt: row.readAt,\n };\n });\n }\n\n private async opensCourse(courseId: string, actor: Actor): Promise<CourseView | null> {\n const reads = actor.permissions.has('courses:read', courseId);\n if (!reads && !(await this.access.hasAccess(actor.orgId, actor.id, courseId))) {\n return null;\n }\n const course = await this.content.findCourse(courseId, this.system(actor));\n return course && (reads || course.status === 'published') ? course : null;\n }\n\n private async opensDownload(downloadId: string, actor: Actor): Promise<DownloadView | null> {\n const reads = actor.permissions.has('downloads:read', downloadId);\n if (!reads && !(await this.access.hasAccess(actor.orgId, actor.id, downloadId))) {\n return null;\n }\n const download = await this.content.findDownload(downloadId, this.system(actor));\n return download && (reads || download.status === 'published') ? download : null;\n }\n\n async listCourses(actor: Actor): Promise<CourseView[]> {\n const ids = await this.access.listAccessibleContent(actor.orgId, actor.id, 'course');\n const system = this.system(actor);\n const courses = await Promise.all(ids.map((id) => this.content.findCourse(id, system)));\n return courses.filter((c): c is CourseView => c !== null && c.status === 'published');\n }\n\n private async requireCourse(courseId: string, actor: Actor): Promise<CourseView> {\n const course = await this.opensCourse(courseId, actor);\n if (!course) {\n throw new NotFoundError('Course', courseId);\n }\n return course;\n }\n\n getCourse(courseId: string, actor: Actor): Promise<CourseView> {\n return this.requireCourse(courseId, actor);\n }\n\n async listModules(courseId: string, actor: Actor): Promise<Module[]> {\n await this.requireCourse(courseId, actor);\n return this.content.listCourseModules(courseId, this.system(actor));\n }\n\n async listActivities(courseId: string, actor: Actor): Promise<ActivityView[]> {\n await this.requireCourse(courseId, actor);\n const activities = await this.content.listCourseActivityViews(courseId, this.system(actor));\n return activities.filter((a) => isActivityPublished(a.settings));\n }\n\n async courseProgress(courseId: string, actor: Actor): Promise<CourseProgressView> {\n await this.requireCourse(courseId, actor);\n const orgId = actor.orgId;\n const courseActivities = await this.content.listCourseActivities(courseId, this.system(actor));\n const ids = courseActivities.filter((a) => isActivityPublished(a.settings)).map((a) => a.id);\n const records = await this.progress.listByTargets(orgId, actor.id, ids);\n const activities: CourseProgressView['activities'] = {};\n const positions: CourseProgressView['positions'] = {};\n let done = 0;\n for (const r of records) {\n if (r.targetType !== 'activity') {\n continue;\n }\n activities[r.targetId] = r.completedAt ? 'completed' : 'in-progress';\n if (r.completedAt) {\n done += 1;\n }\n if (r.position != null) {\n positions[r.targetId] = r.position;\n }\n }\n const courseRecord = await this.progress.get(orgId, {\n orgUserId: actor.id,\n targetType: 'course',\n targetId: courseId,\n });\n return {\n activities,\n percent: ids.length > 0 ? Math.round((done / ids.length) * 100) : 0,\n completed: courseRecord?.completedAt != null,\n positions,\n };\n }\n\n async listDownloads(actor: Actor): Promise<DownloadView[]> {\n const ids = await this.access.listAccessibleContent(actor.orgId, actor.id, 'download');\n const system = this.system(actor);\n const rows = await Promise.all(ids.map((id) => this.content.findDownload(id, system)));\n return rows.filter((d): d is DownloadView => d !== null && d.status === 'published');\n }\n\n async getDownload(\n downloadId: string,\n actor: Actor,\n ): Promise<{ download: DownloadView; assets: DownloadFile[] }> {\n const download = await this.opensDownload(downloadId, actor);\n if (!download) {\n throw new NotFoundError('Download', downloadId);\n }\n const assets = await this.content.listDownloadAssets(downloadId, this.system(actor));\n return { download, assets };\n }\n\n private async seeEvents(ids: string[], actor: Actor): Promise<SeenEvent[]> {\n if (ids.length === 0) {\n return [];\n }\n const system = this.system(actor);\n const [events, attended] = await Promise.all([\n this.content.findEvents(ids, system),\n ids.length === 1\n ? this.access\n .hasAccess(actor.orgId, actor.id, ids[0]!)\n .then((attending) => (attending ? [ids[0]!] : []))\n : this.access.listAccessibleContent(actor.orgId, actor.id, 'event'),\n ]);\n const attending = new Set(attended);\n const byId = new Map(events.map((event) => [event.id, event]));\n return ids.flatMap((id) => {\n const event = byId.get(id);\n if (!event) {\n return [];\n }\n const sight = eventSight(event, {\n canRead: actor.permissions.has('events:read', id),\n attending: attending.has(id),\n });\n return sight ? [{ event, sight, attending: attending.has(id) }] : [];\n });\n }\n\n private async requireEvent(eventId: string, actor: Actor): Promise<SeenEvent> {\n const [seen] = await this.seeEvents([eventId], actor);\n if (!seen) {\n throw new NotFoundError('Event', eventId);\n }\n return seen;\n }\n\n private async eventSummaries(\n seen: SeenEvent[],\n actor: Actor,\n now: Date,\n ): Promise<MemberEventSummary[]> {\n const ids = seen.map((item) => item.event.id);\n const attended = seen.filter((item) => item.attending).map((item) => item.event.id);\n const [faces, direct] = await Promise.all([\n this.repo.eventFaces(actor.orgId, ids, EVENT_FACES),\n attended.length > 0\n ? this.repo.directEventGrants(actor.orgId, actor.id, attended)\n : Promise.resolve([]),\n ]);\n const leaves = new Set(direct);\n return seen.map((item) =>\n this.eventSummary(\n item,\n faces[item.event.id] ?? { total: 0, rows: [] },\n leaves.has(item.event.id),\n actor,\n now,\n ),\n );\n }\n\n private eventSummary(\n seen: SeenEvent,\n faces: EventFaces,\n canLeave: boolean,\n actor: Actor,\n now: Date,\n ): MemberEventSummary {\n const { event, sight, attending } = seen;\n const id = event.id;\n const canOpen = sight !== 'public';\n const capacity = event.settings.capacity;\n const attendeeCount = faces.total;\n const ended = event.endsAt.getTime() <= now.getTime();\n const recording =\n sight === 'read' || (sight === 'attendee' && ended && event.settings.replay)\n ? event.recording\n : null;\n const writes = actor.permissions.has('events:write', id);\n const canManageAttendees = actor.permissions.has('access:write') || writes;\n return {\n orgId: event.orgId,\n id,\n type: event.type,\n title: event.title,\n thumbnailAssetId: event.thumbnailAssetId,\n thumbnailUrl: event.thumbnailUrl,\n status: event.status,\n startsAt: event.startsAt,\n endsAt: event.endsAt,\n timeZone: event.timeZone,\n location: canOpen ? event.location : null,\n recording,\n settings: {\n type: event.settings.type,\n capacity,\n replay: event.settings.replay,\n },\n attendeeCount,\n attendees: faces.rows.map((row) => attendeeFace(actor.orgId, row, this.fileUrls)),\n placesLeft: capacity === null ? null : Math.max(capacity - attendeeCount, 0),\n viewer: {\n attending,\n canRegister:\n event.status === 'published' &&\n !attending &&\n event.settings.type === 'public' &&\n event.startsAt.getTime() > now.getTime() &&\n (capacity === null || attendeeCount < capacity),\n canOpen,\n canLeave,\n canManageAttendees,\n canAddAttendees: canManageAttendees && event.status === 'published' && !ended,\n canEdit: writes,\n canCancel: writes,\n canDelete: actor.permissions.has('events:delete', id),\n },\n };\n }\n\n async getEvent(eventId: string, actor: Actor): Promise<MemberEvent> {\n const seen = await this.requireEvent(eventId, actor);\n const now = this.now();\n const [[summary], files] = await Promise.all([\n this.eventSummaries([seen], actor, now),\n seen.sight === 'public'\n ? Promise.resolve({} as Record<string, EventFile[]>)\n : this.content.eventFilesFor([eventId], this.system(actor)),\n ]);\n return { ...summary!, description: seen.event.description, files: files[eventId] ?? [] };\n }\n\n async listEvents(query: MemberEventsQuery, actor: Actor): Promise<Page<MemberEventSummary>> {\n const now = this.now();\n const reads = actor.permissions.where('events:read');\n const page = await this.repo.events(\n actor.orgId,\n query,\n { orgUserId: actor.id, readAll: reads.org, readIds: reads.content },\n now,\n );\n const rows = await this.eventSummaries(await this.seeEvents(page.rows, actor), actor, now);\n return { ...page, rows };\n }\n\n async eventCalendar(eventId: string, actor: Actor): Promise<string> {\n const { event, sight } = await this.requireEvent(eventId, actor);\n return eventCalendar(event, {\n pageUrl: `${this.studentPortalUrl}/events/${encodeURIComponent(event.id)}`,\n withLocation: sight !== 'public',\n now: this.now(),\n });\n }\n\n async eventAttendees(\n eventId: string,\n query: { page: number; pageSize: number },\n actor: Actor,\n ): Promise<Page<EventAttendee>> {\n actor.permissions.require('events:read', eventId);\n if (!(await this.content.findEvent(eventId, this.system(actor)))) {\n throw new NotFoundError('Event', eventId);\n }\n const page = await this.repo.eventAttendees(actor.orgId, eventId, query);\n return {\n ...page,\n rows: page.rows.map((row) => attendeeView(actor.orgId, row, this.fileUrls)),\n };\n }\n\n async directory(\n communityId: string,\n query: DirectoryQuery,\n actor: Actor,\n ): Promise<Page<DirectoryEntry>> {\n if (!(await this.access.isCommunityMember(communityId, actor))) {\n throw new NotFoundError('Community', communityId);\n }\n const orgId = actor.orgId;\n const community = await this.content.findCommunity(communityId, this.system(actor));\n if (!community) {\n throw new NotFoundError('Community', communityId);\n }\n const readers = await this.people.holders(orgId, 'communities:read', communityId);\n const page = await this.repo.directory(orgId, communityId, query, readers);\n const permissions = await this.people.permissionsOfMany(\n orgId,\n page.rows.map((row) => row.orgUserId),\n );\n return {\n ...page,\n rows: page.rows.map((row) => {\n const held = permissions.get(row.orgUserId);\n const item = held?.roleOn(communityId);\n const org = held?.roleOn();\n return {\n ...row,\n imageUrl: row.imageAssetId\n ? this.fileUrls.publicImage(orgId, row.orgUserId, row.imageAssetId)\n : null,\n team: (item?.team ?? false) || (org?.team ?? false),\n roleName: item?.team ? item.name : org?.team ? org.name : null,\n opensEverySpace:\n (held?.has('communities:read', communityId) ||\n held?.has('communities:moderate', communityId)) ??\n false,\n };\n }),\n };\n }\n\n async communityHome(communityId: string, actor: Actor): Promise<CommunityHome> {\n if (!(await this.access.isCommunityMember(communityId, actor))) {\n throw new NotFoundError('Community', communityId);\n }\n const system = this.system(actor);\n const community = await this.content.findCommunity(communityId, system);\n if (!community) {\n throw new NotFoundError('Community', communityId);\n }\n const [spaces, everySpace] = await Promise.all([\n this.content.listSpaces(communityId, system),\n this.access.opensEverySpace(actor, communityId),\n ]);\n const itemRole = actor.permissions.roleOn(communityId);\n const orgRole = actor.permissions.roleOn();\n const role = itemRole.team || !orgRole.team ? itemRole : orgRole;\n return {\n communityId,\n title: community.title,\n description: community.description,\n thumbnailUrl: showsImages(community.status, communityId, actor)\n ? community.thumbnailUrl\n : null,\n dm: community.settings.dm,\n spaces: await this.homeSpaces(\n actor,\n communityId,\n spaces,\n everySpace,\n showsImages(community.status, communityId, actor),\n ),\n viewer: {\n permissions: PERMISSION_KEYS.filter((p) => actor.permissions.has(p, communityId)),\n opensEverySpace: everySpace,\n team: itemRole.team || orgRole.team,\n roleName: role.name,\n canCreateEvents: actor.permissions.has('events:write'),\n },\n };\n }\n\n async communitySpace(\n communityId: string,\n spaceId: string,\n actor: Actor,\n ): Promise<CommunityHomeSpace> {\n if (!(await this.access.isCommunityMember(communityId, actor))) {\n throw new NotFoundError('Space', spaceId);\n }\n const system = this.system(actor);\n const [space, community] = await Promise.all([\n this.content.getSpace(spaceId, system),\n this.content.findCommunity(communityId, system),\n ]);\n if (!space || space.communityId !== communityId || !community) {\n throw new NotFoundError('Space', spaceId);\n }\n const everySpace = await this.access.opensEverySpace(actor, communityId);\n const [view] = await this.homeSpaces(\n actor,\n communityId,\n [space],\n everySpace,\n showsImages(community.status, communityId, actor),\n );\n if (!view) {\n throw new NotFoundError('Space', spaceId);\n }\n return view;\n }\n\n private async homeSpaces(\n actor: Actor,\n communityId: string,\n spaces: SpaceView[],\n everySpace: boolean,\n images: boolean,\n ): Promise<CommunityHomeSpace[]> {\n const orgId = actor.orgId;\n const excluding = await this.access.everySpaceOpeners(orgId, communityId);\n const [openIds, counts, standings, subscribed] = await Promise.all([\n this.access.openSpaceIds(communityId, actor),\n this.repo.spaceMemberCounts(orgId, communityId, excluding),\n this.access.spaceStandings(communityId, actor),\n this.subscriptions.viewerSubscriptions(\n orgId,\n actor.id,\n 'space',\n spaces.map((space) => space.id),\n new Date(),\n ),\n ]);\n const open = new Set(openIds);\n const seen = spaces.filter((space) =>\n seesSpace(everySpace, space.accessRule, standings[space.id]),\n );\n return Promise.all(\n seen.map(async (space): Promise<CommunityHomeSpace> => {\n const memberCount = counts[space.id] ?? 0;\n const base = {\n id: space.id,\n name: space.name,\n memberCount,\n accessRule: space.accessRule,\n standing: standingOf(space, open.has(space.id), memberCount, standings[space.id]),\n coverUrl: images ? space.coverUrl : null,\n backgroundUrl: images ? space.backgroundUrl : null,\n iconUrl: images ? space.iconUrl : null,\n };\n if (!open.has(space.id)) {\n return { ...base, locked: true };\n }\n return {\n ...base,\n locked: false,\n description: space.description,\n capacity: space.capacity,\n settings: space.settings,\n topics: space.topics.map((topic) => ({ id: topic.id, name: topic.name })),\n links: await this.linkViews(actor, space.links),\n subscription: subscribed[space.id] ?? null,\n };\n }),\n );\n }\n\n private async linkViews(actor: Actor, links: SpaceDetail['links']): Promise<CommunityHomeLink[]> {\n const system = this.system(actor);\n const shown = (type: keyof typeof READ_OF, id: string, status: string) =>\n status === 'published' || actor.permissions.has(READ_OF[type], id);\n const views = await Promise.all(\n links.map(async (link): Promise<CommunityHomeLink | null> => {\n const course = await this.content.findCourse(link.contentId, system);\n if (course) {\n return shown('course', link.contentId, course.status)\n ? { id: link.id, contentId: link.contentId, type: 'course', title: course.title }\n : null;\n }\n const download = await this.content.findDownload(link.contentId, system);\n if (download) {\n return shown('download', link.contentId, download.status)\n ? { id: link.id, contentId: link.contentId, type: 'download', title: download.title }\n : null;\n }\n const community = await this.content.findCommunity(link.contentId, system);\n if (community) {\n return shown('community', link.contentId, community.status)\n ? { id: link.id, contentId: link.contentId, type: 'community', title: community.title }\n : null;\n }\n return null;\n }),\n );\n return views.filter((view): view is CommunityHomeLink => view !== null);\n }\n}\n\nfunction showsImages(status: string, communityId: string, actor: Actor): boolean {\n return status === 'published' || actor.permissions.has('communities:read', communityId);\n}\n\nfunction standingOf(\n space: SpaceDetail,\n open: boolean,\n memberCount: number,\n status: SpaceMember['status'] | undefined,\n): CommunityHomeSpace['standing'] {\n if (space.accessRule === 'all_members' || (open && status !== 'active')) {\n return 'open';\n }\n if (status === 'active') {\n return 'member';\n }\n if (status === 'requested' || status === 'invited') {\n return status;\n }\n if (space.capacity !== null && memberCount >= space.capacity) {\n return 'full';\n }\n return 'none';\n}\n"],"mappings":";;;;;;;;;;AAkEA,SAAS,OAAO,KAAyB;CACvC,OAAO,GAAG,IAAI,KAAK,GAAG,IAAI;AAC5B;AAEA,SAAS,SACP,KACA,QAC6B;CAC7B,MAAM,QAAQ,MAAM,OAAO,OAAO,GAAG,KAAK,KAAA;CAC1C,IAAI,CAAC,OAAO,CAAC,OACX,OAAO;CAET,OAAO,MAAM,YAAY,KAAA,IACrB;EAAE,MAAM,IAAI;EAAM,IAAI,IAAI;EAAI,SAAS,MAAM;CAAQ,IACrD;EAAE,MAAM,IAAI;EAAM,IAAI,IAAI;EAAI,OAAO,MAAM;CAAM;AACvD;AAEA,SAAS,oBAAoB,UAA4B;CACvD,OAAQ,UAA6C,cAAc;AACrE;AAkCA,MAAM,UAAU;CACd,QAAQ;CACR,UAAU;CACV,WAAW;CACX,OAAO;AACT;AAEA,MAAM,cAAc;AA4BpB,IAAa,0BAAb,MAAoE;CAClE;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA,YAAY,QAAmC;EAC7C,KAAK,OAAO,OAAO;EACnB,KAAK,UAAU,OAAO;EACtB,KAAK,WAAW,OAAO;EACvB,KAAK,WAAW,OAAO;EACvB,KAAK,SAAS,OAAO;EACrB,KAAK,SAAS,OAAO;EACrB,KAAK,mBAAmB,OAAO;EAC/B,KAAK,SAAS,OAAO;EACrB,KAAK,gBAAgB,OAAO;EAC5B,KAAK,yBAAyB,OAAO;EACrC,KAAK,uBAAuB,OAAO;EACnC,KAAK,WAAW,OAAO;EACvB,KAAK,mBAAmB,OAAO,iBAAiB,QAAQ,QAAQ,EAAE;EAClE,KAAK,MAAM,OAAO,8BAAc,IAAI,KAAK;CAC3C;CAEA,OAAe,OAAqB;EAClC,OAAO,KAAK,OAAO,YAAY,MAAM,KAAK;CAC5C;CAEA,OAAO,OAA4B;EACjC,MAAM,OAAO,MAAM,YAAY,OAAO;EACtC,OAAO;GACL,WAAW,MAAM;GACjB,SAAS;IAAE,KAAK,KAAK;IAAK,MAAM,KAAK;GAAK;GAC1C,aAAa,gBAAgB,QAAQ,MAAM,MAAM,YAAY,SAAS,CAAC,CAAC;GACxE,gBAAgB,gBAAgB,QAAQ,MAAM,MAAM,YAAY,IAAI,CAAC,CAAC;EACxE;CACF;CAEA,MAAM,8BACJ,aACA,OAC4C;EAC5C,IAAI,CAAE,MAAM,KAAK,OAAO,kBAAkB,aAAa,KAAK,GAC1D,MAAM,IAAI,cAAc,aAAa,WAAW;EAElD,MAAM,QAAQ,MAAM;EACpB,MAAM,SAAS,KAAK,OAAO,KAAK;EAChC,MAAM,CAAC,MAAM,UAAU,UAAU,QAAQ,aAAa,MAAM,QAAQ,IAAI;GACtE,KAAK,uBAAuB,UAAU,IAAI,aAAa,MAAM;GAC7D,KAAK,qBAAqB,IAAI,OAAO,MAAM,QAAQ,EAAE,YAAY,CAAC;GAClE,KAAK,OAAO,aAAa,aAAa,KAAK;GAC3C,KAAK,QAAQ,WAAW,aAAa,MAAM;GAC3C,KAAK,QAAQ,cAAc,aAAa,MAAM;EAChD,CAAC;EACD,MAAM,OAAO,IAAI,IAAI,QAAQ;EAC7B,OAAO;GACL;GACA,QAAQ,WAAW,SAAS,UAAU;GACtC,YAAY,KAAK,aACf,qBAAqB,KAAK,YAAY,MAAM,aAAa,EAAE,YAAY,CAAC,GACxE,UACA,OACG,QAAQ,UAAU,KAAK,IAAI,MAAM,EAAE,CAAC,CAAC,CACrC,KAAK,WAAW;IAAE,SAAS,MAAM;IAAI,MAAM,MAAM;GAAK,EAAE,CAC7D;EACF;CACF;CAEA,MAAM,wBAAwB,OAA0D;EACtF,MAAM,QAAQ,MAAM;EACpB,MAAM,CAAC,MAAM,YAAY,MAAM,QAAQ,IAAI,CACzC,KAAK,uBAAuB,IAAI,IAAI,OAAO,KAAK,OAAO,KAAK,CAAC,GAC7D,KAAK,qBAAqB,IAAI,OAAO,MAAM,QAAQ,KAAK,CAC1D,CAAC;EACD,OAAO,EACL,YAAY,KAAK,aACf,qBAAqB,KAAK,YAAY,MAAM,aAAa,KAAK,GAC9D,UACA,CAAC,CACH,EACF;CACF;CAEA,aACE,YACA,UACA,QACoC;EACpC,MAAM,aAAa,aAAa,KAAK,SAAS,KAAK,CAAC;EACpD,MAAM,cACJ,UACA,YAC0B;GAC1B,MAAM,eAAe,SAAS,QAC3B,YAAY,QAAQ,aAAa,SAAS,OAAO,QAAQ,YAAY,OACxE;GACA,MAAM,WAAW,IAAI,IACnB,gBAAgB,UAAU,cAAc,SAAS,UAAU,CAAC,CAAC,KAAK,aAAa,CAC7E,SAAS,SACT,SAAS,OACX,CAAC,CACH;GACA,OAAO,gBAAgB,UAAU,UAAU,SAAS,UAAU,CAAC,CAAC,KAAK,cAAc;IACjF,GAAG;IACH,SAAS,SAAS,IAAI,SAAS,OAAO,KAAK,SAAS;GACtD,EAAE;EACJ;EACA,OAAO,WAAW,KAAK,aAAa;GAClC,MAAM,WAAW,SAAS,aAAa;GACvC,MAAM,OAAyC;IAC7C,KAAK,SAAS;IACd;IACA,UAAU,WAAW,UAAU,IAAI;GACrC;GACA,IAAI,UACF,KAAK,SAAS,OAAO,KAAK,EAAE,SAAS,YAAY;IAC/C;IACA;IACA,UAAU,WAAW,UAAU,OAAO;GACxC,EAAE;GAEJ,OAAO;EACT,CAAC;CACH;CAEA,MAAM,cACJ,QACA,MACA,OAC4B;EAC5B,MAAM,SAAS,MAAM,KAAK,iBAAiB,KAAK,QAAQ,MAAM,KAAK;EACnE,OAAO;GACL,MAAM,MAAM,KAAK,sBAAsB,OAAO,OAAO,KAAK;GAC1D,OAAO,OAAO;GACd,MAAM,KAAK;GACX,UAAU,KAAK;GACf,aAAa,OAAO;EACtB;CACF;CAEA,MAAM,aAAa,gBAAwB,OAA+C;EACxF,MAAM,QAAQ,MAAM,KAAK,iBAAiB,IAAI,gBAAgB,KAAK;EACnE,MAAM,CAAC,QAAQ,QAAQ,MAAM,KAAK,sBAAsB,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC;EAC3E,IAAI,CAAC,MACH,MAAM,IAAI,cAAc,gBAAgB,cAAc;EAExD,OAAO;CACT;CAEA,MAAM,yBAAyB,OAAuD;EACpF,MAAM,eAAe,MAAM,KAAK,OAAO,iBAAiB,KAAK;EAC7D,OAAO,KAAK,iBAAiB,aAAa,cAAc,KAAK;CAC/D;CAEA,MAAc,sBACZ,OACA,MAC6B;EAC7B,MAAM,QAAQ,MAAM;EACpB,MAAM,OAAO,KAAK,SAAS,QAAQ;GACjC,GAAI,IAAI,UAAU,CAAC,IAAI,OAAO,IAAI,CAAC;GACnC,GAAI,IAAI,SAAS,CAAC,IAAI,MAAM,IAAI,CAAC;GACjC,GAAI,IAAI,cAAc,CAAC;IAAE,MAAM;IAAsB,IAAI,IAAI;GAAY,CAAC,IAAI,CAAC;GAC/E,GAAI,IAAI,UAAU,CAAC;IAAE,MAAM;IAAkB,IAAI,IAAI;GAAQ,CAAC,IAAI,CAAC;EACrE,CAAC;EACD,MAAM,WAAW,CAAC,GAAG,IAAI,IAAI,KAAK,SAAS,QAAQ,IAAI,WAAW,CAAC,CAAC,CAAC,CAAC;EACtE,MAAM,CAAC,QAAQ,kBAAkB,iBAAiB,MAAM,QAAQ,IAAI;GAClE,KAAK,KAAK,OAAO,OAAO,QAAQ;GAChC,KAAK,OAAO,WAAW,UAAU,MAAM,KAAK;GAC5C,KAAK,OAAO,QAAQ,UAAU,MAAM,KAAK;EAC3C,CAAC;EACD,MAAM,SAAS;GAAE,GAAG;GAAe,GAAG;EAAiB;EACvD,MAAM,aAAa,IAAI,IAAI,OAAO,KAAK,WAAW,CAAC,OAAO,WAAW,MAAM,CAAC,CAAC;EAC7E,OAAO,KAAK,KAAK,QAAQ;GACvB,MAAM,SAAS,IAAI,UAAU,WAAW,IAAI,IAAI,OAAO,IAAI,KAAA;GAC3D,MAAM,YAAY,IAAI,cAAc,OAAO,aAAa,IAAI,iBAAiB,KAAA;GAC7E,MAAM,QAAQ,IAAI,UAAU,OAAO,SAAS,IAAI,aAAa,KAAA;GAC7D,OAAO;IACL,IAAI,IAAI;IACR,MAAM,IAAI;IACV,OAAO,IAAI;IACX,SAAS,IAAI;IACb,eAAe,IAAI;IACnB,UAAU,IAAI;IACd,SAAS,IAAI;IACb,WACE,IAAI,eAAe,YAAY;KAAE,IAAI,IAAI;KAAa,OAAO,UAAU;IAAM,IAAI;IACnF,OAAO,IAAI,WAAW,QAAQ;KAAE,IAAI,IAAI;KAAS,MAAM,MAAM;IAAM,IAAI;IACvE,OAAO,SACH;KACE,WAAW,OAAO;KAClB,MAAM,OAAO;KACb,UAAU,OAAO,eACb,KAAK,SAAS,YAAY,OAAO,OAAO,WAAW,OAAO,YAAY,IACtE;IACN,IACA;IACJ,SAAS,SAAS,IAAI,SAAS,MAAM;IACrC,QAAQ,SAAS,IAAI,QAAQ,MAAM;IACnC,WAAW,IAAI;IACf,QAAQ,IAAI;GACd;EACF,CAAC;CACH;CAEA,MAAc,YAAY,UAAkB,OAA0C;EACpF,MAAM,QAAQ,MAAM,YAAY,IAAI,gBAAgB,QAAQ;EAC5D,IAAI,CAAC,SAAS,CAAE,MAAM,KAAK,OAAO,UAAU,MAAM,OAAO,MAAM,IAAI,QAAQ,GACzE,OAAO;EAET,MAAM,SAAS,MAAM,KAAK,QAAQ,WAAW,UAAU,KAAK,OAAO,KAAK,CAAC;EACzE,OAAO,WAAW,SAAS,OAAO,WAAW,eAAe,SAAS;CACvE;CAEA,MAAc,cAAc,YAAoB,OAA4C;EAC1F,MAAM,QAAQ,MAAM,YAAY,IAAI,kBAAkB,UAAU;EAChE,IAAI,CAAC,SAAS,CAAE,MAAM,KAAK,OAAO,UAAU,MAAM,OAAO,MAAM,IAAI,UAAU,GAC3E,OAAO;EAET,MAAM,WAAW,MAAM,KAAK,QAAQ,aAAa,YAAY,KAAK,OAAO,KAAK,CAAC;EAC/E,OAAO,aAAa,SAAS,SAAS,WAAW,eAAe,WAAW;CAC7E;CAEA,MAAM,YAAY,OAAqC;EACrD,MAAM,MAAM,MAAM,KAAK,OAAO,sBAAsB,MAAM,OAAO,MAAM,IAAI,QAAQ;EACnF,MAAM,SAAS,KAAK,OAAO,KAAK;EAEhC,QAAO,MADe,QAAQ,IAAI,IAAI,KAAK,OAAO,KAAK,QAAQ,WAAW,IAAI,MAAM,CAAC,CAAC,EAAA,CACvE,QAAQ,MAAuB,MAAM,QAAQ,EAAE,WAAW,WAAW;CACtF;CAEA,MAAc,cAAc,UAAkB,OAAmC;EAC/E,MAAM,SAAS,MAAM,KAAK,YAAY,UAAU,KAAK;EACrD,IAAI,CAAC,QACH,MAAM,IAAI,cAAc,UAAU,QAAQ;EAE5C,OAAO;CACT;CAEA,UAAU,UAAkB,OAAmC;EAC7D,OAAO,KAAK,cAAc,UAAU,KAAK;CAC3C;CAEA,MAAM,YAAY,UAAkB,OAAiC;EACnE,MAAM,KAAK,cAAc,UAAU,KAAK;EACxC,OAAO,KAAK,QAAQ,kBAAkB,UAAU,KAAK,OAAO,KAAK,CAAC;CACpE;CAEA,MAAM,eAAe,UAAkB,OAAuC;EAC5E,MAAM,KAAK,cAAc,UAAU,KAAK;EAExC,QAAO,MADkB,KAAK,QAAQ,wBAAwB,UAAU,KAAK,OAAO,KAAK,CAAC,EAAA,CACxE,QAAQ,MAAM,oBAAoB,EAAE,QAAQ,CAAC;CACjE;CAEA,MAAM,eAAe,UAAkB,OAA2C;EAChF,MAAM,KAAK,cAAc,UAAU,KAAK;EACxC,MAAM,QAAQ,MAAM;EAEpB,MAAM,OAAM,MADmB,KAAK,QAAQ,qBAAqB,UAAU,KAAK,OAAO,KAAK,CAAC,EAAA,CAChE,QAAQ,MAAM,oBAAoB,EAAE,QAAQ,CAAC,CAAC,CAAC,KAAK,MAAM,EAAE,EAAE;EAC3F,MAAM,UAAU,MAAM,KAAK,SAAS,cAAc,OAAO,MAAM,IAAI,GAAG;EACtE,MAAM,aAA+C,CAAC;EACtD,MAAM,YAA6C,CAAC;EACpD,IAAI,OAAO;EACX,KAAK,MAAM,KAAK,SAAS;GACvB,IAAI,EAAE,eAAe,YACnB;GAEF,WAAW,EAAE,YAAY,EAAE,cAAc,cAAc;GACvD,IAAI,EAAE,aACJ,QAAQ;GAEV,IAAI,EAAE,YAAY,MAChB,UAAU,EAAE,YAAY,EAAE;EAE9B;EACA,MAAM,eAAe,MAAM,KAAK,SAAS,IAAI,OAAO;GAClD,WAAW,MAAM;GACjB,YAAY;GACZ,UAAU;EACZ,CAAC;EACD,OAAO;GACL;GACA,SAAS,IAAI,SAAS,IAAI,KAAK,MAAO,OAAO,IAAI,SAAU,GAAG,IAAI;GAClE,WAAW,cAAc,eAAe;GACxC;EACF;CACF;CAEA,MAAM,cAAc,OAAuC;EACzD,MAAM,MAAM,MAAM,KAAK,OAAO,sBAAsB,MAAM,OAAO,MAAM,IAAI,UAAU;EACrF,MAAM,SAAS,KAAK,OAAO,KAAK;EAEhC,QAAO,MADY,QAAQ,IAAI,IAAI,KAAK,OAAO,KAAK,QAAQ,aAAa,IAAI,MAAM,CAAC,CAAC,EAAA,CACzE,QAAQ,MAAyB,MAAM,QAAQ,EAAE,WAAW,WAAW;CACrF;CAEA,MAAM,YACJ,YACA,OAC6D;EAC7D,MAAM,WAAW,MAAM,KAAK,cAAc,YAAY,KAAK;EAC3D,IAAI,CAAC,UACH,MAAM,IAAI,cAAc,YAAY,UAAU;EAGhD,OAAO;GAAE;GAAU,QAAA,MADE,KAAK,QAAQ,mBAAmB,YAAY,KAAK,OAAO,KAAK,CAAC;EACzD;CAC5B;CAEA,MAAc,UAAU,KAAe,OAAoC;EACzE,IAAI,IAAI,WAAW,GACjB,OAAO,CAAC;EAEV,MAAM,SAAS,KAAK,OAAO,KAAK;EAChC,MAAM,CAAC,QAAQ,YAAY,MAAM,QAAQ,IAAI,CAC3C,KAAK,QAAQ,WAAW,KAAK,MAAM,GACnC,IAAI,WAAW,IACX,KAAK,OACF,UAAU,MAAM,OAAO,MAAM,IAAI,IAAI,EAAG,CAAC,CACzC,MAAM,cAAe,YAAY,CAAC,IAAI,EAAG,IAAI,CAAC,CAAE,IACnD,KAAK,OAAO,sBAAsB,MAAM,OAAO,MAAM,IAAI,OAAO,CACtE,CAAC;EACD,MAAM,YAAY,IAAI,IAAI,QAAQ;EAClC,MAAM,OAAO,IAAI,IAAI,OAAO,KAAK,UAAU,CAAC,MAAM,IAAI,KAAK,CAAC,CAAC;EAC7D,OAAO,IAAI,SAAS,OAAO;GACzB,MAAM,QAAQ,KAAK,IAAI,EAAE;GACzB,IAAI,CAAC,OACH,OAAO,CAAC;GAEV,MAAM,QAAQ,WAAW,OAAO;IAC9B,SAAS,MAAM,YAAY,IAAI,eAAe,EAAE;IAChD,WAAW,UAAU,IAAI,EAAE;GAC7B,CAAC;GACD,OAAO,QAAQ,CAAC;IAAE;IAAO;IAAO,WAAW,UAAU,IAAI,EAAE;GAAE,CAAC,IAAI,CAAC;EACrE,CAAC;CACH;CAEA,MAAc,aAAa,SAAiB,OAAkC;EAC5E,MAAM,CAAC,QAAQ,MAAM,KAAK,UAAU,CAAC,OAAO,GAAG,KAAK;EACpD,IAAI,CAAC,MACH,MAAM,IAAI,cAAc,SAAS,OAAO;EAE1C,OAAO;CACT;CAEA,MAAc,eACZ,MACA,OACA,KAC+B;EAC/B,MAAM,MAAM,KAAK,KAAK,SAAS,KAAK,MAAM,EAAE;EAC5C,MAAM,WAAW,KAAK,QAAQ,SAAS,KAAK,SAAS,CAAC,CAAC,KAAK,SAAS,KAAK,MAAM,EAAE;EAClF,MAAM,CAAC,OAAO,UAAU,MAAM,QAAQ,IAAI,CACxC,KAAK,KAAK,WAAW,MAAM,OAAO,KAAK,WAAW,GAClD,SAAS,SAAS,IACd,KAAK,KAAK,kBAAkB,MAAM,OAAO,MAAM,IAAI,QAAQ,IAC3D,QAAQ,QAAQ,CAAC,CAAC,CACxB,CAAC;EACD,MAAM,SAAS,IAAI,IAAI,MAAM;EAC7B,OAAO,KAAK,KAAK,SACf,KAAK,aACH,MACA,MAAM,KAAK,MAAM,OAAO;GAAE,OAAO;GAAG,MAAM,CAAC;EAAE,GAC7C,OAAO,IAAI,KAAK,MAAM,EAAE,GACxB,OACA,GACF,CACF;CACF;CAEA,aACE,MACA,OACA,UACA,OACA,KACoB;EACpB,MAAM,EAAE,OAAO,OAAO,cAAc;EACpC,MAAM,KAAK,MAAM;EACjB,MAAM,UAAU,UAAU;EAC1B,MAAM,WAAW,MAAM,SAAS;EAChC,MAAM,gBAAgB,MAAM;EAC5B,MAAM,QAAQ,MAAM,OAAO,QAAQ,KAAK,IAAI,QAAQ;EACpD,MAAM,YACJ,UAAU,UAAW,UAAU,cAAc,SAAS,MAAM,SAAS,SACjE,MAAM,YACN;EACN,MAAM,SAAS,MAAM,YAAY,IAAI,gBAAgB,EAAE;EACvD,MAAM,qBAAqB,MAAM,YAAY,IAAI,cAAc,KAAK;EACpE,OAAO;GACL,OAAO,MAAM;GACb;GACA,MAAM,MAAM;GACZ,OAAO,MAAM;GACb,kBAAkB,MAAM;GACxB,cAAc,MAAM;GACpB,QAAQ,MAAM;GACd,UAAU,MAAM;GAChB,QAAQ,MAAM;GACd,UAAU,MAAM;GAChB,UAAU,UAAU,MAAM,WAAW;GACrC;GACA,UAAU;IACR,MAAM,MAAM,SAAS;IACrB;IACA,QAAQ,MAAM,SAAS;GACzB;GACA;GACA,WAAW,MAAM,KAAK,KAAK,QAAQ,aAAa,MAAM,OAAO,KAAK,KAAK,QAAQ,CAAC;GAChF,YAAY,aAAa,OAAO,OAAO,KAAK,IAAI,WAAW,eAAe,CAAC;GAC3E,QAAQ;IACN;IACA,aACE,MAAM,WAAW,eACjB,CAAC,aACD,MAAM,SAAS,SAAS,YACxB,MAAM,SAAS,QAAQ,IAAI,IAAI,QAAQ,MACtC,aAAa,QAAQ,gBAAgB;IACxC;IACA;IACA;IACA,iBAAiB,sBAAsB,MAAM,WAAW,eAAe,CAAC;IACxE,SAAS;IACT,WAAW;IACX,WAAW,MAAM,YAAY,IAAI,iBAAiB,EAAE;GACtD;EACF;CACF;CAEA,MAAM,SAAS,SAAiB,OAAoC;EAClE,MAAM,OAAO,MAAM,KAAK,aAAa,SAAS,KAAK;EACnD,MAAM,MAAM,KAAK,IAAI;EACrB,MAAM,CAAC,CAAC,UAAU,SAAS,MAAM,QAAQ,IAAI,CAC3C,KAAK,eAAe,CAAC,IAAI,GAAG,OAAO,GAAG,GACtC,KAAK,UAAU,WACX,QAAQ,QAAQ,CAAC,CAAgC,IACjD,KAAK,QAAQ,cAAc,CAAC,OAAO,GAAG,KAAK,OAAO,KAAK,CAAC,CAC9D,CAAC;EACD,OAAO;GAAE,GAAG;GAAU,aAAa,KAAK,MAAM;GAAa,OAAO,MAAM,YAAY,CAAC;EAAE;CACzF;CAEA,MAAM,WAAW,OAA0B,OAAiD;EAC1F,MAAM,MAAM,KAAK,IAAI;EACrB,MAAM,QAAQ,MAAM,YAAY,MAAM,aAAa;EACnD,MAAM,OAAO,MAAM,KAAK,KAAK,OAC3B,MAAM,OACN,OACA;GAAE,WAAW,MAAM;GAAI,SAAS,MAAM;GAAK,SAAS,MAAM;EAAQ,GAClE,GACF;EACA,MAAM,OAAO,MAAM,KAAK,eAAe,MAAM,KAAK,UAAU,KAAK,MAAM,KAAK,GAAG,OAAO,GAAG;EACzF,OAAO;GAAE,GAAG;GAAM;EAAK;CACzB;CAEA,MAAM,cAAc,SAAiB,OAA+B;EAClE,MAAM,EAAE,OAAO,UAAU,MAAM,KAAK,aAAa,SAAS,KAAK;EAC/D,OAAO,cAAc,OAAO;GAC1B,SAAS,GAAG,KAAK,iBAAiB,UAAU,mBAAmB,MAAM,EAAE;GACvE,cAAc,UAAU;GACxB,KAAK,KAAK,IAAI;EAChB,CAAC;CACH;CAEA,MAAM,eACJ,SACA,OACA,OAC8B;EAC9B,MAAM,YAAY,QAAQ,eAAe,OAAO;EAChD,IAAI,CAAE,MAAM,KAAK,QAAQ,UAAU,SAAS,KAAK,OAAO,KAAK,CAAC,GAC5D,MAAM,IAAI,cAAc,SAAS,OAAO;EAE1C,MAAM,OAAO,MAAM,KAAK,KAAK,eAAe,MAAM,OAAO,SAAS,KAAK;EACvE,OAAO;GACL,GAAG;GACH,MAAM,KAAK,KAAK,KAAK,QAAQ,aAAa,MAAM,OAAO,KAAK,KAAK,QAAQ,CAAC;EAC5E;CACF;CAEA,MAAM,UACJ,aACA,OACA,OAC+B;EAC/B,IAAI,CAAE,MAAM,KAAK,OAAO,kBAAkB,aAAa,KAAK,GAC1D,MAAM,IAAI,cAAc,aAAa,WAAW;EAElD,MAAM,QAAQ,MAAM;EAEpB,IAAI,CAAC,MADmB,KAAK,QAAQ,cAAc,aAAa,KAAK,OAAO,KAAK,CAAC,GAEhF,MAAM,IAAI,cAAc,aAAa,WAAW;EAElD,MAAM,UAAU,MAAM,KAAK,OAAO,QAAQ,OAAO,oBAAoB,WAAW;EAChF,MAAM,OAAO,MAAM,KAAK,KAAK,UAAU,OAAO,aAAa,OAAO,OAAO;EACzE,MAAM,cAAc,MAAM,KAAK,OAAO,kBACpC,OACA,KAAK,KAAK,KAAK,QAAQ,IAAI,SAAS,CACtC;EACA,OAAO;GACL,GAAG;GACH,MAAM,KAAK,KAAK,KAAK,QAAQ;IAC3B,MAAM,OAAO,YAAY,IAAI,IAAI,SAAS;IAC1C,MAAM,OAAO,MAAM,OAAO,WAAW;IACrC,MAAM,MAAM,MAAM,OAAO;IACzB,OAAO;KACL,GAAG;KACH,UAAU,IAAI,eACV,KAAK,SAAS,YAAY,OAAO,IAAI,WAAW,IAAI,YAAY,IAChE;KACJ,OAAO,MAAM,QAAQ,WAAW,KAAK,QAAQ;KAC7C,UAAU,MAAM,OAAO,KAAK,OAAO,KAAK,OAAO,IAAI,OAAO;KAC1D,kBACG,MAAM,IAAI,oBAAoB,WAAW,KACxC,MAAM,IAAI,wBAAwB,WAAW,MAC/C;IACJ;GACF,CAAC;EACH;CACF;CAEA,MAAM,cAAc,aAAqB,OAAsC;EAC7E,IAAI,CAAE,MAAM,KAAK,OAAO,kBAAkB,aAAa,KAAK,GAC1D,MAAM,IAAI,cAAc,aAAa,WAAW;EAElD,MAAM,SAAS,KAAK,OAAO,KAAK;EAChC,MAAM,YAAY,MAAM,KAAK,QAAQ,cAAc,aAAa,MAAM;EACtE,IAAI,CAAC,WACH,MAAM,IAAI,cAAc,aAAa,WAAW;EAElD,MAAM,CAAC,QAAQ,cAAc,MAAM,QAAQ,IAAI,CAC7C,KAAK,QAAQ,WAAW,aAAa,MAAM,GAC3C,KAAK,OAAO,gBAAgB,OAAO,WAAW,CAChD,CAAC;EACD,MAAM,WAAW,MAAM,YAAY,OAAO,WAAW;EACrD,MAAM,UAAU,MAAM,YAAY,OAAO;EACzC,MAAM,OAAO,SAAS,QAAQ,CAAC,QAAQ,OAAO,WAAW;EACzD,OAAO;GACL;GACA,OAAO,UAAU;GACjB,aAAa,UAAU;GACvB,cAAc,YAAY,UAAU,QAAQ,aAAa,KAAK,IAC1D,UAAU,eACV;GACJ,IAAI,UAAU,SAAS;GACvB,QAAQ,MAAM,KAAK,WACjB,OACA,aACA,QACA,YACA,YAAY,UAAU,QAAQ,aAAa,KAAK,CAClD;GACA,QAAQ;IACN,aAAa,gBAAgB,QAAQ,MAAM,MAAM,YAAY,IAAI,GAAG,WAAW,CAAC;IAChF,iBAAiB;IACjB,MAAM,SAAS,QAAQ,QAAQ;IAC/B,UAAU,KAAK;IACf,iBAAiB,MAAM,YAAY,IAAI,cAAc;GACvD;EACF;CACF;CAEA,MAAM,eACJ,aACA,SACA,OAC6B;EAC7B,IAAI,CAAE,MAAM,KAAK,OAAO,kBAAkB,aAAa,KAAK,GAC1D,MAAM,IAAI,cAAc,SAAS,OAAO;EAE1C,MAAM,SAAS,KAAK,OAAO,KAAK;EAChC,MAAM,CAAC,OAAO,aAAa,MAAM,QAAQ,IAAI,CAC3C,KAAK,QAAQ,SAAS,SAAS,MAAM,GACrC,KAAK,QAAQ,cAAc,aAAa,MAAM,CAChD,CAAC;EACD,IAAI,CAAC,SAAS,MAAM,gBAAgB,eAAe,CAAC,WAClD,MAAM,IAAI,cAAc,SAAS,OAAO;EAE1C,MAAM,aAAa,MAAM,KAAK,OAAO,gBAAgB,OAAO,WAAW;EACvE,MAAM,CAAC,QAAQ,MAAM,KAAK,WACxB,OACA,aACA,CAAC,KAAK,GACN,YACA,YAAY,UAAU,QAAQ,aAAa,KAAK,CAClD;EACA,IAAI,CAAC,MACH,MAAM,IAAI,cAAc,SAAS,OAAO;EAE1C,OAAO;CACT;CAEA,MAAc,WACZ,OACA,aACA,QACA,YACA,QAC+B;EAC/B,MAAM,QAAQ,MAAM;EACpB,MAAM,YAAY,MAAM,KAAK,OAAO,kBAAkB,OAAO,WAAW;EACxE,MAAM,CAAC,SAAS,QAAQ,WAAW,cAAc,MAAM,QAAQ,IAAI;GACjE,KAAK,OAAO,aAAa,aAAa,KAAK;GAC3C,KAAK,KAAK,kBAAkB,OAAO,aAAa,SAAS;GACzD,KAAK,OAAO,eAAe,aAAa,KAAK;GAC7C,KAAK,cAAc,oBACjB,OACA,MAAM,IACN,SACA,OAAO,KAAK,UAAU,MAAM,EAAE,mBAC9B,IAAI,KAAK,CACX;EACF,CAAC;EACD,MAAM,OAAO,IAAI,IAAI,OAAO;EAC5B,MAAM,OAAO,OAAO,QAAQ,UAC1B,UAAU,YAAY,MAAM,YAAY,UAAU,MAAM,GAAG,CAC7D;EACA,OAAO,QAAQ,IACb,KAAK,IAAI,OAAO,UAAuC;GACrD,MAAM,cAAc,OAAO,MAAM,OAAO;GACxC,MAAM,OAAO;IACX,IAAI,MAAM;IACV,MAAM,MAAM;IACZ;IACA,YAAY,MAAM;IAClB,UAAU,WAAW,OAAO,KAAK,IAAI,MAAM,EAAE,GAAG,aAAa,UAAU,MAAM,GAAG;IAChF,UAAU,SAAS,MAAM,WAAW;IACpC,eAAe,SAAS,MAAM,gBAAgB;IAC9C,SAAS,SAAS,MAAM,UAAU;GACpC;GACA,IAAI,CAAC,KAAK,IAAI,MAAM,EAAE,GACpB,OAAO;IAAE,GAAG;IAAM,QAAQ;GAAK;GAEjC,OAAO;IACL,GAAG;IACH,QAAQ;IACR,aAAa,MAAM;IACnB,UAAU,MAAM;IAChB,UAAU,MAAM;IAChB,QAAQ,MAAM,OAAO,KAAK,WAAW;KAAE,IAAI,MAAM;KAAI,MAAM,MAAM;IAAK,EAAE;IACxE,OAAO,MAAM,KAAK,UAAU,OAAO,MAAM,KAAK;IAC9C,cAAc,WAAW,MAAM,OAAO;GACxC;EACF,CAAC,CACH;CACF;CAEA,MAAc,UAAU,OAAc,OAA2D;EAC/F,MAAM,SAAS,KAAK,OAAO,KAAK;EAChC,MAAM,SAAS,MAA4B,IAAY,WACrD,WAAW,eAAe,MAAM,YAAY,IAAI,QAAQ,OAAO,EAAE;EAwBnE,QAAO,MAvBa,QAAQ,IAC1B,MAAM,IAAI,OAAO,SAA4C;GAC3D,MAAM,SAAS,MAAM,KAAK,QAAQ,WAAW,KAAK,WAAW,MAAM;GACnE,IAAI,QACF,OAAO,MAAM,UAAU,KAAK,WAAW,OAAO,MAAM,IAChD;IAAE,IAAI,KAAK;IAAI,WAAW,KAAK;IAAW,MAAM;IAAU,OAAO,OAAO;GAAM,IAC9E;GAEN,MAAM,WAAW,MAAM,KAAK,QAAQ,aAAa,KAAK,WAAW,MAAM;GACvE,IAAI,UACF,OAAO,MAAM,YAAY,KAAK,WAAW,SAAS,MAAM,IACpD;IAAE,IAAI,KAAK;IAAI,WAAW,KAAK;IAAW,MAAM;IAAY,OAAO,SAAS;GAAM,IAClF;GAEN,MAAM,YAAY,MAAM,KAAK,QAAQ,cAAc,KAAK,WAAW,MAAM;GACzE,IAAI,WACF,OAAO,MAAM,aAAa,KAAK,WAAW,UAAU,MAAM,IACtD;IAAE,IAAI,KAAK;IAAI,WAAW,KAAK;IAAW,MAAM;IAAa,OAAO,UAAU;GAAM,IACpF;GAEN,OAAO;EACT,CAAC,CACH,EAAA,CACa,QAAQ,SAAoC,SAAS,IAAI;CACxE;AACF;AAEA,SAAS,YAAY,QAAgB,aAAqB,OAAuB;CAC/E,OAAO,WAAW,eAAe,MAAM,YAAY,IAAI,oBAAoB,WAAW;AACxF;AAEA,SAAS,WACP,OACA,MACA,aACA,QACgC;CAChC,IAAI,MAAM,eAAe,iBAAkB,QAAQ,WAAW,UAC5D,OAAO;CAET,IAAI,WAAW,UACb,OAAO;CAET,IAAI,WAAW,eAAe,WAAW,WACvC,OAAO;CAET,IAAI,MAAM,aAAa,QAAQ,eAAe,MAAM,UAClD,OAAO;CAET,OAAO;AACT"}
@@ -34,7 +34,9 @@ interface StoredObjectInfo {
34
34
  }
35
35
  interface PresignDownloadInput {
36
36
  key: string;
37
- expiresInSeconds?: number;
37
+ /** Links signed with the same key, times and filename are identical. */
38
+ signedAt: Date;
39
+ expiresAt: Date;
38
40
  /** Force a download with this filename (Content-Disposition: attachment). */
39
41
  downloadFilename?: string;
40
42
  }
@@ -46,7 +48,7 @@ interface ObjectStorage {
46
48
  contentType?: string;
47
49
  expiresInSeconds?: number;
48
50
  }): Promise<PresignedUpload>;
49
- /** Presigned URL to fetch/serve a private object (temporary, expiring). */
51
+ /** Signed URL to a private object, served with PUBLIC_CACHE_CONTROL. */
50
52
  presignDownload(input: PresignDownloadInput): Promise<string>;
51
53
  /** Object metadata, or null if it does not exist. */
52
54
  stat(key: string): Promise<StoredObjectInfo | null>;
@@ -1 +1 @@
1
- {"version":3,"file":"ports.js","names":[],"sources":["../../src/types/ports.ts"],"sourcesContent":["// Deployment-swappable ports, implemented by @headless-lms/adapter-* packages.\n\nexport interface Logger {\n debug(msg: string, meta?: Record<string, unknown>): void;\n info(msg: string, meta?: Record<string, unknown>): void;\n warn(msg: string, meta?: Record<string, unknown>): void;\n error(msg: string, meta?: Record<string, unknown>): void;\n /** A logger whose every entry carries `bindings` (call-site meta wins on key clash). */\n child(bindings: Record<string, unknown>): Logger;\n}\n\nexport interface EmailMessage {\n to: string;\n subject: string;\n text: string;\n /** Optional rendered HTML body; transports fall back to text when absent. */\n html?: string;\n}\n\nexport interface EmailSender {\n send(message: EmailMessage): Promise<void>;\n}\n\n// --- Object storage (S3-compatible, opaque keys) ----------------------------\n\n/** A presigned URL the browser uses to PUT a file straight to the store. */\nexport interface PresignedUpload {\n url: string;\n method: 'PUT';\n key: string;\n expiresInSeconds: number;\n /** Headers the client must send on the PUT (e.g. Content-Type). */\n headers: Record<string, string>;\n}\n\nexport interface StoredObjectInfo {\n key: string;\n size: number;\n contentType?: string;\n lastModified?: string;\n}\n\nexport interface PresignDownloadInput {\n key: string;\n expiresInSeconds?: number;\n /** Force a download with this filename (Content-Disposition: attachment). */\n downloadFilename?: string;\n}\n\nexport interface ObjectStorage {\n readonly uploadExpirySeconds: number;\n /** Presigned URL for a direct browser upload (temporary, expiring). */\n presignUpload(input: {\n key: string;\n contentType?: string;\n expiresInSeconds?: number;\n }): Promise<PresignedUpload>;\n /** Presigned URL to fetch/serve a private object (temporary, expiring). */\n presignDownload(input: PresignDownloadInput): Promise<string>;\n /** Object metadata, or null if it does not exist. */\n stat(key: string): Promise<StoredObjectInfo | null>;\n list(prefix: string): Promise<StoredObjectInfo[]>;\n copy(input: { from: string; to: string }): Promise<void>;\n publish(input: { from: string; to: string }): Promise<void>;\n publicUrl(key: string): string;\n /** Permanently remove an object. */\n remove(key: string): Promise<void>;\n}\n\nexport const PUBLIC_PREFIX = 'public/';\nexport const PUBLIC_CACHE_CONTROL = 'public, max-age=31536000, immutable';\n"],"mappings":";AAqEA,MAAa,gBAAgB;AAC7B,MAAa,uBAAuB"}
1
+ {"version":3,"file":"ports.js","names":[],"sources":["../../src/types/ports.ts"],"sourcesContent":["// Deployment-swappable ports, implemented by @headless-lms/adapter-* packages.\n\nexport interface Logger {\n debug(msg: string, meta?: Record<string, unknown>): void;\n info(msg: string, meta?: Record<string, unknown>): void;\n warn(msg: string, meta?: Record<string, unknown>): void;\n error(msg: string, meta?: Record<string, unknown>): void;\n /** A logger whose every entry carries `bindings` (call-site meta wins on key clash). */\n child(bindings: Record<string, unknown>): Logger;\n}\n\nexport interface EmailMessage {\n to: string;\n subject: string;\n text: string;\n /** Optional rendered HTML body; transports fall back to text when absent. */\n html?: string;\n}\n\nexport interface EmailSender {\n send(message: EmailMessage): Promise<void>;\n}\n\n// --- Object storage (S3-compatible, opaque keys) ----------------------------\n\n/** A presigned URL the browser uses to PUT a file straight to the store. */\nexport interface PresignedUpload {\n url: string;\n method: 'PUT';\n key: string;\n expiresInSeconds: number;\n /** Headers the client must send on the PUT (e.g. Content-Type). */\n headers: Record<string, string>;\n}\n\nexport interface StoredObjectInfo {\n key: string;\n size: number;\n contentType?: string;\n lastModified?: string;\n}\n\nexport interface PresignDownloadInput {\n key: string;\n /** Links signed with the same key, times and filename are identical. */\n signedAt: Date;\n expiresAt: Date;\n /** Force a download with this filename (Content-Disposition: attachment). */\n downloadFilename?: string;\n}\n\nexport interface ObjectStorage {\n readonly uploadExpirySeconds: number;\n /** Presigned URL for a direct browser upload (temporary, expiring). */\n presignUpload(input: {\n key: string;\n contentType?: string;\n expiresInSeconds?: number;\n }): Promise<PresignedUpload>;\n /** Signed URL to a private object, served with PUBLIC_CACHE_CONTROL. */\n presignDownload(input: PresignDownloadInput): Promise<string>;\n /** Object metadata, or null if it does not exist. */\n stat(key: string): Promise<StoredObjectInfo | null>;\n list(prefix: string): Promise<StoredObjectInfo[]>;\n copy(input: { from: string; to: string }): Promise<void>;\n publish(input: { from: string; to: string }): Promise<void>;\n publicUrl(key: string): string;\n /** Permanently remove an object. */\n remove(key: string): Promise<void>;\n}\n\nexport const PUBLIC_PREFIX = 'public/';\nexport const PUBLIC_CACHE_CONTROL = 'public, max-age=31536000, immutable';\n"],"mappings":";AAuEA,MAAa,gBAAgB;AAC7B,MAAa,uBAAuB"}