@headless-lms/core 0.6.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 (44) 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/automations/index.d.ts +4 -4
  10. package/dist/automations/notifications/delivery.d.ts +3 -3
  11. package/dist/automations/notifications/delivery.js +1 -1
  12. package/dist/automations/notifications/delivery.js.map +1 -1
  13. package/dist/automations/ports.d.ts +2 -2
  14. package/dist/automations/service.d.ts +1 -8
  15. package/dist/automations/service.js +0 -8
  16. package/dist/automations/service.js.map +1 -1
  17. package/dist/content/service.d.ts +4 -1
  18. package/dist/content/service.js +52 -29
  19. package/dist/content/service.js.map +1 -1
  20. package/dist/discussion/chats.d.ts +1 -2
  21. package/dist/discussion/chats.js +8 -17
  22. package/dist/discussion/chats.js.map +1 -1
  23. package/dist/discussion/ports.d.ts +0 -6
  24. package/dist/discussion/ports.js.map +1 -1
  25. package/dist/discussion/posts.d.ts +1 -4
  26. package/dist/discussion/posts.js +8 -21
  27. package/dist/discussion/posts.js.map +1 -1
  28. package/dist/entitlements/events.d.ts +33 -33
  29. package/dist/identity/events.d.ts +2 -2
  30. package/dist/integrations/events.d.ts +3 -3
  31. package/dist/organizations/events.d.ts +2 -2
  32. package/dist/reporting/member/ports.d.ts +1 -10
  33. package/dist/reporting/member/service.d.ts +2 -11
  34. package/dist/reporting/member/service.js +12 -62
  35. package/dist/reporting/member/service.js.map +1 -1
  36. package/dist/types/automations.d.ts +5 -2
  37. package/dist/types/index.d.ts +2 -2
  38. package/dist/types/ports.d.ts +4 -2
  39. package/dist/types/ports.js.map +1 -1
  40. package/dist/types/schemas/content.d.ts +2 -2
  41. package/dist/types/schemas/discussion.d.ts +14 -14
  42. package/dist/types/schemas/entitlements.d.ts +8 -8
  43. package/dist/types/schemas/notifications.d.ts +3 -3
  44. package/package.json +1 -1
@@ -1,27 +1,13 @@
1
- import { SpaceImage } from "../types/schemas/content.js";
2
- import "../types/index.js";
3
1
  //#region src/assets/file-urls.d.ts
4
- type ThumbnailOwner = 'course' | 'download' | 'community' | 'event';
5
- /** URLs for every file a response refers to. A public image (a profile photo, or the
6
- * thumbnail or cover of a published item) is a public storage URL; every other file is a
7
- * stable, access-checked URL on the API that redirects to a signed storage URL. */
2
+ /** Public storage URLs for public images: a profile photo, or the thumbnail or cover of a
3
+ * published item. Every other file a response refers to carries a signed link from
4
+ * `AssetFiles.fileLinks`. */
8
5
  interface FileUrls {
9
6
  publicImage(orgId: string, ownerId: string, assetId: string): string;
10
- thumbnail(owner: ThumbnailOwner, ownerId: string): string;
11
- spaceImage(spaceId: string, image: SpaceImage): string;
12
- activityAsset(activityId: string, assetId: string): string;
13
- downloadFile(downloadId: string, assetId: string): string;
14
- eventFile(eventId: string, assetId: string): string;
15
- postCover(postId: string): string;
16
- postAsset(postId: string, assetId: string): string;
17
- messageAttachment(chatId: string, messageId: string, assetId: string): string;
18
- memberUpload(assetId: string): string;
19
- libraryFile(assetId: string): string;
20
- libraryDownload(assetId: string): string;
21
7
  }
22
8
  declare function publicImagePrefix(orgId: string, ownerId: string): string;
23
9
  declare function publicImageKey(orgId: string, ownerId: string, assetId: string): string;
24
- declare function createFileUrls(apiUrl: string, publicUrl: (key: string) => string): FileUrls;
10
+ declare function createFileUrls(publicUrl: (key: string) => string): FileUrls;
25
11
  //#endregion
26
- export { FileUrls, ThumbnailOwner, createFileUrls, publicImageKey, publicImagePrefix };
12
+ export { FileUrls, createFileUrls, publicImageKey, publicImagePrefix };
27
13
  //# sourceMappingURL=file-urls.d.ts.map
@@ -1,34 +1,13 @@
1
1
  import { PUBLIC_PREFIX } from "../types/ports.js";
2
2
  //#region src/assets/file-urls.ts
3
- const THUMBNAIL_PATHS = {
4
- course: "courses",
5
- download: "downloads",
6
- community: "communities",
7
- event: "events"
8
- };
9
3
  function publicImagePrefix(orgId, ownerId) {
10
4
  return `${PUBLIC_PREFIX}${orgId}/${ownerId}/`;
11
5
  }
12
6
  function publicImageKey(orgId, ownerId, assetId) {
13
7
  return `${publicImagePrefix(orgId, ownerId)}${assetId}`;
14
8
  }
15
- function createFileUrls(apiUrl, publicUrl) {
16
- const base = apiUrl.replace(/\/+$/, "");
17
- const at = (...segments) => `${base}/api/${segments.map((s) => encodeURIComponent(s)).join("/")}`;
18
- return {
19
- publicImage: (orgId, ownerId, assetId) => publicUrl(publicImageKey(orgId, ownerId, assetId)),
20
- thumbnail: (owner, ownerId) => at("member", THUMBNAIL_PATHS[owner], ownerId, "thumbnail"),
21
- spaceImage: (spaceId, image) => at("member", "spaces", spaceId, "images", image),
22
- activityAsset: (activityId, assetId) => at("member", "activities", activityId, "assets", assetId),
23
- downloadFile: (downloadId, assetId) => at("member", "downloads", downloadId, "assets", assetId),
24
- eventFile: (eventId, assetId) => at("member", "events", eventId, "assets", assetId),
25
- postCover: (postId) => at("member", "posts", postId, "cover"),
26
- postAsset: (postId, assetId) => at("member", "posts", postId, "assets", assetId),
27
- messageAttachment: (chatId, messageId, assetId) => at("member", "chats", chatId, "messages", messageId, "attachments", assetId),
28
- memberUpload: (assetId) => at("member", "uploads", assetId, "file"),
29
- libraryFile: (assetId) => at("assets", assetId, "file"),
30
- libraryDownload: (assetId) => at("assets", assetId, "download")
31
- };
9
+ function createFileUrls(publicUrl) {
10
+ return { publicImage: (orgId, ownerId, assetId) => publicUrl(publicImageKey(orgId, ownerId, assetId)) };
32
11
  }
33
12
  //#endregion
34
13
  export { createFileUrls, publicImageKey, publicImagePrefix };
@@ -1 +1 @@
1
- {"version":3,"file":"file-urls.js","names":[],"sources":["../../src/assets/file-urls.ts"],"sourcesContent":["import { PUBLIC_PREFIX, type SpaceImage } from '../types/index.js';\n\nexport type ThumbnailOwner = 'course' | 'download' | 'community' | 'event';\n\n/** URLs for every file a response refers to. A public image (a profile photo, or the\n * thumbnail or cover of a published item) is a public storage URL; every other file is a\n * stable, access-checked URL on the API that redirects to a signed storage URL. */\nexport interface FileUrls {\n publicImage(orgId: string, ownerId: string, assetId: string): string;\n thumbnail(owner: ThumbnailOwner, ownerId: string): string;\n spaceImage(spaceId: string, image: SpaceImage): string;\n activityAsset(activityId: string, assetId: string): string;\n downloadFile(downloadId: string, assetId: string): string;\n eventFile(eventId: string, assetId: string): string;\n postCover(postId: string): string;\n postAsset(postId: string, assetId: string): string;\n messageAttachment(chatId: string, messageId: string, assetId: string): string;\n memberUpload(assetId: string): string;\n libraryFile(assetId: string): string;\n libraryDownload(assetId: string): string;\n}\n\nconst THUMBNAIL_PATHS: Record<ThumbnailOwner, string> = {\n course: 'courses',\n download: 'downloads',\n community: 'communities',\n event: 'events',\n};\n\nexport function publicImagePrefix(orgId: string, ownerId: string): string {\n return `${PUBLIC_PREFIX}${orgId}/${ownerId}/`;\n}\n\nexport function publicImageKey(orgId: string, ownerId: string, assetId: string): string {\n return `${publicImagePrefix(orgId, ownerId)}${assetId}`;\n}\n\nexport function createFileUrls(apiUrl: string, publicUrl: (key: string) => string): FileUrls {\n const base = apiUrl.replace(/\\/+$/, '');\n const at = (...segments: string[]) =>\n `${base}/api/${segments.map((s) => encodeURIComponent(s)).join('/')}`;\n return {\n publicImage: (orgId, ownerId, assetId) => publicUrl(publicImageKey(orgId, ownerId, assetId)),\n thumbnail: (owner, ownerId) => at('member', THUMBNAIL_PATHS[owner], ownerId, 'thumbnail'),\n spaceImage: (spaceId, image) => at('member', 'spaces', spaceId, 'images', image),\n activityAsset: (activityId, assetId) =>\n at('member', 'activities', activityId, 'assets', assetId),\n downloadFile: (downloadId, assetId) => at('member', 'downloads', downloadId, 'assets', assetId),\n eventFile: (eventId, assetId) => at('member', 'events', eventId, 'assets', assetId),\n postCover: (postId) => at('member', 'posts', postId, 'cover'),\n postAsset: (postId, assetId) => at('member', 'posts', postId, 'assets', assetId),\n messageAttachment: (chatId, messageId, assetId) =>\n at('member', 'chats', chatId, 'messages', messageId, 'attachments', assetId),\n memberUpload: (assetId) => at('member', 'uploads', assetId, 'file'),\n libraryFile: (assetId) => at('assets', assetId, 'file'),\n libraryDownload: (assetId) => at('assets', assetId, 'download'),\n };\n}\n"],"mappings":";;AAsBA,MAAM,kBAAkD;CACtD,QAAQ;CACR,UAAU;CACV,WAAW;CACX,OAAO;AACT;AAEA,SAAgB,kBAAkB,OAAe,SAAyB;CACxE,OAAO,GAAG,gBAAgB,MAAM,GAAG,QAAQ;AAC7C;AAEA,SAAgB,eAAe,OAAe,SAAiB,SAAyB;CACtF,OAAO,GAAG,kBAAkB,OAAO,OAAO,IAAI;AAChD;AAEA,SAAgB,eAAe,QAAgB,WAA8C;CAC3F,MAAM,OAAO,OAAO,QAAQ,QAAQ,EAAE;CACtC,MAAM,MAAM,GAAG,aACb,GAAG,KAAK,OAAO,SAAS,KAAK,MAAM,mBAAmB,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG;CACpE,OAAO;EACL,cAAc,OAAO,SAAS,YAAY,UAAU,eAAe,OAAO,SAAS,OAAO,CAAC;EAC3F,YAAY,OAAO,YAAY,GAAG,UAAU,gBAAgB,QAAQ,SAAS,WAAW;EACxF,aAAa,SAAS,UAAU,GAAG,UAAU,UAAU,SAAS,UAAU,KAAK;EAC/E,gBAAgB,YAAY,YAC1B,GAAG,UAAU,cAAc,YAAY,UAAU,OAAO;EAC1D,eAAe,YAAY,YAAY,GAAG,UAAU,aAAa,YAAY,UAAU,OAAO;EAC9F,YAAY,SAAS,YAAY,GAAG,UAAU,UAAU,SAAS,UAAU,OAAO;EAClF,YAAY,WAAW,GAAG,UAAU,SAAS,QAAQ,OAAO;EAC5D,YAAY,QAAQ,YAAY,GAAG,UAAU,SAAS,QAAQ,UAAU,OAAO;EAC/E,oBAAoB,QAAQ,WAAW,YACrC,GAAG,UAAU,SAAS,QAAQ,YAAY,WAAW,eAAe,OAAO;EAC7E,eAAe,YAAY,GAAG,UAAU,WAAW,SAAS,MAAM;EAClE,cAAc,YAAY,GAAG,UAAU,SAAS,MAAM;EACtD,kBAAkB,YAAY,GAAG,UAAU,SAAS,UAAU;CAChE;AACF"}
1
+ {"version":3,"file":"file-urls.js","names":[],"sources":["../../src/assets/file-urls.ts"],"sourcesContent":["import { PUBLIC_PREFIX } from '../types/index.js';\n\n/** Public storage URLs for public images: a profile photo, or the thumbnail or cover of a\n * published item. Every other file a response refers to carries a signed link from\n * `AssetFiles.fileLinks`. */\nexport interface FileUrls {\n publicImage(orgId: string, ownerId: string, assetId: string): string;\n}\n\nexport function publicImagePrefix(orgId: string, ownerId: string): string {\n return `${PUBLIC_PREFIX}${orgId}/${ownerId}/`;\n}\n\nexport function publicImageKey(orgId: string, ownerId: string, assetId: string): string {\n return `${publicImagePrefix(orgId, ownerId)}${assetId}`;\n}\n\nexport function createFileUrls(publicUrl: (key: string) => string): FileUrls {\n return {\n publicImage: (orgId, ownerId, assetId) => publicUrl(publicImageKey(orgId, ownerId, assetId)),\n };\n}\n"],"mappings":";;AASA,SAAgB,kBAAkB,OAAe,SAAyB;CACxE,OAAO,GAAG,gBAAgB,MAAM,GAAG,QAAQ;AAC7C;AAEA,SAAgB,eAAe,OAAe,SAAiB,SAAyB;CACtF,OAAO,GAAG,kBAAkB,OAAO,OAAO,IAAI;AAChD;AAEA,SAAgB,eAAe,WAA8C;CAC3E,OAAO,EACL,cAAc,OAAO,SAAS,YAAY,UAAU,eAAe,OAAO,SAAS,OAAO,CAAC,EAC7F;AACF"}
@@ -1,9 +1,9 @@
1
1
  import { Page } from "../types/shared.js";
2
2
  import { Asset, AssetKind, AssetSource, AssetStatus, AssetUrls, AssetView, AssetsQuery, RequestUploadInput, UploadTicket } from "../types/schemas/assets.js";
3
3
  import { AssetCreated, AssetDeleted, AssetEvent, AssetReady, assetEvents } from "./events.js";
4
- import { FileUrls, ThumbnailOwner, createFileUrls, publicImageKey, publicImagePrefix } from "./file-urls.js";
4
+ import { FileUrls, createFileUrls, publicImageKey, publicImagePrefix } from "./file-urls.js";
5
5
  import "./model.js";
6
- import { AssetFiles, AssetsRepository, AssetsService, LibraryAssetReader, MemberImageReader, MemberUpload, MemberUploadInput, MemberUploadReader, OrgDirectory, ReadyFile } from "./ports.js";
6
+ import { AssetFiles, AssetsRepository, AssetsService, FileLinkRequest, LibraryAssetReader, MemberImageReader, MemberUpload, MemberUploadInput, MemberUploadReader, OrgDirectory, ReadyFile } from "./ports.js";
7
7
  import { AssetsServiceImpl, MEMBER_IMAGE_TYPE, isMemberImageType, kindOf, memberKindOf } from "./service.js";
8
8
  import { UPLOADS_REMOVE_UNATTACHED_JOB, assetsJobs, assetsRecurringJobs } from "./jobs.js";
9
- export { type Asset, type AssetCreated, type AssetDeleted, type AssetEvent, type AssetFiles, type AssetKind, type AssetReady, type AssetSource, type AssetStatus, type AssetUrls, type AssetView, type AssetsQuery, type AssetsRepository, type AssetsService, AssetsServiceImpl, type FileUrls, type LibraryAssetReader, MEMBER_IMAGE_TYPE, type MemberImageReader, type MemberUpload, type MemberUploadInput, type MemberUploadReader, type OrgDirectory, type Page, type ReadyFile, type RequestUploadInput, type ThumbnailOwner, UPLOADS_REMOVE_UNATTACHED_JOB, type UploadTicket, assetEvents, assetsJobs, assetsRecurringJobs, createFileUrls, isMemberImageType, kindOf, memberKindOf, publicImageKey, publicImagePrefix };
9
+ export { type Asset, type AssetCreated, type AssetDeleted, type AssetEvent, type AssetFiles, type AssetKind, type AssetReady, type AssetSource, type AssetStatus, type AssetUrls, type AssetView, type AssetsQuery, type AssetsRepository, type AssetsService, AssetsServiceImpl, type FileLinkRequest, type FileUrls, type LibraryAssetReader, MEMBER_IMAGE_TYPE, type MemberImageReader, type MemberUpload, type MemberUploadInput, type MemberUploadReader, type OrgDirectory, type Page, type ReadyFile, type RequestUploadInput, UPLOADS_REMOVE_UNATTACHED_JOB, type UploadTicket, assetEvents, assetsJobs, assetsRecurringJobs, createFileUrls, isMemberImageType, kindOf, memberKindOf, publicImageKey, publicImagePrefix };
@@ -31,14 +31,17 @@ interface ReadyFile {
31
31
  contentType: string;
32
32
  size: number;
33
33
  }
34
+ interface FileLinkRequest {
35
+ assetId: string;
36
+ /** Download the file under this name instead of showing it. */
37
+ saveAs?: string;
38
+ }
34
39
  interface AssetFiles {
35
40
  /** Ready files of the org among these ids, with what a download shows of them. */
36
41
  readyFiles(orgId: string, assetIds: readonly string[]): Promise<Record<string, ReadyFile>>;
37
- /** A signed storage URL for a ready file of the org, after the caller has checked
38
- * access. `saveAs` makes it a short-lived download under that name. */
39
- signFile(orgId: string, assetId: string, opts?: {
40
- saveAs?: string;
41
- }): Promise<string | null>;
42
+ /** Signed storage links to the ready files of the org among these, keyed by asset id,
43
+ * for a caller that has already checked access. */
44
+ fileLinks(orgId: string, files: readonly FileLinkRequest[]): Promise<Record<string, string>>;
42
45
  syncPublicImages(orgId: string, ownerId: string, assetIds: readonly string[]): Promise<void>;
43
46
  }
44
47
  interface MemberUploadInput {
@@ -52,10 +55,6 @@ interface AssetsService extends MemberImageReader, MemberUploadReader, LibraryAs
52
55
  confirmOwn(id: string, actor: Actor): Promise<AssetView>;
53
56
  list(query: AssetsQuery, actor: Actor): Promise<Page<AssetView>>;
54
57
  get(id: string, actor: Actor): Promise<AssetView>;
55
- libraryFile(assetId: string, opts: {
56
- download: boolean;
57
- }, actor: Actor): Promise<string>;
58
- ownUploadFile(assetId: string, actor: Actor): Promise<string>;
59
58
  remove(id: string, actor: Actor): Promise<void>;
60
59
  removeOwn(id: string, actor: Actor): Promise<void>;
61
60
  removeUnattached(orgId: string, now: Date): Promise<number>;
@@ -80,5 +79,5 @@ interface AssetsWriteScope {
80
79
  }
81
80
  type AssetsUnitOfWork = UnitOfWork<AssetsWriteScope>;
82
81
  //#endregion
83
- export { AssetFiles, AssetsRepository, AssetsService, AssetsUnitOfWork, AssetsWriteScope, LibraryAssetReader, MemberImageReader, MemberUpload, MemberUploadInput, MemberUploadReader, OrgDirectory, ReadyFile };
82
+ export { AssetFiles, AssetsRepository, AssetsService, AssetsUnitOfWork, AssetsWriteScope, FileLinkRequest, LibraryAssetReader, MemberImageReader, MemberUpload, MemberUploadInput, MemberUploadReader, OrgDirectory, ReadyFile };
84
83
  //# sourceMappingURL=ports.d.ts.map
@@ -3,9 +3,8 @@ import { Logger, ObjectStorage } from "../types/ports.js";
3
3
  import { AssetKind, AssetView, AssetsQuery, RequestUploadInput, UploadTicket } from "../types/schemas/assets.js";
4
4
  import { Actor } from "../shared/actor.js";
5
5
  import "../shared/ports.js";
6
- import { FileUrls } from "./file-urls.js";
7
6
  import "./model.js";
8
- import { AssetsRepository, AssetsService, AssetsUnitOfWork, MemberUpload, MemberUploadInput, ReadyFile } from "./ports.js";
7
+ import { AssetsRepository, AssetsService, AssetsUnitOfWork, FileLinkRequest, MemberUpload, MemberUploadInput, ReadyFile } from "./ports.js";
9
8
  //#region src/assets/service.d.ts
10
9
  declare const MEMBER_IMAGE_TYPE: RegExp;
11
10
  declare function isMemberImageType(contentType: string): boolean;
@@ -17,9 +16,11 @@ type AssetsServiceParams = {
17
16
  uow: AssetsUnitOfWork;
18
17
  memberImageMaxBytes: number;
19
18
  memberFileMaxBytes: number;
20
- downloadLinkExpirySeconds: number;
21
- fileUrls: FileUrls;
19
+ /** A file link is signed for the window it is made in and the one after, so it lives
20
+ * between one and two windows and is the same link all through its window. */
21
+ linkWindowSeconds: number;
22
22
  logger?: Logger;
23
+ now?: () => Date;
23
24
  };
24
25
  declare class AssetsServiceImpl implements AssetsService {
25
26
  private readonly storage;
@@ -27,16 +28,15 @@ declare class AssetsServiceImpl implements AssetsService {
27
28
  private readonly uow;
28
29
  private readonly memberImageMaxBytes;
29
30
  private readonly memberFileMaxBytes;
30
- private readonly downloadLinkExpirySeconds;
31
- private readonly fileUrls;
31
+ private readonly linkWindowMs;
32
32
  private readonly logger;
33
+ private readonly now;
33
34
  constructor(params: AssetsServiceParams);
34
35
  requestUpload(input: RequestUploadInput, actor: Actor): Promise<UploadTicket>;
35
36
  requestMemberUpload(input: MemberUploadInput, actor: Actor): Promise<UploadTicket>;
36
37
  private createUpload;
37
38
  confirm(id: string, actor: Actor): Promise<AssetView>;
38
39
  confirmOwn(id: string, actor: Actor): Promise<AssetView>;
39
- ownUploadFile(assetId: string, actor: Actor): Promise<string>;
40
40
  ownsUpload(orgId: string, assetId: string, orgUserId: string): Promise<boolean>;
41
41
  memberUploads(orgId: string, assetIds: string[]): Promise<MemberUpload[]>;
42
42
  ownsImage(orgId: string, assetId: string, orgUserId: string): Promise<boolean>;
@@ -47,13 +47,9 @@ declare class AssetsServiceImpl implements AssetsService {
47
47
  list(query: AssetsQuery, actor: Actor): Promise<Page<AssetView>>;
48
48
  get(id: string, actor: Actor): Promise<AssetView>;
49
49
  readyFiles(orgId: string, assetIds: readonly string[]): Promise<Record<string, ReadyFile>>;
50
- signFile(orgId: string, assetId: string, opts?: {
51
- saveAs?: string;
52
- }): Promise<string | null>;
50
+ fileLinks(orgId: string, files: readonly FileLinkRequest[]): Promise<Record<string, string>>;
51
+ private signReady;
53
52
  syncPublicImages(orgId: string, ownerId: string, assetIds: readonly string[]): Promise<void>;
54
- libraryFile(assetId: string, opts: {
55
- download: boolean;
56
- }, actor: Actor): Promise<string>;
57
53
  remove(id: string, actor: Actor): Promise<void>;
58
54
  removeOwn(id: string, actor: Actor): Promise<void>;
59
55
  private removeAsset;
@@ -67,6 +63,7 @@ declare class AssetsServiceImpl implements AssetsService {
67
63
  private markReady;
68
64
  private readyAmong;
69
65
  private view;
66
+ private views;
70
67
  private discard;
71
68
  private removePublicCopies;
72
69
  private removeObject;
@@ -49,21 +49,21 @@ var AssetsServiceImpl = class {
49
49
  uow;
50
50
  memberImageMaxBytes;
51
51
  memberFileMaxBytes;
52
- downloadLinkExpirySeconds;
53
- fileUrls;
52
+ linkWindowMs;
54
53
  logger;
54
+ now;
55
55
  constructor(params) {
56
56
  if (!Number.isSafeInteger(params.memberImageMaxBytes) || params.memberImageMaxBytes <= 0) throw new Error(`invalid memberImageMaxBytes "${params.memberImageMaxBytes}" (expected a positive integer)`);
57
57
  if (!Number.isSafeInteger(params.memberFileMaxBytes) || params.memberFileMaxBytes <= 0) throw new Error(`invalid memberFileMaxBytes "${params.memberFileMaxBytes}" (expected a positive integer)`);
58
- if (!Number.isSafeInteger(params.downloadLinkExpirySeconds) || params.downloadLinkExpirySeconds <= 0) throw new Error(`invalid downloadLinkExpirySeconds "${params.downloadLinkExpirySeconds}" (expected a positive integer)`);
58
+ if (!Number.isSafeInteger(params.linkWindowSeconds) || params.linkWindowSeconds <= 0) throw new Error(`invalid linkWindowSeconds "${params.linkWindowSeconds}" (expected a positive integer)`);
59
59
  this.storage = params.storage;
60
60
  this.repo = params.repo;
61
61
  this.uow = params.uow;
62
62
  this.memberImageMaxBytes = params.memberImageMaxBytes;
63
63
  this.memberFileMaxBytes = params.memberFileMaxBytes;
64
- this.downloadLinkExpirySeconds = params.downloadLinkExpirySeconds;
65
- this.fileUrls = params.fileUrls;
64
+ this.linkWindowMs = params.linkWindowSeconds * 1e3;
66
65
  this.logger = params.logger ?? noopLogger;
66
+ this.now = params.now ?? (() => /* @__PURE__ */ new Date());
67
67
  }
68
68
  async requestUpload(input, actor) {
69
69
  const orgId = actor.orgId;
@@ -170,30 +170,18 @@ var AssetsServiceImpl = class {
170
170
  }
171
171
  const confirmed = await this.finalize(orgId, asset);
172
172
  if (!confirmed) throw new NotFoundError("Asset", id);
173
- return confirmed.status === "ready" ? {
173
+ if (confirmed.status !== "ready") return {
174
174
  ...confirmed,
175
- url: this.fileUrls.memberUpload(confirmed.id),
175
+ url: null,
176
176
  downloadUrl: null
177
- } : {
177
+ };
178
+ const links = await this.signReady([confirmed], () => void 0);
179
+ return {
178
180
  ...confirmed,
179
- url: null,
181
+ url: links[confirmed.id] ?? null,
180
182
  downloadUrl: null
181
183
  };
182
184
  }
183
- async ownUploadFile(assetId, actor) {
184
- const orgId = actor.orgId;
185
- if (!await this.ownsUpload(orgId, assetId, actor.id)) {
186
- this.logger.warn("upload file not served — not the uploader", {
187
- orgId,
188
- assetId,
189
- orgUserId: actor.id
190
- });
191
- throw new NotFoundError("Upload", assetId);
192
- }
193
- const url = await this.signFile(orgId, assetId);
194
- if (!url) throw new NotFoundError("Upload", assetId);
195
- return url;
196
- }
197
185
  async ownsUpload(orgId, assetId, orgUserId) {
198
186
  const asset = await this.repo.findById(orgId, assetId);
199
187
  return asset !== null && asset.source === "member" && asset.status === "ready" && asset.uploadedBy === orgUserId;
@@ -232,7 +220,7 @@ var AssetsServiceImpl = class {
232
220
  });
233
221
  return {
234
222
  ...page,
235
- rows: await Promise.all(page.rows.map((asset) => this.view(asset)))
223
+ rows: await this.views(page.rows)
236
224
  };
237
225
  }
238
226
  async get(id, actor) {
@@ -249,20 +237,25 @@ var AssetsServiceImpl = class {
249
237
  size: asset.size
250
238
  }]));
251
239
  }
252
- async signFile(orgId, assetId, opts = {}) {
253
- const [asset] = await this.readyAmong(orgId, [assetId]);
254
- if (!asset) {
255
- this.logger.warn("file not served — no ready asset", {
256
- orgId,
257
- assetId
240
+ async fileLinks(orgId, files) {
241
+ const saveAs = new Map(files.map((file) => [file.assetId, file.saveAs]));
242
+ const ready = await this.readyAmong(orgId, [...saveAs.keys()]);
243
+ return this.signReady(ready, (asset) => saveAs.get(asset.id));
244
+ }
245
+ async signReady(assets, saveAs) {
246
+ const signedAt = new Date(Math.floor(this.now().getTime() / this.linkWindowMs) * this.linkWindowMs);
247
+ const expiresAt = new Date(signedAt.getTime() + 2 * this.linkWindowMs);
248
+ const links = await Promise.all(assets.map(async (asset) => {
249
+ const filename = saveAs(asset);
250
+ const url = await this.storage.presignDownload({
251
+ key: asset.key,
252
+ signedAt,
253
+ expiresAt,
254
+ ...filename === void 0 ? {} : { downloadFilename: filename }
258
255
  });
259
- return null;
260
- }
261
- return opts.saveAs === void 0 ? this.storage.presignDownload({ key: asset.key }) : this.storage.presignDownload({
262
- key: asset.key,
263
- downloadFilename: opts.saveAs,
264
- expiresInSeconds: this.downloadLinkExpirySeconds
265
- });
256
+ return [asset.id, url];
257
+ }));
258
+ return Object.fromEntries(links);
266
259
  }
267
260
  async syncPublicImages(orgId, ownerId, assetIds) {
268
261
  try {
@@ -302,21 +295,6 @@ var AssetsServiceImpl = class {
302
295
  });
303
296
  }
304
297
  }
305
- async libraryFile(assetId, opts, actor) {
306
- const orgId = actor.orgId;
307
- const asset = await this.repo.findById(orgId, assetId);
308
- if (!asset || asset.source !== "library") {
309
- this.logger.warn("library file not served — not a library asset", {
310
- orgId,
311
- assetId
312
- });
313
- throw new NotFoundError("Asset", assetId);
314
- }
315
- await this.requireReach(asset, actor);
316
- const url = await this.signFile(orgId, assetId, opts.download ? { saveAs: asset.filename } : {});
317
- if (!url) throw new NotFoundError("Asset", assetId);
318
- return url;
319
- }
320
298
  async remove(id, actor) {
321
299
  actor.permissions.require("media:delete");
322
300
  await this.removeAsset(actor.orgId, id, actor.id);
@@ -488,17 +466,18 @@ var AssetsServiceImpl = class {
488
466
  if (ids.length === 0) return [];
489
467
  return (await this.repo.findByIds(orgId, ids)).filter((asset) => asset.orgId === orgId && asset.status === "ready");
490
468
  }
491
- view(asset) {
492
- if (asset.source !== "library" || asset.status !== "ready") return {
493
- ...asset,
494
- url: null,
495
- downloadUrl: null
496
- };
497
- return {
469
+ async view(asset) {
470
+ const [view] = await this.views([asset]);
471
+ return view;
472
+ }
473
+ async views(assets) {
474
+ const served = assets.filter((asset) => asset.source === "library" && asset.status === "ready");
475
+ const [urls, downloadUrls] = await Promise.all([this.signReady(served, () => void 0), this.signReady(served, (asset) => asset.filename)]);
476
+ return assets.map((asset) => ({
498
477
  ...asset,
499
- url: this.fileUrls.libraryFile(asset.id),
500
- downloadUrl: this.fileUrls.libraryDownload(asset.id)
501
- };
478
+ url: urls[asset.id] ?? null,
479
+ downloadUrl: downloadUrls[asset.id] ?? null
480
+ }));
502
481
  }
503
482
  async discard(orgId, asset) {
504
483
  const deleted = await this.uow.run(async ({ assets, outbox }) => {
@@ -1 +1 @@
1
- {"version":3,"file":"service.js","names":[],"sources":["../../src/assets/service.ts"],"sourcesContent":["import { genId } from '../shared/id.js';\nimport type { Logger, ObjectStorage } from '../shared/ports.js';\nimport { PUBLIC_PREFIX } from '../types/index.js';\nimport {\n ConflictError,\n ForbiddenError,\n InvalidInputError,\n NotFoundError,\n} from '../shared/errors.js';\nimport type { Actor } from '../shared/actor.js';\nimport type { AssetSource, ContentType, Permission } from '../types/index.js';\nimport type {\n Asset,\n AssetKind,\n AssetView,\n AssetsQuery,\n Page,\n RequestUploadInput,\n UploadTicket,\n} from './model.js';\nimport type {\n AssetsRepository,\n AssetsService,\n AssetsUnitOfWork,\n MemberUpload,\n MemberUploadInput,\n ReadyFile,\n} from './ports.js';\nimport { publicImageKey, publicImagePrefix, type FileUrls } from './file-urls.js';\nimport { noopLogger } from '../shared/logger.js';\nimport { assetEvents } from './events.js';\n\nconst WRITE_PERMISSION: Record<ContentType, Permission> = {\n course: 'courses:write',\n download: 'downloads:write',\n community: 'communities:write',\n event: 'events:write',\n};\n\nconst UNATTACHED_UPLOAD_GRACE_MS = 60 * 60 * 1000;\n\nexport const MEMBER_IMAGE_TYPE = /^image\\/(png|jpeg|gif|webp|avif)$/i;\n\nexport function isMemberImageType(contentType: string): boolean {\n return MEMBER_IMAGE_TYPE.test(contentType.trim());\n}\n\nexport function memberKindOf(contentType: string): AssetKind {\n if (isMemberImageType(contentType)) {\n return 'image';\n }\n const kind = kindOf(contentType);\n return kind === 'image' ? 'document' : kind;\n}\n\nexport function kindOf(contentType: string): AssetKind {\n const type = contentType.trim().toLowerCase();\n if (type.startsWith('image/')) {\n return 'image';\n }\n if (type.startsWith('video/')) {\n return 'video';\n }\n if (type.startsWith('audio/')) {\n return 'audio';\n }\n return 'document';\n}\n\nfunction orgPrefix(orgId: string): string {\n return `org/${orgId}/`;\n}\n\nfunction sanitizeFilename(filename: string): string {\n const base = filename.split(/[\\\\/]/).pop() ?? 'file';\n return base.replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 120) || 'file';\n}\n\nfunction storedKey(orgId: string, folder: string, id: string, filename: string): string {\n return `${orgPrefix(orgId)}${folder}/${id}/${sanitizeFilename(filename)}`;\n}\n\nfunction finalMemberKey(orgId: string, asset: Asset): string {\n return `${orgPrefix(orgId)}${asset.kind}/${asset.id}/${sanitizeFilename(asset.filename)}`;\n}\n\nfunction graceCutoff(now: Date): Date {\n return new Date(now.getTime() - UNATTACHED_UPLOAD_GRACE_MS);\n}\n\nexport type AssetsServiceParams = {\n storage: ObjectStorage;\n repo: AssetsRepository;\n uow: AssetsUnitOfWork;\n memberImageMaxBytes: number;\n memberFileMaxBytes: number;\n downloadLinkExpirySeconds: number;\n fileUrls: FileUrls;\n logger?: Logger;\n};\n\nexport class AssetsServiceImpl implements AssetsService {\n private readonly storage: ObjectStorage;\n private readonly repo: AssetsRepository;\n private readonly uow: AssetsUnitOfWork;\n private readonly memberImageMaxBytes: number;\n private readonly memberFileMaxBytes: number;\n private readonly downloadLinkExpirySeconds: number;\n private readonly fileUrls: FileUrls;\n private readonly logger: Logger;\n\n constructor(params: AssetsServiceParams) {\n if (!Number.isSafeInteger(params.memberImageMaxBytes) || params.memberImageMaxBytes <= 0) {\n throw new Error(\n `invalid memberImageMaxBytes \"${params.memberImageMaxBytes}\" (expected a positive integer)`,\n );\n }\n if (!Number.isSafeInteger(params.memberFileMaxBytes) || params.memberFileMaxBytes <= 0) {\n throw new Error(\n `invalid memberFileMaxBytes \"${params.memberFileMaxBytes}\" (expected a positive integer)`,\n );\n }\n if (\n !Number.isSafeInteger(params.downloadLinkExpirySeconds) ||\n params.downloadLinkExpirySeconds <= 0\n ) {\n throw new Error(\n `invalid downloadLinkExpirySeconds \"${params.downloadLinkExpirySeconds}\" (expected a positive integer)`,\n );\n }\n this.storage = params.storage;\n this.repo = params.repo;\n this.uow = params.uow;\n this.memberImageMaxBytes = params.memberImageMaxBytes;\n this.memberFileMaxBytes = params.memberFileMaxBytes;\n this.downloadLinkExpirySeconds = params.downloadLinkExpirySeconds;\n this.fileUrls = params.fileUrls;\n this.logger = params.logger ?? noopLogger;\n }\n\n async requestUpload(input: RequestUploadInput, actor: Actor): Promise<UploadTicket> {\n const orgId = actor.orgId;\n if (input.contentId === undefined) {\n actor.permissions.require('media:write');\n } else {\n const type = await this.repo.contentTypeOf(orgId, input.contentId);\n if (!type) {\n throw new NotFoundError('Content item', input.contentId);\n }\n if (\n !actor.permissions.has('media:write') &&\n !actor.permissions.has(WRITE_PERMISSION[type], input.contentId)\n ) {\n this.logger.warn('asset upload refused', {\n orgId,\n orgUserId: actor.id,\n contentId: input.contentId,\n });\n throw new ForbiddenError(`You need media:write or ${WRITE_PERMISSION[type]}`);\n }\n }\n return this.createUpload(orgId, {\n filename: input.filename,\n contentType: input.contentType,\n source: 'library',\n uploadedBy: actor.id,\n contentId: input.contentId ?? null,\n });\n }\n\n async requestMemberUpload(input: MemberUploadInput, actor: Actor): Promise<UploadTicket> {\n return this.createUpload(actor.orgId, {\n filename: input.filename,\n contentType: input.contentType,\n source: 'member',\n uploadedBy: actor.id,\n contentId: null,\n });\n }\n\n private async createUpload(\n orgId: string,\n input: {\n filename: string;\n contentType: string;\n source: AssetSource;\n uploadedBy: string;\n contentId: string | null;\n },\n ): Promise<UploadTicket> {\n const id = genId('asset');\n const kind =\n input.source === 'member' ? memberKindOf(input.contentType) : kindOf(input.contentType);\n const folder = input.source === 'member' ? 'uploads' : kind;\n const key = storedKey(orgId, folder, id, input.filename);\n const presigned = await this.storage.presignUpload({ key, contentType: input.contentType });\n const now = new Date();\n const asset = await this.uow.run(async ({ assets, outbox }) => {\n const asset = await assets.insert(orgId, {\n orgId,\n id,\n key,\n kind,\n source: input.source,\n filename: input.filename,\n contentType: input.contentType,\n size: 0,\n status: 'pending',\n uploadedBy: input.uploadedBy,\n contentId: input.contentId,\n createdAt: now,\n updatedAt: now,\n });\n await outbox.append([assetEvents.assetCreated.make({ orgId, data: asset })]);\n return asset;\n });\n this.logger.info('asset upload requested', {\n orgId,\n assetId: id,\n kind,\n source: input.source,\n contentId: input.contentId,\n });\n return {\n asset: { ...asset, url: null, downloadUrl: null },\n uploadUrl: presigned.url,\n method: 'PUT',\n expiresInSeconds: presigned.expiresInSeconds,\n headers: presigned.headers,\n };\n }\n\n async confirm(id: string, actor: Actor): Promise<AssetView> {\n const orgId = actor.orgId;\n const asset = await this.repo.findById(orgId, id);\n if (!asset) {\n throw new NotFoundError('Asset', id);\n }\n await this.requireReach(asset, actor);\n if (asset.source === 'member') {\n throw new ForbiddenError('Only the uploader can finalize a member upload');\n }\n const confirmed = await this.finalize(orgId, asset);\n if (!confirmed) {\n throw new NotFoundError('Asset', id);\n }\n return this.view(confirmed);\n }\n\n async confirmOwn(id: string, actor: Actor): Promise<AssetView> {\n const orgId = actor.orgId;\n const asset = await this.repo.findById(orgId, id);\n if (!asset || asset.source !== 'member' || asset.uploadedBy !== actor.id) {\n this.logger.debug('upload confirm refused', { orgId, assetId: id, orgUserId: actor.id });\n throw new NotFoundError('Asset', id);\n }\n const confirmed = await this.finalize(orgId, asset);\n if (!confirmed) {\n throw new NotFoundError('Asset', id);\n }\n return confirmed.status === 'ready'\n ? { ...confirmed, url: this.fileUrls.memberUpload(confirmed.id), downloadUrl: null }\n : { ...confirmed, url: null, downloadUrl: null };\n }\n\n async ownUploadFile(assetId: string, actor: Actor): Promise<string> {\n const orgId = actor.orgId;\n if (!(await this.ownsUpload(orgId, assetId, actor.id))) {\n this.logger.warn('upload file not served — not the uploader', {\n orgId,\n assetId,\n orgUserId: actor.id,\n });\n throw new NotFoundError('Upload', assetId);\n }\n const url = await this.signFile(orgId, assetId);\n if (!url) {\n throw new NotFoundError('Upload', assetId);\n }\n return url;\n }\n\n async ownsUpload(orgId: string, assetId: string, orgUserId: string): Promise<boolean> {\n const asset = await this.repo.findById(orgId, assetId);\n return (\n asset !== null &&\n asset.source === 'member' &&\n asset.status === 'ready' &&\n asset.uploadedBy === orgUserId\n );\n }\n\n async memberUploads(orgId: string, assetIds: string[]): Promise<MemberUpload[]> {\n const ready = await this.readyAmong(orgId, assetIds);\n return ready\n .filter((asset) => asset.source === 'member')\n .map((asset) => ({\n id: asset.id,\n kind: asset.kind,\n filename: asset.filename,\n contentType: asset.contentType,\n size: asset.size,\n }));\n }\n\n async ownsImage(orgId: string, assetId: string, orgUserId: string): Promise<boolean> {\n const asset = await this.repo.findById(orgId, assetId);\n return (\n asset !== null &&\n asset.source === 'member' &&\n asset.kind === 'image' &&\n asset.status === 'ready' &&\n asset.uploadedBy === orgUserId\n );\n }\n\n async libraryAssetsFor(\n orgId: string,\n assetIds: string[],\n ): Promise<{ id: string; contentId: string | null }[]> {\n if (assetIds.length === 0) {\n return [];\n }\n const rows = await this.repo.findByIds(orgId, [...new Set(assetIds)]);\n const library = new Map(\n rows\n .filter((a) => a.source === 'library' && a.status === 'ready')\n .map((a) => [a.id, a.contentId]),\n );\n return assetIds.flatMap((id) =>\n library.has(id) ? [{ id, contentId: library.get(id) ?? null }] : [],\n );\n }\n\n async list(query: AssetsQuery, actor: Actor): Promise<Page<AssetView>> {\n const orgId = actor.orgId;\n if (!actor.permissions.has('media:read')) {\n if (query.contentId === undefined || (query.source ?? 'library') !== 'library') {\n throw new ForbiddenError('You need media:read');\n }\n await this.requireItemWrite(orgId, query.contentId, actor);\n }\n const page = await this.repo.list(orgId, { ...query, source: query.source ?? 'library' });\n return { ...page, rows: await Promise.all(page.rows.map((asset) => this.view(asset))) };\n }\n\n async get(id: string, actor: Actor): Promise<AssetView> {\n const asset = await this.repo.findById(actor.orgId, id);\n if (!asset) {\n throw new NotFoundError('Asset', id);\n }\n await this.requireReach(asset, actor);\n return this.view(asset);\n }\n\n async readyFiles(orgId: string, assetIds: readonly string[]): Promise<Record<string, ReadyFile>> {\n const ready = await this.readyAmong(orgId, assetIds);\n return Object.fromEntries(\n ready.map((asset) => [\n asset.id,\n { filename: asset.filename, contentType: asset.contentType, size: asset.size },\n ]),\n );\n }\n\n async signFile(\n orgId: string,\n assetId: string,\n opts: { saveAs?: string } = {},\n ): Promise<string | null> {\n const [asset] = await this.readyAmong(orgId, [assetId]);\n if (!asset) {\n this.logger.warn('file not served — no ready asset', { orgId, assetId });\n return null;\n }\n return opts.saveAs === undefined\n ? this.storage.presignDownload({ key: asset.key })\n : this.storage.presignDownload({\n key: asset.key,\n downloadFilename: opts.saveAs,\n expiresInSeconds: this.downloadLinkExpirySeconds,\n });\n }\n\n async syncPublicImages(\n orgId: string,\n ownerId: string,\n assetIds: readonly string[],\n ): Promise<void> {\n try {\n const wanted = new Map(assetIds.map((id) => [publicImageKey(orgId, ownerId, id), id]));\n const held = await this.storage.list(publicImagePrefix(orgId, ownerId));\n for (const object of held) {\n if (!wanted.has(object.key)) {\n await this.storage.remove(object.key);\n }\n }\n const heldKeys = new Set(held.map((object) => object.key));\n const missing = [...wanted].filter(([key]) => !heldKeys.has(key));\n if (missing.length === 0) {\n return;\n }\n const ready = new Map(\n (\n await this.readyAmong(\n orgId,\n missing.map(([, id]) => id),\n )\n ).map((asset) => [asset.id, asset]),\n );\n for (const [key, assetId] of missing) {\n const asset = ready.get(assetId);\n if (!asset || asset.kind !== 'image') {\n this.logger.warn('public image not published — no ready image', {\n orgId,\n ownerId,\n assetId,\n });\n continue;\n }\n await this.storage.publish({ from: asset.key, to: key });\n this.logger.info('public image published', { orgId, ownerId, assetId });\n }\n } catch (err) {\n this.logger.error('public images not synced', { orgId, ownerId, assetIds, err });\n }\n }\n\n async libraryFile(assetId: string, opts: { download: boolean }, actor: Actor): Promise<string> {\n const orgId = actor.orgId;\n const asset = await this.repo.findById(orgId, assetId);\n if (!asset || asset.source !== 'library') {\n this.logger.warn('library file not served — not a library asset', { orgId, assetId });\n throw new NotFoundError('Asset', assetId);\n }\n await this.requireReach(asset, actor);\n const url = await this.signFile(\n orgId,\n assetId,\n opts.download ? { saveAs: asset.filename } : {},\n );\n if (!url) {\n throw new NotFoundError('Asset', assetId);\n }\n return url;\n }\n\n async remove(id: string, actor: Actor): Promise<void> {\n actor.permissions.require('media:delete');\n await this.removeAsset(actor.orgId, id, actor.id);\n }\n\n async removeOwn(id: string, actor: Actor): Promise<void> {\n const asset = await this.repo.findById(actor.orgId, id);\n if (!asset || asset.source !== 'member' || asset.uploadedBy !== actor.id) {\n throw new NotFoundError('Asset', id);\n }\n await this.removeAsset(actor.orgId, id, actor.id);\n }\n\n private async removeAsset(orgId: string, id: string, actorId: string): Promise<void> {\n const asset = await this.repo.findById(orgId, id);\n if (!asset || !(await this.discard(orgId, asset))) {\n throw new NotFoundError('Asset', id);\n }\n this.logger.info('asset removed', { orgId, assetId: id, actorId });\n }\n\n private async requireReach(asset: Asset, actor: Actor): Promise<void> {\n if (actor.permissions.has('media:read')) {\n return;\n }\n if (asset.contentId === null) {\n this.logger.warn('asset refused', {\n orgId: actor.orgId,\n assetId: asset.id,\n orgUserId: actor.id,\n });\n throw new ForbiddenError('You need media:read');\n }\n await this.requireItemWrite(actor.orgId, asset.contentId, actor);\n }\n\n private async requireItemWrite(orgId: string, contentId: string, actor: Actor): Promise<void> {\n const type = await this.repo.contentTypeOf(orgId, contentId);\n if (!type || !actor.permissions.has(WRITE_PERMISSION[type], contentId)) {\n this.logger.warn('asset refused', { orgId, contentId, orgUserId: actor.id });\n throw new ForbiddenError(\n type ? `You need media:read or ${WRITE_PERMISSION[type]}` : 'You need media:read',\n );\n }\n }\n\n async removeUnattached(orgId: string, now: Date): Promise<number> {\n const stale = await this.repo.listUnreferencedMemberUploads(orgId, graceCutoff(now));\n let removed = 0;\n let failed = 0;\n for (const asset of stale) {\n try {\n if (await this.discard(orgId, asset)) {\n removed += 1;\n }\n } catch (err) {\n if (!(err instanceof ConflictError)) {\n failed += 1;\n this.logger.error('unattached upload not removed', { orgId, assetId: asset.id, err });\n }\n }\n }\n const orphans = await this.orphanPublicImages(orgId);\n for (const key of orphans) {\n await this.removeObject(orgId, key.split('/').pop() ?? '', key);\n }\n let stray = 0;\n for (const key of await this.strayUploads(orgId, now)) {\n try {\n await this.storage.remove(key);\n stray += 1;\n } catch (err) {\n failed += 1;\n this.logger.warn('stray upload not removed', { orgId, err });\n }\n }\n if (removed > 0 || stray > 0 || orphans.length > 0 || failed > 0) {\n this.logger.info('unattached uploads removed', {\n orgId,\n count: removed,\n stray,\n publicCopies: orphans.length,\n failed,\n });\n }\n return removed + stray + orphans.length;\n }\n\n private async orphanPublicImages(orgId: string): Promise<string[]> {\n const objects = await this.storage.list(`${PUBLIC_PREFIX}${orgId}/`);\n if (objects.length === 0) {\n return [];\n }\n const assetIdOf = (key: string) => key.split('/').pop() ?? '';\n const live = new Set(\n (\n await this.readyAmong(\n orgId,\n objects.map((o) => assetIdOf(o.key)),\n )\n ).map((a) => a.id),\n );\n return objects.filter((o) => !live.has(assetIdOf(o.key))).map((o) => o.key);\n }\n\n private async strayUploads(orgId: string, now: Date): Promise<string[]> {\n const cutoff = now.getTime() - this.storage.uploadExpirySeconds * 1000;\n const objects = await this.storage.list(`${orgPrefix(orgId)}uploads/`);\n const old = objects\n .filter((o) => o.lastModified !== undefined && Date.parse(o.lastModified) < cutoff)\n .map((o) => o.key);\n if (old.length === 0) {\n return [];\n }\n const held = new Set(await this.repo.heldKeys(orgId, old));\n return old.filter((key) => !held.has(key));\n }\n\n private async finalize(orgId: string, asset: Asset): Promise<Asset | null> {\n if (asset.status !== 'pending') {\n return asset;\n }\n if (asset.source === 'member') {\n return this.finalizeMember(orgId, asset);\n }\n const stat = await this.storage.stat(asset.key);\n if (!stat) {\n this.logger.debug('asset not yet uploaded', { orgId, assetId: asset.id });\n return asset;\n }\n const contentType = stat.contentType ?? asset.contentType;\n const ready = await this.markReady(orgId, asset, {\n key: asset.key,\n kind: kindOf(contentType),\n size: stat.size,\n contentType,\n });\n return ready ?? this.repo.findById(orgId, asset.id);\n }\n\n private async finalizeMember(orgId: string, asset: Asset): Promise<Asset | null> {\n if (!(await this.storage.stat(asset.key))) {\n this.logger.debug('asset not yet uploaded', { orgId, assetId: asset.id });\n return asset;\n }\n const key = finalMemberKey(orgId, asset);\n await this.storage.copy({ from: asset.key, to: key });\n const stat = await this.storage.stat(key);\n if (!stat) {\n throw new Error('copied upload not found');\n }\n const contentType = asset.contentType;\n const kind = memberKindOf(contentType);\n const limit = kind === 'image' ? this.memberImageMaxBytes : this.memberFileMaxBytes;\n const reason =\n stat.contentType !== undefined && stat.contentType !== contentType\n ? 'The uploaded file is not the declared type'\n : stat.size > limit\n ? `This ${kind === 'image' ? 'image' : 'file'} is over the ${sizeLabel(limit)} limit`\n : null;\n if (reason) {\n await this.removeObject(orgId, asset.id, key);\n await this.discard(orgId, asset);\n this.logger.info('member upload refused', { orgId, assetId: asset.id, reason });\n throw new InvalidInputError(reason);\n }\n const ready = await this.markReady(orgId, asset, { key, kind, size: stat.size, contentType });\n if (!ready) {\n return this.repo.findById(orgId, asset.id);\n }\n await this.removeObject(orgId, asset.id, asset.key);\n return ready;\n }\n\n private async markReady(\n orgId: string,\n asset: Asset,\n patch: Pick<Asset, 'key' | 'kind' | 'size' | 'contentType'>,\n ): Promise<Asset | null> {\n const updated = await this.uow.run(async ({ assets, outbox }) => {\n const updated = await assets.markReady(orgId, asset.id, patch);\n if (updated) {\n await outbox.append([assetEvents.assetReady.make({ orgId, data: updated })]);\n }\n return updated;\n });\n if (updated) {\n this.logger.info('asset confirmed', { orgId, assetId: asset.id });\n }\n return updated;\n }\n\n private async readyAmong(orgId: string, assetIds: readonly string[]): Promise<Asset[]> {\n const ids = [...new Set(assetIds)];\n if (ids.length === 0) {\n return [];\n }\n const rows = await this.repo.findByIds(orgId, ids);\n return rows.filter((asset) => asset.orgId === orgId && asset.status === 'ready');\n }\n\n private view(asset: Asset): AssetView {\n if (asset.source !== 'library' || asset.status !== 'ready') {\n return { ...asset, url: null, downloadUrl: null };\n }\n return {\n ...asset,\n url: this.fileUrls.libraryFile(asset.id),\n downloadUrl: this.fileUrls.libraryDownload(asset.id),\n };\n }\n\n private async discard(orgId: string, asset: Asset): Promise<boolean> {\n const deleted = await this.uow.run(async ({ assets, outbox }) => {\n const deleted = await assets.delete(orgId, asset.id);\n if (deleted) {\n await outbox.append([assetEvents.assetDeleted.make({ orgId, data: asset })]);\n }\n return deleted;\n });\n if (deleted) {\n await this.removeObject(orgId, asset.id, asset.key);\n await this.removePublicCopies(orgId, asset.id);\n }\n return deleted;\n }\n\n private async removePublicCopies(orgId: string, assetId: string): Promise<void> {\n try {\n const copies = await this.storage.list(`${PUBLIC_PREFIX}${orgId}/`);\n for (const copy of copies.filter((o) => o.key.endsWith(`/${assetId}`))) {\n await this.removeObject(orgId, assetId, copy.key);\n }\n } catch (err) {\n this.logger.warn('public copies not removed', { orgId, assetId, err });\n }\n }\n\n private async removeObject(orgId: string, assetId: string, key: string): Promise<void> {\n try {\n await this.storage.remove(key);\n } catch (error) {\n this.logger.warn('stored object not removed', { orgId, assetId, err: error });\n }\n }\n}\n\nfunction sizeLabel(bytes: number): string {\n const units = ['B', 'KB', 'MB', 'GB'];\n let value = bytes;\n let unit = 0;\n while (value >= 1024 && unit < units.length - 1) {\n value /= 1024;\n unit += 1;\n }\n return `${Number.isInteger(value) ? value : value.toFixed(1)} ${units[unit]}`;\n}\n"],"mappings":";;;;;;;AAgCA,MAAM,mBAAoD;CACxD,QAAQ;CACR,UAAU;CACV,WAAW;CACX,OAAO;AACT;AAEA,MAAM,6BAA6B,OAAU;AAE7C,MAAa,oBAAoB;AAEjC,SAAgB,kBAAkB,aAA8B;CAC9D,OAAO,kBAAkB,KAAK,YAAY,KAAK,CAAC;AAClD;AAEA,SAAgB,aAAa,aAAgC;CAC3D,IAAI,kBAAkB,WAAW,GAC/B,OAAO;CAET,MAAM,OAAO,OAAO,WAAW;CAC/B,OAAO,SAAS,UAAU,aAAa;AACzC;AAEA,SAAgB,OAAO,aAAgC;CACrD,MAAM,OAAO,YAAY,KAAK,CAAC,CAAC,YAAY;CAC5C,IAAI,KAAK,WAAW,QAAQ,GAC1B,OAAO;CAET,IAAI,KAAK,WAAW,QAAQ,GAC1B,OAAO;CAET,IAAI,KAAK,WAAW,QAAQ,GAC1B,OAAO;CAET,OAAO;AACT;AAEA,SAAS,UAAU,OAAuB;CACxC,OAAO,OAAO,MAAM;AACtB;AAEA,SAAS,iBAAiB,UAA0B;CAElD,QADa,SAAS,MAAM,OAAO,CAAC,CAAC,IAAI,KAAK,OAAA,CAClC,QAAQ,oBAAoB,GAAG,CAAC,CAAC,MAAM,GAAG,GAAG,KAAK;AAChE;AAEA,SAAS,UAAU,OAAe,QAAgB,IAAY,UAA0B;CACtF,OAAO,GAAG,UAAU,KAAK,IAAI,OAAO,GAAG,GAAG,GAAG,iBAAiB,QAAQ;AACxE;AAEA,SAAS,eAAe,OAAe,OAAsB;CAC3D,OAAO,GAAG,UAAU,KAAK,IAAI,MAAM,KAAK,GAAG,MAAM,GAAG,GAAG,iBAAiB,MAAM,QAAQ;AACxF;AAEA,SAAS,YAAY,KAAiB;CACpC,uBAAO,IAAI,KAAK,IAAI,QAAQ,IAAI,0BAA0B;AAC5D;AAaA,IAAa,oBAAb,MAAwD;CACtD;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA,YAAY,QAA6B;EACvC,IAAI,CAAC,OAAO,cAAc,OAAO,mBAAmB,KAAK,OAAO,uBAAuB,GACrF,MAAM,IAAI,MACR,gCAAgC,OAAO,oBAAoB,gCAC7D;EAEF,IAAI,CAAC,OAAO,cAAc,OAAO,kBAAkB,KAAK,OAAO,sBAAsB,GACnF,MAAM,IAAI,MACR,+BAA+B,OAAO,mBAAmB,gCAC3D;EAEF,IACE,CAAC,OAAO,cAAc,OAAO,yBAAyB,KACtD,OAAO,6BAA6B,GAEpC,MAAM,IAAI,MACR,sCAAsC,OAAO,0BAA0B,gCACzE;EAEF,KAAK,UAAU,OAAO;EACtB,KAAK,OAAO,OAAO;EACnB,KAAK,MAAM,OAAO;EAClB,KAAK,sBAAsB,OAAO;EAClC,KAAK,qBAAqB,OAAO;EACjC,KAAK,4BAA4B,OAAO;EACxC,KAAK,WAAW,OAAO;EACvB,KAAK,SAAS,OAAO,UAAU;CACjC;CAEA,MAAM,cAAc,OAA2B,OAAqC;EAClF,MAAM,QAAQ,MAAM;EACpB,IAAI,MAAM,cAAc,KAAA,GACtB,MAAM,YAAY,QAAQ,aAAa;OAClC;GACL,MAAM,OAAO,MAAM,KAAK,KAAK,cAAc,OAAO,MAAM,SAAS;GACjE,IAAI,CAAC,MACH,MAAM,IAAI,cAAc,gBAAgB,MAAM,SAAS;GAEzD,IACE,CAAC,MAAM,YAAY,IAAI,aAAa,KACpC,CAAC,MAAM,YAAY,IAAI,iBAAiB,OAAO,MAAM,SAAS,GAC9D;IACA,KAAK,OAAO,KAAK,wBAAwB;KACvC;KACA,WAAW,MAAM;KACjB,WAAW,MAAM;IACnB,CAAC;IACD,MAAM,IAAI,eAAe,2BAA2B,iBAAiB,OAAO;GAC9E;EACF;EACA,OAAO,KAAK,aAAa,OAAO;GAC9B,UAAU,MAAM;GAChB,aAAa,MAAM;GACnB,QAAQ;GACR,YAAY,MAAM;GAClB,WAAW,MAAM,aAAa;EAChC,CAAC;CACH;CAEA,MAAM,oBAAoB,OAA0B,OAAqC;EACvF,OAAO,KAAK,aAAa,MAAM,OAAO;GACpC,UAAU,MAAM;GAChB,aAAa,MAAM;GACnB,QAAQ;GACR,YAAY,MAAM;GAClB,WAAW;EACb,CAAC;CACH;CAEA,MAAc,aACZ,OACA,OAOuB;EACvB,MAAM,KAAK,MAAM,OAAO;EACxB,MAAM,OACJ,MAAM,WAAW,WAAW,aAAa,MAAM,WAAW,IAAI,OAAO,MAAM,WAAW;EAExF,MAAM,MAAM,UAAU,OADP,MAAM,WAAW,WAAW,YAAY,MAClB,IAAI,MAAM,QAAQ;EACvD,MAAM,YAAY,MAAM,KAAK,QAAQ,cAAc;GAAE;GAAK,aAAa,MAAM;EAAY,CAAC;EAC1F,MAAM,sBAAM,IAAI,KAAK;EACrB,MAAM,QAAQ,MAAM,KAAK,IAAI,IAAI,OAAO,EAAE,QAAQ,aAAa;GAC7D,MAAM,QAAQ,MAAM,OAAO,OAAO,OAAO;IACvC;IACA;IACA;IACA;IACA,QAAQ,MAAM;IACd,UAAU,MAAM;IAChB,aAAa,MAAM;IACnB,MAAM;IACN,QAAQ;IACR,YAAY,MAAM;IAClB,WAAW,MAAM;IACjB,WAAW;IACX,WAAW;GACb,CAAC;GACD,MAAM,OAAO,OAAO,CAAC,YAAY,aAAa,KAAK;IAAE;IAAO,MAAM;GAAM,CAAC,CAAC,CAAC;GAC3E,OAAO;EACT,CAAC;EACD,KAAK,OAAO,KAAK,0BAA0B;GACzC;GACA,SAAS;GACT;GACA,QAAQ,MAAM;GACd,WAAW,MAAM;EACnB,CAAC;EACD,OAAO;GACL,OAAO;IAAE,GAAG;IAAO,KAAK;IAAM,aAAa;GAAK;GAChD,WAAW,UAAU;GACrB,QAAQ;GACR,kBAAkB,UAAU;GAC5B,SAAS,UAAU;EACrB;CACF;CAEA,MAAM,QAAQ,IAAY,OAAkC;EAC1D,MAAM,QAAQ,MAAM;EACpB,MAAM,QAAQ,MAAM,KAAK,KAAK,SAAS,OAAO,EAAE;EAChD,IAAI,CAAC,OACH,MAAM,IAAI,cAAc,SAAS,EAAE;EAErC,MAAM,KAAK,aAAa,OAAO,KAAK;EACpC,IAAI,MAAM,WAAW,UACnB,MAAM,IAAI,eAAe,gDAAgD;EAE3E,MAAM,YAAY,MAAM,KAAK,SAAS,OAAO,KAAK;EAClD,IAAI,CAAC,WACH,MAAM,IAAI,cAAc,SAAS,EAAE;EAErC,OAAO,KAAK,KAAK,SAAS;CAC5B;CAEA,MAAM,WAAW,IAAY,OAAkC;EAC7D,MAAM,QAAQ,MAAM;EACpB,MAAM,QAAQ,MAAM,KAAK,KAAK,SAAS,OAAO,EAAE;EAChD,IAAI,CAAC,SAAS,MAAM,WAAW,YAAY,MAAM,eAAe,MAAM,IAAI;GACxE,KAAK,OAAO,MAAM,0BAA0B;IAAE;IAAO,SAAS;IAAI,WAAW,MAAM;GAAG,CAAC;GACvF,MAAM,IAAI,cAAc,SAAS,EAAE;EACrC;EACA,MAAM,YAAY,MAAM,KAAK,SAAS,OAAO,KAAK;EAClD,IAAI,CAAC,WACH,MAAM,IAAI,cAAc,SAAS,EAAE;EAErC,OAAO,UAAU,WAAW,UACxB;GAAE,GAAG;GAAW,KAAK,KAAK,SAAS,aAAa,UAAU,EAAE;GAAG,aAAa;EAAK,IACjF;GAAE,GAAG;GAAW,KAAK;GAAM,aAAa;EAAK;CACnD;CAEA,MAAM,cAAc,SAAiB,OAA+B;EAClE,MAAM,QAAQ,MAAM;EACpB,IAAI,CAAE,MAAM,KAAK,WAAW,OAAO,SAAS,MAAM,EAAE,GAAI;GACtD,KAAK,OAAO,KAAK,6CAA6C;IAC5D;IACA;IACA,WAAW,MAAM;GACnB,CAAC;GACD,MAAM,IAAI,cAAc,UAAU,OAAO;EAC3C;EACA,MAAM,MAAM,MAAM,KAAK,SAAS,OAAO,OAAO;EAC9C,IAAI,CAAC,KACH,MAAM,IAAI,cAAc,UAAU,OAAO;EAE3C,OAAO;CACT;CAEA,MAAM,WAAW,OAAe,SAAiB,WAAqC;EACpF,MAAM,QAAQ,MAAM,KAAK,KAAK,SAAS,OAAO,OAAO;EACrD,OACE,UAAU,QACV,MAAM,WAAW,YACjB,MAAM,WAAW,WACjB,MAAM,eAAe;CAEzB;CAEA,MAAM,cAAc,OAAe,UAA6C;EAE9E,QAAO,MADa,KAAK,WAAW,OAAO,QAAQ,EAAA,CAEhD,QAAQ,UAAU,MAAM,WAAW,QAAQ,CAAC,CAC5C,KAAK,WAAW;GACf,IAAI,MAAM;GACV,MAAM,MAAM;GACZ,UAAU,MAAM;GAChB,aAAa,MAAM;GACnB,MAAM,MAAM;EACd,EAAE;CACN;CAEA,MAAM,UAAU,OAAe,SAAiB,WAAqC;EACnF,MAAM,QAAQ,MAAM,KAAK,KAAK,SAAS,OAAO,OAAO;EACrD,OACE,UAAU,QACV,MAAM,WAAW,YACjB,MAAM,SAAS,WACf,MAAM,WAAW,WACjB,MAAM,eAAe;CAEzB;CAEA,MAAM,iBACJ,OACA,UACqD;EACrD,IAAI,SAAS,WAAW,GACtB,OAAO,CAAC;EAEV,MAAM,OAAO,MAAM,KAAK,KAAK,UAAU,OAAO,CAAC,GAAG,IAAI,IAAI,QAAQ,CAAC,CAAC;EACpE,MAAM,UAAU,IAAI,IAClB,KACG,QAAQ,MAAM,EAAE,WAAW,aAAa,EAAE,WAAW,OAAO,CAAC,CAC7D,KAAK,MAAM,CAAC,EAAE,IAAI,EAAE,SAAS,CAAC,CACnC;EACA,OAAO,SAAS,SAAS,OACvB,QAAQ,IAAI,EAAE,IAAI,CAAC;GAAE;GAAI,WAAW,QAAQ,IAAI,EAAE,KAAK;EAAK,CAAC,IAAI,CAAC,CACpE;CACF;CAEA,MAAM,KAAK,OAAoB,OAAwC;EACrE,MAAM,QAAQ,MAAM;EACpB,IAAI,CAAC,MAAM,YAAY,IAAI,YAAY,GAAG;GACxC,IAAI,MAAM,cAAc,KAAA,MAAc,MAAM,UAAU,eAAe,WACnE,MAAM,IAAI,eAAe,qBAAqB;GAEhD,MAAM,KAAK,iBAAiB,OAAO,MAAM,WAAW,KAAK;EAC3D;EACA,MAAM,OAAO,MAAM,KAAK,KAAK,KAAK,OAAO;GAAE,GAAG;GAAO,QAAQ,MAAM,UAAU;EAAU,CAAC;EACxF,OAAO;GAAE,GAAG;GAAM,MAAM,MAAM,QAAQ,IAAI,KAAK,KAAK,KAAK,UAAU,KAAK,KAAK,KAAK,CAAC,CAAC;EAAE;CACxF;CAEA,MAAM,IAAI,IAAY,OAAkC;EACtD,MAAM,QAAQ,MAAM,KAAK,KAAK,SAAS,MAAM,OAAO,EAAE;EACtD,IAAI,CAAC,OACH,MAAM,IAAI,cAAc,SAAS,EAAE;EAErC,MAAM,KAAK,aAAa,OAAO,KAAK;EACpC,OAAO,KAAK,KAAK,KAAK;CACxB;CAEA,MAAM,WAAW,OAAe,UAAiE;EAC/F,MAAM,QAAQ,MAAM,KAAK,WAAW,OAAO,QAAQ;EACnD,OAAO,OAAO,YACZ,MAAM,KAAK,UAAU,CACnB,MAAM,IACN;GAAE,UAAU,MAAM;GAAU,aAAa,MAAM;GAAa,MAAM,MAAM;EAAK,CAC/E,CAAC,CACH;CACF;CAEA,MAAM,SACJ,OACA,SACA,OAA4B,CAAC,GACL;EACxB,MAAM,CAAC,SAAS,MAAM,KAAK,WAAW,OAAO,CAAC,OAAO,CAAC;EACtD,IAAI,CAAC,OAAO;GACV,KAAK,OAAO,KAAK,oCAAoC;IAAE;IAAO;GAAQ,CAAC;GACvE,OAAO;EACT;EACA,OAAO,KAAK,WAAW,KAAA,IACnB,KAAK,QAAQ,gBAAgB,EAAE,KAAK,MAAM,IAAI,CAAC,IAC/C,KAAK,QAAQ,gBAAgB;GAC3B,KAAK,MAAM;GACX,kBAAkB,KAAK;GACvB,kBAAkB,KAAK;EACzB,CAAC;CACP;CAEA,MAAM,iBACJ,OACA,SACA,UACe;EACf,IAAI;GACF,MAAM,SAAS,IAAI,IAAI,SAAS,KAAK,OAAO,CAAC,eAAe,OAAO,SAAS,EAAE,GAAG,EAAE,CAAC,CAAC;GACrF,MAAM,OAAO,MAAM,KAAK,QAAQ,KAAK,kBAAkB,OAAO,OAAO,CAAC;GACtE,KAAK,MAAM,UAAU,MACnB,IAAI,CAAC,OAAO,IAAI,OAAO,GAAG,GACxB,MAAM,KAAK,QAAQ,OAAO,OAAO,GAAG;GAGxC,MAAM,WAAW,IAAI,IAAI,KAAK,KAAK,WAAW,OAAO,GAAG,CAAC;GACzD,MAAM,UAAU,CAAC,GAAG,MAAM,CAAC,CAAC,QAAQ,CAAC,SAAS,CAAC,SAAS,IAAI,GAAG,CAAC;GAChE,IAAI,QAAQ,WAAW,GACrB;GAEF,MAAM,QAAQ,IAAI,KAEd,MAAM,KAAK,WACT,OACA,QAAQ,KAAK,GAAG,QAAQ,EAAE,CAC5B,EAAA,CACA,KAAK,UAAU,CAAC,MAAM,IAAI,KAAK,CAAC,CACpC;GACA,KAAK,MAAM,CAAC,KAAK,YAAY,SAAS;IACpC,MAAM,QAAQ,MAAM,IAAI,OAAO;IAC/B,IAAI,CAAC,SAAS,MAAM,SAAS,SAAS;KACpC,KAAK,OAAO,KAAK,+CAA+C;MAC9D;MACA;MACA;KACF,CAAC;KACD;IACF;IACA,MAAM,KAAK,QAAQ,QAAQ;KAAE,MAAM,MAAM;KAAK,IAAI;IAAI,CAAC;IACvD,KAAK,OAAO,KAAK,0BAA0B;KAAE;KAAO;KAAS;IAAQ,CAAC;GACxE;EACF,SAAS,KAAK;GACZ,KAAK,OAAO,MAAM,4BAA4B;IAAE;IAAO;IAAS;IAAU;GAAI,CAAC;EACjF;CACF;CAEA,MAAM,YAAY,SAAiB,MAA6B,OAA+B;EAC7F,MAAM,QAAQ,MAAM;EACpB,MAAM,QAAQ,MAAM,KAAK,KAAK,SAAS,OAAO,OAAO;EACrD,IAAI,CAAC,SAAS,MAAM,WAAW,WAAW;GACxC,KAAK,OAAO,KAAK,iDAAiD;IAAE;IAAO;GAAQ,CAAC;GACpF,MAAM,IAAI,cAAc,SAAS,OAAO;EAC1C;EACA,MAAM,KAAK,aAAa,OAAO,KAAK;EACpC,MAAM,MAAM,MAAM,KAAK,SACrB,OACA,SACA,KAAK,WAAW,EAAE,QAAQ,MAAM,SAAS,IAAI,CAAC,CAChD;EACA,IAAI,CAAC,KACH,MAAM,IAAI,cAAc,SAAS,OAAO;EAE1C,OAAO;CACT;CAEA,MAAM,OAAO,IAAY,OAA6B;EACpD,MAAM,YAAY,QAAQ,cAAc;EACxC,MAAM,KAAK,YAAY,MAAM,OAAO,IAAI,MAAM,EAAE;CAClD;CAEA,MAAM,UAAU,IAAY,OAA6B;EACvD,MAAM,QAAQ,MAAM,KAAK,KAAK,SAAS,MAAM,OAAO,EAAE;EACtD,IAAI,CAAC,SAAS,MAAM,WAAW,YAAY,MAAM,eAAe,MAAM,IACpE,MAAM,IAAI,cAAc,SAAS,EAAE;EAErC,MAAM,KAAK,YAAY,MAAM,OAAO,IAAI,MAAM,EAAE;CAClD;CAEA,MAAc,YAAY,OAAe,IAAY,SAAgC;EACnF,MAAM,QAAQ,MAAM,KAAK,KAAK,SAAS,OAAO,EAAE;EAChD,IAAI,CAAC,SAAS,CAAE,MAAM,KAAK,QAAQ,OAAO,KAAK,GAC7C,MAAM,IAAI,cAAc,SAAS,EAAE;EAErC,KAAK,OAAO,KAAK,iBAAiB;GAAE;GAAO,SAAS;GAAI;EAAQ,CAAC;CACnE;CAEA,MAAc,aAAa,OAAc,OAA6B;EACpE,IAAI,MAAM,YAAY,IAAI,YAAY,GACpC;EAEF,IAAI,MAAM,cAAc,MAAM;GAC5B,KAAK,OAAO,KAAK,iBAAiB;IAChC,OAAO,MAAM;IACb,SAAS,MAAM;IACf,WAAW,MAAM;GACnB,CAAC;GACD,MAAM,IAAI,eAAe,qBAAqB;EAChD;EACA,MAAM,KAAK,iBAAiB,MAAM,OAAO,MAAM,WAAW,KAAK;CACjE;CAEA,MAAc,iBAAiB,OAAe,WAAmB,OAA6B;EAC5F,MAAM,OAAO,MAAM,KAAK,KAAK,cAAc,OAAO,SAAS;EAC3D,IAAI,CAAC,QAAQ,CAAC,MAAM,YAAY,IAAI,iBAAiB,OAAO,SAAS,GAAG;GACtE,KAAK,OAAO,KAAK,iBAAiB;IAAE;IAAO;IAAW,WAAW,MAAM;GAAG,CAAC;GAC3E,MAAM,IAAI,eACR,OAAO,0BAA0B,iBAAiB,UAAU,qBAC9D;EACF;CACF;CAEA,MAAM,iBAAiB,OAAe,KAA4B;EAChE,MAAM,QAAQ,MAAM,KAAK,KAAK,8BAA8B,OAAO,YAAY,GAAG,CAAC;EACnF,IAAI,UAAU;EACd,IAAI,SAAS;EACb,KAAK,MAAM,SAAS,OAClB,IAAI;GACF,IAAI,MAAM,KAAK,QAAQ,OAAO,KAAK,GACjC,WAAW;EAEf,SAAS,KAAK;GACZ,IAAI,EAAE,eAAe,gBAAgB;IACnC,UAAU;IACV,KAAK,OAAO,MAAM,iCAAiC;KAAE;KAAO,SAAS,MAAM;KAAI;IAAI,CAAC;GACtF;EACF;EAEF,MAAM,UAAU,MAAM,KAAK,mBAAmB,KAAK;EACnD,KAAK,MAAM,OAAO,SAChB,MAAM,KAAK,aAAa,OAAO,IAAI,MAAM,GAAG,CAAC,CAAC,IAAI,KAAK,IAAI,GAAG;EAEhE,IAAI,QAAQ;EACZ,KAAK,MAAM,OAAO,MAAM,KAAK,aAAa,OAAO,GAAG,GAClD,IAAI;GACF,MAAM,KAAK,QAAQ,OAAO,GAAG;GAC7B,SAAS;EACX,SAAS,KAAK;GACZ,UAAU;GACV,KAAK,OAAO,KAAK,4BAA4B;IAAE;IAAO;GAAI,CAAC;EAC7D;EAEF,IAAI,UAAU,KAAK,QAAQ,KAAK,QAAQ,SAAS,KAAK,SAAS,GAC7D,KAAK,OAAO,KAAK,8BAA8B;GAC7C;GACA,OAAO;GACP;GACA,cAAc,QAAQ;GACtB;EACF,CAAC;EAEH,OAAO,UAAU,QAAQ,QAAQ;CACnC;CAEA,MAAc,mBAAmB,OAAkC;EACjE,MAAM,UAAU,MAAM,KAAK,QAAQ,KAAK,GAAG,gBAAgB,MAAM,EAAE;EACnE,IAAI,QAAQ,WAAW,GACrB,OAAO,CAAC;EAEV,MAAM,aAAa,QAAgB,IAAI,MAAM,GAAG,CAAC,CAAC,IAAI,KAAK;EAC3D,MAAM,OAAO,IAAI,KAEb,MAAM,KAAK,WACT,OACA,QAAQ,KAAK,MAAM,UAAU,EAAE,GAAG,CAAC,CACrC,EAAA,CACA,KAAK,MAAM,EAAE,EAAE,CACnB;EACA,OAAO,QAAQ,QAAQ,MAAM,CAAC,KAAK,IAAI,UAAU,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,MAAM,EAAE,GAAG;CAC5E;CAEA,MAAc,aAAa,OAAe,KAA8B;EACtE,MAAM,SAAS,IAAI,QAAQ,IAAI,KAAK,QAAQ,sBAAsB;EAElE,MAAM,OAAM,MADU,KAAK,QAAQ,KAAK,GAAG,UAAU,KAAK,EAAE,SAAS,EAAA,CAElE,QAAQ,MAAM,EAAE,iBAAiB,KAAA,KAAa,KAAK,MAAM,EAAE,YAAY,IAAI,MAAM,CAAC,CAClF,KAAK,MAAM,EAAE,GAAG;EACnB,IAAI,IAAI,WAAW,GACjB,OAAO,CAAC;EAEV,MAAM,OAAO,IAAI,IAAI,MAAM,KAAK,KAAK,SAAS,OAAO,GAAG,CAAC;EACzD,OAAO,IAAI,QAAQ,QAAQ,CAAC,KAAK,IAAI,GAAG,CAAC;CAC3C;CAEA,MAAc,SAAS,OAAe,OAAqC;EACzE,IAAI,MAAM,WAAW,WACnB,OAAO;EAET,IAAI,MAAM,WAAW,UACnB,OAAO,KAAK,eAAe,OAAO,KAAK;EAEzC,MAAM,OAAO,MAAM,KAAK,QAAQ,KAAK,MAAM,GAAG;EAC9C,IAAI,CAAC,MAAM;GACT,KAAK,OAAO,MAAM,0BAA0B;IAAE;IAAO,SAAS,MAAM;GAAG,CAAC;GACxE,OAAO;EACT;EACA,MAAM,cAAc,KAAK,eAAe,MAAM;EAO9C,OAAO,MANa,KAAK,UAAU,OAAO,OAAO;GAC/C,KAAK,MAAM;GACX,MAAM,OAAO,WAAW;GACxB,MAAM,KAAK;GACX;EACF,CAAC,KACe,KAAK,KAAK,SAAS,OAAO,MAAM,EAAE;CACpD;CAEA,MAAc,eAAe,OAAe,OAAqC;EAC/E,IAAI,CAAE,MAAM,KAAK,QAAQ,KAAK,MAAM,GAAG,GAAI;GACzC,KAAK,OAAO,MAAM,0BAA0B;IAAE;IAAO,SAAS,MAAM;GAAG,CAAC;GACxE,OAAO;EACT;EACA,MAAM,MAAM,eAAe,OAAO,KAAK;EACvC,MAAM,KAAK,QAAQ,KAAK;GAAE,MAAM,MAAM;GAAK,IAAI;EAAI,CAAC;EACpD,MAAM,OAAO,MAAM,KAAK,QAAQ,KAAK,GAAG;EACxC,IAAI,CAAC,MACH,MAAM,IAAI,MAAM,yBAAyB;EAE3C,MAAM,cAAc,MAAM;EAC1B,MAAM,OAAO,aAAa,WAAW;EACrC,MAAM,QAAQ,SAAS,UAAU,KAAK,sBAAsB,KAAK;EACjE,MAAM,SACJ,KAAK,gBAAgB,KAAA,KAAa,KAAK,gBAAgB,cACnD,+CACA,KAAK,OAAO,QACV,QAAQ,SAAS,UAAU,UAAU,OAAO,eAAe,UAAU,KAAK,EAAE,UAC5E;EACR,IAAI,QAAQ;GACV,MAAM,KAAK,aAAa,OAAO,MAAM,IAAI,GAAG;GAC5C,MAAM,KAAK,QAAQ,OAAO,KAAK;GAC/B,KAAK,OAAO,KAAK,yBAAyB;IAAE;IAAO,SAAS,MAAM;IAAI;GAAO,CAAC;GAC9E,MAAM,IAAI,kBAAkB,MAAM;EACpC;EACA,MAAM,QAAQ,MAAM,KAAK,UAAU,OAAO,OAAO;GAAE;GAAK;GAAM,MAAM,KAAK;GAAM;EAAY,CAAC;EAC5F,IAAI,CAAC,OACH,OAAO,KAAK,KAAK,SAAS,OAAO,MAAM,EAAE;EAE3C,MAAM,KAAK,aAAa,OAAO,MAAM,IAAI,MAAM,GAAG;EAClD,OAAO;CACT;CAEA,MAAc,UACZ,OACA,OACA,OACuB;EACvB,MAAM,UAAU,MAAM,KAAK,IAAI,IAAI,OAAO,EAAE,QAAQ,aAAa;GAC/D,MAAM,UAAU,MAAM,OAAO,UAAU,OAAO,MAAM,IAAI,KAAK;GAC7D,IAAI,SACF,MAAM,OAAO,OAAO,CAAC,YAAY,WAAW,KAAK;IAAE;IAAO,MAAM;GAAQ,CAAC,CAAC,CAAC;GAE7E,OAAO;EACT,CAAC;EACD,IAAI,SACF,KAAK,OAAO,KAAK,mBAAmB;GAAE;GAAO,SAAS,MAAM;EAAG,CAAC;EAElE,OAAO;CACT;CAEA,MAAc,WAAW,OAAe,UAA+C;EACrF,MAAM,MAAM,CAAC,GAAG,IAAI,IAAI,QAAQ,CAAC;EACjC,IAAI,IAAI,WAAW,GACjB,OAAO,CAAC;EAGV,QAAO,MADY,KAAK,KAAK,UAAU,OAAO,GAAG,EAAA,CACrC,QAAQ,UAAU,MAAM,UAAU,SAAS,MAAM,WAAW,OAAO;CACjF;CAEA,KAAa,OAAyB;EACpC,IAAI,MAAM,WAAW,aAAa,MAAM,WAAW,SACjD,OAAO;GAAE,GAAG;GAAO,KAAK;GAAM,aAAa;EAAK;EAElD,OAAO;GACL,GAAG;GACH,KAAK,KAAK,SAAS,YAAY,MAAM,EAAE;GACvC,aAAa,KAAK,SAAS,gBAAgB,MAAM,EAAE;EACrD;CACF;CAEA,MAAc,QAAQ,OAAe,OAAgC;EACnE,MAAM,UAAU,MAAM,KAAK,IAAI,IAAI,OAAO,EAAE,QAAQ,aAAa;GAC/D,MAAM,UAAU,MAAM,OAAO,OAAO,OAAO,MAAM,EAAE;GACnD,IAAI,SACF,MAAM,OAAO,OAAO,CAAC,YAAY,aAAa,KAAK;IAAE;IAAO,MAAM;GAAM,CAAC,CAAC,CAAC;GAE7E,OAAO;EACT,CAAC;EACD,IAAI,SAAS;GACX,MAAM,KAAK,aAAa,OAAO,MAAM,IAAI,MAAM,GAAG;GAClD,MAAM,KAAK,mBAAmB,OAAO,MAAM,EAAE;EAC/C;EACA,OAAO;CACT;CAEA,MAAc,mBAAmB,OAAe,SAAgC;EAC9E,IAAI;GACF,MAAM,SAAS,MAAM,KAAK,QAAQ,KAAK,GAAG,gBAAgB,MAAM,EAAE;GAClE,KAAK,MAAM,QAAQ,OAAO,QAAQ,MAAM,EAAE,IAAI,SAAS,IAAI,SAAS,CAAC,GACnE,MAAM,KAAK,aAAa,OAAO,SAAS,KAAK,GAAG;EAEpD,SAAS,KAAK;GACZ,KAAK,OAAO,KAAK,6BAA6B;IAAE;IAAO;IAAS;GAAI,CAAC;EACvE;CACF;CAEA,MAAc,aAAa,OAAe,SAAiB,KAA4B;EACrF,IAAI;GACF,MAAM,KAAK,QAAQ,OAAO,GAAG;EAC/B,SAAS,OAAO;GACd,KAAK,OAAO,KAAK,6BAA6B;IAAE;IAAO;IAAS,KAAK;GAAM,CAAC;EAC9E;CACF;AACF;AAEA,SAAS,UAAU,OAAuB;CACxC,MAAM,QAAQ;EAAC;EAAK;EAAM;EAAM;CAAI;CACpC,IAAI,QAAQ;CACZ,IAAI,OAAO;CACX,OAAO,SAAS,QAAQ,OAAO,MAAM,SAAS,GAAG;EAC/C,SAAS;EACT,QAAQ;CACV;CACA,OAAO,GAAG,OAAO,UAAU,KAAK,IAAI,QAAQ,MAAM,QAAQ,CAAC,EAAE,GAAG,MAAM;AACxE"}
1
+ {"version":3,"file":"service.js","names":[],"sources":["../../src/assets/service.ts"],"sourcesContent":["import { genId } from '../shared/id.js';\nimport type { Logger, ObjectStorage } from '../shared/ports.js';\nimport { PUBLIC_PREFIX } from '../types/index.js';\nimport {\n ConflictError,\n ForbiddenError,\n InvalidInputError,\n NotFoundError,\n} from '../shared/errors.js';\nimport type { Actor } from '../shared/actor.js';\nimport type { AssetSource, ContentType, Permission } from '../types/index.js';\nimport type {\n Asset,\n AssetKind,\n AssetView,\n AssetsQuery,\n Page,\n RequestUploadInput,\n UploadTicket,\n} from './model.js';\nimport type {\n AssetsRepository,\n AssetsService,\n AssetsUnitOfWork,\n FileLinkRequest,\n MemberUpload,\n MemberUploadInput,\n ReadyFile,\n} from './ports.js';\nimport { publicImageKey, publicImagePrefix } from './file-urls.js';\nimport { noopLogger } from '../shared/logger.js';\nimport { assetEvents } from './events.js';\n\nconst WRITE_PERMISSION: Record<ContentType, Permission> = {\n course: 'courses:write',\n download: 'downloads:write',\n community: 'communities:write',\n event: 'events:write',\n};\n\nconst UNATTACHED_UPLOAD_GRACE_MS = 60 * 60 * 1000;\n\nexport const MEMBER_IMAGE_TYPE = /^image\\/(png|jpeg|gif|webp|avif)$/i;\n\nexport function isMemberImageType(contentType: string): boolean {\n return MEMBER_IMAGE_TYPE.test(contentType.trim());\n}\n\nexport function memberKindOf(contentType: string): AssetKind {\n if (isMemberImageType(contentType)) {\n return 'image';\n }\n const kind = kindOf(contentType);\n return kind === 'image' ? 'document' : kind;\n}\n\nexport function kindOf(contentType: string): AssetKind {\n const type = contentType.trim().toLowerCase();\n if (type.startsWith('image/')) {\n return 'image';\n }\n if (type.startsWith('video/')) {\n return 'video';\n }\n if (type.startsWith('audio/')) {\n return 'audio';\n }\n return 'document';\n}\n\nfunction orgPrefix(orgId: string): string {\n return `org/${orgId}/`;\n}\n\nfunction sanitizeFilename(filename: string): string {\n const base = filename.split(/[\\\\/]/).pop() ?? 'file';\n return base.replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 120) || 'file';\n}\n\nfunction storedKey(orgId: string, folder: string, id: string, filename: string): string {\n return `${orgPrefix(orgId)}${folder}/${id}/${sanitizeFilename(filename)}`;\n}\n\nfunction finalMemberKey(orgId: string, asset: Asset): string {\n return `${orgPrefix(orgId)}${asset.kind}/${asset.id}/${sanitizeFilename(asset.filename)}`;\n}\n\nfunction graceCutoff(now: Date): Date {\n return new Date(now.getTime() - UNATTACHED_UPLOAD_GRACE_MS);\n}\n\nexport type AssetsServiceParams = {\n storage: ObjectStorage;\n repo: AssetsRepository;\n uow: AssetsUnitOfWork;\n memberImageMaxBytes: number;\n memberFileMaxBytes: number;\n /** A file link is signed for the window it is made in and the one after, so it lives\n * between one and two windows and is the same link all through its window. */\n linkWindowSeconds: number;\n logger?: Logger;\n now?: () => Date;\n};\n\nexport class AssetsServiceImpl implements AssetsService {\n private readonly storage: ObjectStorage;\n private readonly repo: AssetsRepository;\n private readonly uow: AssetsUnitOfWork;\n private readonly memberImageMaxBytes: number;\n private readonly memberFileMaxBytes: number;\n private readonly linkWindowMs: number;\n private readonly logger: Logger;\n private readonly now: () => Date;\n\n constructor(params: AssetsServiceParams) {\n if (!Number.isSafeInteger(params.memberImageMaxBytes) || params.memberImageMaxBytes <= 0) {\n throw new Error(\n `invalid memberImageMaxBytes \"${params.memberImageMaxBytes}\" (expected a positive integer)`,\n );\n }\n if (!Number.isSafeInteger(params.memberFileMaxBytes) || params.memberFileMaxBytes <= 0) {\n throw new Error(\n `invalid memberFileMaxBytes \"${params.memberFileMaxBytes}\" (expected a positive integer)`,\n );\n }\n if (!Number.isSafeInteger(params.linkWindowSeconds) || params.linkWindowSeconds <= 0) {\n throw new Error(\n `invalid linkWindowSeconds \"${params.linkWindowSeconds}\" (expected a positive integer)`,\n );\n }\n this.storage = params.storage;\n this.repo = params.repo;\n this.uow = params.uow;\n this.memberImageMaxBytes = params.memberImageMaxBytes;\n this.memberFileMaxBytes = params.memberFileMaxBytes;\n this.linkWindowMs = params.linkWindowSeconds * 1000;\n this.logger = params.logger ?? noopLogger;\n this.now = params.now ?? (() => new Date());\n }\n\n async requestUpload(input: RequestUploadInput, actor: Actor): Promise<UploadTicket> {\n const orgId = actor.orgId;\n if (input.contentId === undefined) {\n actor.permissions.require('media:write');\n } else {\n const type = await this.repo.contentTypeOf(orgId, input.contentId);\n if (!type) {\n throw new NotFoundError('Content item', input.contentId);\n }\n if (\n !actor.permissions.has('media:write') &&\n !actor.permissions.has(WRITE_PERMISSION[type], input.contentId)\n ) {\n this.logger.warn('asset upload refused', {\n orgId,\n orgUserId: actor.id,\n contentId: input.contentId,\n });\n throw new ForbiddenError(`You need media:write or ${WRITE_PERMISSION[type]}`);\n }\n }\n return this.createUpload(orgId, {\n filename: input.filename,\n contentType: input.contentType,\n source: 'library',\n uploadedBy: actor.id,\n contentId: input.contentId ?? null,\n });\n }\n\n async requestMemberUpload(input: MemberUploadInput, actor: Actor): Promise<UploadTicket> {\n return this.createUpload(actor.orgId, {\n filename: input.filename,\n contentType: input.contentType,\n source: 'member',\n uploadedBy: actor.id,\n contentId: null,\n });\n }\n\n private async createUpload(\n orgId: string,\n input: {\n filename: string;\n contentType: string;\n source: AssetSource;\n uploadedBy: string;\n contentId: string | null;\n },\n ): Promise<UploadTicket> {\n const id = genId('asset');\n const kind =\n input.source === 'member' ? memberKindOf(input.contentType) : kindOf(input.contentType);\n const folder = input.source === 'member' ? 'uploads' : kind;\n const key = storedKey(orgId, folder, id, input.filename);\n const presigned = await this.storage.presignUpload({ key, contentType: input.contentType });\n const now = new Date();\n const asset = await this.uow.run(async ({ assets, outbox }) => {\n const asset = await assets.insert(orgId, {\n orgId,\n id,\n key,\n kind,\n source: input.source,\n filename: input.filename,\n contentType: input.contentType,\n size: 0,\n status: 'pending',\n uploadedBy: input.uploadedBy,\n contentId: input.contentId,\n createdAt: now,\n updatedAt: now,\n });\n await outbox.append([assetEvents.assetCreated.make({ orgId, data: asset })]);\n return asset;\n });\n this.logger.info('asset upload requested', {\n orgId,\n assetId: id,\n kind,\n source: input.source,\n contentId: input.contentId,\n });\n return {\n asset: { ...asset, url: null, downloadUrl: null },\n uploadUrl: presigned.url,\n method: 'PUT',\n expiresInSeconds: presigned.expiresInSeconds,\n headers: presigned.headers,\n };\n }\n\n async confirm(id: string, actor: Actor): Promise<AssetView> {\n const orgId = actor.orgId;\n const asset = await this.repo.findById(orgId, id);\n if (!asset) {\n throw new NotFoundError('Asset', id);\n }\n await this.requireReach(asset, actor);\n if (asset.source === 'member') {\n throw new ForbiddenError('Only the uploader can finalize a member upload');\n }\n const confirmed = await this.finalize(orgId, asset);\n if (!confirmed) {\n throw new NotFoundError('Asset', id);\n }\n return this.view(confirmed);\n }\n\n async confirmOwn(id: string, actor: Actor): Promise<AssetView> {\n const orgId = actor.orgId;\n const asset = await this.repo.findById(orgId, id);\n if (!asset || asset.source !== 'member' || asset.uploadedBy !== actor.id) {\n this.logger.debug('upload confirm refused', { orgId, assetId: id, orgUserId: actor.id });\n throw new NotFoundError('Asset', id);\n }\n const confirmed = await this.finalize(orgId, asset);\n if (!confirmed) {\n throw new NotFoundError('Asset', id);\n }\n if (confirmed.status !== 'ready') {\n return { ...confirmed, url: null, downloadUrl: null };\n }\n const links = await this.signReady([confirmed], () => undefined);\n return { ...confirmed, url: links[confirmed.id] ?? null, downloadUrl: null };\n }\n\n async ownsUpload(orgId: string, assetId: string, orgUserId: string): Promise<boolean> {\n const asset = await this.repo.findById(orgId, assetId);\n return (\n asset !== null &&\n asset.source === 'member' &&\n asset.status === 'ready' &&\n asset.uploadedBy === orgUserId\n );\n }\n\n async memberUploads(orgId: string, assetIds: string[]): Promise<MemberUpload[]> {\n const ready = await this.readyAmong(orgId, assetIds);\n return ready\n .filter((asset) => asset.source === 'member')\n .map((asset) => ({\n id: asset.id,\n kind: asset.kind,\n filename: asset.filename,\n contentType: asset.contentType,\n size: asset.size,\n }));\n }\n\n async ownsImage(orgId: string, assetId: string, orgUserId: string): Promise<boolean> {\n const asset = await this.repo.findById(orgId, assetId);\n return (\n asset !== null &&\n asset.source === 'member' &&\n asset.kind === 'image' &&\n asset.status === 'ready' &&\n asset.uploadedBy === orgUserId\n );\n }\n\n async libraryAssetsFor(\n orgId: string,\n assetIds: string[],\n ): Promise<{ id: string; contentId: string | null }[]> {\n if (assetIds.length === 0) {\n return [];\n }\n const rows = await this.repo.findByIds(orgId, [...new Set(assetIds)]);\n const library = new Map(\n rows\n .filter((a) => a.source === 'library' && a.status === 'ready')\n .map((a) => [a.id, a.contentId]),\n );\n return assetIds.flatMap((id) =>\n library.has(id) ? [{ id, contentId: library.get(id) ?? null }] : [],\n );\n }\n\n async list(query: AssetsQuery, actor: Actor): Promise<Page<AssetView>> {\n const orgId = actor.orgId;\n if (!actor.permissions.has('media:read')) {\n if (query.contentId === undefined || (query.source ?? 'library') !== 'library') {\n throw new ForbiddenError('You need media:read');\n }\n await this.requireItemWrite(orgId, query.contentId, actor);\n }\n const page = await this.repo.list(orgId, { ...query, source: query.source ?? 'library' });\n return { ...page, rows: await this.views(page.rows) };\n }\n\n async get(id: string, actor: Actor): Promise<AssetView> {\n const asset = await this.repo.findById(actor.orgId, id);\n if (!asset) {\n throw new NotFoundError('Asset', id);\n }\n await this.requireReach(asset, actor);\n return this.view(asset);\n }\n\n async readyFiles(orgId: string, assetIds: readonly string[]): Promise<Record<string, ReadyFile>> {\n const ready = await this.readyAmong(orgId, assetIds);\n return Object.fromEntries(\n ready.map((asset) => [\n asset.id,\n { filename: asset.filename, contentType: asset.contentType, size: asset.size },\n ]),\n );\n }\n\n async fileLinks(\n orgId: string,\n files: readonly FileLinkRequest[],\n ): Promise<Record<string, string>> {\n const saveAs = new Map(files.map((file) => [file.assetId, file.saveAs]));\n const ready = await this.readyAmong(orgId, [...saveAs.keys()]);\n return this.signReady(ready, (asset) => saveAs.get(asset.id));\n }\n\n private async signReady(\n assets: readonly Asset[],\n saveAs: (asset: Asset) => string | undefined,\n ): Promise<Record<string, string>> {\n const signedAt = new Date(\n Math.floor(this.now().getTime() / this.linkWindowMs) * this.linkWindowMs,\n );\n const expiresAt = new Date(signedAt.getTime() + 2 * this.linkWindowMs);\n const links = await Promise.all(\n assets.map(async (asset) => {\n const filename = saveAs(asset);\n const url = await this.storage.presignDownload({\n key: asset.key,\n signedAt,\n expiresAt,\n ...(filename === undefined ? {} : { downloadFilename: filename }),\n });\n return [asset.id, url] as const;\n }),\n );\n return Object.fromEntries(links);\n }\n\n async syncPublicImages(\n orgId: string,\n ownerId: string,\n assetIds: readonly string[],\n ): Promise<void> {\n try {\n const wanted = new Map(assetIds.map((id) => [publicImageKey(orgId, ownerId, id), id]));\n const held = await this.storage.list(publicImagePrefix(orgId, ownerId));\n for (const object of held) {\n if (!wanted.has(object.key)) {\n await this.storage.remove(object.key);\n }\n }\n const heldKeys = new Set(held.map((object) => object.key));\n const missing = [...wanted].filter(([key]) => !heldKeys.has(key));\n if (missing.length === 0) {\n return;\n }\n const ready = new Map(\n (\n await this.readyAmong(\n orgId,\n missing.map(([, id]) => id),\n )\n ).map((asset) => [asset.id, asset]),\n );\n for (const [key, assetId] of missing) {\n const asset = ready.get(assetId);\n if (!asset || asset.kind !== 'image') {\n this.logger.warn('public image not published — no ready image', {\n orgId,\n ownerId,\n assetId,\n });\n continue;\n }\n await this.storage.publish({ from: asset.key, to: key });\n this.logger.info('public image published', { orgId, ownerId, assetId });\n }\n } catch (err) {\n this.logger.error('public images not synced', { orgId, ownerId, assetIds, err });\n }\n }\n\n async remove(id: string, actor: Actor): Promise<void> {\n actor.permissions.require('media:delete');\n await this.removeAsset(actor.orgId, id, actor.id);\n }\n\n async removeOwn(id: string, actor: Actor): Promise<void> {\n const asset = await this.repo.findById(actor.orgId, id);\n if (!asset || asset.source !== 'member' || asset.uploadedBy !== actor.id) {\n throw new NotFoundError('Asset', id);\n }\n await this.removeAsset(actor.orgId, id, actor.id);\n }\n\n private async removeAsset(orgId: string, id: string, actorId: string): Promise<void> {\n const asset = await this.repo.findById(orgId, id);\n if (!asset || !(await this.discard(orgId, asset))) {\n throw new NotFoundError('Asset', id);\n }\n this.logger.info('asset removed', { orgId, assetId: id, actorId });\n }\n\n private async requireReach(asset: Asset, actor: Actor): Promise<void> {\n if (actor.permissions.has('media:read')) {\n return;\n }\n if (asset.contentId === null) {\n this.logger.warn('asset refused', {\n orgId: actor.orgId,\n assetId: asset.id,\n orgUserId: actor.id,\n });\n throw new ForbiddenError('You need media:read');\n }\n await this.requireItemWrite(actor.orgId, asset.contentId, actor);\n }\n\n private async requireItemWrite(orgId: string, contentId: string, actor: Actor): Promise<void> {\n const type = await this.repo.contentTypeOf(orgId, contentId);\n if (!type || !actor.permissions.has(WRITE_PERMISSION[type], contentId)) {\n this.logger.warn('asset refused', { orgId, contentId, orgUserId: actor.id });\n throw new ForbiddenError(\n type ? `You need media:read or ${WRITE_PERMISSION[type]}` : 'You need media:read',\n );\n }\n }\n\n async removeUnattached(orgId: string, now: Date): Promise<number> {\n const stale = await this.repo.listUnreferencedMemberUploads(orgId, graceCutoff(now));\n let removed = 0;\n let failed = 0;\n for (const asset of stale) {\n try {\n if (await this.discard(orgId, asset)) {\n removed += 1;\n }\n } catch (err) {\n if (!(err instanceof ConflictError)) {\n failed += 1;\n this.logger.error('unattached upload not removed', { orgId, assetId: asset.id, err });\n }\n }\n }\n const orphans = await this.orphanPublicImages(orgId);\n for (const key of orphans) {\n await this.removeObject(orgId, key.split('/').pop() ?? '', key);\n }\n let stray = 0;\n for (const key of await this.strayUploads(orgId, now)) {\n try {\n await this.storage.remove(key);\n stray += 1;\n } catch (err) {\n failed += 1;\n this.logger.warn('stray upload not removed', { orgId, err });\n }\n }\n if (removed > 0 || stray > 0 || orphans.length > 0 || failed > 0) {\n this.logger.info('unattached uploads removed', {\n orgId,\n count: removed,\n stray,\n publicCopies: orphans.length,\n failed,\n });\n }\n return removed + stray + orphans.length;\n }\n\n private async orphanPublicImages(orgId: string): Promise<string[]> {\n const objects = await this.storage.list(`${PUBLIC_PREFIX}${orgId}/`);\n if (objects.length === 0) {\n return [];\n }\n const assetIdOf = (key: string) => key.split('/').pop() ?? '';\n const live = new Set(\n (\n await this.readyAmong(\n orgId,\n objects.map((o) => assetIdOf(o.key)),\n )\n ).map((a) => a.id),\n );\n return objects.filter((o) => !live.has(assetIdOf(o.key))).map((o) => o.key);\n }\n\n private async strayUploads(orgId: string, now: Date): Promise<string[]> {\n const cutoff = now.getTime() - this.storage.uploadExpirySeconds * 1000;\n const objects = await this.storage.list(`${orgPrefix(orgId)}uploads/`);\n const old = objects\n .filter((o) => o.lastModified !== undefined && Date.parse(o.lastModified) < cutoff)\n .map((o) => o.key);\n if (old.length === 0) {\n return [];\n }\n const held = new Set(await this.repo.heldKeys(orgId, old));\n return old.filter((key) => !held.has(key));\n }\n\n private async finalize(orgId: string, asset: Asset): Promise<Asset | null> {\n if (asset.status !== 'pending') {\n return asset;\n }\n if (asset.source === 'member') {\n return this.finalizeMember(orgId, asset);\n }\n const stat = await this.storage.stat(asset.key);\n if (!stat) {\n this.logger.debug('asset not yet uploaded', { orgId, assetId: asset.id });\n return asset;\n }\n const contentType = stat.contentType ?? asset.contentType;\n const ready = await this.markReady(orgId, asset, {\n key: asset.key,\n kind: kindOf(contentType),\n size: stat.size,\n contentType,\n });\n return ready ?? this.repo.findById(orgId, asset.id);\n }\n\n private async finalizeMember(orgId: string, asset: Asset): Promise<Asset | null> {\n if (!(await this.storage.stat(asset.key))) {\n this.logger.debug('asset not yet uploaded', { orgId, assetId: asset.id });\n return asset;\n }\n const key = finalMemberKey(orgId, asset);\n await this.storage.copy({ from: asset.key, to: key });\n const stat = await this.storage.stat(key);\n if (!stat) {\n throw new Error('copied upload not found');\n }\n const contentType = asset.contentType;\n const kind = memberKindOf(contentType);\n const limit = kind === 'image' ? this.memberImageMaxBytes : this.memberFileMaxBytes;\n const reason =\n stat.contentType !== undefined && stat.contentType !== contentType\n ? 'The uploaded file is not the declared type'\n : stat.size > limit\n ? `This ${kind === 'image' ? 'image' : 'file'} is over the ${sizeLabel(limit)} limit`\n : null;\n if (reason) {\n await this.removeObject(orgId, asset.id, key);\n await this.discard(orgId, asset);\n this.logger.info('member upload refused', { orgId, assetId: asset.id, reason });\n throw new InvalidInputError(reason);\n }\n const ready = await this.markReady(orgId, asset, { key, kind, size: stat.size, contentType });\n if (!ready) {\n return this.repo.findById(orgId, asset.id);\n }\n await this.removeObject(orgId, asset.id, asset.key);\n return ready;\n }\n\n private async markReady(\n orgId: string,\n asset: Asset,\n patch: Pick<Asset, 'key' | 'kind' | 'size' | 'contentType'>,\n ): Promise<Asset | null> {\n const updated = await this.uow.run(async ({ assets, outbox }) => {\n const updated = await assets.markReady(orgId, asset.id, patch);\n if (updated) {\n await outbox.append([assetEvents.assetReady.make({ orgId, data: updated })]);\n }\n return updated;\n });\n if (updated) {\n this.logger.info('asset confirmed', { orgId, assetId: asset.id });\n }\n return updated;\n }\n\n private async readyAmong(orgId: string, assetIds: readonly string[]): Promise<Asset[]> {\n const ids = [...new Set(assetIds)];\n if (ids.length === 0) {\n return [];\n }\n const rows = await this.repo.findByIds(orgId, ids);\n return rows.filter((asset) => asset.orgId === orgId && asset.status === 'ready');\n }\n\n private async view(asset: Asset): Promise<AssetView> {\n const [view] = await this.views([asset]);\n return view!;\n }\n\n private async views(assets: readonly Asset[]): Promise<AssetView[]> {\n const served = assets.filter((asset) => asset.source === 'library' && asset.status === 'ready');\n const [urls, downloadUrls] = await Promise.all([\n this.signReady(served, () => undefined),\n this.signReady(served, (asset) => asset.filename),\n ]);\n return assets.map((asset) => ({\n ...asset,\n url: urls[asset.id] ?? null,\n downloadUrl: downloadUrls[asset.id] ?? null,\n }));\n }\n\n private async discard(orgId: string, asset: Asset): Promise<boolean> {\n const deleted = await this.uow.run(async ({ assets, outbox }) => {\n const deleted = await assets.delete(orgId, asset.id);\n if (deleted) {\n await outbox.append([assetEvents.assetDeleted.make({ orgId, data: asset })]);\n }\n return deleted;\n });\n if (deleted) {\n await this.removeObject(orgId, asset.id, asset.key);\n await this.removePublicCopies(orgId, asset.id);\n }\n return deleted;\n }\n\n private async removePublicCopies(orgId: string, assetId: string): Promise<void> {\n try {\n const copies = await this.storage.list(`${PUBLIC_PREFIX}${orgId}/`);\n for (const copy of copies.filter((o) => o.key.endsWith(`/${assetId}`))) {\n await this.removeObject(orgId, assetId, copy.key);\n }\n } catch (err) {\n this.logger.warn('public copies not removed', { orgId, assetId, err });\n }\n }\n\n private async removeObject(orgId: string, assetId: string, key: string): Promise<void> {\n try {\n await this.storage.remove(key);\n } catch (error) {\n this.logger.warn('stored object not removed', { orgId, assetId, err: error });\n }\n }\n}\n\nfunction sizeLabel(bytes: number): string {\n const units = ['B', 'KB', 'MB', 'GB'];\n let value = bytes;\n let unit = 0;\n while (value >= 1024 && unit < units.length - 1) {\n value /= 1024;\n unit += 1;\n }\n return `${Number.isInteger(value) ? value : value.toFixed(1)} ${units[unit]}`;\n}\n"],"mappings":"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"}
@@ -1,19 +1,19 @@
1
1
  import { Page } from "../types/shared.js";
2
2
  import { Automation, AutomationAction, AutomationActionResult, AutomationKind, AutomationRun, AutomationRunStatus, AutomationRunsQuery, AutomationTrigger, AutomationsQuery, AvailableAction, AvailableActions, AvailableTriggers, CreateAutomationInput, CreateWebhookInput, CreatedWebhook, UpdateAutomationInput, UpdateWebhookInput, Webhook, WebhookSecret } from "../types/schemas/automations.js";
3
- import { AutomationDispatch, AutomationEngine, AutomationExecutor, DeliveryDispatch, WebhookRequest, WebhookResponse, WebhookSender } from "../types/automations.js";
3
+ import { AutomationDispatch, AutomationEngine, AutomationExecutor, DeliveryDispatch, DeliveryExecutor, WebhookRequest, WebhookResponse, WebhookSender } from "../types/automations.js";
4
4
  import { ACTION_FAILED, ActionFailedError, AutomationKindMismatchError, InvalidTriggerError, ReservedActionError, actionFailureMessage } from "./model.js";
5
5
  import "./types.js";
6
6
  import { AutomationRunsRepository, AutomationsRepository, AutomationsService, AutomationsUnitOfWork, ChatDebounce, ListedNotification, NewAutomation, NewAutomationRun, NewDelivery, NewNotification, NotificationCategories, NotificationPage, NotificationReads, NotificationsRepository, NotificationsService, RecipientRule, UnreadCounts } from "./ports.js";
7
7
  import { CommunityMailReads, GrantWriter } from "./community-mail.js";
8
8
  import { AutomationActionFailed, AutomationCreated, AutomationDeleted, AutomationDisabled, AutomationEnabled, AutomationEvent, AutomationRunCompleted, AutomationRunFailed, AutomationRunStarted, AutomationUpdated, automationEvents } from "./events.js";
9
- import { CHANNEL_NOT_CONFIGURED, NOT_ACCEPTED, NO_ADDRESS, NotificationDelivery } from "./notifications/delivery.js";
10
- import { DEVICE_WINDOW_MS, IN_APP, MESSAGE_SENT, NotificationProcessor, PUSH } from "./notifications/processor.js";
11
9
  import { AutomationsServiceImpl } from "./service.js";
12
10
  import { cronMatches } from "./schedule.js";
13
11
  import { NotificationsServiceImpl } from "./notifications/service.js";
12
+ import { CHANNEL_NOT_CONFIGURED, NOT_ACCEPTED, NO_ADDRESS, NotificationDelivery } from "./notifications/delivery.js";
13
+ import { DEVICE_WINDOW_MS, IN_APP, MESSAGE_SENT, NotificationProcessor, PUSH } from "./notifications/processor.js";
14
14
  import { CHAT_QUIET_MS, FLUSH_JOB, SchedulerChatDebounce, flushKey, notificationJobs } from "./notifications/debounce.js";
15
15
  import { RECIPIENT_RULES } from "./notifications/rules/index.js";
16
16
  import { NOTIFICATION_TYPES } from "./notifications/types.js";
17
17
  import { DEFAULT_COMMUNITY_CATEGORIES, DEFAULT_ORG_CATEGORIES } from "./notifications/defaults.js";
18
18
  import { applicableCategories } from "./notifications/applicable.js";
19
- export { ACTION_FAILED, ActionFailedError, type Automation, type AutomationAction, type AutomationActionFailed, type AutomationActionResult, type AutomationCreated, type AutomationDeleted, type AutomationDisabled, type AutomationDispatch, type AutomationEnabled, type AutomationEngine, type AutomationEvent, type AutomationExecutor, type AutomationKind, AutomationKindMismatchError, type AutomationRun, type AutomationRunCompleted, type AutomationRunFailed, type AutomationRunStarted, type AutomationRunStatus, type AutomationRunsQuery, type AutomationRunsRepository, type AutomationTrigger, type AutomationUpdated, type AutomationsQuery, type AutomationsRepository, type AutomationsService, AutomationsServiceImpl, type AutomationsUnitOfWork, type AvailableAction, type AvailableActions, type AvailableTriggers, CHANNEL_NOT_CONFIGURED, CHAT_QUIET_MS, type ChatDebounce, type CommunityMailReads, type CreateAutomationInput, type CreateWebhookInput, type CreatedWebhook, DEFAULT_COMMUNITY_CATEGORIES, DEFAULT_ORG_CATEGORIES, DEVICE_WINDOW_MS, type DeliveryDispatch, FLUSH_JOB, type GrantWriter, IN_APP, InvalidTriggerError, type ListedNotification, MESSAGE_SENT, NOTIFICATION_TYPES, NOT_ACCEPTED, NO_ADDRESS, type NewAutomation, type NewAutomationRun, type NewDelivery, type NewNotification, type NotificationCategories, NotificationDelivery, type NotificationPage, NotificationProcessor, type NotificationReads, type NotificationsRepository, type NotificationsService, NotificationsServiceImpl, PUSH, type Page, RECIPIENT_RULES, type RecipientRule, ReservedActionError, SchedulerChatDebounce, type UnreadCounts, type UpdateAutomationInput, type UpdateWebhookInput, type Webhook, type WebhookRequest, type WebhookResponse, type WebhookSecret, type WebhookSender, actionFailureMessage, applicableCategories, automationEvents, cronMatches, flushKey, notificationJobs };
19
+ export { ACTION_FAILED, ActionFailedError, type Automation, type AutomationAction, type AutomationActionFailed, type AutomationActionResult, type AutomationCreated, type AutomationDeleted, type AutomationDisabled, type AutomationDispatch, type AutomationEnabled, type AutomationEngine, type AutomationEvent, type AutomationExecutor, type AutomationKind, AutomationKindMismatchError, type AutomationRun, type AutomationRunCompleted, type AutomationRunFailed, type AutomationRunStarted, type AutomationRunStatus, type AutomationRunsQuery, type AutomationRunsRepository, type AutomationTrigger, type AutomationUpdated, type AutomationsQuery, type AutomationsRepository, type AutomationsService, AutomationsServiceImpl, type AutomationsUnitOfWork, type AvailableAction, type AvailableActions, type AvailableTriggers, CHANNEL_NOT_CONFIGURED, CHAT_QUIET_MS, type ChatDebounce, type CommunityMailReads, type CreateAutomationInput, type CreateWebhookInput, type CreatedWebhook, DEFAULT_COMMUNITY_CATEGORIES, DEFAULT_ORG_CATEGORIES, DEVICE_WINDOW_MS, type DeliveryDispatch, type DeliveryExecutor, FLUSH_JOB, type GrantWriter, IN_APP, InvalidTriggerError, type ListedNotification, MESSAGE_SENT, NOTIFICATION_TYPES, NOT_ACCEPTED, NO_ADDRESS, type NewAutomation, type NewAutomationRun, type NewDelivery, type NewNotification, type NotificationCategories, NotificationDelivery, type NotificationPage, NotificationProcessor, type NotificationReads, type NotificationsRepository, type NotificationsService, NotificationsServiceImpl, PUSH, type Page, RECIPIENT_RULES, type RecipientRule, ReservedActionError, SchedulerChatDebounce, type UnreadCounts, type UpdateAutomationInput, type UpdateWebhookInput, type Webhook, type WebhookRequest, type WebhookResponse, type WebhookSecret, type WebhookSender, actionFailureMessage, applicableCategories, automationEvents, cronMatches, flushKey, notificationJobs };
@@ -1,5 +1,5 @@
1
1
  import { Logger } from "../../types/ports.js";
2
- import { DeliveryDispatch } from "../../types/automations.js";
2
+ import { DeliveryDispatch, DeliveryExecutor } from "../../types/automations.js";
3
3
  import "../../types/index.js";
4
4
  import { ChannelRegistry } from "../../shared/ports.js";
5
5
  import { DevicesService } from "../../identity/ports.js";
@@ -15,13 +15,13 @@ type NotificationDeliveryParams = {
15
15
  devices: Pick<DevicesService, 'removeDead'>;
16
16
  logger?: Logger;
17
17
  };
18
- declare class NotificationDelivery {
18
+ declare class NotificationDelivery implements DeliveryExecutor {
19
19
  private readonly repo;
20
20
  private readonly channels;
21
21
  private readonly devices;
22
22
  private readonly logger;
23
23
  constructor(params: NotificationDeliveryParams);
24
- run(d: DeliveryDispatch, lastAttempt: boolean): Promise<void>;
24
+ runDelivery(d: DeliveryDispatch, lastAttempt: boolean): Promise<void>;
25
25
  }
26
26
  //#endregion
27
27
  export { CHANNEL_NOT_CONFIGURED, NOT_ACCEPTED, NO_ADDRESS, NotificationDelivery, NotificationDeliveryParams };