@headless-lms/core 0.2.0 → 0.4.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 (307) hide show
  1. package/dist/assets/events.d.ts +42 -24
  2. package/dist/assets/events.js +1 -1
  3. package/dist/assets/events.js.map +1 -1
  4. package/dist/assets/file-urls.d.ts +20 -0
  5. package/dist/assets/file-urls.js +25 -0
  6. package/dist/assets/file-urls.js.map +1 -0
  7. package/dist/assets/index.d.ts +6 -4
  8. package/dist/assets/index.js +4 -2
  9. package/dist/assets/jobs.d.ts +15 -0
  10. package/dist/assets/jobs.js +30 -0
  11. package/dist/assets/jobs.js.map +1 -0
  12. package/dist/assets/model.d.ts +2 -2
  13. package/dist/assets/ports.d.ts +46 -15
  14. package/dist/assets/service.d.ts +38 -7
  15. package/dist/assets/service.js +287 -38
  16. package/dist/assets/service.js.map +1 -1
  17. package/dist/automations/actions.d.ts +13 -24
  18. package/dist/automations/actions.js +106 -42
  19. package/dist/automations/actions.js.map +1 -1
  20. package/dist/automations/catalog.js +226 -3
  21. package/dist/automations/catalog.js.map +1 -1
  22. package/dist/automations/community-derivations.d.ts +14 -0
  23. package/dist/automations/community-derivations.js +439 -0
  24. package/dist/automations/community-derivations.js.map +1 -0
  25. package/dist/automations/community-mail.d.ts +98 -0
  26. package/dist/automations/community-mail.js +70 -0
  27. package/dist/automations/community-mail.js.map +1 -0
  28. package/dist/automations/events.d.ts +18 -0
  29. package/dist/automations/index.d.ts +13 -4
  30. package/dist/automations/index.js +10 -2
  31. package/dist/automations/model.d.ts +10 -3
  32. package/dist/automations/model.js +15 -2
  33. package/dist/automations/model.js.map +1 -1
  34. package/dist/automations/notifications/debounce.d.ts +25 -0
  35. package/dist/automations/notifications/debounce.js +93 -0
  36. package/dist/automations/notifications/debounce.js.map +1 -0
  37. package/dist/automations/notifications/defaults.d.ts +8 -0
  38. package/dist/automations/notifications/defaults.js +316 -0
  39. package/dist/automations/notifications/defaults.js.map +1 -0
  40. package/dist/automations/notifications/delivery.d.ts +28 -0
  41. package/dist/automations/notifications/delivery.js +88 -0
  42. package/dist/automations/notifications/delivery.js.map +1 -0
  43. package/dist/automations/notifications/processor.d.ts +64 -0
  44. package/dist/automations/notifications/processor.js +270 -0
  45. package/dist/automations/notifications/processor.js.map +1 -0
  46. package/dist/automations/notifications/rules/content.d.ts +6 -0
  47. package/dist/automations/notifications/rules/content.js +39 -0
  48. package/dist/automations/notifications/rules/content.js.map +1 -0
  49. package/dist/automations/notifications/rules/discussion.d.ts +7 -0
  50. package/dist/automations/notifications/rules/discussion.js +678 -0
  51. package/dist/automations/notifications/rules/discussion.js.map +1 -0
  52. package/dist/automations/notifications/rules/entitlements.d.ts +6 -0
  53. package/dist/automations/notifications/rules/entitlements.js +249 -0
  54. package/dist/automations/notifications/rules/entitlements.js.map +1 -0
  55. package/dist/automations/notifications/rules/index.d.ts +6 -0
  56. package/dist/automations/notifications/rules/index.js +15 -0
  57. package/dist/automations/notifications/rules/index.js.map +1 -0
  58. package/dist/automations/notifications/rules/organizations.d.ts +6 -0
  59. package/dist/automations/notifications/rules/organizations.js +28 -0
  60. package/dist/automations/notifications/rules/organizations.js.map +1 -0
  61. package/dist/automations/notifications/rules/recipients.d.ts +32 -0
  62. package/dist/automations/notifications/rules/recipients.js +99 -0
  63. package/dist/automations/notifications/rules/recipients.js.map +1 -0
  64. package/dist/automations/notifications/service.d.ts +27 -0
  65. package/dist/automations/notifications/service.js +45 -0
  66. package/dist/automations/notifications/service.js.map +1 -0
  67. package/dist/automations/notifications/types.d.ts +7 -0
  68. package/dist/automations/notifications/types.js +473 -0
  69. package/dist/automations/notifications/types.js.map +1 -0
  70. package/dist/automations/ports.d.ts +121 -3
  71. package/dist/automations/schedule.d.ts +6 -0
  72. package/dist/automations/schedule.js +41 -0
  73. package/dist/automations/schedule.js.map +1 -0
  74. package/dist/automations/service.d.ts +20 -4
  75. package/dist/automations/service.js +246 -45
  76. package/dist/automations/service.js.map +1 -1
  77. package/dist/content/events.d.ts +712 -50
  78. package/dist/content/events.js +55 -1
  79. package/dist/content/events.js.map +1 -1
  80. package/dist/content/index.d.ts +6 -4
  81. package/dist/content/index.js +3 -1
  82. package/dist/content/model.d.ts +2 -2
  83. package/dist/content/notification-categories.d.ts +36 -0
  84. package/dist/content/notification-categories.js +64 -0
  85. package/dist/content/notification-categories.js.map +1 -0
  86. package/dist/content/ports.d.ts +90 -18
  87. package/dist/content/service.d.ts +57 -18
  88. package/dist/content/service.js +520 -25
  89. package/dist/content/service.js.map +1 -1
  90. package/dist/content/titles.d.ts +22 -0
  91. package/dist/content/titles.js +69 -0
  92. package/dist/content/titles.js.map +1 -0
  93. package/dist/content/types.d.ts +2 -2
  94. package/dist/discussion/authors.d.ts +10 -0
  95. package/dist/discussion/authors.js +23 -0
  96. package/dist/discussion/authors.js.map +1 -0
  97. package/dist/discussion/chats.d.ts +109 -0
  98. package/dist/discussion/chats.js +831 -0
  99. package/dist/discussion/chats.js.map +1 -0
  100. package/dist/discussion/digests.d.ts +24 -0
  101. package/dist/discussion/digests.js +104 -0
  102. package/dist/discussion/digests.js.map +1 -0
  103. package/dist/discussion/events.d.ts +785 -56
  104. package/dist/discussion/events.js +165 -5
  105. package/dist/discussion/events.js.map +1 -1
  106. package/dist/discussion/index.d.ts +12 -3
  107. package/dist/discussion/index.js +9 -1
  108. package/dist/discussion/jobs.d.ts +16 -0
  109. package/dist/discussion/jobs.js +40 -0
  110. package/dist/discussion/jobs.js.map +1 -0
  111. package/dist/discussion/mentions.d.ts +11 -0
  112. package/dist/discussion/mentions.js +35 -0
  113. package/dist/discussion/mentions.js.map +1 -0
  114. package/dist/discussion/ports.d.ts +372 -28
  115. package/dist/discussion/ports.js +6 -1
  116. package/dist/discussion/ports.js.map +1 -0
  117. package/dist/discussion/posts.d.ts +113 -0
  118. package/dist/discussion/posts.js +775 -0
  119. package/dist/discussion/posts.js.map +1 -0
  120. package/dist/discussion/service.d.ts +42 -11
  121. package/dist/discussion/service.js +364 -85
  122. package/dist/discussion/service.js.map +1 -1
  123. package/dist/discussion/subscriptions.d.ts +37 -0
  124. package/dist/discussion/subscriptions.js +99 -0
  125. package/dist/discussion/subscriptions.js.map +1 -0
  126. package/dist/discussion/titles.d.ts +26 -0
  127. package/dist/discussion/titles.js +84 -0
  128. package/dist/discussion/titles.js.map +1 -0
  129. package/dist/entitlements/events.d.ts +178 -11
  130. package/dist/entitlements/events.js +28 -1
  131. package/dist/entitlements/events.js.map +1 -1
  132. package/dist/entitlements/index.d.ts +6 -4
  133. package/dist/entitlements/index.js +3 -1
  134. package/dist/entitlements/jobs.d.ts +12 -0
  135. package/dist/entitlements/jobs.js +18 -0
  136. package/dist/entitlements/jobs.js.map +1 -0
  137. package/dist/entitlements/model.d.ts +3 -2
  138. package/dist/entitlements/ports.d.ts +128 -11
  139. package/dist/entitlements/rules.d.ts +12 -0
  140. package/dist/entitlements/rules.js +17 -0
  141. package/dist/entitlements/rules.js.map +1 -0
  142. package/dist/entitlements/service.d.ts +57 -3
  143. package/dist/entitlements/service.js +492 -2
  144. package/dist/entitlements/service.js.map +1 -1
  145. package/dist/identity/access.d.ts +6 -0
  146. package/dist/identity/access.js +7 -0
  147. package/dist/identity/access.js.map +1 -0
  148. package/dist/identity/devices.d.ts +30 -0
  149. package/dist/identity/devices.js +107 -0
  150. package/dist/identity/devices.js.map +1 -0
  151. package/dist/identity/events.d.ts +2 -2
  152. package/dist/identity/index.d.ts +7 -4
  153. package/dist/identity/index.js +4 -1
  154. package/dist/identity/model.d.ts +2 -1
  155. package/dist/identity/notification-settings.d.ts +28 -0
  156. package/dist/identity/notification-settings.js +113 -0
  157. package/dist/identity/notification-settings.js.map +1 -0
  158. package/dist/identity/ports.d.ts +97 -3
  159. package/dist/identity/service.d.ts +14 -3
  160. package/dist/identity/service.js +62 -5
  161. package/dist/identity/service.js.map +1 -1
  162. package/dist/integrations/events.d.ts +3 -3
  163. package/dist/integrations/model.js +1 -1
  164. package/dist/integrations/model.js.map +1 -1
  165. package/dist/integrations/service.js +2 -2
  166. package/dist/integrations/service.js.map +1 -1
  167. package/dist/organizations/access-scopes.d.ts +19 -0
  168. package/dist/organizations/access-scopes.js +89 -0
  169. package/dist/organizations/access-scopes.js.map +1 -0
  170. package/dist/organizations/events.d.ts +75 -33
  171. package/dist/organizations/events.js +6 -0
  172. package/dist/organizations/events.js.map +1 -1
  173. package/dist/organizations/index.d.ts +8 -6
  174. package/dist/organizations/index.js +5 -3
  175. package/dist/organizations/members.d.ts +8 -5
  176. package/dist/organizations/members.js.map +1 -1
  177. package/dist/organizations/notification-categories.d.ts +32 -0
  178. package/dist/organizations/notification-categories.js +51 -0
  179. package/dist/organizations/notification-categories.js.map +1 -0
  180. package/dist/organizations/ports.d.ts +17 -11
  181. package/dist/organizations/roles.d.ts +6 -10
  182. package/dist/organizations/roles.js +19 -7
  183. package/dist/organizations/roles.js.map +1 -1
  184. package/dist/organizations/service.d.ts +22 -11
  185. package/dist/organizations/service.js +164 -47
  186. package/dist/organizations/service.js.map +1 -1
  187. package/dist/organizations/types.d.ts +1 -10
  188. package/dist/progress/events.d.ts +14 -14
  189. package/dist/progress/index.d.ts +2 -2
  190. package/dist/progress/ports.d.ts +6 -1
  191. package/dist/progress/service.d.ts +3 -1
  192. package/dist/progress/service.js +6 -2
  193. package/dist/progress/service.js.map +1 -1
  194. package/dist/reporting/admin/index.d.ts +4 -0
  195. package/dist/reporting/admin/index.js +2 -0
  196. package/dist/reporting/admin/model.d.ts +108 -0
  197. package/dist/reporting/admin/ports.d.ts +27 -0
  198. package/dist/reporting/admin/service.d.ts +22 -0
  199. package/dist/reporting/admin/service.js +38 -0
  200. package/dist/reporting/admin/service.js.map +1 -0
  201. package/dist/reporting/member/index.d.ts +5 -0
  202. package/dist/reporting/member/index.js +2 -0
  203. package/dist/reporting/member/model.d.ts +157 -0
  204. package/dist/reporting/member/ports.d.ts +48 -0
  205. package/dist/reporting/member/service.d.ts +92 -0
  206. package/dist/reporting/member/service.js +364 -0
  207. package/dist/reporting/member/service.js.map +1 -0
  208. package/dist/shared/actor.d.ts +9 -0
  209. package/dist/shared/errors.d.ts +19 -1
  210. package/dist/shared/errors.js +27 -1
  211. package/dist/shared/errors.js.map +1 -1
  212. package/dist/shared/id.d.ts +14 -0
  213. package/dist/shared/id.js +15 -1
  214. package/dist/shared/id.js.map +1 -1
  215. package/dist/shared/mailer.d.ts +15 -2
  216. package/dist/shared/mailer.js +32 -5
  217. package/dist/shared/mailer.js.map +1 -1
  218. package/dist/shared/network.d.ts +10 -0
  219. package/dist/shared/network.js +211 -0
  220. package/dist/shared/network.js.map +1 -0
  221. package/dist/shared/notification-categories.d.ts +16 -0
  222. package/dist/shared/notification-categories.js +94 -0
  223. package/dist/shared/notification-categories.js.map +1 -0
  224. package/dist/shared/notification-settings.d.ts +14 -0
  225. package/dist/shared/notification-settings.js +61 -0
  226. package/dist/shared/notification-settings.js.map +1 -0
  227. package/dist/shared/ports.d.ts +48 -2
  228. package/dist/shared/settings.d.ts +2 -1
  229. package/dist/shared/settings.js +1 -0
  230. package/dist/shared/settings.js.map +1 -1
  231. package/dist/types/assets.d.ts +2 -2
  232. package/dist/types/automations.d.ts +15 -1
  233. package/dist/types/content.d.ts +2 -2
  234. package/dist/types/discussion.d.ts +11 -3
  235. package/dist/types/email-templates.d.ts +216 -1
  236. package/dist/types/entitlements.d.ts +2 -2
  237. package/dist/types/identity.d.ts +48 -2
  238. package/dist/types/index.d.ts +9 -8
  239. package/dist/types/notifications.d.ts +157 -0
  240. package/dist/types/ports.d.ts +6 -0
  241. package/dist/types/schemas/assets.d.ts +65 -37
  242. package/dist/types/schemas/assets.js +17 -12
  243. package/dist/types/schemas/assets.js.map +1 -1
  244. package/dist/types/schemas/automations.d.ts +9 -1
  245. package/dist/types/schemas/automations.js +13 -6
  246. package/dist/types/schemas/automations.js.map +1 -1
  247. package/dist/types/schemas/content.d.ts +396 -3
  248. package/dist/types/schemas/content.js +161 -3
  249. package/dist/types/schemas/content.js.map +1 -1
  250. package/dist/types/schemas/discussion.d.ts +443 -3
  251. package/dist/types/schemas/discussion.js +233 -4
  252. package/dist/types/schemas/discussion.js.map +1 -1
  253. package/dist/types/schemas/entitlements.d.ts +36 -2
  254. package/dist/types/schemas/entitlements.js +23 -1
  255. package/dist/types/schemas/entitlements.js.map +1 -1
  256. package/dist/types/schemas/identity.d.ts +27 -1
  257. package/dist/types/schemas/identity.js +14 -2
  258. package/dist/types/schemas/identity.js.map +1 -1
  259. package/dist/types/schemas/index.d.ts +9 -8
  260. package/dist/types/schemas/index.js +9 -8
  261. package/dist/types/schemas/notifications.d.ts +95 -0
  262. package/dist/types/schemas/notifications.js +56 -0
  263. package/dist/types/schemas/notifications.js.map +1 -0
  264. package/dist/types/schemas/organizations.d.ts +8 -0
  265. package/dist/types/schemas/organizations.js +2 -0
  266. package/dist/types/schemas/organizations.js.map +1 -1
  267. package/dist/types/schemas/progress.d.ts +3 -3
  268. package/dist/types/schemas/shared.d.ts +3 -1
  269. package/dist/types/schemas/shared.js +5 -1
  270. package/dist/types/schemas/shared.js.map +1 -1
  271. package/package.json +2 -2
  272. package/dist/reporting/courses/index.d.ts +0 -4
  273. package/dist/reporting/courses/index.js +0 -2
  274. package/dist/reporting/courses/model.d.ts +0 -24
  275. package/dist/reporting/courses/ports.d.ts +0 -13
  276. package/dist/reporting/courses/service.d.ts +0 -19
  277. package/dist/reporting/courses/service.js +0 -20
  278. package/dist/reporting/courses/service.js.map +0 -1
  279. package/dist/reporting/dashboard/index.d.ts +0 -4
  280. package/dist/reporting/dashboard/index.js +0 -2
  281. package/dist/reporting/dashboard/model.d.ts +0 -14
  282. package/dist/reporting/dashboard/ports.d.ts +0 -13
  283. package/dist/reporting/dashboard/service.d.ts +0 -19
  284. package/dist/reporting/dashboard/service.js +0 -20
  285. package/dist/reporting/dashboard/service.js.map +0 -1
  286. package/dist/reporting/learn/index.d.ts +0 -5
  287. package/dist/reporting/learn/index.js +0 -2
  288. package/dist/reporting/learn/model.d.ts +0 -23
  289. package/dist/reporting/learn/ports.d.ts +0 -43
  290. package/dist/reporting/learn/service.d.ts +0 -49
  291. package/dist/reporting/learn/service.js +0 -116
  292. package/dist/reporting/learn/service.js.map +0 -1
  293. package/dist/reporting/students/index.d.ts +0 -4
  294. package/dist/reporting/students/index.js +0 -2
  295. package/dist/reporting/students/model.d.ts +0 -51
  296. package/dist/reporting/students/model.js +0 -1
  297. package/dist/reporting/students/ports.d.ts +0 -15
  298. package/dist/reporting/students/ports.js +0 -1
  299. package/dist/reporting/students/service.d.ts +0 -20
  300. package/dist/reporting/students/service.js +0 -23
  301. package/dist/reporting/students/service.js.map +0 -1
  302. /package/dist/reporting/{courses → admin}/model.js +0 -0
  303. /package/dist/reporting/{courses → admin}/ports.js +0 -0
  304. /package/dist/reporting/{dashboard → member}/model.js +0 -0
  305. /package/dist/reporting/{dashboard → member}/ports.js +0 -0
  306. /package/dist/{reporting/learn/model.js → shared/actor.js} +0 -0
  307. /package/dist/{reporting/learn/ports.js → types/notifications.js} +0 -0
@@ -1,27 +1,59 @@
1
- import { noopLogger } from "../shared/logger.js";
2
- import { genId } from "../shared/id.js";
3
1
  import { assetEvents } from "./events.js";
2
+ import { genId } from "../shared/id.js";
3
+ import { ConflictError, ForbiddenError, InvalidInputError } from "../shared/errors.js";
4
+ import { noopLogger } from "../shared/logger.js";
4
5
  //#region src/assets/service.ts
6
+ const UNATTACHED_UPLOAD_GRACE_MS = 3600 * 1e3;
7
+ 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
+ function isMemberImageType(contentType) {
10
+ return MEMBER_IMAGE_TYPE.test(contentType.trim());
11
+ }
12
+ function kindOf(contentType) {
13
+ const type = contentType.trim().toLowerCase();
14
+ if (type.startsWith("image/")) return "image";
15
+ if (type.startsWith("video/")) return "video";
16
+ if (type.startsWith("audio/")) return "audio";
17
+ return "document";
18
+ }
5
19
  function orgPrefix(orgId) {
6
20
  return `org/${orgId}/`;
7
21
  }
8
22
  function sanitizeFilename(filename) {
9
23
  return (filename.split(/[\\/]/).pop() ?? "file").replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 120) || "file";
10
24
  }
25
+ function storedKey(orgId, folder, id, filename) {
26
+ return `${orgPrefix(orgId)}${folder}/${id}/${sanitizeFilename(filename)}`;
27
+ }
28
+ function finalMemberKey(orgId, asset) {
29
+ return `${orgPrefix(orgId)}image/${asset.id}/${sanitizeFilename(asset.filename)}`;
30
+ }
31
+ function graceCutoff(now) {
32
+ return /* @__PURE__ */ new Date(now.getTime() - UNATTACHED_UPLOAD_GRACE_MS);
33
+ }
11
34
  var AssetsServiceImpl = class {
12
35
  storage;
13
36
  repo;
14
37
  uow;
38
+ memberImageMaxBytes;
39
+ downloadLinkExpirySeconds;
40
+ fileUrls;
15
41
  logger;
16
42
  constructor(params) {
43
+ if (!Number.isSafeInteger(params.memberImageMaxBytes) || params.memberImageMaxBytes <= 0) throw new Error(`invalid memberImageMaxBytes "${params.memberImageMaxBytes}" (expected a positive integer)`);
44
+ if (!Number.isSafeInteger(params.downloadLinkExpirySeconds) || params.downloadLinkExpirySeconds <= 0) throw new Error(`invalid downloadLinkExpirySeconds "${params.downloadLinkExpirySeconds}" (expected a positive integer)`);
17
45
  this.storage = params.storage;
18
46
  this.repo = params.repo;
19
47
  this.uow = params.uow;
48
+ this.memberImageMaxBytes = params.memberImageMaxBytes;
49
+ this.downloadLinkExpirySeconds = params.downloadLinkExpirySeconds;
50
+ this.fileUrls = params.fileUrls;
20
51
  this.logger = params.logger ?? noopLogger;
21
52
  }
22
53
  async requestUpload(orgId, input) {
23
54
  const id = genId("asset");
24
- const key = `${orgPrefix(orgId)}${input.kind}/${id}/${sanitizeFilename(input.filename)}`;
55
+ const kind = kindOf(input.contentType);
56
+ const key = storedKey(orgId, input.source === "member" ? "uploads" : kind, id, input.filename);
25
57
  const presigned = await this.storage.presignUpload({
26
58
  key,
27
59
  contentType: input.contentType
@@ -32,7 +64,8 @@ var AssetsServiceImpl = class {
32
64
  orgId,
33
65
  id,
34
66
  key,
35
- kind: input.kind,
67
+ kind,
68
+ source: input.source,
36
69
  filename: input.filename,
37
70
  contentType: input.contentType,
38
71
  size: 0,
@@ -50,83 +83,299 @@ var AssetsServiceImpl = class {
50
83
  this.logger.info("asset upload requested", {
51
84
  orgId,
52
85
  assetId: id,
53
- kind: input.kind
86
+ kind,
87
+ source: input.source
54
88
  });
55
89
  return {
56
- asset,
90
+ asset: {
91
+ ...asset,
92
+ url: null,
93
+ downloadUrl: null
94
+ },
57
95
  uploadUrl: presigned.url,
58
96
  method: "PUT",
59
97
  expiresInSeconds: presigned.expiresInSeconds,
60
98
  headers: presigned.headers
61
99
  };
62
100
  }
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
+ }
63
110
  async confirm(orgId, id) {
64
111
  const asset = await this.repo.findById(orgId, id);
65
112
  if (!asset) return null;
113
+ if (asset.source === "member") throw new ForbiddenError("Only the uploader can finalize a member upload");
114
+ const confirmed = await this.finalize(orgId, asset);
115
+ return confirmed ? this.view(confirmed) : null;
116
+ }
117
+ async confirmOwn(orgId, id, orgUserId) {
118
+ const asset = await this.repo.findById(orgId, id);
119
+ if (!asset || asset.source !== "member" || asset.uploadedBy !== orgUserId) {
120
+ this.logger.debug("upload confirm refused", {
121
+ orgId,
122
+ assetId: id,
123
+ orgUserId
124
+ });
125
+ return null;
126
+ }
127
+ const confirmed = await this.finalize(orgId, asset);
128
+ if (!confirmed) return null;
129
+ return confirmed.status === "ready" ? {
130
+ ...confirmed,
131
+ url: this.fileUrls.memberUpload(confirmed.id),
132
+ downloadUrl: null
133
+ } : {
134
+ ...confirmed,
135
+ url: null,
136
+ downloadUrl: null
137
+ };
138
+ }
139
+ async ownUploadFile(orgId, assetId, orgUserId) {
140
+ if (!await this.ownsImage(orgId, assetId, orgUserId)) {
141
+ this.logger.warn("upload file not served — not the uploader", {
142
+ orgId,
143
+ assetId,
144
+ orgUserId
145
+ });
146
+ return null;
147
+ }
148
+ return this.signFile(orgId, assetId);
149
+ }
150
+ async ownsImage(orgId, assetId, orgUserId) {
151
+ const asset = await this.repo.findById(orgId, assetId);
152
+ return asset !== null && asset.source === "member" && asset.kind === "image" && asset.status === "ready" && asset.uploadedBy === orgUserId;
153
+ }
154
+ async libraryAssetsAmong(orgId, assetIds) {
155
+ if (assetIds.length === 0) return [];
156
+ 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));
159
+ }
160
+ async list(orgId, query) {
161
+ const page = await this.repo.list(orgId, {
162
+ ...query,
163
+ source: query.source ?? "library"
164
+ });
165
+ return {
166
+ ...page,
167
+ rows: await Promise.all(page.rows.map((asset) => this.view(asset)))
168
+ };
169
+ }
170
+ async get(orgId, id) {
171
+ const asset = await this.repo.findById(orgId, id);
172
+ return asset ? this.view(asset) : null;
173
+ }
174
+ async readyFiles(orgId, assetIds) {
175
+ const ready = await this.readyAmong(orgId, assetIds);
176
+ return Object.fromEntries(ready.map((asset) => [asset.id, {
177
+ filename: asset.filename,
178
+ contentType: asset.contentType,
179
+ size: asset.size
180
+ }]));
181
+ }
182
+ async signFile(orgId, assetId, opts = {}) {
183
+ const [asset] = await this.readyAmong(orgId, [assetId]);
184
+ if (!asset) {
185
+ this.logger.warn("file not served — no ready asset", {
186
+ orgId,
187
+ assetId
188
+ });
189
+ return null;
190
+ }
191
+ return opts.saveAs === void 0 ? this.storage.presignDownload({ key: asset.key }) : this.storage.presignDownload({
192
+ key: asset.key,
193
+ downloadFilename: opts.saveAs,
194
+ expiresInSeconds: this.downloadLinkExpirySeconds
195
+ });
196
+ }
197
+ async libraryFile(orgId, assetId, opts) {
198
+ const asset = await this.repo.findById(orgId, assetId);
199
+ if (!asset || asset.source !== "library") {
200
+ this.logger.warn("library file not served — not a library asset", {
201
+ orgId,
202
+ assetId
203
+ });
204
+ return null;
205
+ }
206
+ return this.signFile(orgId, assetId, opts.download ? { saveAs: asset.filename } : {});
207
+ }
208
+ async remove(orgId, id) {
209
+ 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", {
213
+ orgId,
214
+ assetId: id
215
+ });
216
+ return removed;
217
+ }
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);
222
+ }
223
+ async removeUnattached(orgId, now) {
224
+ const stale = await this.repo.listUnreferencedMemberUploads(orgId, graceCutoff(now));
225
+ let removed = 0;
226
+ let failed = 0;
227
+ for (const asset of stale) try {
228
+ if (await this.discard(orgId, asset)) removed += 1;
229
+ } catch (err) {
230
+ if (!(err instanceof ConflictError)) {
231
+ failed += 1;
232
+ this.logger.error("unattached upload not removed", {
233
+ orgId,
234
+ assetId: asset.id,
235
+ err
236
+ });
237
+ }
238
+ }
239
+ let stray = 0;
240
+ for (const key of await this.strayUploads(orgId, now)) try {
241
+ await this.storage.remove(key);
242
+ stray += 1;
243
+ } catch (err) {
244
+ failed += 1;
245
+ this.logger.warn("stray upload not removed", {
246
+ orgId,
247
+ err
248
+ });
249
+ }
250
+ if (removed > 0 || stray > 0 || failed > 0) this.logger.info("unattached uploads removed", {
251
+ orgId,
252
+ count: removed,
253
+ stray,
254
+ failed
255
+ });
256
+ return removed + stray;
257
+ }
258
+ async strayUploads(orgId, now) {
259
+ const cutoff = now.getTime() - this.storage.uploadExpirySeconds * 1e3;
260
+ const old = (await this.storage.list(`${orgPrefix(orgId)}uploads/`)).filter((o) => o.lastModified !== void 0 && Date.parse(o.lastModified) < cutoff).map((o) => o.key);
261
+ if (old.length === 0) return [];
262
+ const held = new Set(await this.repo.heldKeys(orgId, old));
263
+ return old.filter((key) => !held.has(key));
264
+ }
265
+ async finalize(orgId, asset) {
266
+ if (asset.status !== "pending") return asset;
267
+ if (asset.source === "member") return this.finalizeMember(orgId, asset);
66
268
  const stat = await this.storage.stat(asset.key);
67
269
  if (!stat) {
68
270
  this.logger.debug("asset not yet uploaded", {
69
271
  orgId,
70
- assetId: id
272
+ assetId: asset.id
71
273
  });
72
274
  return asset;
73
275
  }
74
- const updated = await this.uow.run(async ({ assets, outbox }) => {
75
- const updated = await assets.update(id, {
76
- size: stat.size,
77
- contentType: stat.contentType ?? asset.contentType,
78
- status: "ready"
276
+ const contentType = stat.contentType ?? asset.contentType;
277
+ return await this.markReady(orgId, asset, {
278
+ key: asset.key,
279
+ kind: kindOf(contentType),
280
+ size: stat.size,
281
+ contentType
282
+ }) ?? this.repo.findById(orgId, asset.id);
283
+ }
284
+ async finalizeMember(orgId, asset) {
285
+ if (!await this.storage.stat(asset.key)) {
286
+ this.logger.debug("asset not yet uploaded", {
287
+ orgId,
288
+ assetId: asset.id
79
289
  });
290
+ return asset;
291
+ }
292
+ const key = finalMemberKey(orgId, asset);
293
+ await this.storage.copy({
294
+ from: asset.key,
295
+ to: key
296
+ });
297
+ const stat = await this.storage.stat(key);
298
+ if (!stat) throw new Error("copied upload not found");
299
+ 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;
302
+ if (reason) {
303
+ await this.removeObject(orgId, asset.id, key);
304
+ await this.discard(orgId, asset);
305
+ this.logger.info("member upload refused", {
306
+ orgId,
307
+ assetId: asset.id,
308
+ reason
309
+ });
310
+ throw new InvalidInputError(reason);
311
+ }
312
+ const ready = await this.markReady(orgId, asset, {
313
+ key,
314
+ kind,
315
+ size: stat.size,
316
+ contentType
317
+ });
318
+ if (!ready) return this.repo.findById(orgId, asset.id);
319
+ await this.removeObject(orgId, asset.id, asset.key);
320
+ return ready;
321
+ }
322
+ async markReady(orgId, asset, patch) {
323
+ const updated = await this.uow.run(async ({ assets, outbox }) => {
324
+ const updated = await assets.markReady(orgId, asset.id, patch);
80
325
  if (updated) await outbox.append([assetEvents.assetReady.make({
81
326
  orgId,
82
327
  data: updated
83
328
  })]);
84
329
  return updated;
85
330
  });
86
- this.logger.info("asset confirmed", {
331
+ if (updated) this.logger.info("asset confirmed", {
87
332
  orgId,
88
- assetId: id
333
+ assetId: asset.id
89
334
  });
90
335
  return updated;
91
336
  }
92
- list(orgId, query) {
93
- return this.repo.list(orgId, query);
94
- }
95
- get(orgId, id) {
96
- return this.repo.findById(orgId, id);
337
+ async readyAmong(orgId, assetIds) {
338
+ const ids = [...new Set(assetIds)];
339
+ if (ids.length === 0) return [];
340
+ return (await this.repo.findByIds(orgId, ids)).filter((asset) => asset.orgId === orgId && asset.status === "ready");
97
341
  }
98
- async requestDownload(orgId, id, filename, expiresInSeconds) {
99
- const asset = await this.repo.findById(orgId, id);
100
- if (!asset) return null;
342
+ view(asset) {
343
+ if (asset.source !== "library" || asset.status !== "ready") return {
344
+ ...asset,
345
+ url: null,
346
+ downloadUrl: null
347
+ };
101
348
  return {
102
- url: await this.storage.presignDownload({
103
- key: asset.key,
104
- downloadFilename: filename ?? asset.filename,
105
- ...expiresInSeconds === void 0 ? {} : { expiresInSeconds }
106
- }),
107
- asset
349
+ ...asset,
350
+ url: this.fileUrls.libraryFile(asset.id),
351
+ downloadUrl: this.fileUrls.libraryDownload(asset.id)
108
352
  };
109
353
  }
110
- async remove(orgId, id) {
111
- const asset = await this.repo.findById(orgId, id);
112
- if (!asset) return false;
113
- await this.storage.remove(asset.key);
354
+ async discard(orgId, asset) {
114
355
  const deleted = await this.uow.run(async ({ assets, outbox }) => {
115
- const deleted = await assets.delete(id);
356
+ const deleted = await assets.delete(orgId, asset.id);
116
357
  if (deleted) await outbox.append([assetEvents.assetDeleted.make({
117
358
  orgId,
118
359
  data: asset
119
360
  })]);
120
361
  return deleted;
121
362
  });
122
- this.logger.info("asset removed", {
123
- orgId,
124
- assetId: id
125
- });
363
+ if (deleted) await this.removeObject(orgId, asset.id, asset.key);
126
364
  return deleted;
127
365
  }
366
+ async removeObject(orgId, assetId, key) {
367
+ try {
368
+ await this.storage.remove(key);
369
+ } catch (error) {
370
+ this.logger.warn("stored object not removed", {
371
+ orgId,
372
+ assetId,
373
+ err: error
374
+ });
375
+ }
376
+ }
128
377
  };
129
378
  //#endregion
130
- export { AssetsServiceImpl };
379
+ export { AssetsServiceImpl, MEMBER_IMAGE_TYPE, isMemberImageType, kindOf };
131
380
 
132
381
  //# sourceMappingURL=service.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"service.js","names":[],"sources":["../../src/assets/service.ts"],"sourcesContent":["// assets context — service. Owns org-scoped key construction and the asset\n// lifecycle (register → upload → confirm → serve → remove); delegates the\n// actual object operations to the ObjectStorage port. Org isolation is enforced\n// by scoping every lookup to the caller's org (cross-org reads return null).\nimport { genId } from '../shared/id.js';\nimport type { Logger, ObjectStorage } from '../shared/ports.js';\nimport type {\n Asset,\n AssetsQuery,\n DownloadTicket,\n Page,\n RequestUploadInput,\n UploadTicket,\n} from './model.js';\nimport type { AssetsRepository, AssetsService, AssetsUnitOfWork } from './ports.js';\nimport { noopLogger } from '../shared/logger.js';\nimport { assetEvents } from './events.js';\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\nexport type AssetsServiceParams = {\n storage: ObjectStorage;\n repo: AssetsRepository;\n uow: AssetsUnitOfWork;\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 logger: Logger;\n\n constructor(params: AssetsServiceParams) {\n this.storage = params.storage;\n this.repo = params.repo;\n this.uow = params.uow;\n this.logger = params.logger ?? noopLogger;\n }\n\n async requestUpload(orgId: string, input: RequestUploadInput): Promise<UploadTicket> {\n const id = genId('asset');\n const key = `${orgPrefix(orgId)}${input.kind}/${id}/${sanitizeFilename(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: input.kind,\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', { orgId, assetId: id, kind: input.kind });\n return {\n asset,\n uploadUrl: presigned.url,\n method: 'PUT',\n expiresInSeconds: presigned.expiresInSeconds,\n headers: presigned.headers,\n };\n }\n\n async confirm(orgId: string, id: string): Promise<Asset | null> {\n const asset = await this.repo.findById(orgId, id);\n if (!asset) {\n return null;\n }\n const stat = await this.storage.stat(asset.key);\n if (!stat) {\n this.logger.debug('asset not yet uploaded', { orgId, assetId: id });\n return asset; // not uploaded yet — stays pending\n }\n const updated = await this.uow.run(async ({ assets, outbox }) => {\n const updated = await assets.update(id, {\n size: stat.size,\n contentType: stat.contentType ?? asset.contentType,\n status: 'ready',\n });\n if (updated) {\n await outbox.append([assetEvents.assetReady.make({ orgId, data: updated })]);\n }\n return updated;\n });\n this.logger.info('asset confirmed', { orgId, assetId: id });\n return updated;\n }\n\n list(orgId: string, query: AssetsQuery): Promise<Page<Asset>> {\n return this.repo.list(orgId, query);\n }\n\n get(orgId: string, id: string): Promise<Asset | null> {\n return this.repo.findById(orgId, id);\n }\n\n async requestDownload(\n orgId: string,\n id: string,\n filename?: string,\n expiresInSeconds?: number,\n ): Promise<DownloadTicket | null> {\n const asset = await this.repo.findById(orgId, id);\n if (!asset) {\n return null;\n }\n const url = await this.storage.presignDownload({\n key: asset.key,\n downloadFilename: filename ?? asset.filename,\n ...(expiresInSeconds === undefined ? {} : { expiresInSeconds }),\n });\n return { url, asset };\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 await this.storage.remove(asset.key);\n const deleted = await this.uow.run(async ({ assets, outbox }) => {\n const deleted = await assets.delete(id);\n if (deleted) {\n await outbox.append([assetEvents.assetDeleted.make({ orgId, data: asset })]);\n }\n return deleted;\n });\n this.logger.info('asset removed', { orgId, assetId: id });\n return deleted;\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 { 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,32 +1,21 @@
1
1
  import { DomainEvent } from "../types/shared.js";
2
- import { EmailTemplateId, EmailTemplateParams } from "../types/email-templates.js";
2
+ import { EmailTemplateId } from "../types/email-templates.js";
3
3
  import { AutomationAction } from "../types/schemas/automations.js";
4
- import { WebhookSender } from "../types/automations.js";
5
4
  import "../types/index.js";
6
- import { CredentialStore } from "../shared/ports.js";
7
- import { Mailer, MailerLookups } from "../shared/mailer.js";
5
+ import "../shared/ports.js";
8
6
  import "./model.js";
9
- import "./ports.js";
7
+ import { ActionDeps, EmailRecipient } from "./community-mail.js";
8
+ import { EmailDerivation } from "./community-derivations.js";
10
9
  //#region src/automations/actions.d.ts
11
- interface ActionDeps {
12
- mailer: Pick<Mailer, 'send'>;
13
- lookups: MailerLookups;
14
- credentials: Pick<CredentialStore, 'reveal'>;
15
- webhooks: WebhookSender;
16
- }
17
- interface SendEmailDerivation<K extends EmailTemplateId> {
18
- /** The only event type this template's params can be derived from. */
19
- trigger: string;
20
- /** Undefined = the recipient or content behind the event's ids cannot be resolved. */
21
- derive(event: DomainEvent, lookups: MailerLookups): Promise<{
22
- to: string;
23
- params: EmailTemplateParams[K];
24
- } | undefined>;
25
- }
26
- type SendEmailDerivations = { [K in EmailTemplateId]?: SendEmailDerivation<K>; };
10
+ type SendEmailDerivations = { [K in EmailTemplateId]?: EmailDerivation<K>; };
27
11
  declare const SEND_EMAIL_DERIVATIONS: SendEmailDerivations;
28
- /** Throws on any failure — the engine owns retry policy and failure bookkeeping. */
29
- declare function executeAction(action: AutomationAction, event: DomainEvent, deps: ActionDeps): Promise<void>;
12
+ /** Who an action reached, and who a delivery to failed for (a partial failure does not throw). */
13
+ interface ActionOutcome {
14
+ recipients: string[];
15
+ failed: string[];
16
+ }
17
+ /** Throws on any failure but a partial fan-out — the engine owns retry policy and failure bookkeeping. */
18
+ declare function executeAction(action: AutomationAction, event: DomainEvent, deps: ActionDeps): Promise<ActionOutcome>;
30
19
  //#endregion
31
- export { ActionDeps, SEND_EMAIL_DERIVATIONS, executeAction };
20
+ export { type ActionDeps, ActionOutcome, type EmailRecipient, SEND_EMAIL_DERIVATIONS, executeAction };
32
21
  //# sourceMappingURL=actions.d.ts.map