@headless-lms/core 0.4.0 → 0.5.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 (249) hide show
  1. package/dist/assets/events.d.ts +27 -21
  2. package/dist/assets/file-urls.d.ts +11 -7
  3. package/dist/assets/file-urls.js +14 -4
  4. package/dist/assets/file-urls.js.map +1 -1
  5. package/dist/assets/index.d.ts +4 -4
  6. package/dist/assets/index.js +3 -3
  7. package/dist/assets/ports.d.ts +34 -15
  8. package/dist/assets/service.d.ts +30 -14
  9. package/dist/assets/service.js +230 -56
  10. package/dist/assets/service.js.map +1 -1
  11. package/dist/automations/actions.js.map +1 -1
  12. package/dist/automations/catalog.js +69 -4
  13. package/dist/automations/catalog.js.map +1 -1
  14. package/dist/automations/community-derivations.js +16 -16
  15. package/dist/automations/community-derivations.js.map +1 -1
  16. package/dist/automations/community-mail.d.ts +34 -4
  17. package/dist/automations/community-mail.js +32 -4
  18. package/dist/automations/community-mail.js.map +1 -1
  19. package/dist/automations/event-permissions.d.ts +12 -0
  20. package/dist/automations/event-permissions.js +38 -0
  21. package/dist/automations/event-permissions.js.map +1 -0
  22. package/dist/automations/index.d.ts +2 -1
  23. package/dist/automations/index.js +2 -1
  24. package/dist/automations/notifications/applicable.d.ts +8 -0
  25. package/dist/automations/notifications/applicable.js +12 -0
  26. package/dist/automations/notifications/applicable.js.map +1 -0
  27. package/dist/automations/notifications/debounce.js +1 -1
  28. package/dist/automations/notifications/debounce.js.map +1 -1
  29. package/dist/automations/notifications/defaults.js +33 -3
  30. package/dist/automations/notifications/defaults.js.map +1 -1
  31. package/dist/automations/notifications/processor.js +3 -3
  32. package/dist/automations/notifications/processor.js.map +1 -1
  33. package/dist/automations/notifications/rules/content.js +60 -3
  34. package/dist/automations/notifications/rules/content.js.map +1 -1
  35. package/dist/automations/notifications/rules/discussion.js +29 -29
  36. package/dist/automations/notifications/rules/discussion.js.map +1 -1
  37. package/dist/automations/notifications/rules/entitlements.js +36 -9
  38. package/dist/automations/notifications/rules/entitlements.js.map +1 -1
  39. package/dist/automations/notifications/rules/events.d.ts +9 -0
  40. package/dist/automations/notifications/rules/events.js +27 -0
  41. package/dist/automations/notifications/rules/events.js.map +1 -0
  42. package/dist/automations/notifications/rules/organizations.js +17 -10
  43. package/dist/automations/notifications/rules/organizations.js.map +1 -1
  44. package/dist/automations/notifications/rules/recipients.js +0 -1
  45. package/dist/automations/notifications/rules/recipients.js.map +1 -1
  46. package/dist/automations/notifications/service.d.ts +8 -6
  47. package/dist/automations/notifications/service.js +13 -13
  48. package/dist/automations/notifications/service.js.map +1 -1
  49. package/dist/automations/notifications/types.js +94 -7
  50. package/dist/automations/notifications/types.js.map +1 -1
  51. package/dist/automations/ports.d.ts +30 -31
  52. package/dist/automations/service.d.ts +20 -17
  53. package/dist/automations/service.js +91 -62
  54. package/dist/automations/service.js.map +1 -1
  55. package/dist/content/event-sight.d.ts +11 -0
  56. package/dist/content/event-sight.js +11 -0
  57. package/dist/content/event-sight.js.map +1 -0
  58. package/dist/content/events.d.ts +413 -72
  59. package/dist/content/events.js +34 -1
  60. package/dist/content/events.js.map +1 -1
  61. package/dist/content/index.d.ts +6 -4
  62. package/dist/content/index.js +3 -1
  63. package/dist/content/jobs.d.ts +16 -0
  64. package/dist/content/jobs.js +37 -0
  65. package/dist/content/jobs.js.map +1 -0
  66. package/dist/content/model.d.ts +2 -2
  67. package/dist/content/notification-categories.d.ts +5 -4
  68. package/dist/content/notification-categories.js +15 -6
  69. package/dist/content/notification-categories.js.map +1 -1
  70. package/dist/content/ports.d.ts +113 -68
  71. package/dist/content/service.d.ts +99 -67
  72. package/dist/content/service.js +680 -139
  73. package/dist/content/service.js.map +1 -1
  74. package/dist/content/titles.d.ts +6 -3
  75. package/dist/content/titles.js +37 -24
  76. package/dist/content/titles.js.map +1 -1
  77. package/dist/content/types.d.ts +2 -2
  78. package/dist/discussion/authors.d.ts +4 -2
  79. package/dist/discussion/authors.js +9 -5
  80. package/dist/discussion/authors.js.map +1 -1
  81. package/dist/discussion/chats.d.ts +56 -37
  82. package/dist/discussion/chats.js +393 -161
  83. package/dist/discussion/chats.js.map +1 -1
  84. package/dist/discussion/events.d.ts +77 -39
  85. package/dist/discussion/events.js +41 -3
  86. package/dist/discussion/events.js.map +1 -1
  87. package/dist/discussion/index.d.ts +8 -5
  88. package/dist/discussion/index.js +5 -3
  89. package/dist/discussion/live.d.ts +106 -0
  90. package/dist/discussion/live.js +642 -0
  91. package/dist/discussion/live.js.map +1 -0
  92. package/dist/discussion/mentions.js +2 -2
  93. package/dist/discussion/mentions.js.map +1 -1
  94. package/dist/discussion/ports.d.ts +182 -100
  95. package/dist/discussion/ports.js.map +1 -1
  96. package/dist/discussion/posts.d.ts +45 -39
  97. package/dist/discussion/posts.js +268 -154
  98. package/dist/discussion/posts.js.map +1 -1
  99. package/dist/discussion/rules.d.ts +18 -0
  100. package/dist/discussion/rules.js +22 -0
  101. package/dist/discussion/rules.js.map +1 -0
  102. package/dist/discussion/service.d.ts +37 -31
  103. package/dist/discussion/service.js +252 -188
  104. package/dist/discussion/service.js.map +1 -1
  105. package/dist/discussion/subscriptions.d.ts +8 -8
  106. package/dist/discussion/subscriptions.js +29 -13
  107. package/dist/discussion/subscriptions.js.map +1 -1
  108. package/dist/discussion/titles.d.ts +4 -2
  109. package/dist/discussion/titles.js +27 -13
  110. package/dist/discussion/titles.js.map +1 -1
  111. package/dist/entitlements/events.d.ts +20 -20
  112. package/dist/entitlements/index.d.ts +4 -4
  113. package/dist/entitlements/index.js +2 -2
  114. package/dist/entitlements/model.d.ts +2 -2
  115. package/dist/entitlements/ports.d.ts +65 -39
  116. package/dist/entitlements/rules.d.ts +3 -6
  117. package/dist/entitlements/rules.js +5 -10
  118. package/dist/entitlements/rules.js.map +1 -1
  119. package/dist/entitlements/service.d.ts +40 -28
  120. package/dist/entitlements/service.js +370 -163
  121. package/dist/entitlements/service.js.map +1 -1
  122. package/dist/identity/devices.d.ts +3 -3
  123. package/dist/identity/devices.js +7 -5
  124. package/dist/identity/devices.js.map +1 -1
  125. package/dist/identity/events.d.ts +2 -2
  126. package/dist/identity/index.d.ts +2 -2
  127. package/dist/identity/index.js +2 -2
  128. package/dist/identity/notification-settings.d.ts +1 -1
  129. package/dist/identity/notification-settings.js +3 -2
  130. package/dist/identity/notification-settings.js.map +1 -1
  131. package/dist/identity/ports.d.ts +7 -7
  132. package/dist/identity/service.d.ts +8 -7
  133. package/dist/identity/service.js +17 -13
  134. package/dist/identity/service.js.map +1 -1
  135. package/dist/integrations/events.d.ts +3 -3
  136. package/dist/integrations/ports.d.ts +9 -8
  137. package/dist/integrations/service.d.ts +9 -8
  138. package/dist/integrations/service.js +33 -16
  139. package/dist/integrations/service.js.map +1 -1
  140. package/dist/organizations/default-roles.d.ts +22 -0
  141. package/dist/organizations/default-roles.js +89 -0
  142. package/dist/organizations/default-roles.js.map +1 -0
  143. package/dist/organizations/effective-permissions.d.ts +45 -0
  144. package/dist/organizations/effective-permissions.js +113 -0
  145. package/dist/organizations/effective-permissions.js.map +1 -0
  146. package/dist/organizations/events.d.ts +320 -181
  147. package/dist/organizations/events.js +90 -18
  148. package/dist/organizations/events.js.map +1 -1
  149. package/dist/organizations/index.d.ts +14 -10
  150. package/dist/organizations/index.js +7 -3
  151. package/dist/organizations/members.d.ts +66 -8
  152. package/dist/organizations/members.js.map +1 -1
  153. package/dist/organizations/notification-catalog.d.ts +31 -0
  154. package/dist/organizations/notification-catalog.js +32 -0
  155. package/dist/organizations/notification-catalog.js.map +1 -0
  156. package/dist/organizations/notification-categories.d.ts +6 -4
  157. package/dist/organizations/notification-categories.js +25 -10
  158. package/dist/organizations/notification-categories.js.map +1 -1
  159. package/dist/organizations/org-roles.d.ts +33 -0
  160. package/dist/organizations/org-roles.js +49 -0
  161. package/dist/organizations/org-roles.js.map +1 -0
  162. package/dist/organizations/permissions.d.ts +311 -0
  163. package/dist/organizations/permissions.js +327 -0
  164. package/dist/organizations/permissions.js.map +1 -0
  165. package/dist/organizations/ports.d.ts +184 -65
  166. package/dist/organizations/service.d.ts +102 -35
  167. package/dist/organizations/service.js +1245 -328
  168. package/dist/organizations/service.js.map +1 -1
  169. package/dist/organizations/testing.d.ts +18 -0
  170. package/dist/organizations/testing.js +37 -0
  171. package/dist/organizations/testing.js.map +1 -0
  172. package/dist/organizations/types.d.ts +5 -16
  173. package/dist/progress/events.d.ts +14 -14
  174. package/dist/progress/index.d.ts +2 -2
  175. package/dist/progress/ports.d.ts +11 -3
  176. package/dist/progress/service.d.ts +6 -5
  177. package/dist/progress/service.js +23 -8
  178. package/dist/progress/service.js.map +1 -1
  179. package/dist/reporting/admin/index.d.ts +6 -4
  180. package/dist/reporting/admin/model.d.ts +36 -3
  181. package/dist/reporting/admin/ports.d.ts +39 -14
  182. package/dist/reporting/admin/service.d.ts +32 -11
  183. package/dist/reporting/admin/service.js +102 -18
  184. package/dist/reporting/admin/service.js.map +1 -1
  185. package/dist/reporting/events/index.d.ts +27 -0
  186. package/dist/reporting/events/index.js +20 -0
  187. package/dist/reporting/events/index.js.map +1 -0
  188. package/dist/reporting/member/calendar.d.ts +11 -0
  189. package/dist/reporting/member/calendar.js +75 -0
  190. package/dist/reporting/member/calendar.js.map +1 -0
  191. package/dist/reporting/member/index.d.ts +3 -2
  192. package/dist/reporting/member/model.d.ts +26 -5
  193. package/dist/reporting/member/ports.d.ts +48 -29
  194. package/dist/reporting/member/service.d.ts +56 -29
  195. package/dist/reporting/member/service.js +328 -113
  196. package/dist/reporting/member/service.js.map +1 -1
  197. package/dist/shared/actor.d.ts +22 -3
  198. package/dist/shared/errors.d.ts +2 -1
  199. package/dist/shared/errors.js +3 -1
  200. package/dist/shared/errors.js.map +1 -1
  201. package/dist/shared/id.d.ts +4 -0
  202. package/dist/shared/id.js +4 -0
  203. package/dist/shared/id.js.map +1 -1
  204. package/dist/shared/logger.d.ts +2 -1
  205. package/dist/shared/logger.js +10 -1
  206. package/dist/shared/logger.js.map +1 -1
  207. package/dist/shared/ports.d.ts +25 -5
  208. package/dist/types/content.d.ts +2 -2
  209. package/dist/types/discussion.d.ts +4 -5
  210. package/dist/types/email-templates.d.ts +22 -3
  211. package/dist/types/entitlements.d.ts +2 -2
  212. package/dist/types/identity.d.ts +13 -16
  213. package/dist/types/index.d.ts +9 -9
  214. package/dist/types/index.js +2 -1
  215. package/dist/types/notifications.d.ts +4 -4
  216. package/dist/types/organizations.d.ts +3 -2
  217. package/dist/types/ports.d.ts +8 -1
  218. package/dist/types/ports.js +7 -1
  219. package/dist/types/ports.js.map +1 -0
  220. package/dist/types/schemas/assets.d.ts +5 -5
  221. package/dist/types/schemas/assets.js +4 -3
  222. package/dist/types/schemas/assets.js.map +1 -1
  223. package/dist/types/schemas/content.d.ts +537 -13
  224. package/dist/types/schemas/content.js +201 -7
  225. package/dist/types/schemas/content.js.map +1 -1
  226. package/dist/types/schemas/discussion.d.ts +282 -59
  227. package/dist/types/schemas/discussion.js +77 -3
  228. package/dist/types/schemas/discussion.js.map +1 -1
  229. package/dist/types/schemas/entitlements.d.ts +14 -1
  230. package/dist/types/schemas/entitlements.js +11 -4
  231. package/dist/types/schemas/entitlements.js.map +1 -1
  232. package/dist/types/schemas/index.d.ts +5 -5
  233. package/dist/types/schemas/index.js +5 -5
  234. package/dist/types/schemas/notifications.d.ts +13 -12
  235. package/dist/types/schemas/notifications.js +3 -2
  236. package/dist/types/schemas/notifications.js.map +1 -1
  237. package/dist/types/schemas/organizations.d.ts +96 -60
  238. package/dist/types/schemas/organizations.js +78 -27
  239. package/dist/types/schemas/organizations.js.map +1 -1
  240. package/dist/types/schemas/progress.d.ts +3 -4
  241. package/dist/types/schemas/progress.js +0 -1
  242. package/dist/types/schemas/progress.js.map +1 -1
  243. package/package.json +1 -1
  244. package/dist/organizations/access-scopes.d.ts +0 -19
  245. package/dist/organizations/access-scopes.js +0 -89
  246. package/dist/organizations/access-scopes.js.map +0 -1
  247. package/dist/organizations/roles.d.ts +0 -19
  248. package/dist/organizations/roles.js +0 -82
  249. package/dist/organizations/roles.js.map +0 -1
@@ -1,14 +1,26 @@
1
- import { assetEvents } from "./events.js";
2
- import { genId } from "../shared/id.js";
3
- import { ConflictError, ForbiddenError, InvalidInputError } from "../shared/errors.js";
4
1
  import { noopLogger } from "../shared/logger.js";
2
+ import { ConflictError, ForbiddenError, InvalidInputError, NotFoundError } from "../shared/errors.js";
3
+ import { genId } from "../shared/id.js";
4
+ import { PUBLIC_PREFIX } from "../types/ports.js";
5
+ import { publicImageKey, publicImagePrefix } from "./file-urls.js";
6
+ import { assetEvents } from "./events.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)}image/${asset.id}/${sanitizeFilename(asset.filename)}`;
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(orgId, input) {
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 requestMemberUpload(orgId, orgUserId, input) {
102
- if (!isMemberImageType(input.contentType)) throw new InvalidInputError(MEMBER_IMAGE_REFUSAL);
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) return null;
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
- return confirmed ? this.view(confirmed) : null;
157
+ if (!confirmed) throw new NotFoundError("Asset", id);
158
+ return this.view(confirmed);
116
159
  }
117
- async confirmOwn(orgId, id, orgUserId) {
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 !== orgUserId) {
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
- return null;
169
+ throw new NotFoundError("Asset", id);
126
170
  }
127
171
  const confirmed = await this.finalize(orgId, asset);
128
- if (!confirmed) return null;
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(orgId, assetId, orgUserId) {
140
- if (!await this.ownsImage(orgId, assetId, orgUserId)) {
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
- return null;
191
+ throw new NotFoundError("Upload", assetId);
147
192
  }
148
- return this.signFile(orgId, assetId);
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 libraryAssetsAmong(orgId, assetIds) {
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 Set(rows.filter((a) => a.source === "library" && a.status === "ready").map((a) => a.id));
158
- return assetIds.filter((id) => library.has(id));
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(orgId, query) {
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(orgId, id) {
171
- const asset = await this.repo.findById(orgId, id);
172
- return asset ? this.view(asset) : null;
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,94 @@ var AssetsServiceImpl = class {
194
264
  expiresInSeconds: this.downloadLinkExpirySeconds
195
265
  });
196
266
  }
197
- async libraryFile(orgId, assetId, opts) {
267
+ async syncPublicImage(orgId, ownerId, assetId) {
268
+ try {
269
+ const wanted = assetId === null ? null : publicImageKey(orgId, ownerId, assetId);
270
+ const held = await this.storage.list(publicImagePrefix(orgId, ownerId));
271
+ for (const object of held) if (object.key !== wanted) await this.storage.remove(object.key);
272
+ if (wanted === null || held.some((object) => object.key === wanted)) return;
273
+ const [asset] = await this.readyAmong(orgId, [assetId]);
274
+ if (!asset || asset.kind !== "image") {
275
+ this.logger.warn("public image not published — no ready image", {
276
+ orgId,
277
+ ownerId,
278
+ assetId
279
+ });
280
+ return;
281
+ }
282
+ await this.storage.publish({
283
+ from: asset.key,
284
+ to: wanted
285
+ });
286
+ this.logger.info("public image published", {
287
+ orgId,
288
+ ownerId,
289
+ assetId
290
+ });
291
+ } catch (err) {
292
+ this.logger.error("public image not synced", {
293
+ orgId,
294
+ ownerId,
295
+ assetId,
296
+ err
297
+ });
298
+ }
299
+ }
300
+ async libraryFile(assetId, opts, actor) {
301
+ const orgId = actor.orgId;
198
302
  const asset = await this.repo.findById(orgId, assetId);
199
303
  if (!asset || asset.source !== "library") {
200
304
  this.logger.warn("library file not served — not a library asset", {
201
305
  orgId,
202
306
  assetId
203
307
  });
204
- return null;
308
+ throw new NotFoundError("Asset", assetId);
205
309
  }
206
- return this.signFile(orgId, assetId, opts.download ? { saveAs: asset.filename } : {});
310
+ await this.requireReach(asset, actor);
311
+ const url = await this.signFile(orgId, assetId, opts.download ? { saveAs: asset.filename } : {});
312
+ if (!url) throw new NotFoundError("Asset", assetId);
313
+ return url;
314
+ }
315
+ async remove(id, actor) {
316
+ actor.permissions.require("media:delete");
317
+ await this.removeAsset(actor.orgId, id, actor.id);
207
318
  }
208
- async remove(orgId, id) {
319
+ async removeOwn(id, actor) {
320
+ const asset = await this.repo.findById(actor.orgId, id);
321
+ if (!asset || asset.source !== "member" || asset.uploadedBy !== actor.id) throw new NotFoundError("Asset", id);
322
+ await this.removeAsset(actor.orgId, id, actor.id);
323
+ }
324
+ async removeAsset(orgId, id, actorId) {
209
325
  const asset = await this.repo.findById(orgId, id);
210
- if (!asset) return false;
211
- const removed = await this.discard(orgId, asset);
212
- if (removed) this.logger.info("asset removed", {
326
+ if (!asset || !await this.discard(orgId, asset)) throw new NotFoundError("Asset", id);
327
+ this.logger.info("asset removed", {
213
328
  orgId,
214
- assetId: id
329
+ assetId: id,
330
+ actorId
215
331
  });
216
- return removed;
217
332
  }
218
- async removeOwn(orgId, id, orgUserId) {
219
- const asset = await this.repo.findById(orgId, id);
220
- if (!asset || asset.source !== "member" || asset.uploadedBy !== orgUserId) return false;
221
- return this.remove(orgId, id);
333
+ async requireReach(asset, actor) {
334
+ if (actor.permissions.has("media:read")) return;
335
+ if (asset.contentId === null) {
336
+ this.logger.warn("asset refused", {
337
+ orgId: actor.orgId,
338
+ assetId: asset.id,
339
+ orgUserId: actor.id
340
+ });
341
+ throw new ForbiddenError("You need media:read");
342
+ }
343
+ await this.requireItemWrite(actor.orgId, asset.contentId, actor);
344
+ }
345
+ async requireItemWrite(orgId, contentId, actor) {
346
+ const type = await this.repo.contentTypeOf(orgId, contentId);
347
+ if (!type || !actor.permissions.has(WRITE_PERMISSION[type], contentId)) {
348
+ this.logger.warn("asset refused", {
349
+ orgId,
350
+ contentId,
351
+ orgUserId: actor.id
352
+ });
353
+ throw new ForbiddenError(type ? `You need media:read or ${WRITE_PERMISSION[type]}` : "You need media:read");
354
+ }
222
355
  }
223
356
  async removeUnattached(orgId, now) {
224
357
  const stale = await this.repo.listUnreferencedMemberUploads(orgId, graceCutoff(now));
@@ -236,6 +369,8 @@ var AssetsServiceImpl = class {
236
369
  });
237
370
  }
238
371
  }
372
+ const orphans = await this.orphanPublicImages(orgId);
373
+ for (const key of orphans) await this.removeObject(orgId, key.split("/").pop() ?? "", key);
239
374
  let stray = 0;
240
375
  for (const key of await this.strayUploads(orgId, now)) try {
241
376
  await this.storage.remove(key);
@@ -247,13 +382,21 @@ var AssetsServiceImpl = class {
247
382
  err
248
383
  });
249
384
  }
250
- if (removed > 0 || stray > 0 || failed > 0) this.logger.info("unattached uploads removed", {
385
+ if (removed > 0 || stray > 0 || orphans.length > 0 || failed > 0) this.logger.info("unattached uploads removed", {
251
386
  orgId,
252
387
  count: removed,
253
388
  stray,
389
+ publicCopies: orphans.length,
254
390
  failed
255
391
  });
256
- return removed + stray;
392
+ return removed + stray + orphans.length;
393
+ }
394
+ async orphanPublicImages(orgId) {
395
+ const objects = await this.storage.list(`${PUBLIC_PREFIX}${orgId}/`);
396
+ if (objects.length === 0) return [];
397
+ const assetIdOf = (key) => key.split("/").pop() ?? "";
398
+ const live = new Set((await this.readyAmong(orgId, objects.map((o) => assetIdOf(o.key)))).map((a) => a.id));
399
+ return objects.filter((o) => !live.has(assetIdOf(o.key))).map((o) => o.key);
257
400
  }
258
401
  async strayUploads(orgId, now) {
259
402
  const cutoff = now.getTime() - this.storage.uploadExpirySeconds * 1e3;
@@ -297,8 +440,9 @@ var AssetsServiceImpl = class {
297
440
  const stat = await this.storage.stat(key);
298
441
  if (!stat) throw new Error("copied upload not found");
299
442
  const contentType = asset.contentType;
300
- const kind = kindOf(contentType);
301
- const reason = !isMemberImageType(contentType) ? MEMBER_IMAGE_REFUSAL : stat.contentType !== void 0 && stat.contentType !== contentType ? "The uploaded file is not the declared type" : stat.size > this.memberImageMaxBytes ? "This image is too large" : null;
443
+ const kind = memberKindOf(contentType);
444
+ const limit = kind === "image" ? this.memberImageMaxBytes : this.memberFileMaxBytes;
445
+ 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
446
  if (reason) {
303
447
  await this.removeObject(orgId, asset.id, key);
304
448
  await this.discard(orgId, asset);
@@ -360,9 +504,24 @@ var AssetsServiceImpl = class {
360
504
  })]);
361
505
  return deleted;
362
506
  });
363
- if (deleted) await this.removeObject(orgId, asset.id, asset.key);
507
+ if (deleted) {
508
+ await this.removeObject(orgId, asset.id, asset.key);
509
+ await this.removePublicCopies(orgId, asset.id);
510
+ }
364
511
  return deleted;
365
512
  }
513
+ async removePublicCopies(orgId, assetId) {
514
+ try {
515
+ const copies = await this.storage.list(`${PUBLIC_PREFIX}${orgId}/`);
516
+ for (const copy of copies.filter((o) => o.key.endsWith(`/${assetId}`))) await this.removeObject(orgId, assetId, copy.key);
517
+ } catch (err) {
518
+ this.logger.warn("public copies not removed", {
519
+ orgId,
520
+ assetId,
521
+ err
522
+ });
523
+ }
524
+ }
366
525
  async removeObject(orgId, assetId, key) {
367
526
  try {
368
527
  await this.storage.remove(key);
@@ -375,7 +534,22 @@ var AssetsServiceImpl = class {
375
534
  }
376
535
  }
377
536
  };
537
+ function sizeLabel(bytes) {
538
+ const units = [
539
+ "B",
540
+ "KB",
541
+ "MB",
542
+ "GB"
543
+ ];
544
+ let value = bytes;
545
+ let unit = 0;
546
+ while (value >= 1024 && unit < units.length - 1) {
547
+ value /= 1024;
548
+ unit += 1;
549
+ }
550
+ return `${Number.isInteger(value) ? value : value.toFixed(1)} ${units[unit]}`;
551
+ }
378
552
  //#endregion
379
- export { AssetsServiceImpl, MEMBER_IMAGE_TYPE, isMemberImageType, kindOf };
553
+ export { AssetsServiceImpl, MEMBER_IMAGE_TYPE, isMemberImageType, kindOf, memberKindOf };
380
554
 
381
555
  //# 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":"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"}
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 syncPublicImage(orgId: string, ownerId: string, assetId: string | null): Promise<void> {\n try {\n const wanted = assetId === null ? null : publicImageKey(orgId, ownerId, assetId);\n const held = await this.storage.list(publicImagePrefix(orgId, ownerId));\n for (const object of held) {\n if (object.key !== wanted) {\n await this.storage.remove(object.key);\n }\n }\n if (wanted === null || held.some((object) => object.key === wanted)) {\n return;\n }\n const [asset] = await this.readyAmong(orgId, [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 return;\n }\n await this.storage.publish({ from: asset.key, to: wanted });\n this.logger.info('public image published', { orgId, ownerId, assetId });\n } catch (err) {\n this.logger.error('public image not synced', { orgId, ownerId, assetId, 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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"}