@headless-lms/core 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/events.d.ts +15 -9
- package/dist/assets/file-urls.d.ts +14 -7
- package/dist/assets/file-urls.js +15 -4
- package/dist/assets/file-urls.js.map +1 -1
- package/dist/assets/index.d.ts +4 -4
- package/dist/assets/index.js +3 -3
- package/dist/assets/ports.d.ts +34 -15
- package/dist/assets/service.d.ts +30 -14
- package/dist/assets/service.js +233 -54
- package/dist/assets/service.js.map +1 -1
- package/dist/automations/actions.js.map +1 -1
- package/dist/automations/catalog.js +68 -3
- package/dist/automations/catalog.js.map +1 -1
- package/dist/automations/community-derivations.js +16 -16
- package/dist/automations/community-derivations.js.map +1 -1
- package/dist/automations/community-mail.d.ts +34 -4
- package/dist/automations/community-mail.js +32 -4
- package/dist/automations/community-mail.js.map +1 -1
- package/dist/automations/event-permissions.d.ts +12 -0
- package/dist/automations/event-permissions.js +38 -0
- package/dist/automations/event-permissions.js.map +1 -0
- package/dist/automations/index.d.ts +2 -1
- package/dist/automations/index.js +2 -1
- package/dist/automations/notifications/applicable.d.ts +8 -0
- package/dist/automations/notifications/applicable.js +12 -0
- package/dist/automations/notifications/applicable.js.map +1 -0
- package/dist/automations/notifications/debounce.js +1 -1
- package/dist/automations/notifications/debounce.js.map +1 -1
- package/dist/automations/notifications/defaults.js +33 -3
- package/dist/automations/notifications/defaults.js.map +1 -1
- package/dist/automations/notifications/processor.js +1 -1
- package/dist/automations/notifications/processor.js.map +1 -1
- package/dist/automations/notifications/rules/content.js +60 -3
- package/dist/automations/notifications/rules/content.js.map +1 -1
- package/dist/automations/notifications/rules/discussion.js +29 -29
- package/dist/automations/notifications/rules/discussion.js.map +1 -1
- package/dist/automations/notifications/rules/entitlements.js +36 -9
- package/dist/automations/notifications/rules/entitlements.js.map +1 -1
- package/dist/automations/notifications/rules/events.d.ts +9 -0
- package/dist/automations/notifications/rules/events.js +27 -0
- package/dist/automations/notifications/rules/events.js.map +1 -0
- package/dist/automations/notifications/rules/organizations.js +17 -10
- package/dist/automations/notifications/rules/organizations.js.map +1 -1
- package/dist/automations/notifications/rules/recipients.js +0 -1
- package/dist/automations/notifications/rules/recipients.js.map +1 -1
- package/dist/automations/notifications/service.d.ts +8 -6
- package/dist/automations/notifications/service.js +12 -12
- package/dist/automations/notifications/service.js.map +1 -1
- package/dist/automations/notifications/types.js +94 -7
- package/dist/automations/notifications/types.js.map +1 -1
- package/dist/automations/ports.d.ts +30 -31
- package/dist/automations/service.d.ts +20 -17
- package/dist/automations/service.js +90 -61
- package/dist/automations/service.js.map +1 -1
- package/dist/content/event-sight.d.ts +11 -0
- package/dist/content/event-sight.js +11 -0
- package/dist/content/event-sight.js.map +1 -0
- package/dist/content/events.d.ts +465 -88
- package/dist/content/events.js +34 -1
- package/dist/content/events.js.map +1 -1
- package/dist/content/index.d.ts +6 -4
- package/dist/content/index.js +3 -1
- package/dist/content/jobs.d.ts +16 -0
- package/dist/content/jobs.js +37 -0
- package/dist/content/jobs.js.map +1 -0
- package/dist/content/model.d.ts +2 -2
- package/dist/content/notification-categories.d.ts +5 -4
- package/dist/content/notification-categories.js +15 -6
- package/dist/content/notification-categories.js.map +1 -1
- package/dist/content/ports.d.ts +113 -68
- package/dist/content/service.d.ts +103 -67
- package/dist/content/service.js +739 -152
- package/dist/content/service.js.map +1 -1
- package/dist/content/titles.d.ts +6 -3
- package/dist/content/titles.js +37 -24
- package/dist/content/titles.js.map +1 -1
- package/dist/content/types.d.ts +2 -2
- package/dist/discussion/authors.d.ts +4 -2
- package/dist/discussion/authors.js +9 -5
- package/dist/discussion/authors.js.map +1 -1
- package/dist/discussion/chats.d.ts +56 -37
- package/dist/discussion/chats.js +392 -160
- package/dist/discussion/chats.js.map +1 -1
- package/dist/discussion/events.d.ts +85 -47
- package/dist/discussion/events.js +41 -3
- package/dist/discussion/events.js.map +1 -1
- package/dist/discussion/index.d.ts +8 -5
- package/dist/discussion/index.js +5 -3
- package/dist/discussion/live.d.ts +106 -0
- package/dist/discussion/live.js +642 -0
- package/dist/discussion/live.js.map +1 -0
- package/dist/discussion/mentions.js +2 -2
- package/dist/discussion/mentions.js.map +1 -1
- package/dist/discussion/ports.d.ts +182 -100
- package/dist/discussion/ports.js.map +1 -1
- package/dist/discussion/posts.d.ts +45 -39
- package/dist/discussion/posts.js +266 -152
- package/dist/discussion/posts.js.map +1 -1
- package/dist/discussion/rules.d.ts +18 -0
- package/dist/discussion/rules.js +22 -0
- package/dist/discussion/rules.js.map +1 -0
- package/dist/discussion/service.d.ts +37 -31
- package/dist/discussion/service.js +250 -186
- package/dist/discussion/service.js.map +1 -1
- package/dist/discussion/subscriptions.d.ts +8 -8
- package/dist/discussion/subscriptions.js +28 -12
- package/dist/discussion/subscriptions.js.map +1 -1
- package/dist/discussion/titles.d.ts +4 -2
- package/dist/discussion/titles.js +27 -13
- package/dist/discussion/titles.js.map +1 -1
- package/dist/entitlements/events.d.ts +58 -58
- package/dist/entitlements/index.d.ts +4 -4
- package/dist/entitlements/index.js +2 -2
- package/dist/entitlements/model.d.ts +2 -2
- package/dist/entitlements/ports.d.ts +65 -39
- package/dist/entitlements/rules.d.ts +3 -6
- package/dist/entitlements/rules.js +5 -10
- package/dist/entitlements/rules.js.map +1 -1
- package/dist/entitlements/service.d.ts +40 -28
- package/dist/entitlements/service.js +369 -162
- package/dist/entitlements/service.js.map +1 -1
- package/dist/identity/devices.d.ts +3 -3
- package/dist/identity/devices.js +6 -4
- package/dist/identity/devices.js.map +1 -1
- package/dist/identity/index.d.ts +2 -2
- package/dist/identity/index.js +2 -2
- package/dist/identity/notification-settings.d.ts +1 -1
- package/dist/identity/notification-settings.js +2 -1
- package/dist/identity/notification-settings.js.map +1 -1
- package/dist/identity/ports.d.ts +7 -7
- package/dist/identity/service.d.ts +8 -7
- package/dist/identity/service.js +16 -12
- package/dist/identity/service.js.map +1 -1
- package/dist/integrations/events.d.ts +3 -3
- package/dist/integrations/ports.d.ts +9 -8
- package/dist/integrations/service.d.ts +9 -8
- package/dist/integrations/service.js +31 -14
- package/dist/integrations/service.js.map +1 -1
- package/dist/organizations/default-roles.d.ts +22 -0
- package/dist/organizations/default-roles.js +89 -0
- package/dist/organizations/default-roles.js.map +1 -0
- package/dist/organizations/effective-permissions.d.ts +47 -0
- package/dist/organizations/effective-permissions.js +118 -0
- package/dist/organizations/effective-permissions.js.map +1 -0
- package/dist/organizations/events.d.ts +326 -181
- package/dist/organizations/events.js +90 -18
- package/dist/organizations/events.js.map +1 -1
- package/dist/organizations/index.d.ts +14 -10
- package/dist/organizations/index.js +7 -3
- package/dist/organizations/members.d.ts +66 -8
- package/dist/organizations/members.js.map +1 -1
- package/dist/organizations/model.d.ts +2 -2
- package/dist/organizations/notification-catalog.d.ts +31 -0
- package/dist/organizations/notification-catalog.js +32 -0
- package/dist/organizations/notification-catalog.js.map +1 -0
- package/dist/organizations/notification-categories.d.ts +6 -4
- package/dist/organizations/notification-categories.js +25 -10
- package/dist/organizations/notification-categories.js.map +1 -1
- package/dist/organizations/org-roles.d.ts +33 -0
- package/dist/organizations/org-roles.js +49 -0
- package/dist/organizations/org-roles.js.map +1 -0
- package/dist/organizations/permissions.d.ts +311 -0
- package/dist/organizations/permissions.js +327 -0
- package/dist/organizations/permissions.js.map +1 -0
- package/dist/organizations/ports.d.ts +187 -65
- package/dist/organizations/service.d.ts +102 -35
- package/dist/organizations/service.js +1254 -327
- package/dist/organizations/service.js.map +1 -1
- package/dist/organizations/testing.d.ts +18 -0
- package/dist/organizations/testing.js +37 -0
- package/dist/organizations/testing.js.map +1 -0
- package/dist/organizations/types.d.ts +5 -16
- package/dist/progress/events.d.ts +14 -14
- package/dist/progress/index.d.ts +2 -2
- package/dist/progress/ports.d.ts +11 -3
- package/dist/progress/service.d.ts +6 -5
- package/dist/progress/service.js +21 -6
- package/dist/progress/service.js.map +1 -1
- package/dist/reporting/admin/index.d.ts +6 -4
- package/dist/reporting/admin/model.d.ts +36 -3
- package/dist/reporting/admin/ports.d.ts +39 -14
- package/dist/reporting/admin/service.d.ts +32 -11
- package/dist/reporting/admin/service.js +102 -18
- package/dist/reporting/admin/service.js.map +1 -1
- package/dist/reporting/events/index.d.ts +27 -0
- package/dist/reporting/events/index.js +20 -0
- package/dist/reporting/events/index.js.map +1 -0
- package/dist/reporting/member/calendar.d.ts +11 -0
- package/dist/reporting/member/calendar.js +75 -0
- package/dist/reporting/member/calendar.js.map +1 -0
- package/dist/reporting/member/index.d.ts +3 -2
- package/dist/reporting/member/model.d.ts +29 -5
- package/dist/reporting/member/ports.d.ts +49 -29
- package/dist/reporting/member/service.d.ts +57 -29
- package/dist/reporting/member/service.js +345 -114
- package/dist/reporting/member/service.js.map +1 -1
- package/dist/shared/actor.d.ts +23 -3
- package/dist/shared/errors.d.ts +2 -1
- package/dist/shared/errors.js +3 -1
- package/dist/shared/errors.js.map +1 -1
- package/dist/shared/id.d.ts +4 -0
- package/dist/shared/id.js +4 -0
- package/dist/shared/id.js.map +1 -1
- package/dist/shared/logger.d.ts +2 -1
- package/dist/shared/logger.js +10 -1
- package/dist/shared/logger.js.map +1 -1
- package/dist/shared/ports.d.ts +25 -5
- package/dist/types/content.d.ts +2 -2
- package/dist/types/discussion.d.ts +4 -5
- package/dist/types/email-templates.d.ts +22 -3
- package/dist/types/entitlements.d.ts +2 -2
- package/dist/types/identity.d.ts +13 -16
- package/dist/types/index.d.ts +9 -9
- package/dist/types/index.js +2 -1
- package/dist/types/notifications.d.ts +4 -4
- package/dist/types/organizations.d.ts +3 -2
- package/dist/types/ports.d.ts +8 -1
- package/dist/types/ports.js +7 -1
- package/dist/types/ports.js.map +1 -0
- package/dist/types/schemas/assets.d.ts +5 -5
- package/dist/types/schemas/assets.js +4 -3
- package/dist/types/schemas/assets.js.map +1 -1
- package/dist/types/schemas/content.d.ts +599 -13
- package/dist/types/schemas/content.js +219 -8
- package/dist/types/schemas/content.js.map +1 -1
- package/dist/types/schemas/discussion.d.ts +280 -57
- package/dist/types/schemas/discussion.js +77 -3
- package/dist/types/schemas/discussion.js.map +1 -1
- package/dist/types/schemas/entitlements.d.ts +21 -8
- package/dist/types/schemas/entitlements.js +11 -4
- package/dist/types/schemas/entitlements.js.map +1 -1
- package/dist/types/schemas/index.d.ts +5 -5
- package/dist/types/schemas/index.js +5 -5
- package/dist/types/schemas/notifications.d.ts +11 -10
- package/dist/types/schemas/notifications.js +3 -2
- package/dist/types/schemas/notifications.js.map +1 -1
- package/dist/types/schemas/organizations.d.ts +99 -60
- package/dist/types/schemas/organizations.js +80 -27
- package/dist/types/schemas/organizations.js.map +1 -1
- package/dist/types/schemas/progress.d.ts +3 -4
- package/dist/types/schemas/progress.js +0 -1
- package/dist/types/schemas/progress.js.map +1 -1
- package/package.json +1 -1
- package/dist/organizations/access-scopes.d.ts +0 -19
- package/dist/organizations/access-scopes.js +0 -89
- package/dist/organizations/access-scopes.js.map +0 -1
- package/dist/organizations/roles.d.ts +0 -19
- package/dist/organizations/roles.js +0 -82
- package/dist/organizations/roles.js.map +0 -1
package/dist/assets/service.js
CHANGED
|
@@ -1,14 +1,26 @@
|
|
|
1
1
|
import { assetEvents } from "./events.js";
|
|
2
|
+
import { PUBLIC_PREFIX } from "../types/ports.js";
|
|
3
|
+
import { publicImageKey, publicImagePrefix } from "./file-urls.js";
|
|
2
4
|
import { genId } from "../shared/id.js";
|
|
3
|
-
import { ConflictError, ForbiddenError, InvalidInputError } from "../shared/errors.js";
|
|
5
|
+
import { ConflictError, ForbiddenError, InvalidInputError, NotFoundError } from "../shared/errors.js";
|
|
4
6
|
import { noopLogger } from "../shared/logger.js";
|
|
5
7
|
//#region src/assets/service.ts
|
|
8
|
+
const WRITE_PERMISSION = {
|
|
9
|
+
course: "courses:write",
|
|
10
|
+
download: "downloads:write",
|
|
11
|
+
community: "communities:write",
|
|
12
|
+
event: "events:write"
|
|
13
|
+
};
|
|
6
14
|
const UNATTACHED_UPLOAD_GRACE_MS = 3600 * 1e3;
|
|
7
15
|
const MEMBER_IMAGE_TYPE = /^image\/(png|jpeg|gif|webp|avif)$/i;
|
|
8
|
-
const MEMBER_IMAGE_REFUSAL = "A member upload must be a PNG, JPEG, GIF, WebP or AVIF image";
|
|
9
16
|
function isMemberImageType(contentType) {
|
|
10
17
|
return MEMBER_IMAGE_TYPE.test(contentType.trim());
|
|
11
18
|
}
|
|
19
|
+
function memberKindOf(contentType) {
|
|
20
|
+
if (isMemberImageType(contentType)) return "image";
|
|
21
|
+
const kind = kindOf(contentType);
|
|
22
|
+
return kind === "image" ? "document" : kind;
|
|
23
|
+
}
|
|
12
24
|
function kindOf(contentType) {
|
|
13
25
|
const type = contentType.trim().toLowerCase();
|
|
14
26
|
if (type.startsWith("image/")) return "image";
|
|
@@ -26,7 +38,7 @@ function storedKey(orgId, folder, id, filename) {
|
|
|
26
38
|
return `${orgPrefix(orgId)}${folder}/${id}/${sanitizeFilename(filename)}`;
|
|
27
39
|
}
|
|
28
40
|
function finalMemberKey(orgId, asset) {
|
|
29
|
-
return `${orgPrefix(orgId)}
|
|
41
|
+
return `${orgPrefix(orgId)}${asset.kind}/${asset.id}/${sanitizeFilename(asset.filename)}`;
|
|
30
42
|
}
|
|
31
43
|
function graceCutoff(now) {
|
|
32
44
|
return /* @__PURE__ */ new Date(now.getTime() - UNATTACHED_UPLOAD_GRACE_MS);
|
|
@@ -36,23 +48,58 @@ var AssetsServiceImpl = class {
|
|
|
36
48
|
repo;
|
|
37
49
|
uow;
|
|
38
50
|
memberImageMaxBytes;
|
|
51
|
+
memberFileMaxBytes;
|
|
39
52
|
downloadLinkExpirySeconds;
|
|
40
53
|
fileUrls;
|
|
41
54
|
logger;
|
|
42
55
|
constructor(params) {
|
|
43
56
|
if (!Number.isSafeInteger(params.memberImageMaxBytes) || params.memberImageMaxBytes <= 0) throw new Error(`invalid memberImageMaxBytes "${params.memberImageMaxBytes}" (expected a positive integer)`);
|
|
57
|
+
if (!Number.isSafeInteger(params.memberFileMaxBytes) || params.memberFileMaxBytes <= 0) throw new Error(`invalid memberFileMaxBytes "${params.memberFileMaxBytes}" (expected a positive integer)`);
|
|
44
58
|
if (!Number.isSafeInteger(params.downloadLinkExpirySeconds) || params.downloadLinkExpirySeconds <= 0) throw new Error(`invalid downloadLinkExpirySeconds "${params.downloadLinkExpirySeconds}" (expected a positive integer)`);
|
|
45
59
|
this.storage = params.storage;
|
|
46
60
|
this.repo = params.repo;
|
|
47
61
|
this.uow = params.uow;
|
|
48
62
|
this.memberImageMaxBytes = params.memberImageMaxBytes;
|
|
63
|
+
this.memberFileMaxBytes = params.memberFileMaxBytes;
|
|
49
64
|
this.downloadLinkExpirySeconds = params.downloadLinkExpirySeconds;
|
|
50
65
|
this.fileUrls = params.fileUrls;
|
|
51
66
|
this.logger = params.logger ?? noopLogger;
|
|
52
67
|
}
|
|
53
|
-
async requestUpload(
|
|
68
|
+
async requestUpload(input, actor) {
|
|
69
|
+
const orgId = actor.orgId;
|
|
70
|
+
if (input.contentId === void 0) actor.permissions.require("media:write");
|
|
71
|
+
else {
|
|
72
|
+
const type = await this.repo.contentTypeOf(orgId, input.contentId);
|
|
73
|
+
if (!type) throw new NotFoundError("Content item", input.contentId);
|
|
74
|
+
if (!actor.permissions.has("media:write") && !actor.permissions.has(WRITE_PERMISSION[type], input.contentId)) {
|
|
75
|
+
this.logger.warn("asset upload refused", {
|
|
76
|
+
orgId,
|
|
77
|
+
orgUserId: actor.id,
|
|
78
|
+
contentId: input.contentId
|
|
79
|
+
});
|
|
80
|
+
throw new ForbiddenError(`You need media:write or ${WRITE_PERMISSION[type]}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return this.createUpload(orgId, {
|
|
84
|
+
filename: input.filename,
|
|
85
|
+
contentType: input.contentType,
|
|
86
|
+
source: "library",
|
|
87
|
+
uploadedBy: actor.id,
|
|
88
|
+
contentId: input.contentId ?? null
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
async requestMemberUpload(input, actor) {
|
|
92
|
+
return this.createUpload(actor.orgId, {
|
|
93
|
+
filename: input.filename,
|
|
94
|
+
contentType: input.contentType,
|
|
95
|
+
source: "member",
|
|
96
|
+
uploadedBy: actor.id,
|
|
97
|
+
contentId: null
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
async createUpload(orgId, input) {
|
|
54
101
|
const id = genId("asset");
|
|
55
|
-
const kind = kindOf(input.contentType);
|
|
102
|
+
const kind = input.source === "member" ? memberKindOf(input.contentType) : kindOf(input.contentType);
|
|
56
103
|
const key = storedKey(orgId, input.source === "member" ? "uploads" : kind, id, input.filename);
|
|
57
104
|
const presigned = await this.storage.presignUpload({
|
|
58
105
|
key,
|
|
@@ -71,6 +118,7 @@ var AssetsServiceImpl = class {
|
|
|
71
118
|
size: 0,
|
|
72
119
|
status: "pending",
|
|
73
120
|
uploadedBy: input.uploadedBy,
|
|
121
|
+
contentId: input.contentId,
|
|
74
122
|
createdAt: now,
|
|
75
123
|
updatedAt: now
|
|
76
124
|
});
|
|
@@ -84,7 +132,8 @@ var AssetsServiceImpl = class {
|
|
|
84
132
|
orgId,
|
|
85
133
|
assetId: id,
|
|
86
134
|
kind,
|
|
87
|
-
source: input.source
|
|
135
|
+
source: input.source,
|
|
136
|
+
contentId: input.contentId
|
|
88
137
|
});
|
|
89
138
|
return {
|
|
90
139
|
asset: {
|
|
@@ -98,34 +147,29 @@ var AssetsServiceImpl = class {
|
|
|
98
147
|
headers: presigned.headers
|
|
99
148
|
};
|
|
100
149
|
}
|
|
101
|
-
async
|
|
102
|
-
|
|
103
|
-
return this.requestUpload(orgId, {
|
|
104
|
-
filename: input.filename,
|
|
105
|
-
contentType: input.contentType,
|
|
106
|
-
source: "member",
|
|
107
|
-
uploadedBy: orgUserId
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
async confirm(orgId, id) {
|
|
150
|
+
async confirm(id, actor) {
|
|
151
|
+
const orgId = actor.orgId;
|
|
111
152
|
const asset = await this.repo.findById(orgId, id);
|
|
112
|
-
if (!asset)
|
|
153
|
+
if (!asset) throw new NotFoundError("Asset", id);
|
|
154
|
+
await this.requireReach(asset, actor);
|
|
113
155
|
if (asset.source === "member") throw new ForbiddenError("Only the uploader can finalize a member upload");
|
|
114
156
|
const confirmed = await this.finalize(orgId, asset);
|
|
115
|
-
|
|
157
|
+
if (!confirmed) throw new NotFoundError("Asset", id);
|
|
158
|
+
return this.view(confirmed);
|
|
116
159
|
}
|
|
117
|
-
async confirmOwn(
|
|
160
|
+
async confirmOwn(id, actor) {
|
|
161
|
+
const orgId = actor.orgId;
|
|
118
162
|
const asset = await this.repo.findById(orgId, id);
|
|
119
|
-
if (!asset || asset.source !== "member" || asset.uploadedBy !==
|
|
163
|
+
if (!asset || asset.source !== "member" || asset.uploadedBy !== actor.id) {
|
|
120
164
|
this.logger.debug("upload confirm refused", {
|
|
121
165
|
orgId,
|
|
122
166
|
assetId: id,
|
|
123
|
-
orgUserId
|
|
167
|
+
orgUserId: actor.id
|
|
124
168
|
});
|
|
125
|
-
|
|
169
|
+
throw new NotFoundError("Asset", id);
|
|
126
170
|
}
|
|
127
171
|
const confirmed = await this.finalize(orgId, asset);
|
|
128
|
-
if (!confirmed)
|
|
172
|
+
if (!confirmed) throw new NotFoundError("Asset", id);
|
|
129
173
|
return confirmed.status === "ready" ? {
|
|
130
174
|
...confirmed,
|
|
131
175
|
url: this.fileUrls.memberUpload(confirmed.id),
|
|
@@ -136,28 +180,52 @@ var AssetsServiceImpl = class {
|
|
|
136
180
|
downloadUrl: null
|
|
137
181
|
};
|
|
138
182
|
}
|
|
139
|
-
async ownUploadFile(
|
|
140
|
-
|
|
183
|
+
async ownUploadFile(assetId, actor) {
|
|
184
|
+
const orgId = actor.orgId;
|
|
185
|
+
if (!await this.ownsUpload(orgId, assetId, actor.id)) {
|
|
141
186
|
this.logger.warn("upload file not served — not the uploader", {
|
|
142
187
|
orgId,
|
|
143
188
|
assetId,
|
|
144
|
-
orgUserId
|
|
189
|
+
orgUserId: actor.id
|
|
145
190
|
});
|
|
146
|
-
|
|
191
|
+
throw new NotFoundError("Upload", assetId);
|
|
147
192
|
}
|
|
148
|
-
|
|
193
|
+
const url = await this.signFile(orgId, assetId);
|
|
194
|
+
if (!url) throw new NotFoundError("Upload", assetId);
|
|
195
|
+
return url;
|
|
196
|
+
}
|
|
197
|
+
async ownsUpload(orgId, assetId, orgUserId) {
|
|
198
|
+
const asset = await this.repo.findById(orgId, assetId);
|
|
199
|
+
return asset !== null && asset.source === "member" && asset.status === "ready" && asset.uploadedBy === orgUserId;
|
|
200
|
+
}
|
|
201
|
+
async memberUploads(orgId, assetIds) {
|
|
202
|
+
return (await this.readyAmong(orgId, assetIds)).filter((asset) => asset.source === "member").map((asset) => ({
|
|
203
|
+
id: asset.id,
|
|
204
|
+
kind: asset.kind,
|
|
205
|
+
filename: asset.filename,
|
|
206
|
+
contentType: asset.contentType,
|
|
207
|
+
size: asset.size
|
|
208
|
+
}));
|
|
149
209
|
}
|
|
150
210
|
async ownsImage(orgId, assetId, orgUserId) {
|
|
151
211
|
const asset = await this.repo.findById(orgId, assetId);
|
|
152
212
|
return asset !== null && asset.source === "member" && asset.kind === "image" && asset.status === "ready" && asset.uploadedBy === orgUserId;
|
|
153
213
|
}
|
|
154
|
-
async
|
|
214
|
+
async libraryAssetsFor(orgId, assetIds) {
|
|
155
215
|
if (assetIds.length === 0) return [];
|
|
156
216
|
const rows = await this.repo.findByIds(orgId, [...new Set(assetIds)]);
|
|
157
|
-
const library = new
|
|
158
|
-
return assetIds.
|
|
217
|
+
const library = new Map(rows.filter((a) => a.source === "library" && a.status === "ready").map((a) => [a.id, a.contentId]));
|
|
218
|
+
return assetIds.flatMap((id) => library.has(id) ? [{
|
|
219
|
+
id,
|
|
220
|
+
contentId: library.get(id) ?? null
|
|
221
|
+
}] : []);
|
|
159
222
|
}
|
|
160
|
-
async list(
|
|
223
|
+
async list(query, actor) {
|
|
224
|
+
const orgId = actor.orgId;
|
|
225
|
+
if (!actor.permissions.has("media:read")) {
|
|
226
|
+
if (query.contentId === void 0 || (query.source ?? "library") !== "library") throw new ForbiddenError("You need media:read");
|
|
227
|
+
await this.requireItemWrite(orgId, query.contentId, actor);
|
|
228
|
+
}
|
|
161
229
|
const page = await this.repo.list(orgId, {
|
|
162
230
|
...query,
|
|
163
231
|
source: query.source ?? "library"
|
|
@@ -167,9 +235,11 @@ var AssetsServiceImpl = class {
|
|
|
167
235
|
rows: await Promise.all(page.rows.map((asset) => this.view(asset)))
|
|
168
236
|
};
|
|
169
237
|
}
|
|
170
|
-
async get(
|
|
171
|
-
const asset = await this.repo.findById(orgId, id);
|
|
172
|
-
|
|
238
|
+
async get(id, actor) {
|
|
239
|
+
const asset = await this.repo.findById(actor.orgId, id);
|
|
240
|
+
if (!asset) throw new NotFoundError("Asset", id);
|
|
241
|
+
await this.requireReach(asset, actor);
|
|
242
|
+
return this.view(asset);
|
|
173
243
|
}
|
|
174
244
|
async readyFiles(orgId, assetIds) {
|
|
175
245
|
const ready = await this.readyAmong(orgId, assetIds);
|
|
@@ -194,31 +264,99 @@ var AssetsServiceImpl = class {
|
|
|
194
264
|
expiresInSeconds: this.downloadLinkExpirySeconds
|
|
195
265
|
});
|
|
196
266
|
}
|
|
197
|
-
async
|
|
267
|
+
async syncPublicImages(orgId, ownerId, assetIds) {
|
|
268
|
+
try {
|
|
269
|
+
const wanted = new Map(assetIds.map((id) => [publicImageKey(orgId, ownerId, id), id]));
|
|
270
|
+
const held = await this.storage.list(publicImagePrefix(orgId, ownerId));
|
|
271
|
+
for (const object of held) if (!wanted.has(object.key)) await this.storage.remove(object.key);
|
|
272
|
+
const heldKeys = new Set(held.map((object) => object.key));
|
|
273
|
+
const missing = [...wanted].filter(([key]) => !heldKeys.has(key));
|
|
274
|
+
if (missing.length === 0) return;
|
|
275
|
+
const ready = new Map((await this.readyAmong(orgId, missing.map(([, id]) => id))).map((asset) => [asset.id, asset]));
|
|
276
|
+
for (const [key, assetId] of missing) {
|
|
277
|
+
const asset = ready.get(assetId);
|
|
278
|
+
if (!asset || asset.kind !== "image") {
|
|
279
|
+
this.logger.warn("public image not published — no ready image", {
|
|
280
|
+
orgId,
|
|
281
|
+
ownerId,
|
|
282
|
+
assetId
|
|
283
|
+
});
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
await this.storage.publish({
|
|
287
|
+
from: asset.key,
|
|
288
|
+
to: key
|
|
289
|
+
});
|
|
290
|
+
this.logger.info("public image published", {
|
|
291
|
+
orgId,
|
|
292
|
+
ownerId,
|
|
293
|
+
assetId
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
} catch (err) {
|
|
297
|
+
this.logger.error("public images not synced", {
|
|
298
|
+
orgId,
|
|
299
|
+
ownerId,
|
|
300
|
+
assetIds,
|
|
301
|
+
err
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
async libraryFile(assetId, opts, actor) {
|
|
306
|
+
const orgId = actor.orgId;
|
|
198
307
|
const asset = await this.repo.findById(orgId, assetId);
|
|
199
308
|
if (!asset || asset.source !== "library") {
|
|
200
309
|
this.logger.warn("library file not served — not a library asset", {
|
|
201
310
|
orgId,
|
|
202
311
|
assetId
|
|
203
312
|
});
|
|
204
|
-
|
|
313
|
+
throw new NotFoundError("Asset", assetId);
|
|
205
314
|
}
|
|
206
|
-
|
|
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
|
+
async remove(id, actor) {
|
|
321
|
+
actor.permissions.require("media:delete");
|
|
322
|
+
await this.removeAsset(actor.orgId, id, actor.id);
|
|
207
323
|
}
|
|
208
|
-
async
|
|
324
|
+
async removeOwn(id, actor) {
|
|
325
|
+
const asset = await this.repo.findById(actor.orgId, id);
|
|
326
|
+
if (!asset || asset.source !== "member" || asset.uploadedBy !== actor.id) throw new NotFoundError("Asset", id);
|
|
327
|
+
await this.removeAsset(actor.orgId, id, actor.id);
|
|
328
|
+
}
|
|
329
|
+
async removeAsset(orgId, id, actorId) {
|
|
209
330
|
const asset = await this.repo.findById(orgId, id);
|
|
210
|
-
if (!asset)
|
|
211
|
-
|
|
212
|
-
if (removed) this.logger.info("asset removed", {
|
|
331
|
+
if (!asset || !await this.discard(orgId, asset)) throw new NotFoundError("Asset", id);
|
|
332
|
+
this.logger.info("asset removed", {
|
|
213
333
|
orgId,
|
|
214
|
-
assetId: id
|
|
334
|
+
assetId: id,
|
|
335
|
+
actorId
|
|
215
336
|
});
|
|
216
|
-
return removed;
|
|
217
337
|
}
|
|
218
|
-
async
|
|
219
|
-
|
|
220
|
-
if (
|
|
221
|
-
|
|
338
|
+
async requireReach(asset, actor) {
|
|
339
|
+
if (actor.permissions.has("media:read")) return;
|
|
340
|
+
if (asset.contentId === null) {
|
|
341
|
+
this.logger.warn("asset refused", {
|
|
342
|
+
orgId: actor.orgId,
|
|
343
|
+
assetId: asset.id,
|
|
344
|
+
orgUserId: actor.id
|
|
345
|
+
});
|
|
346
|
+
throw new ForbiddenError("You need media:read");
|
|
347
|
+
}
|
|
348
|
+
await this.requireItemWrite(actor.orgId, asset.contentId, actor);
|
|
349
|
+
}
|
|
350
|
+
async requireItemWrite(orgId, contentId, actor) {
|
|
351
|
+
const type = await this.repo.contentTypeOf(orgId, contentId);
|
|
352
|
+
if (!type || !actor.permissions.has(WRITE_PERMISSION[type], contentId)) {
|
|
353
|
+
this.logger.warn("asset refused", {
|
|
354
|
+
orgId,
|
|
355
|
+
contentId,
|
|
356
|
+
orgUserId: actor.id
|
|
357
|
+
});
|
|
358
|
+
throw new ForbiddenError(type ? `You need media:read or ${WRITE_PERMISSION[type]}` : "You need media:read");
|
|
359
|
+
}
|
|
222
360
|
}
|
|
223
361
|
async removeUnattached(orgId, now) {
|
|
224
362
|
const stale = await this.repo.listUnreferencedMemberUploads(orgId, graceCutoff(now));
|
|
@@ -236,6 +374,8 @@ var AssetsServiceImpl = class {
|
|
|
236
374
|
});
|
|
237
375
|
}
|
|
238
376
|
}
|
|
377
|
+
const orphans = await this.orphanPublicImages(orgId);
|
|
378
|
+
for (const key of orphans) await this.removeObject(orgId, key.split("/").pop() ?? "", key);
|
|
239
379
|
let stray = 0;
|
|
240
380
|
for (const key of await this.strayUploads(orgId, now)) try {
|
|
241
381
|
await this.storage.remove(key);
|
|
@@ -247,13 +387,21 @@ var AssetsServiceImpl = class {
|
|
|
247
387
|
err
|
|
248
388
|
});
|
|
249
389
|
}
|
|
250
|
-
if (removed > 0 || stray > 0 || failed > 0) this.logger.info("unattached uploads removed", {
|
|
390
|
+
if (removed > 0 || stray > 0 || orphans.length > 0 || failed > 0) this.logger.info("unattached uploads removed", {
|
|
251
391
|
orgId,
|
|
252
392
|
count: removed,
|
|
253
393
|
stray,
|
|
394
|
+
publicCopies: orphans.length,
|
|
254
395
|
failed
|
|
255
396
|
});
|
|
256
|
-
return removed + stray;
|
|
397
|
+
return removed + stray + orphans.length;
|
|
398
|
+
}
|
|
399
|
+
async orphanPublicImages(orgId) {
|
|
400
|
+
const objects = await this.storage.list(`${PUBLIC_PREFIX}${orgId}/`);
|
|
401
|
+
if (objects.length === 0) return [];
|
|
402
|
+
const assetIdOf = (key) => key.split("/").pop() ?? "";
|
|
403
|
+
const live = new Set((await this.readyAmong(orgId, objects.map((o) => assetIdOf(o.key)))).map((a) => a.id));
|
|
404
|
+
return objects.filter((o) => !live.has(assetIdOf(o.key))).map((o) => o.key);
|
|
257
405
|
}
|
|
258
406
|
async strayUploads(orgId, now) {
|
|
259
407
|
const cutoff = now.getTime() - this.storage.uploadExpirySeconds * 1e3;
|
|
@@ -297,8 +445,9 @@ var AssetsServiceImpl = class {
|
|
|
297
445
|
const stat = await this.storage.stat(key);
|
|
298
446
|
if (!stat) throw new Error("copied upload not found");
|
|
299
447
|
const contentType = asset.contentType;
|
|
300
|
-
const kind =
|
|
301
|
-
const
|
|
448
|
+
const kind = memberKindOf(contentType);
|
|
449
|
+
const limit = kind === "image" ? this.memberImageMaxBytes : this.memberFileMaxBytes;
|
|
450
|
+
const reason = stat.contentType !== void 0 && stat.contentType !== contentType ? "The uploaded file is not the declared type" : stat.size > limit ? `This ${kind === "image" ? "image" : "file"} is over the ${sizeLabel(limit)} limit` : null;
|
|
302
451
|
if (reason) {
|
|
303
452
|
await this.removeObject(orgId, asset.id, key);
|
|
304
453
|
await this.discard(orgId, asset);
|
|
@@ -360,9 +509,24 @@ var AssetsServiceImpl = class {
|
|
|
360
509
|
})]);
|
|
361
510
|
return deleted;
|
|
362
511
|
});
|
|
363
|
-
if (deleted)
|
|
512
|
+
if (deleted) {
|
|
513
|
+
await this.removeObject(orgId, asset.id, asset.key);
|
|
514
|
+
await this.removePublicCopies(orgId, asset.id);
|
|
515
|
+
}
|
|
364
516
|
return deleted;
|
|
365
517
|
}
|
|
518
|
+
async removePublicCopies(orgId, assetId) {
|
|
519
|
+
try {
|
|
520
|
+
const copies = await this.storage.list(`${PUBLIC_PREFIX}${orgId}/`);
|
|
521
|
+
for (const copy of copies.filter((o) => o.key.endsWith(`/${assetId}`))) await this.removeObject(orgId, assetId, copy.key);
|
|
522
|
+
} catch (err) {
|
|
523
|
+
this.logger.warn("public copies not removed", {
|
|
524
|
+
orgId,
|
|
525
|
+
assetId,
|
|
526
|
+
err
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
}
|
|
366
530
|
async removeObject(orgId, assetId, key) {
|
|
367
531
|
try {
|
|
368
532
|
await this.storage.remove(key);
|
|
@@ -375,7 +539,22 @@ var AssetsServiceImpl = class {
|
|
|
375
539
|
}
|
|
376
540
|
}
|
|
377
541
|
};
|
|
542
|
+
function sizeLabel(bytes) {
|
|
543
|
+
const units = [
|
|
544
|
+
"B",
|
|
545
|
+
"KB",
|
|
546
|
+
"MB",
|
|
547
|
+
"GB"
|
|
548
|
+
];
|
|
549
|
+
let value = bytes;
|
|
550
|
+
let unit = 0;
|
|
551
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
552
|
+
value /= 1024;
|
|
553
|
+
unit += 1;
|
|
554
|
+
}
|
|
555
|
+
return `${Number.isInteger(value) ? value : value.toFixed(1)} ${units[unit]}`;
|
|
556
|
+
}
|
|
378
557
|
//#endregion
|
|
379
|
-
export { AssetsServiceImpl, MEMBER_IMAGE_TYPE, isMemberImageType, kindOf };
|
|
558
|
+
export { AssetsServiceImpl, MEMBER_IMAGE_TYPE, isMemberImageType, kindOf, memberKindOf };
|
|
380
559
|
|
|
381
560
|
//# sourceMappingURL=service.js.map
|
|
@@ -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 { ConflictError, ForbiddenError, InvalidInputError } from '../shared/errors.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 MemberUploadInput,\n ReadyFile,\n} from './ports.js';\nimport type { FileUrls } from './file-urls.js';\nimport { noopLogger } from '../shared/logger.js';\nimport { assetEvents } from './events.js';\n\nconst UNATTACHED_UPLOAD_GRACE_MS = 60 * 60 * 1000;\n\nexport const MEMBER_IMAGE_TYPE = /^image\\/(png|jpeg|gif|webp|avif)$/i;\nconst MEMBER_IMAGE_REFUSAL = 'A member upload must be a PNG, JPEG, GIF, WebP or AVIF image';\n\nexport function isMemberImageType(contentType: string): boolean {\n return MEMBER_IMAGE_TYPE.test(contentType.trim());\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)}image/${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 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 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 (\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.downloadLinkExpirySeconds = params.downloadLinkExpirySeconds;\n this.fileUrls = params.fileUrls;\n this.logger = params.logger ?? noopLogger;\n }\n\n async requestUpload(orgId: string, input: RequestUploadInput): Promise<UploadTicket> {\n const id = genId('asset');\n const kind = 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 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 });\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 requestMemberUpload(\n orgId: string,\n orgUserId: string,\n input: MemberUploadInput,\n ): Promise<UploadTicket> {\n if (!isMemberImageType(input.contentType)) {\n throw new InvalidInputError(MEMBER_IMAGE_REFUSAL);\n }\n return this.requestUpload(orgId, {\n filename: input.filename,\n contentType: input.contentType,\n source: 'member',\n uploadedBy: orgUserId,\n });\n }\n\n async confirm(orgId: string, id: string): Promise<AssetView | null> {\n const asset = await this.repo.findById(orgId, id);\n if (!asset) {\n return null;\n }\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 return confirmed ? this.view(confirmed) : null;\n }\n\n async confirmOwn(orgId: string, id: string, orgUserId: string): Promise<AssetView | null> {\n const asset = await this.repo.findById(orgId, id);\n if (!asset || asset.source !== 'member' || asset.uploadedBy !== orgUserId) {\n this.logger.debug('upload confirm refused', { orgId, assetId: id, orgUserId });\n return null;\n }\n const confirmed = await this.finalize(orgId, asset);\n if (!confirmed) {\n return null;\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(orgId: string, assetId: string, orgUserId: string): Promise<string | null> {\n if (!(await this.ownsImage(orgId, assetId, orgUserId))) {\n this.logger.warn('upload file not served — not the uploader', { orgId, assetId, orgUserId });\n return null;\n }\n return this.signFile(orgId, assetId);\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 libraryAssetsAmong(orgId: string, assetIds: string[]): Promise<string[]> {\n if (assetIds.length === 0) {\n return [];\n }\n const rows = await this.repo.findByIds(orgId, [...new Set(assetIds)]);\n const library = new Set(\n rows.filter((a) => a.source === 'library' && a.status === 'ready').map((a) => a.id),\n );\n return assetIds.filter((id) => library.has(id));\n }\n\n async list(orgId: string, query: AssetsQuery): Promise<Page<AssetView>> {\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(orgId: string, id: string): Promise<AssetView | null> {\n const asset = await this.repo.findById(orgId, id);\n return asset ? this.view(asset) : null;\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 libraryFile(\n orgId: string,\n assetId: string,\n opts: { download: boolean },\n ): Promise<string | null> {\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 return null;\n }\n return this.signFile(orgId, assetId, opts.download ? { saveAs: asset.filename } : {});\n }\n\n async remove(orgId: string, id: string): Promise<boolean> {\n const asset = await this.repo.findById(orgId, id);\n if (!asset) {\n return false;\n }\n const removed = await this.discard(orgId, asset);\n if (removed) {\n this.logger.info('asset removed', { orgId, assetId: id });\n }\n return removed;\n }\n\n async removeOwn(orgId: string, id: string, orgUserId: string): Promise<boolean> {\n const asset = await this.repo.findById(orgId, id);\n if (!asset || asset.source !== 'member' || asset.uploadedBy !== orgUserId) {\n return false;\n }\n return this.remove(orgId, id);\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 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 || failed > 0) {\n this.logger.info('unattached uploads removed', { orgId, count: removed, stray, failed });\n }\n return removed + stray;\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 = kindOf(contentType);\n const reason = !isMemberImageType(contentType)\n ? MEMBER_IMAGE_REFUSAL\n : stat.contentType !== undefined && stat.contentType !== contentType\n ? 'The uploaded file is not the declared type'\n : stat.size > this.memberImageMaxBytes\n ? 'This image is too large'\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 }\n return deleted;\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"],"mappings":";;;;;AAuBA,MAAM,6BAA6B,OAAU;AAE7C,MAAa,oBAAoB;AACjC,MAAM,uBAAuB;AAE7B,SAAgB,kBAAkB,aAA8B;CAC9D,OAAO,kBAAkB,KAAK,YAAY,KAAK,CAAC;AAClD;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,EAAE,QAAQ,MAAM,GAAG,GAAG,iBAAiB,MAAM,QAAQ;AAChF;AAEA,SAAS,YAAY,KAAiB;CACpC,uBAAO,IAAI,KAAK,IAAI,QAAQ,IAAI,0BAA0B;AAC5D;AAYA,IAAa,oBAAb,MAAwD;CACtD;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,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,4BAA4B,OAAO;EACxC,KAAK,WAAW,OAAO;EACvB,KAAK,SAAS,OAAO,UAAU;CACjC;CAEA,MAAM,cAAc,OAAe,OAAkD;EACnF,MAAM,KAAK,MAAM,OAAO;EACxB,MAAM,OAAO,OAAO,MAAM,WAAW;EAErC,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;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;EAChB,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,oBACJ,OACA,WACA,OACuB;EACvB,IAAI,CAAC,kBAAkB,MAAM,WAAW,GACtC,MAAM,IAAI,kBAAkB,oBAAoB;EAElD,OAAO,KAAK,cAAc,OAAO;GAC/B,UAAU,MAAM;GAChB,aAAa,MAAM;GACnB,QAAQ;GACR,YAAY;EACd,CAAC;CACH;CAEA,MAAM,QAAQ,OAAe,IAAuC;EAClE,MAAM,QAAQ,MAAM,KAAK,KAAK,SAAS,OAAO,EAAE;EAChD,IAAI,CAAC,OACH,OAAO;EAET,IAAI,MAAM,WAAW,UACnB,MAAM,IAAI,eAAe,gDAAgD;EAE3E,MAAM,YAAY,MAAM,KAAK,SAAS,OAAO,KAAK;EAClD,OAAO,YAAY,KAAK,KAAK,SAAS,IAAI;CAC5C;CAEA,MAAM,WAAW,OAAe,IAAY,WAA8C;EACxF,MAAM,QAAQ,MAAM,KAAK,KAAK,SAAS,OAAO,EAAE;EAChD,IAAI,CAAC,SAAS,MAAM,WAAW,YAAY,MAAM,eAAe,WAAW;GACzE,KAAK,OAAO,MAAM,0BAA0B;IAAE;IAAO,SAAS;IAAI;GAAU,CAAC;GAC7E,OAAO;EACT;EACA,MAAM,YAAY,MAAM,KAAK,SAAS,OAAO,KAAK;EAClD,IAAI,CAAC,WACH,OAAO;EAET,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,OAAe,SAAiB,WAA2C;EAC7F,IAAI,CAAE,MAAM,KAAK,UAAU,OAAO,SAAS,SAAS,GAAI;GACtD,KAAK,OAAO,KAAK,6CAA6C;IAAE;IAAO;IAAS;GAAU,CAAC;GAC3F,OAAO;EACT;EACA,OAAO,KAAK,SAAS,OAAO,OAAO;CACrC;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,mBAAmB,OAAe,UAAuC;EAC7E,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,KAAK,QAAQ,MAAM,EAAE,WAAW,aAAa,EAAE,WAAW,OAAO,CAAC,CAAC,KAAK,MAAM,EAAE,EAAE,CACpF;EACA,OAAO,SAAS,QAAQ,OAAO,QAAQ,IAAI,EAAE,CAAC;CAChD;CAEA,MAAM,KAAK,OAAe,OAA8C;EACtE,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,OAAe,IAAuC;EAC9D,MAAM,QAAQ,MAAM,KAAK,KAAK,SAAS,OAAO,EAAE;EAChD,OAAO,QAAQ,KAAK,KAAK,KAAK,IAAI;CACpC;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,YACJ,OACA,SACA,MACwB;EACxB,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,OAAO;EACT;EACA,OAAO,KAAK,SAAS,OAAO,SAAS,KAAK,WAAW,EAAE,QAAQ,MAAM,SAAS,IAAI,CAAC,CAAC;CACtF;CAEA,MAAM,OAAO,OAAe,IAA8B;EACxD,MAAM,QAAQ,MAAM,KAAK,KAAK,SAAS,OAAO,EAAE;EAChD,IAAI,CAAC,OACH,OAAO;EAET,MAAM,UAAU,MAAM,KAAK,QAAQ,OAAO,KAAK;EAC/C,IAAI,SACF,KAAK,OAAO,KAAK,iBAAiB;GAAE;GAAO,SAAS;EAAG,CAAC;EAE1D,OAAO;CACT;CAEA,MAAM,UAAU,OAAe,IAAY,WAAqC;EAC9E,MAAM,QAAQ,MAAM,KAAK,KAAK,SAAS,OAAO,EAAE;EAChD,IAAI,CAAC,SAAS,MAAM,WAAW,YAAY,MAAM,eAAe,WAC9D,OAAO;EAET,OAAO,KAAK,OAAO,OAAO,EAAE;CAC9B;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,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,SAAS,GACvC,KAAK,OAAO,KAAK,8BAA8B;GAAE;GAAO,OAAO;GAAS;GAAO;EAAO,CAAC;EAEzF,OAAO,UAAU;CACnB;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,OAAO,WAAW;EAC/B,MAAM,SAAS,CAAC,kBAAkB,WAAW,IACzC,uBACA,KAAK,gBAAgB,KAAA,KAAa,KAAK,gBAAgB,cACrD,+CACA,KAAK,OAAO,KAAK,sBACf,4BACA;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,SACF,MAAM,KAAK,aAAa,OAAO,MAAM,IAAI,MAAM,GAAG;EAEpD,OAAO;CACT;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"}
|
|
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":"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"}
|